Documentation

Hesper.CUDA.FFI

CUDA Driver API FFI Bindings #

Minimal bindings to the CUDA Driver API for PTX JIT compilation and kernel execution. No nvcc or CUDA runtime needed — only the NVIDIA driver (libcuda.so).

Usage: cuDriverInit let dev ← cuDeviceGet 0 let ctx ← cuCtxCreate dev let mod ← cuModuleLoadData ptxString let func ← cuModuleGetFunction mod "main" let buf ← cuMalloc 1024 cuLaunchKernel func (gridX, gridY, gridZ) (blockX, blockY, blockZ) 0 #[buf]

@[reducible, inline]
Equations
Instances For
    @[reducible, inline]
    Equations
    Instances For
      @[reducible, inline]
      Equations
      Instances For
        @[reducible, inline]
        Equations
        Instances For
          @[reducible, inline]
          Equations
          Instances For

            Driver initialization #

            @[extern lean_hesper_cuda_init]
            @[extern lean_hesper_cuda_device_count]
            @[extern lean_hesper_cuda_device_get]
            @[extern lean_hesper_cuda_device_name]
            @[extern lean_hesper_cuda_compute_capability]
            @[extern lean_hesper_cuda_total_mem]

            Context #

            @[extern lean_hesper_cuda_ctx_create]
            @[extern lean_hesper_cuda_ctx_destroy]

            Module (PTX JIT) #

            @[extern lean_hesper_cuda_module_load_data]
            @[extern lean_hesper_cuda_module_load_data_bytes]

            Variant that takes raw bytes (cubin / fatbin / pre-compiled PTX blobs containing non-UTF-8 bytes). Skips disk-cache and JIT paths; the input must already be in a form the driver accepts directly.

            @[extern lean_hesper_cuda_module_get_function]
            @[extern lean_hesper_cuda_module_unload]
            @[extern lean_hesper_cuda_func_set_max_dynamic_smem]

            Raise a kernel's dynamic shared-memory limit (required for kernels requesting > 48 KB of smem). Wraps cuFuncSetAttribute with CU_FUNC_ATTRIBUTE_MAX_DYNAMIC_SHARED_SIZE_BYTES.

            Utilities #

            @[extern lean_hesper_fast_string_hash]

            Memory-mapped file I/O #

            @[extern lean_hesper_mmap_file]

            mmap a file. Returns (pointer, size).

            @[extern lean_hesper_munmap]
            opaque Hesper.CUDA.munmap (ptr size : USize) :
            @[extern lean_hesper_mmap_slice_to_bytes]
            opaque Hesper.CUDA.mmapSliceToBytes (ptr offset size : USize) :

            Copy a slice from mmapped memory to a Lean ByteArray (for metadata).

            @[extern lean_hesper_mmap_to_gpu]
            opaque Hesper.CUDA.mmapToGPU (mmapPtr offset gpuPtr size : USize) :

            Copy mmapped data directly to GPU buffer (zero Lean-side copy).

            @[extern lean_hesper_read_file_fast]

            Fast file read: mmap + memcpy. Faster than IO.FS.readBinFile for large files.

            Persistent mmap with GC-managed lifetime #

            MMappedFile is an opaque Lean external object backed by a C-side mmap region. Its finalizer (registered on the external class at registration time) calls munmap when the last Lean reference is dropped — matching llama.cpp's unique_ptr<llama_mmap> RAII pattern.

            Unlike the legacy mmapFile above, this path never copies the file into a ByteArray. Tensor weights are kept as (mmap, offset, size) triples and uploaded to the GPU via cuMemcpyHtoDAsync directly from the mmap region — saving ~5 GB of Lean-heap allocation and enabling async H2D (the mmap region stays alive until all in-flight copies finish, because the stream sync point still holds a Lean reference).

            Opaque handle to a memory-mapped file. GC-finalised with munmap.

            @[extern lean_hesper_mmap_open_persistent]
            @[extern lean_hesper_mmap_size]
            @[extern lean_hesper_mmap_slice_to_bytes_persistent]

            Copy a slice of the mmap into a fresh Lean ByteArray. Use only for metadata / parsing; tensor weights should use cuMemcpyHtoDFromMmap instead.

            @[extern lean_hesper_cuda_memcpy_htod_from_mmap]
            opaque Hesper.CUDA.cuMemcpyHtoDFromMmap (dst : USize) (h : MMappedFile) (offset size stream : USize) :

            H2D copy from mmap region directly to GPU. When stream = 0, issues the sync cuMemcpyHtoD; otherwise async on the given stream. Caller must keep the MMappedFile referenced until the stream has synchronised (Lean GC automatically does this via any structure that holds both the GPU buffer handle and the mmap).

            @[extern lean_hesper_mmap_register_region]

            Pin a sub-range of an mmap region and map it into CUDA's unified VA space. Returns the device-side pointer that kernels can ld.global directly — the driver pulls pages over PCIe on demand. Same trick llama.cpp uses for tok_embd_per_layer (its getrows kernel reads the host pointer of the CPU_Mapped buffer). offset must be 4 KB-aligned; size is rounded up to a multiple of 4 KB. Cost: ~200 ms/GB on the first call, once at model load — no per-token cuMemcpy afterwards.

            Memory #

            @[extern lean_hesper_cuda_malloc]
            @[extern lean_hesper_cuda_free]
            @[extern lean_hesper_cuda_memcpy_htod]
            opaque Hesper.CUDA.cuMemcpyHtoD (dst : CUdeviceptr) (src : ByteArray) (offset size : USize) :
            @[extern lean_hesper_cuda_memcpy_dtoh]
            @[extern lean_hesper_cuda_memset]
            opaque Hesper.CUDA.cuMemset (ptr : CUdeviceptr) (size : USize) :

            Kernel launch #

            @[extern lean_hesper_cuda_launch_kernel]
            opaque Hesper.CUDA.cuLaunchKernel (func : CUfunction) (gridX gridY gridZ blockX blockY blockZ sharedMem : UInt32) (args : Array USize) :

            Metadata-free launch descriptor table (Option B+) #

            These register kernel launch metadata (func, grid/block, arg ptrs) in C-owned storage, then fire by numeric id. The Lean side holds only a USize id per kernel and makes exactly one FFI call per launch — no Array USize allocation, no ByteArray for params, no closure for buffer resolution. See docs/llama-fusion-analysis/53-metadata-free- forward-design.md.

            @[extern lean_hesper_desc_reset]

            Reset the descriptor pool. Call once when rebuilding an inference schedule (not per token).

            @[extern lean_hesper_desc_register]
            opaque Hesper.CUDA.descRegister (func : CUfunction) (gridX gridY gridZ blockX blockY blockZ sharedMem : UInt32) (args : Array USize) :

            Register a descriptor with immutable grid/block and a snapshot of buffer pointers. Returns the descriptor id.

            @[extern lean_hesper_desc_rebind]
            opaque Hesper.CUDA.descRebind (descId slot newPtr : USize) :

            Rebind one buffer pointer in an existing descriptor (used only for sites whose buffers genuinely change between calls).

            @[extern lean_hesper_desc_launch]
            opaque Hesper.CUDA.descLaunch (descId stream : USize) :

            Fast launch via descriptor id. This is the Lean-heap-free hot path. stream=0 = default stream.

            @[extern lean_hesper_desc_launch_with_args]
            opaque Hesper.CUDA.descLaunchWithArgs (descId stream : USize) (args : Array USize) :

            Fused rebind-all-args + launch. Used when the descriptor's buffer pointers may differ between calls (e.g. same logical call site across 42 layers with per-layer weights). Writes each arg into the C-owned arg_storage in place — still zero Lean heap past the FFI boundary. args.size must equal the descriptor's n_args.

            @[extern lean_hesper_cuda_launch_kernel_raw]
            opaque Hesper.CUDA.cuLaunchKernelRaw (func : CUfunction) (gridX gridY gridZ blockX blockY blockZ sharedMem : UInt32) (argBytes : ByteArray) (argOffsets : Array USize) :

            Raw-bytes launch for external PTX (e.g. llama.cpp) whose kernels take mixed-type args (uint3 structs, f16 scalars, ...). argBytes holds packed arg values; argOffsets[i] = byte offset of the i-th arg. CUDA receives void** where each entry points at argBytes + offset.

            @[extern lean_hesper_cuda_launch_kernel_raw_on_stream]
            opaque Hesper.CUDA.cuLaunchKernelRawOnStream {CUstream : Sort u_1} (func : CUfunction) (gridX gridY gridZ blockX blockY blockZ sharedMem : UInt32) (argBytes : ByteArray) (argOffsets : Array USize) (stream : CUstream) :

            Stream-aware variant of cuLaunchKernelRaw. Required when the caller is inside cuStreamBeginCapture — launches on the default stream are NOT captured into the graph, so their execution ordering diverges from the captured hesper kernels on graph replay, producing garbage output. stream = 0 (default stream) matches the old behaviour.

            L2 cache persistence (Ampere+ / Ada / Hopper, CC ≥ 8.0) #

            @[extern lean_hesper_cuda_set_l2_persist_limit]

            Set the persisting-L2 limit on the current context. The value is clamped to the device's MAX_PERSISTING_L2_CACHE_SIZE attribute. Returns the effective limit the driver applied.

            @[extern lean_hesper_cuda_set_l2_access_window]

            Install a persisting access-policy window on the default (null) stream. Subsequent launches on this stream will prefer keeping [ptr, ptr+size) in L2 across kernel boundaries.

            @[extern lean_hesper_cuda_reset_l2_persisting_cache]

            Evict all persisting lines from L2 and return the cache to its default non-persistent behaviour.

            CUDA Graphs #

            Capture a sequence of kernel launches once, then replay the whole graph per decode token with a single driver call. llama.cpp uses this to amortise the ~1.2 µs/dispatch host overhead that dominates our ~10 ms/tok host budget. See docs/llama-fusion-analysis/12-complete-cuda-flow.md §5 for the llama.cpp reference flow.

            Opaque handles are size_t on the C side; we marshall as USize.

            cudaStream_t handle (null stream ≡ 0).

            Equations
            Instances For

              cudaGraph_t handle (capture product).

              Equations
              Instances For

                cudaGraphExec_t handle (instantiated graph ready to launch).

                Equations
                Instances For
                  @[extern lean_hesper_cuda_stream_create]

                  Create a dedicated non-blocking stream on which subsequent launches can be captured. Call once at context init.

                  @[extern lean_hesper_cuda_stream_create_default]

                  Create a blocking (default-synchronising) stream. Ops on this stream implicitly sync with the null stream, so readBuffer (which goes through the null stream) observes prior work without an explicit cuStreamSynchronize.

                  @[extern lean_hesper_cuda_stream_destroy]
                  @[extern lean_hesper_cuda_stream_begin_capture]

                  Begin graph capture on stream. All cuLaunchKernelOnStream launches between this call and cuStreamEndCapture are recorded into the produced cudaGraph_t.

                  @[extern lean_hesper_cuda_stream_end_capture]

                  End capture and return the captured graph.

                  @[extern lean_hesper_cuda_graph_instantiate]

                  Turn a captured graph into an executable that the driver can replay with one launch. Must be called before the first cuGraphLaunch.

                  @[extern lean_hesper_cuda_graph_exec_destroy]
                  @[extern lean_hesper_cuda_graph_destroy]
                  @[extern lean_hesper_cuda_graph_launch]

                  Replay a previously-instantiated graph on stream. Returns when the kernels have been submitted (they still run asynchronously).

                  @[extern lean_hesper_cuda_launch_kernel_on_stream]
                  opaque Hesper.CUDA.cuLaunchKernelOnStream (func : CUfunction) (gridX gridY gridZ blockX blockY blockZ sharedMem : UInt32) (stream : CUstream) (args : Array USize) :

                  Launch a kernel onto a specific stream — needed so we can capture the launches into a graph (the default cuLaunchKernel goes to the null stream, which is not captureable).

                  @[extern lean_hesper_cuda_stream_synchronize]

                  Synchronise a stream. Required after cuGraphLaunch when the caller wants to read back results.

                  @[extern lean_hesper_cuda_memcpy_htod_async]
                  opaque Hesper.CUDA.cuMemcpyHtoDAsync (dst : CUdeviceptr) (src : ByteArray) (offset size : USize) (stream : CUstream) :

                  Host→device memcpy on an explicit stream. Needed so writes issued DURING stream capture get recorded as memcpy nodes in the resulting graph (rather than forcing a sync). The driver captures the (src-host-ptr, dst-device-ptr, size) triple; on each replay the memcpy re-reads src from host memory, so subsequent tokens' values flow through without re-capturing.

                  Pinned host memory (staging buffers for CUDA Graphs) #

                  ByteArray is Lean-GC'd and its address is not stable across writeBufferOffset calls. CUDA Graph capture records the pointer, so replay against a freed ByteArray tombstones with CUDA_ERROR_ILLEGAL_ADDRESS. The correct source for capturable writes is page-locked (pinned) host memory allocated via cuMemHostAlloc and held for the whole session. llama.cpp uses the same trick for its per-token scalar uploads.

                  @[extern lean_hesper_cuda_mem_alloc_host]

                  Allocate size bytes of pinned host memory. Returns the host virtual-address as a USize. The memory survives until cuMemFreeHost is called (or the process exits).

                  @[extern lean_hesper_cuda_mem_free_host]
                  @[extern lean_hesper_cuda_mem_alloc_host_mapped]

                  Allocate size bytes of pinned host-mapped memory and return (hostPtr, devPtr). The device pointer aliases the same physical memory through CUDA's unified VA space, so kernels can st.global into it directly and the host can read the result with no driver call once the producing stream has been synchronised. Used to eliminate the per-token cuMemcpyDtoH(4 byte) argmax bubble that drains the stream implicitly (~9.8 ms/tok on graphs-OFF).

                  @[extern lean_hesper_cuda_read_pinned_u32]

                  Read a UInt32 from a pinned host pointer with no driver call. Caller must have synchronised the stream that wrote it (cuStreamSynchronize once is enough — independent of how many kernels updated the slot).

                  @[extern lean_hesper_cuda_write_pinned]
                  opaque Hesper.CUDA.cuWritePinned (hostPtr offset : USize) (src : ByteArray) (size : USize) :

                  Write a small scalar (≤ 8 bytes) into a pinned host buffer. The data is plain Lean bytes; the C++ side memcpys them into the pinned region. No GPU involvement. Use before every graph launch to update a captured memcpy node's source.

                  @[extern lean_hesper_cuda_memcpy_htod_from_pinned]
                  opaque Hesper.CUDA.cuMemcpyHtoDFromPinned (dst : CUdeviceptr) (hostPtr offset size : USize) (stream : CUstream) :

                  Host→device memcpy where the source is a pinned host pointer (stable across the session). Safe to use inside stream capture; the graph records the host pointer, and every replay reads the current contents.

                  @[extern lean_hesper_cuda_pinned_write_and_copy]
                  opaque Hesper.CUDA.cuPinnedWriteAndCopy (dst : CUdeviceptr) (hostPtr offset : USize) (src : ByteArray) (size : USize) (stream : CUstream) :

                  Fused: write src bytes to pinned slot (hostPtr+offset) then immediately queue an async H2D copy to (dst) on stream. One FFI crossing instead of two (cuWritePinned + cuMemcpyHtoDFromPinned), eliminating ~half the Lean→C boundary overhead on hot per-scalar write sites.