pub struct AtlasCudaBackend { /* private fields */ }Expand description
Production GPU backend wrapping AtlasRegistry + raw CUDA driver API.
Owns this model’s kernel modules. The registry used to be a process
singleton reached through AtlasRegistry::get(); it is now loaded per model
and propagated from here, so a swapped-in model cannot run the previous
model’s kernels. Dropping the last backend unloads them.
Implementations§
Source§impl AtlasCudaBackend
impl AtlasCudaBackend
Sourcepub fn live_bytes(&self) -> usize
pub fn live_bytes(&self) -> usize
Total live device bytes this backend has allocated and not freed.
Sourcepub fn alloc_report(&self, top_n: usize, min_mb: usize) -> String
pub fn alloc_report(&self, top_n: usize, min_mb: usize) -> String
Human-readable attribution of live device memory, biggest site first.
Aggregated by allocating call site rather than by pointer: one site
looping over 48 SSM layers is one line reading 9.7 GB across 48
allocations, which is the shape that makes an over-sized pool obvious.
Sites below min_mb are folded into a remainder line so the report
stays readable while still summing to the true total.
Source§impl AtlasCudaBackend
impl AtlasCudaBackend
Sourcepub fn new(
ordinal: usize,
ptx_modules: &[(&'static str, &'static [u8])],
) -> Result<Self>
pub fn new( ordinal: usize, ptx_modules: &[(&'static str, &'static [u8])], ) -> Result<Self>
Initialize the CUDA backend on the given GPU ordinal.
Loads the provided PTX modules for THIS model. Use
atlas_kernels::ptx_for_model() or ptx_modules() to obtain the
correct module set. Each call produces an independent module set — the
CUDA context and stream are shared, nothing else is.
Sourcepub fn poison_redzones(&self, lo: usize, hi: usize) -> Result<()>
pub fn poison_redzones(&self, lo: usize, hi: usize) -> Result<()>
Re-poison every guard band: 0xEE for zones whose creation index is in [lo, hi),
0x00 for all the others.
The BISECTION half of the A55 red-zone hunt. The zones themselves never move, so
every call leaves the device heap byte-for-byte identical and only the CONTENTS of the
guard bands change — which is exactly the variable the read detector proved matters.
Narrowing [lo, hi) until the completion flips names the allocation being read past.
Sourcepub fn scan_redzones(&self) -> Result<usize>
pub fn scan_redzones(&self) -> Result<usize>
Read every guard band back and report the ones that no longer hold the fill byte.
Returns the number of violated zones. Each violation is logged with the allocation’s
creation index, its size, and the first byte of the pad that changed — the size is
what identifies the buffer (cross-reference the arena sizes in BufferSizes), and the
offset is how far past the end the writer reached.
RE-FILLS every violated zone before returning, so a repeat offender is reported once per scan rather than once and then forever.
Sourcepub fn sweep_unreleased(&self) -> usize
pub fn sweep_unreleased(&self) -> usize
Free every allocation this backend made and nobody released.
The backstop for allocations no ModelResource covers — chiefly the
loaders’ fused weights, which are owned by layer structs rather than by
any pool. Returns how many were reclaimed; since 2026-08-19 the ledger
also carries each one’s size and call site, so the sweep can say how
many BYTES had no owner and name the sites they came from instead of
only counting them. A non-zero count after a clean teardown is a leak,
and the log line now points at the code that made it.
Runs LAST in teardown, after every ModelResource::release, so it only
ever sees what those missed — and each free here has already been
removed from the ledger by forget_alloc, so it cannot double-free.
pub fn registry(&self) -> &Arc<AtlasRegistry>
Trait Implementations§
Source§impl Drop for AtlasCudaBackend
Last-resort reclamation for a backend that never reached model teardown.
impl Drop for AtlasCudaBackend
Last-resort reclamation for a backend that never reached model teardown.
A load that FAILS part-way leaves whatever it had already allocated on the
ledger, and no Model is ever built to tear down. On a hot-swap that memory
is not merely leaked, it is actively harmful: the outgoing model is already
gone, and the restore then loads into a budget the dead attempt is still
holding. That is not hypothetical — a 35B swap failed at kernel selection
and the 27B restore died with “only 14.08 GB remains but 17.38 GB is
needed”, leaving the server with no model at all.
On the normal path this frees nothing: Model::teardown drains the ledger
first, so the sweep finds an empty set. Freeing here is the safe case
described in atlas_core::scope — nothing is allocating against a backend
that is being dropped.
Source§impl GpuBackend for AtlasCudaBackend
impl GpuBackend for AtlasCudaBackend
Source§fn scan_redzones(&self) -> Result<usize>
fn scan_redzones(&self) -> Result<usize>
Ok(0) when ATLAS_REDZONE is
unset or the backend has no red zones — every backend but CUDA.Source§fn poison_redzones(&self, lo: usize, hi: usize) -> Result<()>
fn poison_redzones(&self, lo: usize, hi: usize) -> Result<()>
[lo, hi) with 0xEE and the rest with 0x00.
Layout-preserving by construction — nothing is allocated, moved or resized.Source§fn alloc_managed(&self, bytes: usize) -> Result<DevicePtr>
fn alloc_managed(&self, bytes: usize) -> Result<DevicePtr>
Source§fn live_bytes(&self) -> Option<usize>
fn live_bytes(&self) -> Option<usize>
None for backends with no ledger (mock/CPU).Source§fn alloc_report(&self, top_n: usize, min_mb: usize) -> Option<String>
fn alloc_report(&self, top_n: usize, min_mb: usize) -> Option<String>
None for backends with no ledger.Source§fn sweep_unreleased(&self) -> usize
fn sweep_unreleased(&self) -> usize
Source§fn copy_d2h_on_stream(
&self,
src: DevicePtr,
dst: &mut [u8],
stream: u64,
) -> Result<()>
fn copy_d2h_on_stream( &self, src: DevicePtr, dst: &mut [u8], stream: u64, ) -> Result<()>
stream. Read moreSource§fn copy_d2h_async(
&self,
src: DevicePtr,
dst: &mut [u8],
stream: u64,
) -> Result<()>
fn copy_d2h_async( &self, src: DevicePtr, dst: &mut [u8], stream: u64, ) -> Result<()>
Source§fn copy_d2d(&self, src: DevicePtr, dst: DevicePtr, bytes: usize) -> Result<()>
fn copy_d2d(&self, src: DevicePtr, dst: DevicePtr, bytes: usize) -> Result<()>
Source§fn launch(
&self,
func: KernelHandle,
grid: [u32; 3],
block: [u32; 3],
shared_mem: u32,
stream: u64,
params: &mut [*mut c_void],
) -> Result<()>
fn launch( &self, func: KernelHandle, grid: [u32; 3], block: [u32; 3], shared_mem: u32, stream: u64, params: &mut [*mut c_void], ) -> Result<()>
Source§fn stream_is_capturing(&self, stream: u64) -> bool
fn stream_is_capturing(&self, stream: u64) -> bool
stream is inside an active CUDA-graph capture. Telemetry
taps MUST check this before any sync/D2H on a potentially-captured
stream — those calls invalidate the capture (CUDA 901) and wedge the
serve. Default false (backends without capture, or without a query
API, never capture through this trait’s eager paths).Source§fn synchronize(&self, stream: u64) -> Result<()>
fn synchronize(&self, stream: u64) -> Result<()>
Source§fn default_stream(&self) -> u64
fn default_stream(&self) -> u64
Source§fn op_cache(&self) -> &OpCache
fn op_cache(&self) -> &OpCache
Source§fn debug_sync_kernels(&self) -> bool
fn debug_sync_kernels(&self) -> bool
getenv.Source§fn kernel_registry(&self) -> Option<Arc<AtlasRegistry>>
fn kernel_registry(&self) -> Option<Arc<AtlasRegistry>>
__device__ symbol, for instance. None on backends that have no such
concept, which is why it is an accessor rather than a downcast.Source§fn kernel(&self, module: &str, func_name: &str) -> Result<KernelHandle>
fn kernel(&self, module: &str, func_name: &str) -> Result<KernelHandle>
Source§fn copy_h2d_async(&self, src: &[u8], dst: DevicePtr, stream: u64) -> Result<()>
fn copy_h2d_async(&self, src: &[u8], dst: DevicePtr, stream: u64) -> Result<()>
src may be dropped or overwritten the
moment this returns. Read moreSource§fn copy_h2d_async_retained(
&self,
src: &[u8],
dst: DevicePtr,
stream: u64,
) -> Result<()>
fn copy_h2d_async_retained( &self, src: &[u8], dst: DevicePtr, stream: u64, ) -> Result<()>
Source§fn copy_d2d_async(
&self,
src: DevicePtr,
dst: DevicePtr,
bytes: usize,
stream: u64,
) -> Result<()>
fn copy_d2d_async( &self, src: DevicePtr, dst: DevicePtr, bytes: usize, stream: u64, ) -> Result<()>
Source§fn copy_d2d_2d_async(
&self,
src: DevicePtr,
src_pitch: usize,
dst: DevicePtr,
dst_pitch: usize,
width_bytes: usize,
height: usize,
stream: u64,
) -> Result<()>
fn copy_d2d_2d_async( &self, src: DevicePtr, src_pitch: usize, dst: DevicePtr, dst_pitch: usize, width_bytes: usize, height: usize, stream: u64, ) -> Result<()>
height rows of
width_bytes, source rows spaced by src_pitch, dest rows by
dst_pitch. Default = per-row copy_d2d_async loop; the CUDA backend
overrides with ONE cudaMemcpy2DAsync (replaces the per-token Z-copy
loop = up to num_tokens×num_ssm_layers launches/forward).Source§fn begin_capture(&self, stream: u64) -> Result<()>
fn begin_capture(&self, stream: u64) -> Result<()>
stream into a graph. Read moreSource§fn end_capture(&self, stream: u64) -> Result<GraphHandle>
fn end_capture(&self, stream: u64) -> Result<GraphHandle>
Source§fn abort_capture_if_active(&self, stream: u64)
fn abort_capture_if_active(&self, stream: u64)
stream is mid graph-capture, end that capture so the
stream returns to normal mode (discarding any partial graph). Call this
on an error path that unwound out of a begin_capture/end_capture
region (e.g. a fold refuse bailed mid-capture) — otherwise the stream is
left recording and every subsequent op fails with
STREAM_CAPTURE_UNSUPPORTED, bricking the server. No-op if not capturing.Source§fn launch_graph(&self, graph: GraphHandle, stream: u64) -> Result<()>
fn launch_graph(&self, graph: GraphHandle, stream: u64) -> Result<()>
stream.Source§fn destroy_graph(&self, graph: GraphHandle) -> Result<()>
fn destroy_graph(&self, graph: GraphHandle) -> Result<()>
Source§fn memset(&self, ptr: DevicePtr, value: u8, bytes: usize) -> Result<()>
fn memset(&self, ptr: DevicePtr, value: u8, bytes: usize) -> Result<()>
Source§fn memset_async(
&self,
ptr: DevicePtr,
value: u8,
bytes: usize,
stream: u64,
) -> Result<()>
fn memset_async( &self, ptr: DevicePtr, value: u8, bytes: usize, stream: u64, ) -> Result<()>
Source§fn total_memory(&self) -> Result<usize>
fn total_memory(&self) -> Result<usize>
Source§fn free_memory(&self) -> Result<usize>
fn free_memory(&self) -> Result<usize>
Source§fn device_free_memory(&self) -> Result<usize>
fn device_free_memory(&self) -> Result<usize>
Source§fn live_alloc_count(&self) -> usize
fn live_alloc_count(&self) -> usize
Source§fn sm_count(&self) -> Result<u32>
fn sm_count(&self) -> Result<u32>
Source§fn create_stream(&self) -> Result<u64>
fn create_stream(&self) -> Result<u64>
Source§fn create_event(&self) -> Result<u64>
fn create_event(&self) -> Result<u64>
Source§fn record_event(&self, event: u64, stream: u64) -> Result<()>
fn record_event(&self, event: u64, stream: u64) -> Result<()>
Source§fn stream_wait_event(&self, stream: u64, event: u64) -> Result<()>
fn stream_wait_event(&self, stream: u64, event: u64) -> Result<()>
Source§fn event_synchronize(&self, event: u64) -> Result<()>
fn event_synchronize(&self, event: u64) -> Result<()>
record_event, then event_synchronize
right before the host dereferences the destination pinned buffer.
Cheaper than synchronize(stream) when the stream has work beyond
the event you care about: this only waits for the recorded point,
not for everything subsequently enqueued.Source§fn host_ptr_to_device(&self, host: *mut u8) -> Result<DevicePtr>
fn host_ptr_to_device(&self, host: *mut u8) -> Result<DevicePtr>
Self::alloc_host_pinned (cuMemHostGetDevicePointer). On UMA parts
(GB10) this lets a KERNEL write results directly into host-visible
memory, eliminating the copy-engine op for tiny readbacks entirely.
Default: unsupported.