How to use libmathdx?#

This guide documents the libmathdx C API for use with the shipped headers, library binary, and the examples. libmathdx JIT-compiles MathDx operations (cuFFTDx, cuBLASDx, cuSolverDx, cuRANDDx) and returns device code (LTOIR or PTX) ready to be linked and executed.

libmathdx C API reference (per-library operators, traits, and examples):

MathDx API documentation (C++ API concepts, operators, and traits; the libmathdx C API mirrors these):

Using libmathdx from CMake#

The installed libmathdx ships a CMake package config under <prefix>/lib/cmake/libmathdx/, so a downstream CMake project can locate it with find_package and link against an imported target:

find_package(libmathdx CONFIG REQUIRED)
target_link_libraries(my_app PRIVATE libmathdx::libmathdx)         # shared
# ...or, for the static archive:
target_link_libraries(my_app PRIVATE libmathdx::libmathdx_static)  # static

Both targets carry the public include directory and the transitive CUDA::nvrtc dependency; the static target additionally re-exposes system link deps (libdl on UNIX) so consumers’ final link step resolves cleanly. If libmathdx is installed in a non-standard prefix, point CMake at it with -Dlibmathdx_ROOT=<prefix> or -DCMAKE_PREFIX_PATH=<prefix> at configure time.

Common workflow (all domains)#

The following steps apply to all libmathdx domains (cuFFTDx, cuBLASDx, cuSolverDx, cuRANDDx), with domain-specific details in the respective API sections. In the steps below, <lib> stands for the library-specific prefix (cufftdx, cublasdx, cusolverdx, or curanddx).

  1. Create descriptor — Call <lib>CreateDescriptor(&handle) for the domain (for example cufftdxCreateDescriptor() or cublasdxCreateDescriptor()).

    cufftdxDescriptor desc;
    cufftdxCreateDescriptor(&desc);
    
  2. Set operators — Use <lib>SetOperatorInt64 and <lib>SetOperatorInt64s to define the problem: sizes, precision, API, execution model, target SM, and related properties.

    cufftdxSetOperatorInt64(desc, CUFFTDX_OPERATOR_SIZE, 128);
    cufftdxSetOperatorInt64(desc, CUFFTDX_OPERATOR_PRECISION, COMMONDX_PRECISION_F32);
    cufftdxSetOperatorInt64(desc, CUFFTDX_OPERATOR_SM, 800);
    
  3. Set descriptor options — Optionally set the generated symbol name with <lib>SetOptionStr(handle, COMMONDX_OPTION_SYMBOL_NAME, "my_symbol").

    cufftdxSetOptionStr(desc, COMMONDX_OPTION_SYMBOL_NAME, "my_fft");
    
  4. Query traits — Use <lib>GetTraitInt64, <lib>GetTraitInt64s, <lib>GetTraitStr, and <lib>GetTraitStrSize on the descriptor to get shared memory size, block dimensions, symbol name, and other launch data.

    int64_t block_dim[3];
    cufftdxGetTraitInt64s(desc, CUFFTDX_TRAIT_BLOCK_DIM, block_dim);
    
  5. Generate code and get LTOIR — The code handle creation, code options, finalization, and LTOIR extraction are typically done together:

    1. Create a commondxCode handle with commondxCreateCode().

      commondxCode code;
      commondxCreateCode(&code);
      
    2. Set code options such as the target SM with commondxSetCodeOptionInt64().

      commondxSetCodeOptionInt64(code, COMMONDX_OPTION_TARGET_SM, 800);
      
    3. Finalize the code with <lib>FinalizeCode(code, handle) for the domain. For the cuBLASDx pipeline path, use cublasdxFinalizeDeviceFunctions() instead.

      cufftdxFinalizeCode(code, desc);
      
    4. Extract the generated LTOIR with commondxGetCodeLTOIRSize() and commondxGetCodeLTOIR(). For multiple LTOIR blobs, use commondxGetCodeNumLTOIRs(), commondxGetCodeLTOIRSizes(), and commondxGetCodeLTOIRs().

      size_t ltoir_size = 0;
      commondxGetCodeLTOIRSize(code, &ltoir_size);
      commondxGetCodeLTOIR(code, ltoir_size, ltoir_buffer);
      
  6. Link and run — Link the LTOIR with the application kernel (for example an NVRTC-compiled wrapper) via nvJitLink with input type NVJITLINK_INPUT_LTOIR. Then load it with the CUDA driver and launch the kernel that calls the generated device function.

    nvJitLinkAddData(linker, NVJITLINK_INPUT_LTOIR, ltoir_buffer, ltoir_size,
                     "libmathdx.ltoir");
    
  7. Destroy — Destroy the code handle and descriptor when they are no longer needed.

    commondxDestroyCode(code);
    cufftdxDestroyDescriptor(desc);
    

Steps 3 and 4 can be done in any order. Operators should not be changed after querying traits or finalizing code.

Pointer APIs#

In this mode, the (LTOIR) device function API is implicitly defined by the API operator such as cublasdxApi_t, cufftdxApi_t and cusolverdxApi_t.

Taking cuBLASDx as an example (See cuBLASDx “pointer API” example), the API works like this:

  1. Create a descriptor using cublasdxCreateDescriptor()

  2. Populate the descriptor with operators using cublasdxSetOperatorInt64() and related APIs.

  3. Query traits using cublasdxGetTraitInt64() and related APIs.

  4. Generate the LTOIR by

    1. Creating a code handle with commondxCreateCode()

    2. Setting options (e.g. target SM) with commondxSetCodeOptionInt64() and related APIs

    3. Generate the code with cublasdxFinalizeCode() and extract it with commondxGetCodeLTOIR() and related APIs.

    4. Query properties of the code (e.g. ISA version) with commondxGetCodeOptionInt64()

    5. Destroy the code handle with commondxDestroyCode()

  5. Destroy the descriptor with cublasdxDestroyDescriptor()

Step 3 and 4 can be done in any order. However, note that operators cannot be set after step 3 and/or 4.

Opaque Tensors APIs#

In this mode, the (LTOIR) device function API is defined by the input and output tensor specified when creating the device function through cublasdxCreateDeviceFunction()

This is an extension to the previous workflow (See cuBLASDx “tensor API” example).

  1. Create a descriptor using cublasdxCreateDescriptor()

  2. Populate the descriptor with operators using cublasdxSetOperatorInt64() APIs.

  3. Query traits using cublasdxGetTraitInt64() APIs

  4. Create opaque tensors using cublasdxCreateTensor() and cublasdxFinalizeTensors(). cublasdxFinalizeTensors() can be done on a set of tensors.

    1. Tensor traits can then be queried using cublasdxGetTensorTraitInt64() APIs. This must be done after calling cublasdxFinalizeTensors().

  5. Define the device function to be generated using cublasdxCreateDeviceFunction(). Device functions are defined as operations (e.g. copy, execution, etc) on a set of input and output tensors. Tensors must be finalized using cublasdxFinalizeTensors() before calling cublasdxCreateDeviceFunction().

    1. Device function traits can then be queried using cublasdxGetDeviceFunctionTraitStrSize() and cublasdxGetDeviceFunctionTraitStr() APIs. This must be done after calling cublasdxCreateDeviceFunction().

  6. Generate the device function code

    1. Creating a code handle with commondxCreateCode()

    2. Setting options (e.g. target SM) with commondxSetCodeOptionInt64() APIs

    3. Generate the code with cublasdxFinalizeDeviceFunctions() and extract it with commondxGetCodeLTOIR() APIs

    4. Query properties of the code (e.g. ISA version) with commondxGetCodeOptionInt64()

    5. Destroy the code handle with commondxDestroyCode()

  7. Destroy the device functions, tensors, and descriptor with their corresponding destroy APIs (cublasdxDestroyDeviceFunction(), cublasdxDestroyTensor(), and cublasdxDestroyDescriptor()).

How to call the device function (API & ABI)?#

For pointer APIs,

  1. Determine the device function symbol name using the *_SYMBOL_NAME (e.g. CUBLASDX_TRAIT_SYMBOL_NAME) trait. The function is extern “C” and can be called as is.

  2. Determine the device function API from the specific Dx lib implementation. Scalars (real and complex) and integers are passed by pointers. Pointers are passed by value.

  3. The function can be forward-declared and called as-is.

For tensor APIs,

  1. Determine the device function API from cublasdxDeviceFunctionType_t. Scalars (real and complex) are passed by pointers, all tensor arguments (see cublasdxTensorType_t) are trivial and are passed by value.

  2. When calling the device function from CUDA C++ or from non-CUDA C++ (e.g. LLVM):

    1. Determine the device function symbol name (e.g. cublasdx_execute_8745486) using the CUBLASDX_DEVICE_FUNCTION_TRAIT_SYMBOL trait. The function is extern “C” and can be called as is.

    2. Global memory tensor names are struct libmathdx_gmem_tensor { void* ptr; long long int ld; } with ld the leading dimension. Shared and register memory tensors are struct libmathdx_tensor { void* ptr; }.

    3. Forward-declare the function using its symbol name and each of the argument name, and call the function when needed.

Non CUDA C++ users must adhere to the calling convention as defined in the Itanium ABI. See also this.

Error handling#

All API return values are of type commondxStatusType. Check for COMMONDX_SUCCESS. On failure (e.g. COMMONDX_COMPILATION_ERROR), use commondxGetLastErrorStrSize() and commondxGetLastErrorStr() to retrieve the last error message.

Common API (libcommondx.h)#

Using the generated code (LTOIR)#

  1. Compile the kernel — Use NVRTC or the CUDA compiler with -dlto and --relocatable-device-code=true so the application kernel (or wrapper that calls the generated device function) is compiled to LTOIR.

  2. Link — Link the application LTOIR and the libmathdx LTOIR with nvJitLink (input type NVJITLINK_INPUT_LTOIR) for the target SM to produce cubin or PTX.

  3. Load — Load the linked result with cuModuleLoadDataEx() and obtain the kernel or device function with cuModuleGetFunction().

  4. Launch — Allocate device memory and launch using the block dimensions and shared memory size from the traits of the descriptor.

The shipped examples provide a helper that compiles a string kernel with NVRTC and links with LTOIR via nvJitLink; this pattern or an equivalent can be reused. For the exact API calls and order for each feature, see the full example source on the cuFFTDx, cuBLASDx, cuSolverDx, and cuRANDDx pages.