pub struct MetraleCudaBackend { /* private fields */ }Expand description
2026-09-25: The CUDA GpuBackend, holding one model’s kernel modules.
The modules unload when the last Arc<MetraleRegistry> handle drops
(MetraleRegistry’s Drop); this struct holds one of those handles.
Implementations§
Source§impl MetraleCudaBackend
impl MetraleCudaBackend
Sourcepub fn poison_redzones(&self, lo: usize, hi: usize) -> Result<()>
pub fn poison_redzones(&self, lo: usize, hi: usize) -> Result<()>
2026-09-25: Re-fill every guard band: 0xEE for zones whose creation
index is in [lo, hi), 0x00 for all the others. Only guard-band
bytes are written.
For bisecting a read past the end: narrowing [lo, hi) to the range
whose fill changes the output finds the allocation being read past.
Sourcepub fn scan_redzones(&self) -> Result<usize>
pub fn scan_redzones(&self) -> Result<usize>
2026-09-25: Read every guard band back and count the ones holding a
byte other than redzone_fill(). Each is logged at ERROR with its
creation index, size, the first and last changed pad offsets, how many
pad bytes changed, and the first changed value, and is then re-filled,
so a later scan reports only new writes.
Source§impl MetraleCudaBackend
impl MetraleCudaBackend
Sourcepub fn live_allocations(&self) -> Vec<(DevicePtr, usize, String)>
pub fn live_allocations(&self) -> Vec<(DevicePtr, usize, String)>
2026-10-02: Every live allocation: address, bytes and call site (file:line).
Sourcepub fn live_bytes(&self) -> usize
pub fn live_bytes(&self) -> usize
2026-09-25: Total bytes on the ledger.
Sourcepub fn chunk_slack_bytes(&self) -> usize
pub fn chunk_slack_bytes(&self) -> usize
2026-10-01: chunk_slack of the live allocations, at SMALL_ALLOC_CHUNK.
Sourcepub fn ledger_probe(&self) -> LedgerProbe
pub fn ledger_probe(&self) -> LedgerProbe
2026-10-01: A LedgerProbe on this backend’s ledger.
Sourcepub fn alloc_report(&self, top_n: usize, min_mb: usize) -> String
pub fn alloc_report(&self, top_n: usize, min_mb: usize) -> String
2026-09-25: A text report of the ledger: the total, then up to top_n
call sites of at least min_mb MiB, largest first, then (if any are
left) one line summing the other sites, then up to top_n source
files, largest first.
Source§impl MetraleCudaBackend
impl MetraleCudaBackend
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>
2026-09-25: Load ptx_modules on GPU ordinal and build the backend.
The module set (a TargetPtxSet’s modules, e.g. from
metrale_kernels::ptx_for_model) is loaded afresh for each call; the
CUDA context and stream are the process host’s, shared by every
backend (cuda_host::host). Fails if no context is current after the
load.
Sourcepub fn sweep_unreleased(&self) -> usize
pub fn sweep_unreleased(&self) -> usize
2026-09-25: Drain the ledger and free every allocation on it. Returns how many there were; when there were any, one warning gives their total size and the five largest allocating call sites.
TransformerModel::release_pools calls it last, after every
ModelResource::release. A freed pointer is already off the ledger
(free calls forget_alloc first), so nothing is freed twice.
pub fn registry(&self) -> &Arc<MetraleRegistry>
Trait Implementations§
Source§impl Drop for MetraleCudaBackend
2026-09-25: Dropping the backend runs sweep_unreleased.
impl Drop for MetraleCudaBackend
2026-09-25: Dropping the backend runs sweep_unreleased.
A load that fails part-way builds no model, so no teardown frees what it
had allocated; this does. After a model’s teardown the ledger is already
drained (TransformerModel::release_pools), and the sweep frees nothing.
Source§impl GpuBackend for MetraleCudaBackend
impl GpuBackend for MetraleCudaBackend
Source§fn alloc(&self, bytes: usize) -> Result<DevicePtr>
fn alloc(&self, bytes: usize) -> Result<DevicePtr>
bytes of device memory. Read moreSource§fn scan_redzones(&self) -> Result<usize>
fn scan_redzones(&self) -> Result<usize>
Ok(0) when METRALE_REDZONE is unset,
and on 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; nothing is allocated, moved or
resized. A no-op when METRALE_REDZONE is unset, and on every backend but
CUDA.Source§fn alloc_managed(&self, bytes: usize) -> Result<DevicePtr>
fn alloc_managed(&self, bytes: usize) -> Result<DevicePtr>
cuMemAllocManaged on CUDA, plain
alloc on Metal and the mock.Source§fn free(&self, ptr: DevicePtr) -> Result<()>
fn free(&self, ptr: DevicePtr) -> Result<()>
Source§fn live_bytes(&self) -> Option<usize>
fn live_bytes(&self) -> Option<usize>
Some on CUDA and the mock, None (the default) on Metal.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 (the
default) on every backend but CUDA.Source§fn chunk_slack_bytes(&self) -> Option<usize>
fn chunk_slack_bytes(&self) -> Option<usize>
cuda_backend::alloc_ledger::chunk_slack): Some on CUDA, None (the
default) on a backend without an allocation ledger.Source§fn live_allocations(&self) -> Option<Vec<(DevicePtr, usize, String)>>
fn live_allocations(&self) -> Option<Vec<(DevicePtr, usize, String)>>
file:line); None (the default) on every backend but CUDA.Source§fn sweep_unreleased(&self) -> usize
fn sweep_unreleased(&self) -> usize
Source§fn copy_h2d(&self, src: &[u8], dst: DevicePtr) -> Result<()>
fn copy_h2d(&self, src: &[u8], dst: DevicePtr) -> Result<()>
Source§fn copy_d2h(&self, src: DevicePtr, dst: &mut [u8]) -> Result<()>
fn copy_d2h(&self, src: DevicePtr, dst: &mut [u8]) -> Result<()>
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. copy_d2h orders only against the default stream; use this
to read what kernels on another stream just wrote. The CUDA backend
enqueues the copy on stream and waits; the default synchronises
stream and then calls copy_d2h.Source§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<()>
stream without waiting.
dst must stay valid, and must not be read or reused, until the next
synchronisation point on stream. On CUDA, copy_d2h and
copy_d2h_on_stream each wait inside the call, so this is the D2H to use
for many chunks followed by one synchronize. The default, used by Metal,
is the blocking copy_d2h; the mock copies at once and counts the call.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<()>
launch_typed.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. Check
it before a sync or D2H on a stream that may be capturing: those invalidate
the capture (CUDA_ERROR_STREAM_CAPTURE_INVALIDATED, 901). The CUDA
backend reports a failed query as capturing, and metrale_scale builds
always report false; the default is false.Source§fn synchronize(&self, stream: u64) -> Result<()>
fn synchronize(&self, stream: u64) -> Result<()>
stream has completed.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
KernelLaunch::launch synchronises after every
launch, so an asynchronous fault is reported at the kernel that caused it.
The CUDA backend reads METRALE_DEBUG_SYNC_KERNELS=1 once, when it is
built; the default is false.Source§fn kernel_registry(&self) -> Option<Arc<MetraleRegistry>>
fn kernel_registry(&self) -> Option<Arc<MetraleRegistry>>
None (the default) on every backend but CUDA.Source§fn kernel(&self, module: &str, func_name: &str) -> Result<KernelHandle>
fn kernel(&self, module: &str, func_name: &str) -> Result<KernelHandle>
Source§fn has_module(&self, module: &str) -> bool
fn has_module(&self, module: &str) -> bool
module is loaded in this backend. Ask before looking
up a kernel that only some targets carry: the CUDA backend’s kernel audit
records every failed lookup.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<()>
stream; src may be dropped or
overwritten as soon as 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<()>
stream for a source the caller keeps
alive: src must stay valid and unchanged until the next synchronisation
point on stream. The CUDA backend adds no synchronisation; the default
is copy_h2d_async.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<()>
stream. The default is
the blocking copy_d2d.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 src_pitch apart and destination rows dst_pitch apart. The
default loops copy_d2d_async per row; the CUDA backend issues one
cudaMemcpy2DAsync.Source§fn begin_capture(&self, stream: u64) -> Result<()>
fn begin_capture(&self, stream: u64) -> Result<()>
stream into a graph
(cuStreamBeginCapture on CUDA); captured work is recorded, not run. The
default does nothing.Source§fn end_capture(&self, stream: u64) -> Result<GraphHandle>
fn end_capture(&self, stream: u64) -> Result<GraphHandle>
GraphHandle(0).Source§fn abort_capture_if_active(&self, stream: u64)
fn abort_capture_if_active(&self, stream: u64)
stream and discard
the partial graph, so the stream runs work again. Call it on an error path
that left a begin_capture/end_capture region early. The default does
nothing.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<()>
stream on
CUDA. Metal and the mock write at once.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>
MemAvailable.Source§fn device_free_memory(&self) -> Result<usize>
fn device_free_memory(&self) -> Result<usize>
cuMemGetInfo on CUDA), never substituted by host memory. The default
is free_memory.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 bind_to_thread(&self) -> Result<()>
fn bind_to_thread(&self) -> Result<()>
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<()>
event at the current point of stream’s work.Source§fn stream_wait_event(&self, stream: u64, event: u64) -> Result<()>
fn stream_wait_event(&self, stream: u64, event: u64) -> Result<()>
stream wait for event on the device; the host does
not block.Source§fn event_synchronize(&self, event: u64) -> Result<()>
fn event_synchronize(&self, event: u64) -> Result<()>
event has
completed. Unlike synchronize(stream), it does not wait for work
enqueued after the event.Source§fn event_query(&self, event: u64) -> Result<bool>
fn event_query(&self, event: u64) -> Result<bool>
event has completed; never
blocks. The default returns true.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_v2 on CUDA), so a
kernel can write into host-visible memory. The default returns an error.Source§fn alloc_host_pinned(&self, bytes: usize) -> Result<*mut u8>
fn alloc_host_pinned(&self, bytes: usize) -> Result<*mut u8>
bytes of host memory for H2D staging: page-locked on
CUDA (cuMemAllocHost_v2), a shared MTLBuffer on Metal, and a 64-byte
aligned heap block by default. Release it with free_host_pinned. Read moreSource§fn free_host_pinned(&self, ptr: *mut u8, _bytes: usize) -> Result<()>
fn free_host_pinned(&self, ptr: *mut u8, _bytes: usize) -> Result<()>
alloc_host_pinned; bytes must be the
allocated size.Source§fn alloc_arena(&self, bytes: usize) -> Result<DevicePtr>
fn alloc_arena(&self, bytes: usize) -> Result<DevicePtr>
free_arena. The
CUDA backend allocates it off the allocation ledger, whose live_bytes
factory::build counts as this process’s own memory when it sizes the KV
cache. The default is the ledgered alloc.fn free_arena(&self, ptr: DevicePtr) -> Result<()>
Source§fn launch_typed(
&self,
func: KernelHandle,
grid: [u32; 3],
block: [u32; 3],
shared_mem: u32,
stream: u64,
args: &[KernelArg<'_>],
) -> Result<()>
fn launch_typed( &self, func: KernelHandle, grid: [u32; 3], block: [u32; 3], shared_mem: u32, stream: u64, args: &[KernelArg<'_>], ) -> Result<()>
Source§fn kernel_n_tile(&self, kernel: KernelHandle) -> Result<u32>
fn kernel_n_tile(&self, kernel: KernelHandle) -> Result<u32>
kernel publishes as the
extern "C" __device__ unsigned int <entry>_n_tile of its module, read when the
handle was resolved. A launch whose grid covers N in tiles must size it from this,
never from a width assumed at the call site: one entry name can carry different
tiles in different target trees. An error for a kernel that publishes none, a
zero width included.Source§fn kernel_casts_a_to_e4m3(&self, kernel: KernelHandle) -> bool
fn kernel_casts_a_to_e4m3(&self, kernel: KernelHandle) -> bool
kernel casts its BF16 A operand to E4M3 with no scale, as its
module declares with extern "C" __device__ unsigned int <entry>_a_e4m3. Values
past 448 saturate there; launchers check the range in debug builds
(ops::check_e4m3_activation_range in model-layers).