ILGPU Documentation

repository·master·Indexed 23 days ago

https://github.com/m4rs-mt/ilgpu

ILGPU is a high-performance JIT compiler for .NET that enables developers to write GPU kernels in C# without native dependencies. It supports multiple accelerator types, including CUDA and a multi-threaded CPU accelerator for debugging. The library provides abstractions for hardware management via Context and Accelerator objects, and handles data movement between Host and Device using MemoryBuffers and ArrayViews. It includes the ILGPU.Algorithms library for high-level operations like sorting and prefix sums.

Tokens
23.4K
Snippets
45
Records
88
Agent score
82%

What's inside ILGPU

  1. Overview of ILGPU

    master

    ILGPU is a JIT (just-in-time) compiler for high-performance GPU programs written in .NET-based languages. It is written entirely in C# without native dependencies, providing a bridge between the convenience of C++ AMP and the high performance of CUDA.

    Key features include:

    • Kernel Flexibility: Functions within kernels are standard C# functions (no special annotations required) and can work on value types.
    • CPU Acceleration: All kernels, including those utilizing hardware features like shared memory and atomics, can be executed and debugged on the CPU using an integrated multi-threaded CPU accelerator.
  2. Optimize GPU performance using Struct of Arrays (SoA)

    master

    When designing data structures for GPU kernels in ILGPU, you can choose between two primary memory layouts: Array of Structs (AoS) and Struct of Arrays (SoA).

    Array of Structs (AoS)

    In AoS, data for a single entity is grouped together in memory. For example, an array of Particle structs where each element contains pos, vel, and accel.

    Memory Layout:

    p0: [pos, vel, accel]
    p1: [pos, vel, accel]

    Struct of Arrays (SoA)

    In SoA, each field of the struct is stored in its own separate array. This allows the GPU to perform "coherent" or "chunked" memory loads, as multiple values of the same type (e.g., all positions) are stored contiguously.

    Memory Layout:

    pos0, pos1, ...
    vel0, vel1, ...
    accel0, accel1, ...

    Performance Impact

    While SoA is more complex to implement and manage, it significantly improves memory throughput on the GPU. For large datasets (e.g., 50,000 particles), switching from AoS to SoA can result in performance gains of up to 5x because the GPU can load data more efficiently.

  3. Understand the difference between CPU (SIMD) and GPU (SIMT) execution

    master

    When programming for a GPU using ILGPU, it is critical to understand that you cannot program it like a CPU. While CPUs use SIMD (Single Instruction Multiple Data) to perform math operations on multiple pieces of data at once, GPUs use SIMT (Single Instruction Multiple Threads).

    In SIMT, the GPU assumes that a group of threads (typically 32) will execute the exact same instruction stream. It performs a single fetch and decode for the entire group, then executes the instruction across all threads simultaneously. To write efficient kernels, you must pay close attention to:

    1. Memory Access
    2. Data Locality
    3. Threading
  4. Create and use Accelerators

    master

    An Accelerator represents a specific hardware or software GPU (CPU, CUDA, or OpenCL). Every ILGPU program requires at least one accelerator.

    Accelerator Types

    | Type | Requirements | Best Use Case | | :--- | :--- | : | | CPUAccelerator | None | Debugging and fallback. Allows use of C# debugging features. | | CudaAccelerator | NVIDIA GPU (GTX 680+) | High-performance NVIDIA hardware. | | CLAccelerator | OpenCL 2.0+ capable GPU | AMD or Intel GPUs. |

    Creating Accelerators

    You can create specific accelerators using the Context methods. The integer parameter denotes the index of the device to use in multi-device systems.

    • CPU: context.CreateCPUAccelerator(0); (Requires using ILGPU.CPU;)
    • CUDA: context.CreateCudaAccelerator(0); (Requires using ILGPU.Cuda;)
    • OpenCL: context.CreateCLAccelerator(0); (Requires using ILGPU.OpenCL;)

    Lifecycle and Disposal

    Accelerators must be disposed of in the reverse order of their creation. For example, if you create a Context and then an Accelerator, you must dispose of the Accelerator before the Context is disposed. Device instances do not require disposal.

  5. How implicitly grouped kernels work

    master

    Implicitly grouped kernels provide a high-level programming model where ILGPU manages the thread grid and group sizes automatically.

    Key Characteristics:

    • Abstraction: The details of kernel invocation (group sizes, thread participation) are hidden from the user.
    • Limitations: Because group sizes are managed by ILGPU, you must not use shared memory, group functionality, or warp-specific intrinsics in these kernels.
    • Usage: Use the first parameter as an index type (Index1D, Index2D, or Index3D) to access the global index of the thread.
    class ...
    {
        static void ImplicitlyGrouped_Kernel(
            [Index1D|Index2D|Index3D] index, 
            [Kernel Parameters]...)
        {
            // Use the index parameter to access the global index of i-th thread in the global thread grid
        }
    }
    class ...
    {
        static void ImplicitlyGrouped_Kernel(
            [Index1D|Index2D|Index3D] index, 
            [Kernel Parameters]...)
        {
            // Kernel code
    
            // Use the index parameter to access the global index of i-th thread in the global thread grid
        }
    }
  6. Manage memory with MemoryBuffer

    master

    MemoryBuffer represents an allocated memory region (an array) of a specific value type on an accelerator.

    Key usage rules:

    • Manual Disposal: While MemoryBuffer instances are eventually released by the Garbage Collector or when the parent Accelerator is disposed, you should manually dispose of them using using blocks or .Dispose() to ensure immediate and explicit control over GPU memory.
    • Kernel Usage: You cannot pass a MemoryBuffer directly to a kernel. Instead, you must pass an ArrayView that points to the buffer's memory region.
    • Dimensions: ILGPU provides built-in support for 1D, 2D, and 3D buffers. nD-buffers can also be managed using custom index types.
    // Allocate a memory buffer on the current accelerator device.
    using (var buffer = accelerator.Allocate1D<int>(1024))
    {
        // Perform operations using views...
    }
    // Dispose the buffer after performing all operations
  7. Understand Backends and IRContext

    master

    Backends

    A Backend represents target-specific code-generation functionality for a specific device. While you can use them to manually compile kernels, you do not need to create custom backend instances when using the standard ILGPU runtime; Accelerator instances already carry configured backends.

    IRContext

    An IRContext manages and caches intermediate-representation (IR) code for reuse during compilation.

    • It is not tied to a specific Backend and can be reused across different hardware architectures.
    • A main ILGPU Context already has an associated IRContext used for high-level kernel loading, so manual management is usually unnecessary.
  8. Organize the documentation section

    master

    The documentation section is automatically generated from the /Docs folder. To maintain a clean and predictable structure, follow these rules:

    • Index: The /Docs folder must contain a ReadMe.md file, which serves as the section index.
    • Naming: Markdown filenames should only contain letters, digits, and hyphens.
    • Grouping: Use sub-folders to group files. Note that only 1-depth sub-folders are supported for sections.
    • Ordering: Prefix filenames or sub-folders with DD_ (where D is a digit) to control the display order (e.g., 01_intro.md, 02_advanced.md).
  9. Use Dynamic Specialization for optimized kernels

    master

    Dynamic specialization allows you to define kernels with constant values that are not known until runtime. This enables the compiler to perform optimizations like constant propagation and loop unrolling that are otherwise impossible with standard parameters.

    How it works:

    1. Use the SpecializedValue<T> type for kernel parameters.
    2. The kernel is precompiled during loading.
    3. A final compilation step occurs during the first call with a new, non-cached SpecializedValue<T> combination.
    4. Subsequent calls with the same value use the cached specialized instance.

    Requirement: Values passed via SpecializedValue<T> must implement the IEquatable interface to ensure correct caching.

    Example:

    // Standard kernel: 'c' is a runtime variable
    static void GenericKernel(ArrayView<int> data, int c)
    {
        var globalIndex = Grid.GlobalIndex.X;
        data[globalIndex] = c + 2;
    }
    
    // Specialized kernel: 'c' is treated as an inlined constant
    static void SpecializedKernel(ArrayView<int> data, SpecializedValue<int> c)
    {
        var globalIndex = Grid.GlobalIndex.X;
        data[globalIndex] = c + 2;
    }
    
    // Launching
    // Generic launch
    var genericKernel = accl.LoadStreamKernel<ArrayView<int>, int>(GenericKernel);
    genericKernel((<GridDim>, <GroupDim>), buffer.View, 40);
    
    // Specialized launch
    var specializedKernel = accl.LoadStreamKernel<ArrayView<int>, SpecializedValue<int>>(SpecializedKernel);
    specializedKernel((<GridDim>, <GroupDim>), buffer.View, SpecializedValue.New(40));
    class ...
    {
        static void GenericKernel(ArrayView<int> data, int c)
        {
            var globalIndex = Grid.GlobalIndex.X;
            // Generates code that loads <i>c</i> and adds the value <i>2</i> at runtime of the kernel
            data[globalIndex] = c + 2;
        }
    
        static void SpecializedKernel(ArrayView<int> data, SpecializedValue<int> c)
        {
            var globalIndex = Grid.GlobalIndex.X;
            // Generates code that has an inlined constant value
            data[globalIndex] = c + 2; // Will be specialized for every value <i>c</i>
        }
    
        static void ...(...)
        {
            using var context = new Context();
            using var accl = new CudaAccelerator(context);
    
            var genericKernel = accl.LoadStreamKernel<ArrayView<int>, int>(GenericKernel);
            ...
            genericKernel((<GridDim>, <GroupDim>), buffer.View, 40);
    
            var specializedKernel = accl.LoadStreamKernel<ArrayView<int>, SpecializedValue<int>>(GenericKernel);
            ...
            specializedKernel((<GridDim>, <GroupDim>), buffer.View, SpecializedValue.New(40));
            ...
        }
    }
  10. Assign names to Cuda/OpenCL kernels

    master

    You can use either the .NET function name or a custom name as the entry point for Cuda/OpenCL kernels. This makes profiling and debugging easier by ensuring multiple kernels have distinct names in the output.

    Constraint: Custom kernel names must consist of ASCII characters only. Any other characters will be automatically mapped to _ in the assembly code.

  11. Choose the appropriate math class for GPU operations

    master

    To ensure operations are performed on 32-bit floats on GPU hardware rather than 64-bit doubles, choose the correct math class based on your needs:

    • IntrinsicMath: Provides basic math operations that are supported across all target platforms.
    • XMath (available in the algorithms library): Provides support for all common 32-bit float and 64-bit float math operations.

    Using these instead of the standard .NET Math class helps avoid unnecessary 64-bit precision overhead.