cuPyNumeric Documentation

repository·main·Indexed 21 days ago

https://github.com/nv-legate/cupynumeric

A high-performance array computing library that implements the NumPy API on top of the Legate framework. cuPyNumeric enables the scaling of NumPy workflows from single CPUs to distributed GPU clusters with minimal code changes. It provides a NumPy-compatible interface for distributed numerical computations via the cupynumeric.ndarray object and offers incremental support for the Array API standard.

Tokens
75.4K
Snippets
183
Records
342
Agent score
76%

What's inside cuPyNumeric

  1. Overview of NVIDIA cuPyNumeric

    main

    NVIDIA cuPyNumeric implements the NumPy API on top of the Legate framework. It provides transparent accelerated computing that scales across different hardware configurations, including:

    • Single CPU
    • Single GPU
    • Multi-node, multi-GPU systems (e.g., scaling to 2048 A100 GPUs on a DGX SuperPOD).

    Because it implements the NumPy API, existing NumPy-based code (such as Python CFD courses) can often be run completely unmodified to achieve high-performance scaling.

  2. Overview of cuPyNumeric

    main

    cuPyNumeric is a high-performance array computing library that implements the NumPy API on top of the Legate framework. It is designed to allow existing NumPy workflows to run on GPUs and distributed systems with minimal to no code changes.

    Key Capabilities:

    • Seamless Scaling: Scale workloads from a single CPU to a single GPU, and up to thousands of GPUs across multiple nodes.
    • Use Cases: Large-scale data analysis, complex simulations, and machine learning.
  3. What is a Legate task?

    main

    A Legate task is a Python function used to extend cuPyNumeric with custom, scale-out algorithms. By annotating a function with the @legate.core.task.task decorator, you inform the Legate runtime that the function is a parallel task capable of being distributed across multiple CPUs, GPUs, and nodes.

    Legate tasks are automatically available when using cuPyNumeric and require no additional installation. They allow you to implement novel research algorithms or leverage external libraries while maintaining transparent scaling across distributed resources.

    import legate.core.task
    
    @legate.core.task.task
    def my_parallel_task(input_data, output_data):
        # Task logic here
        pass
  4. Understand cuPyNumeric API support and implementation status

    main

    cuPyNumeric provides a distributed NumPy-compatible API. When assessing migration readiness, use the following legend to understand the implementation status of NumPy functions:

    • ✓✓ Implemented and works on multi-GPU: The ideal path; supports distributed multi-GPU execution (and implies single-GPU support).
    • Implemented but single-GPU/CPU only: Works, but does not support multi-node/multi-GPU distributed execution.
    • 🟡 Partial support: Implementation is incomplete or has specific constraints (e.g., limited batching modes).
    • Not implemented: Not available on the cuPyNumeric distributed path. Depending on the version, this may route through host NumPy or raise an exception. Using these in a hot-path is a migration blocker.

    Naming Convention: cuPyNumeric follows the pattern cupynumeric.<tail> of the original NumPy name. For example, numpy.fft.fft is accessed via cupynumeric.fft.fft.

  5. Identify performance bottlenecks in cuPyNumeric using Legate Profilers

    main

    The Legate Profiler provides a timeline view of resource utilization to help identify inefficient code patterns. The timeline is organized by resource lanes (CPU, GPU, Utility, I/O, System, and Channel) where the x-axis is time and the y-axis represents resource streams.

    Interpreting Resource Lanes:

    • CPU: Shows user compute tasks.
      • Good: Long, solid bars (large tasks).
      • Bad: Dense "barcode" slivers (many tiny tasks/high overhead).
    • Utility: Shows Legate runtime "meta" work (dependency analysis, mapping, task launch).
      • Good: Short, discrete bursts around big operations.
      • Bad: A sustained high plateau (the scheduler is a bottleneck).
    • I/O: Shows TopLevelTask / driver time and file operations.
      • Good: Brief bursts only during reads/writes.
      • Bad: A long, steady baseline (many small coordination events).
    • System: Shows low-level OS activity (allocations, thread setup). Should ideally be quiet and flat during computation.
    • Channel (chan): Records data movement between host and device.

    Task State Shading:

    • Darkest shade: Actively executing.
    • Intermediate shade: Ready state.
    • Lightest shade: Task is blocked.
    • Gray: Groups of tiny tasks.
  6. Determine migration verdict (Strong-go vs. Weak-go vs. No-go)

    main

    After passing the gates, categorize your migration effort into one of three composite verdicts:

    Strong-go ("Migrate this quarter")

    • Hardware: Pass Gate 1.
    • Size: 100M+ elements per hot-path array.
    • Shape: READY or LIGHT REFACTOR.
    • Pattern: Stencil, GEMM, or reduction-dominated.
    • Boundary: > 70% wall time in array code.
    • Numerical: Tolerant of ULP-level differences.

    Weak-go ("Pilot first")

    • Hardware: Pass Gate 1.
    • Size: $\ge$ 10M per array.
    • Shape: SIGNIFICANT REFACTOR with clear recipes.
    • Pattern: Mixed compute patterns.
    • Boundary: 30–70% array-bound.
    • Numerical: Tolerant of differences.

    No-go ("Use a different tool")

    • Any Gate 1 failure.
    • Array size < 1M per array.
    • Shape is NOT RECOMMENDED (unvectorizable element loops).
    • Pattern is graph, sparse, sequential, or ML.
    • Hard requirement for absolute determinism.
  7. Understand the cuPyNumeric lazy execution model

    main

    cuPyNumeric uses a lazy / deferred, asynchronous, and task-parallel execution model. When you call a NumPy function (e.g., c = a + b), the Python call returns immediately with a DeferredArray thunk. No actual computation occurs at the moment of the call. Instead, a task is built and submitted to the Legate runtime, which schedules the work on available processors (GPU, OMP, or CPU) asynchronously.

    Key Mental Model:

    • Submission: Synchronous from Python's perspective (the API returns immediately).
    • Execution: Asynchronous; the Legate runtime dispatches kernels/tasks.
    • Completion: Invisible to Python until a sync point is reached.
  8. When NOT to use cuPyNumeric (Sparse and sklearn workloads)

    main

    cuPyNumeric is a dense-array runtime and is NOT RECOMMENDED for workloads that are fundamentally sparse or rely heavily on scipy.sparse or sklearn estimators.

    Why it is not suitable

    • No Sparse Support: cuPyNumeric has no first-class support for scipy.sparse.csr_matrix or similar types. Converting sparse matrices to dense arrays for cuPyNumeric would cause massive memory inflation (10–1000×).
    • Host-Side Execution: sklearn pipelines and scipy.sparse operations are orchestrated on the host (CPU). Swapping numpy for cupynumeric in these pipelines will not provide GPU parallelism; instead, it may force data to be moved back and forth between host and device, degrading performance.
    • Partitioning Mismatch: Sparse partitioning (where row counts vary wildly) does not fit the Legate auto-partitioner's load-balancing model.
    • For sparse + ML workloads, use RAPIDS cuML instead.
    • If a workload has a significant dense-numeric component that is separable from the sparse/ML pipeline, assess that isolated module separately as a cuPyNumeric candidate.
  9. Handle semantic differences in `np.diag`, `np.flip`, and `.flatten()`

    main

    In NumPy, functions like np.diag, np.flip, .flat, and .flatten() often return views of the original array. In cuPyNumeric, these operations return copies.

    Warning: If you mutate the result of these functions expecting to modify the original array, your changes will be lost because you are mutating a copy.

    Fix: If you need to mutate the data, write through to the original array using explicit indexing.

    # BAD: Mutation fails because np.diag returns a copy in cuPyNumeric
    d = np.diag(matrix)
    d[0] = 5  # matrix remains unchanged
    
    # GOOD: Explicitly write to the original
    matrix[range(n), range(n)] = 5.0
  10. Avoid object-dtype arrays (R107)

    main

    cuPyNumeric only supports numeric datatypes. dtype=object arrays are not supported on the distributed path.

    Fix: Restructure data into numeric representations:

    • Variable-length strings: Use fixed-width or pad with sentinels + a lengths array.
    • Heterogeneous records: Use a Structure-of-Arrays (one numeric array per field).
    • Variable-length sequences: Use flat concatenation + an offsets array.
  11. R006 — Pre-allocation via `out=` parameter

    main

    Using the out= parameter in NumPy functions allows you to reuse existing FBMEM allocations instead of creating new temporaries. This is critical for performance in hot loops.

    Why it matters:

    • cuPyNumeric does not JIT-fuse adjacent kernels in the mainline. Without out=, an expression like result = a + b * c allocates two intermediate temporary arrays.
    • Avoiding temporaries reduces memory churn, prevents fragmentation, and reduces scheduling overhead.

    Caveats:

    • The out array must match the required shape and dtype of the operation.
    • Some operations (like reductions with keepdims=False) may not accept out= if the output shape differs from the input.
    np.add(a, b, out=result)
    np.multiply(result, scale, out=result)
    np.matmul(A, B, out=C)
    np.sum(arr, axis=0, out=row_sums)
  12. Scale linalg.qr and linalg.svd via batching

    main

    Functions like linalg.qr and linalg.svd are single-device only in cuPyNumeric; adding more GPUs will not speed up a single factorization.

    To achieve multi-GPU scaling, you must batch your operations along the leading axis. This transforms the problem into a data-parallel workload where multiple independent factorizations are distributed across GPUs.