1use 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
45static D2H_COUNT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
55
56fn d2h_trace_tick() {
57 use std::sync::atomic::Ordering;
58 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
85fn 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
97fn 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 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 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 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 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 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 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 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 #[cfg(atlas_scale)]
325 {
326 let _ = stream;
327 false
328 }
329 #[cfg(not(atlas_scale))]
330 {
331 let mut status: u32 = 0;
332 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 let site = std::panic::Location::caller();
370 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 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 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 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 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 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}