atlas_core/
registry.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! Global kernel registry — load PTX once, cache modules/functions/streams.
4//!
5//! Eliminates ~0.06-0.26ms overhead per kernel call from:
6//! - CudaContext::new (driver init)
7//! - CudaContext::load_module (PTX JIT compilation)
8//! - CudaContext::new_stream (stream creation)
9//! - cuModuleGetFunction (function lookup) — now cached after first call
10//!
11//! Usage:
12//!   let reg = AtlasRegistry::get_or_init(ordinal, &[("gemm", PTX_SRC), ...])?;
13//!   let func = reg.function("gemm", "dense_gemm_tc_bf16")?;
14//!   unsafe { reg.stream.launch_builder(&func).arg(&ptr).launch(cfg)?; }
15//!   reg.stream.synchronize()?;
16
17use std::collections::HashMap;
18use std::ffi::{CString, c_void};
19use std::sync::{Arc, OnceLock};
20
21use cudarc::driver::{CudaContext, CudaFunction, CudaModule, CudaStream, LaunchConfig};
22use cudarc::nvrtc::Ptx;
23
24pub use crate::cuda_host::{CudaHost, host, release};
25use crate::error::{AtlasError, Result};
26
27// Raw CUDA driver API. (`cuModuleLoadData`/`cuModuleUnload` left this list
28// when the raw handles became views into the cudarc-loaded modules — the
29// registry no longer loads or unloads anything through the raw API.)
30unsafe extern "C" {
31    fn cuModuleGetFunction(hfunc: *mut *mut c_void, hmod: *mut c_void, name: *const i8) -> i32;
32    fn cuLaunchKernel(
33        f: *mut c_void,
34        gridDimX: u32,
35        gridDimY: u32,
36        gridDimZ: u32,
37        blockDimX: u32,
38        blockDimY: u32,
39        blockDimZ: u32,
40        sharedMemBytes: u32,
41        hStream: *mut c_void,
42        kernelParams: *mut *mut c_void,
43        extra: *mut *mut c_void,
44    ) -> i32;
45    fn cuFuncSetAttribute(hfunc: *mut c_void, attrib: i32, value: i32) -> i32;
46    fn cuGetErrorName(error: i32, pStr: *mut *const i8) -> i32;
47    fn cuGetErrorString(error: i32, pStr: *mut *const i8) -> i32;
48    // Resolve a `__device__` symbol in a loaded CUmodule into a device pointer
49    // + size in bytes. Used by drivers that need to read/write device globals
50    // (e.g. InnerQ calibration state) without round-tripping through a kernel.
51    fn cuModuleGetGlobal_v2(
52        dptr: *mut u64,
53        bytes: *mut usize,
54        hmod: *mut c_void,
55        name: *const i8,
56    ) -> i32;
57    fn cuMemcpyHtoDAsync_v2(dst: u64, src: *const c_void, bytes: usize, stream: u64) -> i32;
58    fn cuMemcpyDtoHAsync_v2(dst: *mut c_void, src: u64, bytes: usize, stream: u64) -> i32;
59    fn cuStreamSynchronize(stream: u64) -> i32;
60}
61
62/// Resolve a CUresult status code into `"<NAME>: <description>"` via
63/// cuGetErrorName + cuGetErrorString. Returns "CUDA_UNKNOWN" / "(no message)"
64/// if the driver doesn't recognize the code.
65pub fn cuda_error_text(status: i32) -> String {
66    use std::ffi::CStr;
67    let mut name_ptr: *const i8 = std::ptr::null();
68    let mut msg_ptr: *const i8 = std::ptr::null();
69    let name = unsafe {
70        if cuGetErrorName(status, &mut name_ptr) == 0 && !name_ptr.is_null() {
71            CStr::from_ptr(name_ptr as *const std::os::raw::c_char)
72                .to_string_lossy()
73                .into_owned()
74        } else {
75            "CUDA_UNKNOWN".to_string()
76        }
77    };
78    let msg = unsafe {
79        if cuGetErrorString(status, &mut msg_ptr) == 0 && !msg_ptr.is_null() {
80            CStr::from_ptr(msg_ptr as *const std::os::raw::c_char)
81                .to_string_lossy()
82                .into_owned()
83        } else {
84            "(no message)".to_string()
85        }
86    };
87    format!("{name} ({status}): {msg}")
88}
89
90/// `CUDA_ERROR_DEINITIALIZED`. The driver tears the primary context down in its
91/// own `atexit` handler, which can run before our `Drop` impls do. Every
92/// module unload and every host free then reports this code.
93///
94/// It is **not a failure**: a module cannot leak out of a context that no
95/// longer exists, and the memory it occupied went with it. Reporting 158 of
96/// them at exit is pure noise that buries anything real.
97pub const CUDA_ERROR_DEINITIALIZED: i32 = 4;
98
99/// Whether a CUresult means "the context is already gone, nothing to do".
100///
101/// Also covers `CUDA_ERROR_INVALID_CONTEXT` (201) and
102/// `CUDA_ERROR_CONTEXT_IS_DESTROYED` (709), which arrive by the same route
103/// depending on how far the driver got before we ran.
104pub fn is_teardown_noop(status: i32) -> bool {
105    matches!(status, CUDA_ERROR_DEINITIALIZED | 201 | 709)
106}
107
108/// Wrapper for raw CUfunction handle (Send+Sync safe — handles are context-wide).
109#[derive(Clone, Copy)]
110pub struct RawCudaFunc(pub *mut c_void);
111// SAFETY: CUfunction handles returned by `cuModuleGetFunction` remain valid
112// for the lifetime of the owning CUcontext (the Atlas registry binds the
113// process-wide context once at startup and never destroys it). The handle
114// itself is opaque metadata — actual kernel launches go through cuLaunchKernel
115// with caller-supplied stream synchronisation, so `Sync` does not imply
116// concurrent execution, only concurrent reads of an immutable pointer.
117unsafe impl Send for RawCudaFunc {}
118unsafe impl Sync for RawCudaFunc {}
119
120/// The PTX/CUBIN modules for **one** loaded model.
121///
122/// Model-scoped: the blob set comes from `atlas_kernels::ptx_for_model`, so it
123/// changes with the checkpoint. Previously this was fused into a process
124/// `OnceLock` singleton whose `get_or_init(ordinal, kernel_blobs)` silently
125/// discarded the second caller's blobs — a swapped-in model would have run the
126/// *previous* model's kernels with no error at all.
127///
128/// Obtain one with [`AtlasRegistry::load`] and propagate it (`Arc<AtlasRegistry>`);
129/// there is deliberately no global accessor. Dropping the last handle unloads
130/// the modules.
131pub struct AtlasRegistry {
132    host: Arc<CudaHost>,
133    modules: HashMap<&'static str, Arc<CudaModule>>,
134    /// Raw CUmodule handles for direct cuLaunchKernel access.
135    raw_modules: HashMap<&'static str, *mut c_void>,
136}
137
138impl Drop for AtlasRegistry {
139    /// Unloads this model's modules. Reached when the last `Arc` handle goes,
140    /// which — because there is no global accessor — happens exactly when the
141    /// owning run ends.
142    fn drop(&mut self) {
143        let failures = self.unload_raw();
144        if !failures.is_empty() {
145            // No `tracing` in atlas-core's dependency budget, and a `Drop` has
146            // nowhere to return an error to. `release` below is the path that
147            // reports properly; this is the backstop.
148            eprintln!(
149                "atlas: {} module(s) failed to unload: {}",
150                failures.len(),
151                failures.join("; ")
152            );
153        }
154    }
155}
156
157// SAFETY: Same rationale as `RawCudaFunc`: the `raw_modules` map holds
158// CUmodule handles obtained at startup from a single CUcontext. The map is
159// populated once during registry init and is read-only from that point on,
160// so concurrent reads are race-free at the Rust level. CUDA itself
161// serializes kernel launches via the stream the caller supplies — this impl
162// only asserts that the *handle metadata* is shareable across threads.
163unsafe impl Send for AtlasRegistry {}
164unsafe impl Sync for AtlasRegistry {}
165
166impl AtlasRegistry {
167    /// Load this model's kernel modules into the process CUDA context.
168    ///
169    /// Each call produces a fresh, independent module set; nothing is shared
170    /// with a previously loaded model except the context and stream.
171    pub fn load(
172        ordinal: usize,
173        kernel_blobs: &[(&'static str, &'static [u8])],
174    ) -> Result<Arc<Self>> {
175        Ok(Arc::new(Self::init(host(ordinal)?, kernel_blobs)?))
176    }
177
178    /// The process CUDA context this registry's modules live in.
179    pub fn host(&self) -> &Arc<CudaHost> {
180        &self.host
181    }
182
183    pub fn ctx(&self) -> &Arc<CudaContext> {
184        &self.host.ctx
185    }
186
187    pub fn stream(&self) -> &Arc<CudaStream> {
188        &self.host.stream
189    }
190
191    /// Module names this registry loaded, for diagnostics.
192    pub fn module_names(&self) -> impl Iterator<Item = &'static str> + '_ {
193        self.modules.keys().copied()
194    }
195
196    fn init(
197        host: Arc<CudaHost>,
198        kernel_blobs: &[(&'static str, &'static [u8])],
199    ) -> Result<AtlasRegistry> {
200        let ctx = &host.ctx;
201
202        let mut modules = HashMap::new();
203        let mut raw_modules = HashMap::new();
204        for &(name, blob) in kernel_blobs {
205            // NVIDIA emits PTX (ASCII text); SCALE/AMD (gfx1151) and HIP
206            // emit a binary code object (ELF / clang offload bundle).
207            // `cuModuleLoadData` accepts either, but PTX must arrive
208            // NUL-terminated (the driver JIT parses it as a C string)
209            // while a binary object is self-describing. Sniff per blob.
210            let is_binary = blob.starts_with(b"\x7fELF")
211                || blob.starts_with(b"__CLANG_OFFLOAD_BUNDLE__")
212                || std::str::from_utf8(&blob[..blob.len().min(64)]).is_err();
213
214            // Load via cudarc (safe API) — backs `function()` lookups.
215            let ptx = if is_binary {
216                Ptx::from_binary(blob.to_vec())
217            } else {
218                let src = std::str::from_utf8(blob).map_err(|e| {
219                    AtlasError::ModuleLoad(format!("{name}: PTX not valid UTF-8: {e}"))
220                })?;
221                Ptx::from_src(src)
222            };
223            let module = ctx
224                .load_module(ptx)
225                .map_err(|e| AtlasError::ModuleLoad(format!("{name}: {e}")))?;
226
227            // The raw handle for launch_on_stream (which avoids cudarc's
228            // struct layouts) is the SAME module: derive it instead of
229            // JIT-compiling the blob a second time through
230            // `cuModuleLoadData`. The double load kept a second copy of
231            // every module's SASS resident and doubled driver-JIT time at
232            // boot for the entire kernel set. Lifetime: the handle is owned
233            // by the `Arc<CudaModule>` stored right beside it — `modules`
234            // and `raw_modules` live and die together in this struct, and
235            // `unload_raw` no longer unloads (cudarc's `Drop` does).
236            raw_modules.insert(name, module.cu_module_raw() as *mut c_void);
237            modules.insert(name, module);
238        }
239
240        Ok(AtlasRegistry {
241            host,
242            modules,
243            raw_modules,
244        })
245    }
246
247    /// Look up a cached function handle (cudarc safe API).
248    pub fn function(&self, module_name: &str, func_name: &str) -> Result<CudaFunction> {
249        let module = self
250            .modules
251            .get(module_name)
252            .ok_or_else(|| AtlasError::ModuleLoad(format!("Module '{module_name}' not loaded")))?;
253        module
254            .load_function(func_name)
255            .map_err(|e| AtlasError::ModuleLoad(format!("{module_name}::{func_name}: {e}")))
256    }
257
258    /// Look up a function handle with OnceLock caching (cudarc safe API).
259    pub fn function_cached(
260        &self,
261        cache: &OnceLock<CudaFunction>,
262        module_name: &str,
263        func_name: &str,
264    ) -> Result<CudaFunction> {
265        if let Some(f) = cache.get() {
266            return Ok(f.clone());
267        }
268        let func = self.function(module_name, func_name)?;
269        let _ = cache.set(func.clone());
270        Ok(func)
271    }
272
273    /// Look up a raw CUfunction handle with OnceLock caching.
274    /// Uses the raw CUDA driver API — no cudarc struct layout dependency.
275    pub fn raw_function_cached(
276        &self,
277        cache: &OnceLock<RawCudaFunc>,
278        module_name: &str,
279        func_name: &str,
280    ) -> Result<RawCudaFunc> {
281        if let Some(f) = cache.get() {
282            return Ok(*f);
283        }
284        let raw_mod = self
285            .raw_modules
286            .get(module_name)
287            .ok_or_else(|| AtlasError::ModuleLoad(format!("Module '{module_name}' not loaded")))?;
288        let c_name = CString::new(func_name).map_err(|e| {
289            AtlasError::ModuleLoad(format!("{module_name}::{func_name}: CString: {e}"))
290        })?;
291        let mut func: *mut c_void = std::ptr::null_mut();
292        let status =
293            // SAFETY: pointer cast handles the platform difference between
294            // `c_char = i8` (x86_64) and `c_char = u8` (aarch64); we use
295            // `.cast()` rather than `as *const i8` so clippy's
296            // `unnecessary_cast` is satisfied on x86_64 builds while the
297            // call still type-checks on aarch64 (Atlas's actual GB10 target).
298            unsafe { cuModuleGetFunction(&mut func, *raw_mod, c_name.as_ptr().cast()) };
299        if status != 0 {
300            return Err(AtlasError::ModuleLoad(format!(
301                "{module_name}::{func_name}: cuModuleGetFunction failed: {}",
302                cuda_error_text(status)
303            )));
304        }
305        let raw = RawCudaFunc(func);
306        let _ = cache.set(raw);
307        Ok(raw)
308    }
309
310    /// Retire the raw handles. Idempotent; `Drop` calls it.
311    ///
312    /// Since the raw map stopped being a second `cuModuleLoadData` of every
313    /// blob and became views into the cudarc-owned `modules`, there is
314    /// nothing to `cuModuleUnload` here — the `Arc<CudaModule>`s unload the
315    /// one real copy when they drop. Draining first keeps the invariant
316    /// that no raw handle survives its module: the maps are torn down
317    /// together, raw side first.
318    pub(crate) fn unload_raw(&mut self) -> Vec<String> {
319        self.raw_modules.drain().for_each(drop);
320        self.modules.drain().for_each(drop);
321        Vec::new()
322    }
323
324    /// Get the raw CUstream handle for Atlas's own stream.
325    pub fn raw_stream(&self) -> u64 {
326        self.host.stream.cu_stream() as u64
327    }
328
329    /// Resolve a `__device__` symbol in a loaded PTX module to its device
330    /// pointer + byte length. Required for drivers that read/write device
331    /// globals without launching a kernel (e.g. InnerQ calibration state).
332    /// `symbol` must be the linker-visible name — C++ namespace symbols are
333    /// Itanium-mangled (`_ZN7tq_plus14d_innerq_scaleE`).
334    pub fn device_symbol(&self, module_name: &str, symbol: &str) -> Result<(u64, usize)> {
335        let raw_mod = self
336            .raw_modules
337            .get(module_name)
338            .ok_or_else(|| AtlasError::ModuleLoad(format!("Module '{module_name}' not loaded")))?;
339        let c_sym = CString::new(symbol).map_err(|e| {
340            AtlasError::ModuleLoad(format!("{module_name}::{symbol}: CString: {e}"))
341        })?;
342        let mut dptr: u64 = 0;
343        let mut bytes: usize = 0;
344        let status =
345            unsafe { cuModuleGetGlobal_v2(&mut dptr, &mut bytes, *raw_mod, c_sym.as_ptr().cast()) };
346        if status != 0 {
347            return Err(AtlasError::ModuleLoad(format!(
348                "{module_name}::{symbol}: cuModuleGetGlobal_v2 failed: {}",
349                cuda_error_text(status)
350            )));
351        }
352        Ok((dptr, bytes))
353    }
354
355    /// Async H2D copy into a previously-resolved device pointer.
356    ///
357    /// # Safety
358    /// Caller must ensure `dst` is a valid device pointer and the bytes
359    /// pointed to by `src` outlive the copy (host buffers must persist
360    /// until the next sync on `stream`).
361    pub unsafe fn copy_h2d_async(
362        &self,
363        dst: u64,
364        src: *const c_void,
365        bytes: usize,
366        stream: u64,
367    ) -> Result<()> {
368        let status = unsafe { cuMemcpyHtoDAsync_v2(dst, src, bytes, stream) };
369        if status != 0 {
370            return Err(AtlasError::KernelLaunch(format!(
371                "cuMemcpyHtoDAsync_v2 failed: {}",
372                cuda_error_text(status)
373            )));
374        }
375        Ok(())
376    }
377
378    /// Async D2H copy from a device pointer. Same lifetime caveats as the
379    /// H2D variant.
380    ///
381    /// # Safety
382    /// Caller must keep `dst` alive until `stream` is synchronised.
383    pub unsafe fn copy_d2h_async(
384        &self,
385        dst: *mut c_void,
386        src: u64,
387        bytes: usize,
388        stream: u64,
389    ) -> Result<()> {
390        let status = unsafe { cuMemcpyDtoHAsync_v2(dst, src, bytes, stream) };
391        if status != 0 {
392            return Err(AtlasError::KernelLaunch(format!(
393                "cuMemcpyDtoHAsync_v2 failed: {}",
394                cuda_error_text(status)
395            )));
396        }
397        Ok(())
398    }
399
400    /// Block the calling thread until all prior work on `stream` completes.
401    pub fn stream_synchronize(&self, stream: u64) -> Result<()> {
402        let status = unsafe { cuStreamSynchronize(stream) };
403        if status != 0 {
404            return Err(AtlasError::KernelLaunch(format!(
405                "cuStreamSynchronize failed: {}",
406                cuda_error_text(status)
407            )));
408        }
409        Ok(())
410    }
411
412    /// Launch a kernel on a specified raw CUDA stream.
413    ///
414    /// When `stream_ptr` comes from the caller (e.g. `torch.cuda.current_stream().cuda_stream`),
415    /// this ensures kernels are captured during CUDA graph recording.
416    ///
417    /// # Safety
418    /// - `kernel_params` must contain valid pointers to arguments matching the kernel signature.
419    /// - `stream_ptr` must be a valid CUstream handle (or 0 to use Atlas's own stream).
420    /// - `raw_func` must be a valid CUfunction obtained from `raw_function_cached`.
421    pub unsafe fn launch_on_stream(
422        &self,
423        raw_func: RawCudaFunc,
424        cfg: LaunchConfig,
425        stream_ptr: u64,
426        kernel_params: &mut [*mut c_void],
427    ) -> Result<()> {
428        // Always use the caller's stream directly. When stream_ptr=0, CUDA
429        // treats it as the legacy default stream which has implicit
430        // synchronization with all other streams in the same context.
431        // Never fall back to Atlas's private stream — that breaks ordering
432        // with PyTorch operations and prevents CUDA graph capture.
433        let stream = stream_ptr;
434        // Opt in to >48KB dynamic shared memory when requested.
435        if cfg.shared_mem_bytes > 48 * 1024 {
436            const CU_FUNC_ATTRIBUTE_MAX_DYNAMIC_SHARED_SIZE_BYTES: i32 = 8;
437            let attr_status = unsafe {
438                cuFuncSetAttribute(
439                    raw_func.0,
440                    CU_FUNC_ATTRIBUTE_MAX_DYNAMIC_SHARED_SIZE_BYTES,
441                    cfg.shared_mem_bytes as i32,
442                )
443            };
444            if attr_status != 0 {
445                return Err(AtlasError::KernelLaunch(format!(
446                    "cuFuncSetAttribute(MAX_DYNAMIC_SHARED={}) failed: {}",
447                    cfg.shared_mem_bytes,
448                    cuda_error_text(attr_status)
449                )));
450            }
451        }
452        let status = unsafe {
453            cuLaunchKernel(
454                raw_func.0,
455                cfg.grid_dim.0,
456                cfg.grid_dim.1,
457                cfg.grid_dim.2,
458                cfg.block_dim.0,
459                cfg.block_dim.1,
460                cfg.block_dim.2,
461                cfg.shared_mem_bytes,
462                stream as *mut c_void,
463                kernel_params.as_mut_ptr(),
464                std::ptr::null_mut(),
465            )
466        };
467        if status != 0 {
468            return Err(AtlasError::KernelLaunch(format!(
469                "cuLaunchKernel failed: {} (grid=[{},{},{}], block=[{},{},{}], shared_mem={})",
470                cuda_error_text(status),
471                cfg.grid_dim.0,
472                cfg.grid_dim.1,
473                cfg.grid_dim.2,
474                cfg.block_dim.0,
475                cfg.block_dim.1,
476                cfg.block_dim.2,
477                cfg.shared_mem_bytes
478            )));
479        }
480        Ok(())
481    }
482}