spark_runtime/cuda_backend/
gpu_impl.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! `impl GpuBackend for AtlasCudaBackend` — production CUDA backend trait body.
4//!
5//! ## Safety contract for the `unsafe { cu*(...) }` calls below
6//!
7//! Every unsafe block in this file wraps a single CUDA Driver API call.
8//! The invariants the driver requires are uniform:
9//!
10//! - **Context bound**: a CUDA primary context for the device is current
11//!   on the calling thread. `AtlasCudaBackend::new` binds it once via
12//!   `cuCtxSetCurrent`, and we never run on a thread that hasn't been
13//!   bound.
14//! - **Pointer provenance**: every `DevicePtr` came from a prior
15//!   successful `cuMemAlloc_v2` / `cuMemAllocHost_v2` /
16//!   `cuMemAllocManaged` and has not yet been freed. `DevicePtr(0)` is
17//!   treated as "not allocated" by callers.
18//! - **Sizes in bytes**: every `bytes: usize` argument is the exact
19//!   byte count of the allocation (callers compute it from typed
20//!   sizes); the driver does no bounds-checking.
21//! - **Stream / event lifetimes**: handles are owned by `Self` and
22//!   freed in `Drop` after `cuStreamSynchronize`, so they outlive every
23//!   in-flight launch that captured them.
24//! - **`extern "C"` ABI**: matches the cudarc-generated bindings used
25//!   in `super::*` imports; see `cudarc` for the full ABI surface.
26//!
27//! Per-site `// SAFETY:` comments are omitted because the contract is
28//! identical for every call. Anything that *deviates* from this
29//! contract gets a per-site `// SAFETY:` comment explaining the
30//! exception.
31
32use std::ffi::c_void;
33use std::sync::OnceLock;
34
35use anyhow::{Result, bail};
36use atlas_core::registry::{RawCudaFunc, cuda_error_text};
37use cudarc::driver::LaunchConfig;
38
39use super::{
40    AtlasCudaBackend, cuMemAlloc_v2, cuMemAllocManaged, cuMemFree_v2, cuMemGetInfo_v2,
41    cuMemcpyDtoDAsync_v2, cuMemcpyDtoHAsync_v2, cuMemcpyHtoDAsync_v2, cuStreamSynchronize,
42};
43use crate::gpu::{DevicePtr, GpuBackend, GraphHandle, KernelHandle};
44
45/// D2H call counter + one-shot caller identification
46/// (`ATLAS_D2H_TRACE=<N>`: log a backtrace on the Nth call, and the running
47/// count on every 10000th).
48///
49/// Every `copy_d2h*` below pairs its async copy with a `cuStreamSynchronize`,
50/// so each call BLOCKS the host until the GPU drains. An nsys trace of a 1K
51/// Laguna prefill counted 32,343 D2H + 32,533 syncs inside the prefill span,
52/// accounting for 212.8 ms of 306 ms of GPU starvation (58% idle). This exists
53/// to name whoever is issuing them.
54static D2H_COUNT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
55
56fn d2h_trace_tick() {
57    use std::sync::atomic::Ordering;
58    // ★ RESOLVED ONCE, AND CHECKED BEFORE THE COUNTER. This runs on EVERY D2H
59    // copy — the doc above counts 32,343 in one 1K prefill — and used to call
60    // `std::env::var` on each, which allocates and takes the process-wide env
61    // lock, serialising the copies against every other thread's getenv.
62    // `None` (the shipped state) makes this a single relaxed load; the counter
63    // sits below because nothing reads it when the trace is off. `unwrap_or(0)`
64    // keeps the original reading: SETTING the variable arms the 10,000th report.
65    static TARGET: std::sync::OnceLock<Option<u64>> = std::sync::OnceLock::new();
66    let Some(target) = *TARGET.get_or_init(|| {
67        std::env::var("ATLAS_D2H_TRACE")
68            .ok()
69            .map(|v| v.parse().unwrap_or(0))
70    }) else {
71        return;
72    };
73    let n = D2H_COUNT.fetch_add(1, Ordering::Relaxed) + 1;
74    if target != 0 && n == target {
75        tracing::warn!(
76            "ATLAS_D2H_TRACE: call #{n} backtrace:\n{}",
77            std::backtrace::Backtrace::force_capture()
78        );
79    }
80    if n.is_multiple_of(10_000) {
81        tracing::warn!("ATLAS_D2H_TRACE: {n} D2H copies so far (each forces a stream sync)");
82    }
83}
84
85/// Enqueue an H2D copy on `stream` and return without waiting. Shared by both
86/// async H2D entry points so the two differ ONLY in the ordering they add
87/// afterwards, never in the copy itself.
88fn h2d_enqueue(src: &[u8], dst: DevicePtr, stream: u64) -> Result<()> {
89    let status =
90        unsafe { cuMemcpyHtoDAsync_v2(dst.0, src.as_ptr() as *const c_void, src.len(), stream) };
91    if status != 0 {
92        bail!("cuMemcpyHtoDAsync_v2 failed: status {status}");
93    }
94    Ok(())
95}
96
97/// Say once, loudly, that a page-locked buffer reached the transient H2D path.
98///
99/// This is the tripwire the whole `pinned_hosts` registry exists to arm. It is a
100/// warning and not a `bail!` because the copy is still CORRECT — the sync above
101/// restores the guarantee — but it is a real, silent latency regression, and the
102/// call site almost certainly wants `copy_h2d_async_retained` instead.
103fn warn_pinned_transient_source() {
104    static ONCE: std::sync::Once = std::sync::Once::new();
105    ONCE.call_once(|| {
106        tracing::warn!(
107            "copy_h2d_async was handed a PAGE-LOCKED source. That copy is genuinely \
108             asynchronous, so the promise that the caller may drop the buffer on return \
109             is now being paid for with a cuStreamSynchronize on every such call. If the \
110             source outlives the next sync, switch the call site to \
111             copy_h2d_async_retained; if it does not, this sync is what keeps it from \
112             being a use-after-free."
113        );
114    });
115}
116
117impl GpuBackend for AtlasCudaBackend {
118    #[track_caller]
119    fn alloc(&self, bytes: usize) -> Result<DevicePtr> {
120        let site = std::panic::Location::caller();
121        let mut dptr: u64 = 0;
122        // A55 RED ZONE (`ATLAS_REDZONE=<bytes>`, default 0 = off). Over-allocate by `pad`
123        // and hand the caller the base, so the buffer it sees is unchanged and correctly
124        // aligned (cuMemAlloc is 256-byte aligned; padding the TAIL keeps that). The pad is
125        // poisoned at birth and read back by `scan_redzones`.
126        //
127        // This is the detector compute-sanitizer could not be: Atlas suballocates from pools,
128        // so an overrun that stays inside a pooled block is invisible to memcheck but lands
129        // squarely in a red zone here.
130        let seq = super::ALLOC_SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
131        let pad = if seq >= super::redzone_min_idx() {
132            super::redzone_bytes()
133        } else {
134            0
135        };
136        let status = unsafe { cuMemAlloc_v2(&mut dptr, bytes + pad) };
137        if status != 0 {
138            let mut free: usize = 0;
139            let mut total: usize = 0;
140            unsafe { cuMemGetInfo_v2(&mut free, &mut total) };
141            bail!(
142                "cuMemAlloc_v2 failed: status {status}, requested {bytes} bytes \
143                 (device reports {:.1} MB free / {:.1} GB total)",
144                free as f64 / (1024.0 * 1024.0),
145                total as f64 / (1024.0 * 1024.0 * 1024.0),
146            );
147        }
148        if pad > 0 {
149            let st = unsafe {
150                super::cuMemsetD8Async(dptr + bytes as u64, super::redzone_fill(), pad, 0)
151            };
152            if st != 0 {
153                bail!("ATLAS_REDZONE: poisoning the guard band failed: status {st}");
154            }
155            self.record_redzone(dptr, bytes, pad, seq);
156            // One line per guarded allocation, in creation order. The bisection reports an
157            // INDEX; this is what turns that index into a buffer you can name by its size,
158            // and `ATLAS_REDZONE_TRACE_IDX` adds the call site for the one that matters.
159            tracing::info!("redzone: alloc#{seq} bytes={bytes} ptr={dptr:#x}");
160            if super::redzone_trace_idx() == Some(seq) {
161                tracing::error!(
162                    "redzone: alloc#{seq} bytes={bytes} backtrace:\n{}",
163                    std::backtrace::Backtrace::force_capture()
164                );
165            }
166        }
167        self.record_alloc(DevicePtr(dptr), bytes, site);
168        // Large-allocation tracing for memory attribution (GB10 unified
169        // memory: every cuMemAlloc consumes host RAM, and a runtime alloc
170        // outside the util pledge is how the box ends up in swap). Debug
171        // level so production INFO stays quiet; RUST_LOG=spark_runtime=debug
172        // turns the trail on.
173        if bytes >= 32 * 1024 * 1024 {
174            tracing::debug!(
175                "alloc {:.1} MB (device ptr {dptr:#x})",
176                bytes as f64 / (1024.0 * 1024.0)
177            );
178        }
179        Ok(DevicePtr(dptr))
180    }
181
182    fn scan_redzones(&self) -> Result<usize> {
183        if super::redzone_bytes() == 0 {
184            return Ok(0);
185        }
186        AtlasCudaBackend::scan_redzones(self)
187    }
188
189    fn poison_redzones(&self, lo: usize, hi: usize) -> Result<()> {
190        if super::redzone_bytes() == 0 {
191            return Ok(());
192        }
193        AtlasCudaBackend::poison_redzones(self, lo, hi)
194    }
195
196    #[track_caller]
197    fn alloc_managed(&self, bytes: usize) -> Result<DevicePtr> {
198        let site = std::panic::Location::caller();
199        let mut dptr: u64 = 0;
200        const CU_MEM_ATTACH_GLOBAL: u32 = 0x1;
201        let status = unsafe { cuMemAllocManaged(&mut dptr, bytes, CU_MEM_ATTACH_GLOBAL) };
202        if status != 0 {
203            bail!(
204                "cuMemAllocManaged failed: status {status}, requested {bytes} bytes. \
205                 Check system swap space: swapon --show"
206            );
207        }
208        self.record_alloc(DevicePtr(dptr), bytes, site);
209        Ok(DevicePtr(dptr))
210    }
211
212    fn free(&self, ptr: DevicePtr) -> Result<()> {
213        if ptr.is_null() {
214            return Ok(());
215        }
216        // Off the ledger BEFORE the free: an entry that survives a successful
217        // free would be double-freed at teardown.
218        self.forget_alloc(ptr);
219        if super::redzone_bytes() > 0 {
220            self.forget_redzone(ptr.0);
221        }
222        let status = unsafe { cuMemFree_v2(ptr.0) };
223        // A context that is already being destroyed reports every free as
224        // failing, and at process exit that is the normal case, not an error:
225        // the driver has reclaimed the allocation by definition. Two other
226        // free paths in this crate already consult `is_teardown_noop`; this one
227        // did not, so wiring `Model::teardown` into shutdown turned a benign
228        // status 4 into `ERROR model teardown reported a failure` on every
229        // clean exit — the exact species of false alarm this work set out to
230        // remove.
231        if status != 0 && !atlas_core::registry::is_teardown_noop(status) {
232            bail!("cuMemFree_v2 failed: status {status}, ptr {ptr}");
233        }
234        Ok(())
235    }
236
237    fn live_bytes(&self) -> Option<usize> {
238        Some(AtlasCudaBackend::live_bytes(self))
239    }
240
241    fn alloc_report(&self, top_n: usize, min_mb: usize) -> Option<String> {
242        Some(AtlasCudaBackend::alloc_report(self, top_n, min_mb))
243    }
244
245    fn sweep_unreleased(&self) -> usize {
246        AtlasCudaBackend::sweep_unreleased(self)
247    }
248
249    fn copy_h2d(&self, src: &[u8], dst: DevicePtr) -> Result<()> {
250        AtlasCudaBackend::copy_h2d_impl(self, src, dst)
251    }
252
253    fn copy_d2h(&self, src: DevicePtr, dst: &mut [u8]) -> Result<()> {
254        d2h_trace_tick();
255        AtlasCudaBackend::copy_d2h_impl(self, src, dst)
256    }
257
258    fn copy_d2h_on_stream(&self, src: DevicePtr, dst: &mut [u8], stream: u64) -> Result<()> {
259        d2h_trace_tick();
260        AtlasCudaBackend::copy_d2h_on_stream_impl(self, src, dst, stream)
261    }
262
263    fn copy_d2h_async(&self, src: DevicePtr, dst: &mut [u8], stream: u64) -> Result<()> {
264        // Deliberately NO cuStreamSynchronize — that is the entire point.
265        // `copy_d2h`/`copy_d2h_on_stream` drain the stream inside every call,
266        // so a multi-chunk gather pays one full drain per chunk (the SSM spill's
267        // 60 chunks × 66 MB measured ~400 ms = ~165 MB/s, vs ~28 ms for the
268        // async H2D scatter of the same bytes). The caller MUST issue exactly
269        // one `synchronize(stream)` before touching `dst`.
270        d2h_trace_tick();
271        let status = unsafe {
272            cuMemcpyDtoHAsync_v2(dst.as_mut_ptr() as *mut c_void, src.0, dst.len(), stream)
273        };
274        if status != 0 {
275            bail!("cuMemcpyDtoHAsync_v2 (async) failed: status {status}");
276        }
277        Ok(())
278    }
279
280    fn copy_d2d(&self, src: DevicePtr, dst: DevicePtr, bytes: usize) -> Result<()> {
281        if crate::launch_trace::on() {
282            crate::launch_trace::record(crate::launch_trace::Entry {
283                kind: "d2d",
284                func: 0,
285                grid: [0, 0, 0],
286                block: [0, 0, 0],
287                smem: 0,
288                args: vec![src.0, dst.0, bytes as u64],
289            });
290        }
291        AtlasCudaBackend::copy_d2d_impl(self, src, dst, bytes)
292    }
293
294    fn launch(
295        &self,
296        func: KernelHandle,
297        grid: [u32; 3],
298        block: [u32; 3],
299        shared_mem: u32,
300        stream: u64,
301        params: &mut [*mut c_void],
302    ) -> Result<()> {
303        let raw_func = RawCudaFunc(func.0 as *mut c_void);
304        let cfg = LaunchConfig {
305            grid_dim: (grid[0], grid[1], grid[2]),
306            block_dim: (block[0], block[1], block[2]),
307            shared_mem_bytes: shared_mem,
308        };
309        let registry = self.registry();
310        unsafe { registry.launch_on_stream(raw_func, cfg, stream, params) }.map_err(|e| {
311            // A launch failure may have destroyed the CUDA context. Probe and
312            // latch before the error is flattened into a string and bubbled —
313            // see `fault_probe`. This does not change control flow: the caller
314            // still receives its error either way.
315            super::fault_probe::note_failure("kernel launch", &e.to_string());
316            anyhow::anyhow!("Kernel launch failed: {e}")
317        })
318    }
319
320    fn stream_is_capturing(&self, stream: u64) -> bool {
321        // SCALE's libcuda does not export cuStreamIsCapturing; report
322        // not-capturing there (gfx1151 telemetry taps then sample eagerly —
323        // acceptable for a default-off measurement knob).
324        #[cfg(atlas_scale)]
325        {
326            let _ = stream;
327            false
328        }
329        #[cfg(not(atlas_scale))]
330        {
331            let mut status: u32 = 0;
332            // CU_STREAM_CAPTURE_STATUS_NONE = 0; treat query failure as
333            // capturing (conservative: the tap skips its sample).
334            let rc = unsafe { super::cuStreamIsCapturing(stream, &mut status) };
335            rc != 0 || status != 0
336        }
337    }
338
339    fn synchronize(&self, stream: u64) -> Result<()> {
340        let status = unsafe { cuStreamSynchronize(stream) };
341        if status != 0 {
342            bail!("cuStreamSynchronize failed: {}", cuda_error_text(status));
343        }
344        Ok(())
345    }
346
347    fn default_stream(&self) -> u64 {
348        self.default_stream
349    }
350
351    fn op_cache(&self) -> &crate::op_cache::OpCache {
352        &self.op_cache
353    }
354
355    fn debug_sync_kernels(&self) -> bool {
356        AtlasCudaBackend::debug_sync_kernels(self)
357    }
358
359    fn kernel_registry(&self) -> Option<std::sync::Arc<atlas_core::registry::AtlasRegistry>> {
360        Some(self.registry().clone())
361    }
362
363    #[track_caller]
364    fn kernel(&self, module: &str, func_name: &str) -> Result<KernelHandle> {
365        // The DISPATCH SITE, not this line: `#[track_caller]` here and on the
366        // trait declaration carries the `.kernel(…)` / `try_kernel(…)` caller's
367        // `file:line` through, which is the only part of an unresolved-lookup
368        // report an operator can act on.
369        let site = std::panic::Location::caller();
370        // Ephemeral OnceLock — no cross-call caching, but kernel() is only
371        // called at model init time. Layers store the returned KernelHandle.
372        let cache: OnceLock<RawCudaFunc> = OnceLock::new();
373        let registry = self.registry();
374        match registry.raw_function_cached(&cache, module, func_name) {
375            Ok(raw) => {
376                crate::kernel_audit::record(module, func_name, true, site);
377                crate::launch_trace::name_kernel(raw.0 as u64, module, func_name);
378                Ok(KernelHandle(raw.0 as u64))
379            }
380            Err(e) => {
381                // Optional kernels (try_kernel) land here and fall back silently;
382                // the audit makes that visible in the startup kernel table.
383                crate::kernel_audit::record(module, func_name, false, site);
384                Err(anyhow::anyhow!("Kernel lookup {module}::{func_name}: {e}"))
385            }
386        }
387    }
388
389    fn has_module(&self, module: &str) -> bool {
390        self.registry().has_module(module)
391    }
392
393    fn copy_h2d_async(&self, src: &[u8], dst: DevicePtr, stream: u64) -> Result<()> {
394        h2d_enqueue(src, dst, stream)?;
395        // The trait promises the caller may drop `src` right now. From PAGEABLE
396        // memory the driver already made that true by staging the bytes before
397        // returning. From PAGE-LOCKED memory it did not — the DMA engine reads
398        // these pages after the enqueue — so buy the same guarantee with an
399        // explicit wait rather than let ~90 call sites that drop their source
400        // immediately turn into use-after-frees the day a buffer gets pinned.
401        //
402        // Costs nothing on the path everything takes today: no Atlas call site
403        // reaches here with a pinned source (the ones that own pinned staging
404        // use `copy_h2d_async_retained`), so `is_pinned` is a lock-free-ish read
405        // of a three-entry table that says "no".
406        if crate::pinned_hosts::is_pinned(src) {
407            warn_pinned_transient_source();
408            let sync = unsafe { cuStreamSynchronize(stream) };
409            if sync != 0 {
410                bail!(
411                    "cuStreamSynchronize after pinned-source H2D failed: {}",
412                    cuda_error_text(sync)
413                );
414            }
415        }
416        Ok(())
417    }
418
419    fn copy_h2d_async_retained(&self, src: &[u8], dst: DevicePtr, stream: u64) -> Result<()> {
420        // The caller has promised `src` outlives the next sync on `stream`, so
421        // no implicit ordering is added — that is the whole reason this variant
422        // exists (a 60-chunk pinned scatter must not pay 60 stream drains).
423        h2d_enqueue(src, dst, stream)
424    }
425
426    fn copy_d2d_async(
427        &self,
428        src: DevicePtr,
429        dst: DevicePtr,
430        bytes: usize,
431        stream: u64,
432    ) -> Result<()> {
433        let status = unsafe { cuMemcpyDtoDAsync_v2(dst.0, src.0, bytes, stream) };
434        if status != 0 {
435            // See copy_d2d_impl: on 901 the backtrace names the reporter,
436            // which bounds where the capture-poisoning op ran.
437            tracing::error!(
438                "copy_d2d_async failed (status {status}) at:\n{}",
439                std::backtrace::Backtrace::force_capture()
440            );
441            bail!("cuMemcpyDtoDAsync_v2 (copy_d2d_async) failed: status {status}");
442        }
443        Ok(())
444    }
445
446    fn copy_d2d_2d_async(
447        &self,
448        src: DevicePtr,
449        src_pitch: usize,
450        dst: DevicePtr,
451        dst_pitch: usize,
452        width_bytes: usize,
453        height: usize,
454        stream: u64,
455    ) -> Result<()> {
456        // One pitched copy (cudaMemcpyDeviceToDevice = 3) on the caller's stream,
457        // replacing a per-row copy_d2d_async loop. cudart is linked (cutlass/
458        // flashinfer use the runtime API); a CUstream handle is a valid
459        // cudaStream_t.
460        unsafe extern "C" {
461            fn cudaMemcpy2DAsync(
462                dst: *mut c_void,
463                dpitch: usize,
464                src: *const c_void,
465                spitch: usize,
466                width: usize,
467                height: usize,
468                kind: i32,
469                stream: u64,
470            ) -> i32;
471        }
472        let status = unsafe {
473            cudaMemcpy2DAsync(
474                dst.0 as *mut c_void,
475                dst_pitch,
476                src.0 as *const c_void,
477                src_pitch,
478                width_bytes,
479                height,
480                3,
481                stream,
482            )
483        };
484        if status != 0 {
485            bail!("cudaMemcpy2DAsync failed: status {status}");
486        }
487        Ok(())
488    }
489
490    fn begin_capture(&self, stream: u64) -> Result<()> {
491        self.begin_capture_cu(stream)
492    }
493    fn end_capture(&self, stream: u64) -> Result<GraphHandle> {
494        self.end_capture_cu(stream)
495    }
496
497    fn abort_capture_if_active(&self, stream: u64) {
498        self.abort_capture_if_active_cu(stream)
499    }
500
501    fn launch_graph(&self, graph: GraphHandle, stream: u64) -> Result<()> {
502        self.launch_graph_cu(graph, stream)
503    }
504    fn destroy_graph(&self, graph: GraphHandle) -> Result<()> {
505        self.destroy_graph_cu(graph)
506    }
507    fn memset(&self, ptr: DevicePtr, value: u8, bytes: usize) -> Result<()> {
508        self.memset_cu(ptr, value, bytes)
509    }
510    fn memset_async(&self, ptr: DevicePtr, value: u8, bytes: usize, stream: u64) -> Result<()> {
511        if crate::launch_trace::on() {
512            crate::launch_trace::record(crate::launch_trace::Entry {
513                kind: "memset",
514                func: 0,
515                grid: [0, 0, 0],
516                block: [0, 0, 0],
517                smem: 0,
518                args: vec![ptr.0, value as u64, bytes as u64],
519            });
520        }
521        self.memset_async_cu(ptr, value, bytes, stream)
522    }
523    fn total_memory(&self) -> Result<usize> {
524        self.total_memory_cu()
525    }
526    fn free_memory(&self) -> Result<usize> {
527        self.free_memory_cu()
528    }
529    fn device_free_memory(&self) -> Result<usize> {
530        self.device_free_memory_cu()
531    }
532    fn live_alloc_count(&self) -> usize {
533        self.live_alloc_len()
534    }
535    fn sm_count(&self) -> Result<u32> {
536        self.sm_count_cu()
537    }
538    fn create_stream(&self) -> Result<u64> {
539        self.create_stream_cu()
540    }
541    fn bind_to_thread(&self) -> Result<()> {
542        self.bind_to_thread_cu()
543    }
544    fn create_event(&self) -> Result<u64> {
545        self.create_event_cu()
546    }
547    fn record_event(&self, event: u64, stream: u64) -> Result<()> {
548        self.record_event_cu(event, stream)
549    }
550    fn stream_wait_event(&self, stream: u64, event: u64) -> Result<()> {
551        self.stream_wait_event_cu(stream, event)
552    }
553    fn event_synchronize(&self, event: u64) -> Result<()> {
554        self.event_synchronize_cu(event)
555    }
556    fn destroy_event(&self, event: u64) -> Result<()> {
557        self.destroy_event_cu(event)
558    }
559    fn host_ptr_to_device(&self, host: *mut u8) -> Result<DevicePtr> {
560        let mut dptr: u64 = 0;
561        let status =
562            unsafe { super::cuMemHostGetDevicePointer_v2(&mut dptr, host as *mut c_void, 0) };
563        if status != 0 {
564            bail!("cuMemHostGetDevicePointer_v2 failed: status {status}");
565        }
566        Ok(DevicePtr(dptr))
567    }
568
569    fn alloc_host_pinned(&self, bytes: usize) -> Result<*mut u8> {
570        if bytes >= 32 * 1024 * 1024 {
571            tracing::debug!(
572                "alloc_host_pinned {:.1} MB",
573                bytes as f64 / (1024.0 * 1024.0)
574            );
575        }
576        self.alloc_host_pinned_cu(bytes)
577    }
578    fn free_host_pinned(&self, ptr: *mut u8, _bytes: usize) -> Result<()> {
579        self.free_host_pinned_cu(ptr, _bytes)
580    }
581}