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    /// Whether a module of this name was loaded for this run.
276    pub fn has_module(&self, module_name: &str) -> bool {
277        self.raw_modules.contains_key(module_name)
278    }
279
280    pub fn raw_function_cached(
281        &self,
282        cache: &OnceLock<RawCudaFunc>,
283        module_name: &str,
284        func_name: &str,
285    ) -> Result<RawCudaFunc> {
286        if let Some(f) = cache.get() {
287            return Ok(*f);
288        }
289        let raw_mod = self
290            .raw_modules
291            .get(module_name)
292            .ok_or_else(|| AtlasError::ModuleLoad(format!("Module '{module_name}' not loaded")))?;
293        let c_name = CString::new(func_name).map_err(|e| {
294            AtlasError::ModuleLoad(format!("{module_name}::{func_name}: CString: {e}"))
295        })?;
296        let mut func: *mut c_void = std::ptr::null_mut();
297        let status =
298            // SAFETY: pointer cast handles the platform difference between
299            // `c_char = i8` (x86_64) and `c_char = u8` (aarch64); we use
300            // `.cast()` rather than `as *const i8` so clippy's
301            // `unnecessary_cast` is satisfied on x86_64 builds while the
302            // call still type-checks on aarch64 (Atlas's actual GB10 target).
303            unsafe { cuModuleGetFunction(&mut func, *raw_mod, c_name.as_ptr().cast()) };
304        if status != 0 {
305            return Err(AtlasError::ModuleLoad(format!(
306                "{module_name}::{func_name}: cuModuleGetFunction failed: {}",
307                cuda_error_text(status)
308            )));
309        }
310        let raw = RawCudaFunc(func);
311        let _ = cache.set(raw);
312        Ok(raw)
313    }
314
315    /// Retire the raw handles. Idempotent; `Drop` calls it.
316    ///
317    /// Since the raw map stopped being a second `cuModuleLoadData` of every
318    /// blob and became views into the cudarc-owned `modules`, there is
319    /// nothing to `cuModuleUnload` here — the `Arc<CudaModule>`s unload the
320    /// one real copy when they drop. Draining first keeps the invariant
321    /// that no raw handle survives its module: the maps are torn down
322    /// together, raw side first.
323    pub(crate) fn unload_raw(&mut self) -> Vec<String> {
324        self.raw_modules.drain().for_each(drop);
325        self.modules.drain().for_each(drop);
326        Vec::new()
327    }
328
329    /// Get the raw CUstream handle for Atlas's own stream.
330    pub fn raw_stream(&self) -> u64 {
331        self.host.stream.cu_stream() as u64
332    }
333
334    /// Resolve a `__device__` symbol in a loaded PTX module to its device
335    /// pointer + byte length. Required for drivers that read/write device
336    /// globals without launching a kernel (e.g. InnerQ calibration state).
337    /// `symbol` must be the linker-visible name — C++ namespace symbols are
338    /// Itanium-mangled (`_ZN7tq_plus14d_innerq_scaleE`).
339    pub fn device_symbol(&self, module_name: &str, symbol: &str) -> Result<(u64, usize)> {
340        let raw_mod = self
341            .raw_modules
342            .get(module_name)
343            .ok_or_else(|| AtlasError::ModuleLoad(format!("Module '{module_name}' not loaded")))?;
344        let c_sym = CString::new(symbol).map_err(|e| {
345            AtlasError::ModuleLoad(format!("{module_name}::{symbol}: CString: {e}"))
346        })?;
347        let mut dptr: u64 = 0;
348        let mut bytes: usize = 0;
349        let status =
350            unsafe { cuModuleGetGlobal_v2(&mut dptr, &mut bytes, *raw_mod, c_sym.as_ptr().cast()) };
351        if status != 0 {
352            return Err(AtlasError::ModuleLoad(format!(
353                "{module_name}::{symbol}: cuModuleGetGlobal_v2 failed: {}",
354                cuda_error_text(status)
355            )));
356        }
357        Ok((dptr, bytes))
358    }
359
360    /// Async H2D copy into a previously-resolved device pointer.
361    ///
362    /// # Safety
363    /// Caller must ensure `dst` is a valid device pointer and the bytes
364    /// pointed to by `src` outlive the copy (host buffers must persist
365    /// until the next sync on `stream`).
366    pub unsafe fn copy_h2d_async(
367        &self,
368        dst: u64,
369        src: *const c_void,
370        bytes: usize,
371        stream: u64,
372    ) -> Result<()> {
373        let status = unsafe { cuMemcpyHtoDAsync_v2(dst, src, bytes, stream) };
374        if status != 0 {
375            return Err(AtlasError::KernelLaunch(format!(
376                "cuMemcpyHtoDAsync_v2 failed: {}",
377                cuda_error_text(status)
378            )));
379        }
380        Ok(())
381    }
382
383    /// Async D2H copy from a device pointer. Same lifetime caveats as the
384    /// H2D variant.
385    ///
386    /// # Safety
387    /// Caller must keep `dst` alive until `stream` is synchronised.
388    pub unsafe fn copy_d2h_async(
389        &self,
390        dst: *mut c_void,
391        src: u64,
392        bytes: usize,
393        stream: u64,
394    ) -> Result<()> {
395        let status = unsafe { cuMemcpyDtoHAsync_v2(dst, src, bytes, stream) };
396        if status != 0 {
397            return Err(AtlasError::KernelLaunch(format!(
398                "cuMemcpyDtoHAsync_v2 failed: {}",
399                cuda_error_text(status)
400            )));
401        }
402        Ok(())
403    }
404
405    /// Block the calling thread until all prior work on `stream` completes.
406    pub fn stream_synchronize(&self, stream: u64) -> Result<()> {
407        let status = unsafe { cuStreamSynchronize(stream) };
408        if status != 0 {
409            return Err(AtlasError::KernelLaunch(format!(
410                "cuStreamSynchronize failed: {}",
411                cuda_error_text(status)
412            )));
413        }
414        Ok(())
415    }
416
417    /// Launch a kernel on a specified raw CUDA stream.
418    ///
419    /// When `stream_ptr` comes from the caller (e.g. `torch.cuda.current_stream().cuda_stream`),
420    /// this ensures kernels are captured during CUDA graph recording.
421    ///
422    /// # Safety
423    /// - `kernel_params` must contain valid pointers to arguments matching the kernel signature.
424    /// - `stream_ptr` must be a valid CUstream handle (or 0 to use Atlas's own stream).
425    /// - `raw_func` must be a valid CUfunction obtained from `raw_function_cached`.
426    pub unsafe fn launch_on_stream(
427        &self,
428        raw_func: RawCudaFunc,
429        cfg: LaunchConfig,
430        stream_ptr: u64,
431        kernel_params: &mut [*mut c_void],
432    ) -> Result<()> {
433        // Always use the caller's stream directly. When stream_ptr=0, CUDA
434        // treats it as the legacy default stream which has implicit
435        // synchronization with all other streams in the same context.
436        // Never fall back to Atlas's private stream — that breaks ordering
437        // with PyTorch operations and prevents CUDA graph capture.
438        let stream = stream_ptr;
439        // Opt in to >48KB dynamic shared memory when requested.
440        if cfg.shared_mem_bytes > 48 * 1024 {
441            const CU_FUNC_ATTRIBUTE_MAX_DYNAMIC_SHARED_SIZE_BYTES: i32 = 8;
442            let attr_status = unsafe {
443                cuFuncSetAttribute(
444                    raw_func.0,
445                    CU_FUNC_ATTRIBUTE_MAX_DYNAMIC_SHARED_SIZE_BYTES,
446                    cfg.shared_mem_bytes as i32,
447                )
448            };
449            if attr_status != 0 {
450                return Err(AtlasError::KernelLaunch(format!(
451                    "cuFuncSetAttribute(MAX_DYNAMIC_SHARED={}) failed: {}",
452                    cfg.shared_mem_bytes,
453                    cuda_error_text(attr_status)
454                )));
455            }
456        }
457        let status = unsafe {
458            cuLaunchKernel(
459                raw_func.0,
460                cfg.grid_dim.0,
461                cfg.grid_dim.1,
462                cfg.grid_dim.2,
463                cfg.block_dim.0,
464                cfg.block_dim.1,
465                cfg.block_dim.2,
466                cfg.shared_mem_bytes,
467                stream as *mut c_void,
468                kernel_params.as_mut_ptr(),
469                std::ptr::null_mut(),
470            )
471        };
472        if status != 0 {
473            return Err(AtlasError::KernelLaunch(format!(
474                "cuLaunchKernel failed: {} (grid=[{},{},{}], block=[{},{},{}], shared_mem={})",
475                cuda_error_text(status),
476                cfg.grid_dim.0,
477                cfg.grid_dim.1,
478                cfg.grid_dim.2,
479                cfg.block_dim.0,
480                cfg.block_dim.1,
481                cfg.block_dim.2,
482                cfg.shared_mem_bytes
483            )));
484        }
485        Ok(())
486    }
487}