MatX Documentation

repository·main·Indexed 23 days ago

https://github.com/nvidia/matx

MatX is a C++20 library for high-performance numerical computing featuring a NumPy-like tensor expression language. It supports execution on NVIDIA GPUs via CUDA and JIT fusion, as well as CPUs using optimized backends such as NVPL, FFTW, or OpenBLAS.

Tokens
84.3K
Snippets
143
Records
531
Agent score
80%

What's inside MatX

  1. Overview of MatX features

    main

    MatX is a modern C++ library designed for numerical computing on NVIDIA GPUs and CPUs. It provides a high-level syntax similar to Python or MATLAB while maintaining near-native performance through compile-time expression evaluation and JIT-compiled GPU kernels.

    Key capabilities include:

    • Kernel Fusion: Compile-time expression evaluation for generating optimized GPU kernels and JIT fusion across library boundaries.
    • Hardware Acceleration: Optimized execution for both NVIDIA GPUs and CPUs (supporting ARM and x86 via libraries like NVPL, FFTW, OpenBLAS, and BLIS).
    • Ease of Use: Header-only library with a single tensor type used across the entire API and intuitive error messages.
    • Extensibility: Provides easy frontend APIs to many popular CUDA and CPU libraries.
  2. Common CMake logic with rapids_cmake

    main
    The rapids_cmake functions provide shared CMake logic used across various projects. This includes utilities for build types, downloading with retries, installing library directories, version parsing, Conda environment support, and writing Git revision or version files.
  3. What is an Operator in MatX

    main

    An Operator is the fundamental abstraction in MatX. Any type that implements the Operator interface can be used in arithmetic expressions, transforms, and assignments. This interface allows MatX to treat tensors, generators, and mathematical expressions uniformly.

    To be considered an Operator, a type must implement the following functions:

    • Size
    • Shape
    • Stride
    • Rank
    • operator() (required for rvalues; optionally for lvalues)

    Because Operators are a superset, types like Tensors and Generators are all considered Operators and can be used anywhere an operator is accepted.

    // Create a tensor. "t" is an operator
    auto t    = make_tensor<float>({10});
    
    // Create a sin operator that operates on "t" and name it "op"
    auto op   = sin(t);
    
    // Create a Hamming window generator and assign it to the variable "win"
    auto win  = hamming({10});
    
    // Launch a kernel where "win" is copied into "t"
    (t = win).run();
  4. Use Caching for State-Dependent Transforms

    main

    For transforms that require expensive state (like a plan or a handle), use a cache to improve performance. This avoids re-creating the state for every call with the same signature.

    To implement caching:

    1. Define a Params Key: A struct containing all parameters that define the transform's state (e.g., dimensions, strides, stream, execution type).
    2. Implement a Hash function (FftCUDAParamsKeyHash): Provides a quick hash for initial map lookups.
    3. Implement an Equality function (FftCUDAParamsKeyEq): Performs a full comparison of all parameters once a hash match is found to ensure a true cache hit.
    4. Use detail::GetCache().LookupAndExec<CacheType>(...) in your impl function. This method takes the cache ID, the parameters, a factory function to create the cached object on a miss, and an execution function to run the transform on a hit.

    Example of the LookupAndExec pattern:

      using cache_val_type = detail::matxCUDAFFTPlan1D_t<decltype(out), decltype(in)>;
      detail::GetCache().LookupAndExec<detail::fft_cuda_cache_t>(
        detail::GetCacheIdFromType<detail::fft_cuda_cache_t>(),
        params,
        [&]() {
          return std::make_shared<cache_val_type>(out, in, stream);
        },
        [&](std::shared_ptr<cache_val_type> ctype) {
          ctype->Forward(out, in, stream, norm);
        }
      );
    using cache_val_type = detail::matxCUDAFFTPlan1D_t<decltype(out), decltype(in)>;
      detail::GetCache().LookupAndExec<detail::fft_cuda_cache_t>(
        detail::GetCacheIdFromType<detail::fft_cuda_cache_t>(),
        params,
        [&]() {
          return std::make_shared<cache_val_type>(out, in, stream);
        },
        [&](std::shared_ptr<cache_val_type> ctype) {
          ctype->Forward(out, in, stream, norm);
        }
      );
  5. Configure print formatting styles

    main
    MatX supports different print formatting styles, allowing you to output tensor data in formats that can be directly copied and pasted into MATLAB or Python. Refer to the set_print_format_type_func documentation for details on how to switch between these styles.
  6. Use proprietary binaries (nvcomp only)

    main

    The proprietary_binary field allows downloading pre-built proprietary versions of a library. This is currently only supported for the nvcomp package.

    How it works

    The search logic follows this priority:

    1. Search for a local version matching the version key (unless always_download is true).
    2. Download the proprietary version if a valid <arch>-<os> key exists in the proprietary_binary dictionary and USE_PROPRIETARY_BLOB is set to ON.
    3. Fallback to using the git_url and git_tag.

    Configuration

    The keys in the proprietary_binary dictionary must match the lowercase value of <arch>-<os>, where arch is CMAKE_SYSTEM_PROCESSOR and os is CMAKE_SYSTEM_NAME.

    Placeholders

    Supported placeholders in binary URLs include:

    • ${rapids-cmake-version}
    • ${cuda-toolkit-version}
    • ${cuda-toolkit-version-major}
    • ${cuda-toolkit-version-mapping}: Uses values from the proprietary_binary_cuda_version_mapping dictionary.
    • $ENV{variable}
  7. How element-wise operator fusion works in MatX

    main

    MatX uses lazy evaluation to perform element-wise operator fusion. Instead of executing arithmetic operations immediately and storing intermediate results in memory (which causes performance penalties due to high memory latency), MatX overloads operators to return objects that represent the computation.

    When you define an expression, MatX builds a single C++ type representing the entire equation. The actual computation only occurs when you request a specific element or call .run(). This allows MatX to fuse multiple operations into a single pass, significantly reducing the number of loads and stores.

    Key benefits:

    • Reduced Memory Overhead: Avoids intermediate memory writes/reads.
    • Readability: You can store complex expressions in variables without triggering execution.
    • Transform Fusion: Complex transforms (like fft) can be fused with standard operators at compile-time.
  8. Use lazy projections for JIT fusion in QR decompositions

    main

    When -DMATX_EN_MATHDX=ON is enabled, you can use lazy projection members to trigger CUDA JIT fusion. This allows the QR decomposition components to be fused directly into subsequent operations in a numerical pipeline.

    Supported Projections:

    • Full QR: qr(A).Q and qr(A).R (Limited to square matrices).
    • Economic QR: qr_econ(A).Q (Limited to m >= n) and qr_econ(A).R (Supports rectangular matrices if cuSolverDx supports the shape).
    • QR Solver: qr_solver(A).Out and qr_solver(A).Tau (Supports rectangular matrices if cuSolverDx supports the shape).

    Requirements:

    • Must use a CUDA executor.
    • Runtime shape/type must be supported by cuSolverDx.
    • Projection JIT currently supports ranks 2 through 4 and float, double, complex<float>, and complex<double> inputs.
  9. Limitations of CUDAJITExecutor

    main

    The CUDAJITExecutor has several constraints:

    • Complete Lowering: It only runs expressions that MatX can lower completely into a single JIT kernel. If any operator in the chain is unsupported, the executor will throw an error rather than falling back.
    • Launch Requirement Compatibility: MathDx-backed operators must have compatible runtime launch requirements (e.g., matching block dimensions and shared-memory usage). If one operator requires 2 elements per thread and another requires 1, the expression cannot be fused.
    • Multi-output Solvers: For solvers with multiple outputs, fusion is only available through lazy projection members (e.g., qr_econ(A).R) rather than the mtie assignment form. Only the specific projection requested is generated by the JIT path.
  10. Execute operations using executors

    main

    The .run() method requires an executor to define where and how the work is launched.

    • CUDA Executor (Default): By default, calling .run() (or .run(stream)) launches the operation on the GPU using a CUDA stream. This is asynchronous.
    • HostExecutor: To run code on the CPU, pass a HostExecutor{} to the .run() method. Host executors are synchronous and will block the calling thread until the operation is complete.

    In MatX terminology, the act of launching an operator via .run() is called an executor.

  11. Understand the rapids-cmake CPM JSON format

    main

    rapids-cmake uses a JSON file to manage project versions and download instructions. The file structure is a root object containing a packages object. The packages object is a case-sensitive map where each key is a project name and each value is a project object defining how to locate or fetch that project.

    To be valid, every project object must include a version string and one of the following pairs:

    1. git_url AND git_tag (for Git-based cloning)
    2. url AND url_hash (for tarball downloads)

    Common placeholders available in these fields include:

    • ${rapids-cmake-version}: Evaluates to the 'major.minor' of the current rapids-cmake CalVer.
    • ${version}: Evaluates to the value of the project's own version field.
    • $ENV{variable}: Evaluates to the contents of a specified environment variable.