packages/gpu/src/gpu.nu — backend-neutral GPU compute interface.
v0.1.0 ships ONE backend: CUDA (src/cuda.nu). This file is the surface a program codes against — Gpu / GpuKernel / GpuBuffer plus open / compile / alloc / upload / download / launch / sync. The CUDA specifics (driver handles, NVRTC, the void** kernel-param ABI) stay in cuda.nu so a second backend (ROCm/HIP, OpenCL, a CPU fallback) can slot in behind the same names without touching callers.
Handles are small value structs (by-value is fine — they wrap opaque i64 device handles, no owned NURL heap state). Kernel arguments are passed as a Vec i of i64-encoded values built with the gpu_arg_ encoders; gpu_launch lays them out as the void* array CUDA expects.
& c @ nurl_f32_to_bits f32 x → if32 → its 32-bit IEEE-754 bit pattern (for scalar kernel args). The C param is float, so it must be declared f32 (not f/double) or the double NURL passes lands in the register wrong (alpha → 0).
: Gpu { i ordinal i dev i ctx } // an initialised device + context── handle types ──────────────────────────────────────────────────
: GpuKernel { i module i func } // a compiled, loaded __global__ fn: GpuBuffer { i dptr i bytes } // a device-memory allocation@ gpu_device_count → iNumber of CUDA-capable devices visible to the process.
@ gpu_open i ordinal → GpuInitialise the driver, bind device ordinal, and create a context. Returns a Gpu with ctx == 0 on failure.
@ gpu_ok Gpu g → b@ gpu_name Gpu g → sHuman-readable device name (e.g. "NVIDIA GeForce RTX 4090").
@ gpu_close Gpu g → v@ gpu_sync Gpu g → iBlock until all submitted work on the context completes. 0 == success.
@ gpu_compile Gpu g s src s name → GpuKernelCompile CUDA-C src at runtime (NVRTC) and load entry point name. The kernel must be declared extern "C" __global__. Returns a GpuKernel with func == 0 on a compile/load error (log on stderr).
@ gpu_kernel_ok GpuKernel k → b@ gpu_kernel_free GpuKernel k → v@ gpu_host_alloc i bytes → *u@ gpu_host_free *u buf → v@ gpu_host_set_f32 *u buf i idx f v → v@ gpu_host_get_f32 *u buf i idx → f@ gpu_host_set_i32 *u buf i idx i v → v@ gpu_host_get_i32 *u buf i idx → i@ gpu_alloc Gpu g i bytes → GpuBuffer@ gpu_free GpuBuffer b → v@ gpu_upload GpuBuffer dst *u host → iCopy the buffer's worth of bytes host → device. 0 == success.
@ gpu_download *u host GpuBuffer src → iCopy the buffer's worth of bytes device → host. 0 == success.
@ gpu_arg_buffer GpuBuffer b → i@ gpu_arg_i32 i v → i@ gpu_arg_i64 i v → i@ gpu_arg_f32 f v → i@ gpu_launch GpuKernel k i grid i block ( Vec i ) args → i── launch ──────────────────────────────────────────────────────── 1-D launch: grid blocks of block threads. args is the Vec built from gpu_arg_; its contiguous i64 backing IS the argument-value array, so we only build the pointer (void*) layer over it. 0 == success.
@ gpu_grid i n i block → iConvenience: ceil-divide for picking a grid size from N and block size.
packages/gpu/src/main.nu — packages/gpu PoC demo.
Proves GPU compute works from pure NURL: it lists the CUDA devices, compiles two CUDA-C kernels at runtime (NVRTC), runs them on the GPU over the neutral gpu.nu interface, and verifies the results against a CPU reference. No CUDA toolkit calls in the program — just gpu.nu.
gpu # run the demo on device 0 gpu <ordinal> # run on a specific device
Build: nurlpkg install gpu (or ./nurl.sh packages/gpu/src/main.nu)
@ k_vadd → s── kernels (CUDA-C, compiled at runtime) ─────────────────────────
@ k_saxpy → s@ approx f a f b → b── helpers ───────────────────────────────────────────────────────
@ say s msg → v@ demo_vadd Gpu g i n → i── demo: vector add c = a + b ───────────────────────────────────
@ demo_saxpy Gpu g i n → i── demo: SAXPY y = alpha*x + y (scalar float + int args) ───────
@ main → ipackages/gpu/src/cuda.nu — the CUDA backend for packages/gpu.
Binds the CUDA Driver API (libcuda, ships with the NVIDIA driver — no toolkit needed) and NVRTC (libnvrtc, runtime CUDA-C → PTX compilation) directly via NURL FFI. NO runtime.c bridge: the & \cuda\` / & \nvrtc\` symbols auto-link through nurl.sh ONLY when a program references them, so a GPU-less target (e.g. MILK-V Duo) never grows a CUDA dependency.
This file is the only place external GPU libraries are imported. The neutral surface in gpu.nu delegates here; a future backend (ROCm/HIP, OpenCL, a CPU fallback) would be a sibling file behind the same gpu.nu.
Handle model: every opaque CUDA handle (CUcontext / CUmodule / CUfunction) and every device address (CUdeviceptr, a u64) is carried as a plain i (i64) in NURL and cast to *u at the FFI boundary. Result codes (CUresult / nvrtcResult) are returned as i; 0 == success.
& cuda @ cuInit i32 flags → i32── CUDA Driver API ───────────────────────────────────────────────
& cuda @ cuDeviceGetCount *u count → i32& cuda @ cuDeviceGet *u device i32 ordinal → i32& cuda @ cuDeviceGetName *u name i32 len i32 dev → i32& cuda @ cuCtxCreate *u pctx i32 flags i32 dev → i32& cuda @ cuCtxDestroy *u ctx → i32& cuda @ cuCtxSynchronize → i32& cuda @ cuModuleLoadData *u module *u image → i32& cuda @ cuModuleUnload *u module → i32& cuda @ cuModuleGetFunction *u hfunc *u hmod s name → i32& cuda @ cuMemAlloc *u dptr i bytesize → i32& cuda @ cuMemFree i dptr → i32& cuda @ cuMemcpyHtoD i dst *u src i n → i32& cuda @ cuMemcpyDtoH *u dst i src i n → i32& cuda @ cuLaunchKernel *u f i32 gx i32 gy i32 gz i32 bx i32 by i32 bz i32 sh *u stream *u params *u extra → i32& cuda @ cuGetErrorName i32 err *u pstr → i32& nvrtc @ nvrtcCreateProgram *u prog s src s name i32 nh *u headers *u incs → i32── NVRTC (runtime CUDA-C → PTX) ──────────────────────────────────
& nvrtc @ nvrtcCompileProgram *u prog i32 nopt *u opts → i32& nvrtc @ nvrtcGetPTXSize *u prog *u sz → i32& nvrtc @ nvrtcGetPTX *u prog *u ptx → i32& nvrtc @ nvrtcGetProgramLogSize *u prog *u sz → i32& nvrtc @ nvrtcGetProgramLog *u prog *u log → i32& nvrtc @ nvrtcDestroyProgram *u prog → i32& c @ nurl_peek_f32 *u base i idx → f── typed 4-byte buffer accessors (runtime.c, always linked) ────── f32 / i32 arrays are the native GPU element layout; the stock 8-byte nurl_peek/poke can't address them at their natural stride.
& c @ nurl_poke_f32 *u base i idx f val → v& c @ nurl_peek_i32 *u base i idx → i& c @ nurl_poke_i32 *u base i idx i val → v@ cuda_init → i@ cuda_device_count → i@ cuda_device i ordinal → iDevice ordinal → CUdevice (itself a small int handle). -1 on failure.
@ cuda_device_name i dev → sCUdevice → device name string (borrowed copy into a fresh buffer).
@ cuda_ctx_create i dev → iCreate a context on a CUdevice → CUcontext handle (0 on failure).
@ cuda_ctx_destroy i ctx → i@ cuda_sync → i@ cuda_compile s src s name → *u── NVRTC: compile CUDA-C source → PTX text buffer ──────────────── Returns a NUL-terminated PTX buffer (caller passes to cuda_module_load), or 0 on a compile error after printing the NVRTC log to stderr.
@ cuda_module_load *u ptx → i── module / function ─────────────────────────────────────────────
@ cuda_module_unload i module → i@ cuda_function i module s name → i@ cuda_malloc i bytes → i── device memory ─────────────────────────────────────────────────
@ cuda_free i dptr → i@ cuda_htod i dptr *u host i bytes → i@ cuda_dtoh *u host i dptr i bytes → i@ cuda_launch i func i grid i block *u params → i── launch ──────────────────────────────────────────────────────── params is a void** array of pointers to the argument values (built by gpu.nu from a Vec of i64-encoded args). 1-D grid/block for now.
@ cuda_error_name i code → sCUresult code → driver error-name string (e.g. "CUDA_ERROR_INVALID_VALUE").