inducer-pycuda

repository·main·Indexed 24 days ago

https://github.com/inducer/pycuda

A Python package providing the GPUArray class, a numpy.ndarray work-alike for performing computations on GPU devices. It includes support for CUDA vector types as NumPy dtypes, data transfer between host and device, elementwise math functions via pycuda.cumath, and high-quality random number generation using CURAND-based generators such as XORWOW and MRG32k3a.

Tokens
14.9K
Snippets
14
Records
91
Agent score
84%

What's inside inducer-pycuda

  1. Overview of PyCUDA

    main

    PyCUDA provides Pythonic access to NVIDIA's CUDA parallel computation API. It is designed to make CUDA programming more convenient and safer through several key features:

    • RAII-style Object Cleanup: Resource cleanup is tied to the lifetime of Python objects. PyCUDA manages dependencies to ensure, for example, that a context is not detached before all memory allocated within it has been freed.
    • High-level Abstractions: Provides convenient wrappers like pycuda.driver.SourceModule for managing CUDA source code and pycuda.gpuarray.GPUArray for managing data on the GPU.
    • Full Driver API Access: Offers complete access to the CUDA driver API and includes interoperability support for OpenGL.
    • Automatic Error Checking: CUDA errors are automatically caught and translated into standard Python exceptions.
    • Performance: The core layer is implemented in C++ to ensure minimal overhead.
  2. Introduction to PyCUDA

    main

    PyCUDA provides Pythonic access to Nvidia's CUDA parallel computation API. It simplifies CUDA programming through several key features:

    • RAII-based Resource Management: Object cleanup is tied to the lifetime of Python objects, preventing memory leaks and crashes. PyCUDA manages dependencies, ensuring a context is not detached until all memory allocated within it is freed.
    • Convenience Abstractions: High-level classes like pycuda.compiler.SourceModule and pycuda.gpuarray.GPUArray simplify kernel compilation and memory management.
    • Full Driver API Access: Provides access to the complete CUDA driver API.
    • Automatic Error Checking: CUDA errors are automatically translated into Python exceptions.
    • Performance: The core layer is written in C++ to minimize overhead.
  3. Why use metaprogramming with PyCUDA?

    main

    Metaprogramming in PyCUDA involves writing Python code that generates CUDA source code at runtime. This approach is used to solve several common CUDA optimization challenges:

    • Automated Tuning: Instead of relying on unreliable heuristics for parameters like threads per block or shared memory size, you can benchmark different configurations at runtime and select the fastest one for the current hardware.
    • Dynamic Data Types: Generate specialized kernels for specific types (e.g., float vs double) exactly when they are needed, avoiding the need to precompile multiple versions.
    • Problem Specialization: Generate code tailored to the specific problem size or constraints, which allows for optimizations that generic code cannot achieve.
    • Constant Optimization: Compiling problem sizes or specific values into the code as constants rather than variables. This reduces register pressure and improves performance (e.g., making multiplications more efficient).
    • Manual Loop Unrolling: Since nvcc may not always unroll loops as expected with #pragma unroll, you can use Python to dynamically unroll loops to the required size during code generation.
  4. Bind Arrays to Texture and Surface References

    main

    PyCUDA allows binding memory to texture and surface units for specialized access patterns.

    Texture References

    Use TextureReference() to bind linear memory or an Array to a texture unit.

    • set_array(array): Binds an Array object. The texture reference keeps the array alive.
    • set_address(devptr, bytes, allow_offset=False): Binds a chunk of linear memory.
    • set_address_2d(devptr, descr, pitch): Binds a 2D chunk of global memory.

    Surface References

    Use SurfaceReference() (constructed via Module.get_surfref) to bind an Array for read/write access.

    • set_array(array, flags=0): Binds the surface to an Array.

    Creating Arrays for Textures

    To convert NumPy arrays or GPU arrays into Array objects suitable for textures/surfaces, use:

    • np_to_array(nparray, order, allowSurfaceBind=False)
    • gpuarray_to_array(gpuparray, order, allowSurfaceBind=False)
    • matrix_to_array(matrix, order)
    • make_multichannel_2d_array(matrix, order)
  5. Use Managed (Unified) Memory

    main

    Managed memory (available in CUDA 6.0+) creates a virtual memory space visible to both CPU and GPU. The OS migrates pages between them automatically.

    Warning: Accessing managed memory on the host while a GPU kernel is executing is strictly forbidden and will cause a segmentation fault that terminates the Python interpreter immediately.

    Allocation Functions:

    • managed_empty(shape, dtype, order="C", mem_flags=0)
    • managed_zeros(shape, dtype, order="C", mem_flags=0)
    • managed_empty_like(array, mem_flags=0)
    • managed_zeros_like(array, mem_flags=0)

    Usage Pattern: Managed arrays can be used on the host, passed to a kernel, and accessed on the host again without explicit memcpy calls. Always call context.synchronize() before accessing the array on the host after a kernel launch.

    Visibility Control: Use ManagedAllocation.attach(mem_flags, stream=None) to change visibility using mem_attach_flags (e.g., GLOBAL or HOST).

    from pycuda.autoinit import context
    import pycuda.driver as cuda
    import numpy as np
    from pycuda.compiler import SourceModule
    
    # 1. Allocate managed memory
    a = cuda.managed_empty(shape=10, dtype=np.float32, mem_flags=cuda.mem_attach_flags.GLOBAL)
    a[:] = np.linspace(0, 9, len(a)) # Fill on host
    
    # 2. Use in kernel
    mod = SourceModule("""
    __global__ void doublify(float *a) { a[threadIdx.x] *= 2; }
    """)
    doublify = mod.get_function("doublify")
    doublify(a, grid=(1,1), block=(len(a),1,1))
    
    # 3. CRITICAL: Synchronize before host access
    context.synchronize()
    
    # 4. Access on host
    median = np.median(a)
  6. Use CURAND-based random number generators

    main

    For high-quality pseudorandom or quasirandom numbers, use the CURAND-based classes in pycuda.curandom.

    Important Resource Note: These generators run on the GPU, and each thread uses its own generator. Creating these objects is resource-intensive. On older Tesla devices, you may be limited to ~256 active generators; Fermi devices allow up to ~1024. If you encounter errors creating these objects, reduce the number of active threads/generators.

    Pseudorandom Generators

    • XORWOWRandomNumberGenerator: Provides pseudorandom numbers with a period of at least $2^{190}$. (CUDA 3.2+)
    • MRG32k3aRandomNumberGenerator: Provides pseudorandom numbers. (CUDA 4.1+)

    Quasirandom Generators

    Quasirandom sequences are designed to fill n-dimensional space more evenly than pseudorandom sequences, though they are more expensive to generate.

    • Sobol32RandomNumberGenerator: Period of $2^{32}$. (CUDA 3.2+)
    • ScrambledSobol32RandomNumberGenerator: Scrambled version of Sobol32. (CUDA 4.0+)
    • Sobol64RandomNumberGenerator: Period of $2^{64}$. (CUDA 4.0+)
    • ScrambledSobol64RandomNumberGenerator: Scrambled version of Sobol64. (CUDA 4.0+)
  7. Interoperate with libraries using the CUDA Array Interface

    main

    PyCuda supports interoperability with other libraries that implement the CUDA Array Interface (such as CuPy and Numba). You can pass pycuda.gpuarray.GPUArray instances directly into kernels from these libraries.

    Example with CuPy:

    import cupy as cp
    # cupy_a is a CuPy array
    func(cupy_a, block=(4, 4, 1), grid=(1, 1))

    Example with Numba:

    from numba import cuda
    import pycuda.gpuarray as gpuarray
    
    a_gpu = gpuarray.to_gpu(numpy.random.randn(4, 4).astype(numpy.float32))
    
    @cuda.jit
    def double(x):
        i, j = cuda.grid(2)
        x[i, j] *= 2
    
    double[(4, 4), (1, 1)](a_gpu)
    import cupy as cp
    
    cupy_a = cp.random.randn(4, 4).astype(cp.float32)
    func = mod.get_function("double_array")
    func(cupy_a, block=(4, 4, 1), grid=(1, 1))
  8. Use DeviceMemoryPool to optimize device allocations

    main

    Frequent calls to pycuda.driver.mem_alloc (e.g., when using gpuarray.GPUArray) can be slow. DeviceMemoryPool mitigates this by holding onto memory blocks instead of returning them to the system, allowing for faster reuse of similarly-sized blocks.

    Key Components:

    • DeviceMemoryPool: Manages a pool of linear device memory.
    • allocate(size): Returns a PooledDeviceAllocation object.
    • PooledDeviceAllocation: Represents the allocated memory. It can be cast to an int to get the starting device address. Calling .free() explicitly returns the memory to the pool.

    Management Methods:

    • free_held(): Frees all unused memory currently held by the pool.
    • stop_holding(): Instructs the pool to immediately free memory returned to it instead of holding it. This is useful for cleanup.

    Warning: Allocations made outside the pool may encounter out-of-memory errors if the pool has already claimed most of the available device memory.

  9. Use CUDA vector types as NumPy dtypes

    main
    The pycuda.gpuarray.vec module provides access to all of CUDA's supported vector types (e.g., float3, long4) as numpy.dtype instances. These types use field names x, y, z, and w to match CUDA's convention. They can be used for passing data between Python and CUDA kernels. You can also use make_type functions to create these types (e.g., make_float3(x, y, z)).
  10. Initialize PyCuda

    main

    To use PyCuda, you must import the driver and initialize the context. While you can perform initialization, context creation, and cleanup manually, the simplest way is to use pycuda.autoinit, which handles these tasks automatically.

    import pycuda.driver as cuda
    import pycuda.autoinit
    from pycuda.compiler import SourceModule
  11. Automatically initialize CUDA with pycuda.autoinit

    main

    The pycuda.autoinit module automates the setup required to submit compute kernels. When imported, it performs all necessary steps to prepare CUDA and creates a default compute context using pycuda.tools.make_default_context.

    It provides two useful attributes:

    • device: The pycuda.driver.Device instance used for initialization.
    • context: The default-constructed pycuda.driver.Context instance on that device.

    Alternatively, use pycuda.autoprimaryctx if you want to retain the device's primary context instead of creating a new one via make_default_context.

  12. Initialize XORWOWRandomNumberGenerator with seeds

    main

    To initialize an XORWOWRandomNumberGenerator, you can provide a seed_getter function. This function, given an integer count, must yield an int32 GPUArray of seeds. PyCUDA provides helper functions to generate these seeds:

    • seed_getter_uniform(N): Returns a GPUArray filled with one random int32 repeated N times.
    • seed_getter_unique(N): Returns a GPUArray filled with N random int32 values.