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 let n = D2H_COUNT.fetch_add(1, Ordering::Relaxed) + 1;
59 let Ok(target) = std::env::var("ATLAS_D2H_TRACE") else {
60 return;
61 };
62 let target: u64 = target.parse().unwrap_or(0);
63 if target != 0 && n == target {
64 tracing::warn!(
65 "ATLAS_D2H_TRACE: call #{n} backtrace:\n{}",
66 std::backtrace::Backtrace::force_capture()
67 );
68 }
69 if n.is_multiple_of(10_000) {
70 tracing::warn!("ATLAS_D2H_TRACE: {n} D2H copies so far (each forces a stream sync)");
71 }
72}
73
74fn h2d_enqueue(src: &[u8], dst: DevicePtr, stream: u64) -> Result<()> {
78 let status =
79 unsafe { cuMemcpyHtoDAsync_v2(dst.0, src.as_ptr() as *const c_void, src.len(), stream) };
80 if status != 0 {
81 bail!("cuMemcpyHtoDAsync_v2 failed: status {status}");
82 }
83 Ok(())
84}
85
86fn warn_pinned_transient_source() {
93 static ONCE: std::sync::Once = std::sync::Once::new();
94 ONCE.call_once(|| {
95 tracing::warn!(
96 "copy_h2d_async was handed a PAGE-LOCKED source. That copy is genuinely \
97 asynchronous, so the promise that the caller may drop the buffer on return \
98 is now being paid for with a cuStreamSynchronize on every such call. If the \
99 source outlives the next sync, switch the call site to \
100 copy_h2d_async_retained; if it does not, this sync is what keeps it from \
101 being a use-after-free."
102 );
103 });
104}
105
106impl GpuBackend for AtlasCudaBackend {
107 #[track_caller]
108 fn alloc(&self, bytes: usize) -> Result<DevicePtr> {
109 let site = std::panic::Location::caller();
110 let mut dptr: u64 = 0;
111 let seq = super::ALLOC_SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
120 let pad = if seq >= super::redzone_min_idx() {
121 super::redzone_bytes()
122 } else {
123 0
124 };
125 let status = unsafe { cuMemAlloc_v2(&mut dptr, bytes + pad) };
126 if status != 0 {
127 let mut free: usize = 0;
128 let mut total: usize = 0;
129 unsafe { cuMemGetInfo_v2(&mut free, &mut total) };
130 bail!(
131 "cuMemAlloc_v2 failed: status {status}, requested {bytes} bytes \
132 (device reports {:.1} MB free / {:.1} GB total)",
133 free as f64 / (1024.0 * 1024.0),
134 total as f64 / (1024.0 * 1024.0 * 1024.0),
135 );
136 }
137 if pad > 0 {
138 let st = unsafe {
139 super::cuMemsetD8Async(dptr + bytes as u64, super::redzone_fill(), pad, 0)
140 };
141 if st != 0 {
142 bail!("ATLAS_REDZONE: poisoning the guard band failed: status {st}");
143 }
144 self.record_redzone(dptr, bytes, pad, seq);
145 tracing::info!("redzone: alloc#{seq} bytes={bytes} ptr={dptr:#x}");
149 if super::redzone_trace_idx() == Some(seq) {
150 tracing::error!(
151 "redzone: alloc#{seq} bytes={bytes} backtrace:\n{}",
152 std::backtrace::Backtrace::force_capture()
153 );
154 }
155 }
156 self.record_alloc(DevicePtr(dptr), bytes, site);
157 if bytes >= 32 * 1024 * 1024 {
163 tracing::debug!(
164 "alloc {:.1} MB (device ptr {dptr:#x})",
165 bytes as f64 / (1024.0 * 1024.0)
166 );
167 }
168 Ok(DevicePtr(dptr))
169 }
170
171 fn scan_redzones(&self) -> Result<usize> {
172 if super::redzone_bytes() == 0 {
173 return Ok(0);
174 }
175 AtlasCudaBackend::scan_redzones(self)
176 }
177
178 fn poison_redzones(&self, lo: usize, hi: usize) -> Result<()> {
179 if super::redzone_bytes() == 0 {
180 return Ok(());
181 }
182 AtlasCudaBackend::poison_redzones(self, lo, hi)
183 }
184
185 #[track_caller]
186 fn alloc_managed(&self, bytes: usize) -> Result<DevicePtr> {
187 let site = std::panic::Location::caller();
188 let mut dptr: u64 = 0;
189 const CU_MEM_ATTACH_GLOBAL: u32 = 0x1;
190 let status = unsafe { cuMemAllocManaged(&mut dptr, bytes, CU_MEM_ATTACH_GLOBAL) };
191 if status != 0 {
192 bail!(
193 "cuMemAllocManaged failed: status {status}, requested {bytes} bytes. \
194 Check system swap space: swapon --show"
195 );
196 }
197 self.record_alloc(DevicePtr(dptr), bytes, site);
198 Ok(DevicePtr(dptr))
199 }
200
201 fn free(&self, ptr: DevicePtr) -> Result<()> {
202 if ptr.is_null() {
203 return Ok(());
204 }
205 self.forget_alloc(ptr);
208 if super::redzone_bytes() > 0 {
209 self.forget_redzone(ptr.0);
210 }
211 let status = unsafe { cuMemFree_v2(ptr.0) };
212 if status != 0 && !atlas_core::registry::is_teardown_noop(status) {
221 bail!("cuMemFree_v2 failed: status {status}, ptr {ptr}");
222 }
223 Ok(())
224 }
225
226 fn live_bytes(&self) -> Option<usize> {
227 Some(AtlasCudaBackend::live_bytes(self))
228 }
229
230 fn alloc_report(&self, top_n: usize, min_mb: usize) -> Option<String> {
231 Some(AtlasCudaBackend::alloc_report(self, top_n, min_mb))
232 }
233
234 fn sweep_unreleased(&self) -> usize {
235 AtlasCudaBackend::sweep_unreleased(self)
236 }
237
238 fn copy_h2d(&self, src: &[u8], dst: DevicePtr) -> Result<()> {
239 AtlasCudaBackend::copy_h2d_impl(self, src, dst)
240 }
241
242 fn copy_d2h(&self, src: DevicePtr, dst: &mut [u8]) -> Result<()> {
243 d2h_trace_tick();
244 AtlasCudaBackend::copy_d2h_impl(self, src, dst)
245 }
246
247 fn copy_d2h_on_stream(&self, src: DevicePtr, dst: &mut [u8], stream: u64) -> Result<()> {
248 d2h_trace_tick();
249 AtlasCudaBackend::copy_d2h_on_stream_impl(self, src, dst, stream)
250 }
251
252 fn copy_d2h_async(&self, src: DevicePtr, dst: &mut [u8], stream: u64) -> Result<()> {
253 d2h_trace_tick();
260 let status = unsafe {
261 cuMemcpyDtoHAsync_v2(dst.as_mut_ptr() as *mut c_void, src.0, dst.len(), stream)
262 };
263 if status != 0 {
264 bail!("cuMemcpyDtoHAsync_v2 (async) failed: status {status}");
265 }
266 Ok(())
267 }
268
269 fn copy_d2d(&self, src: DevicePtr, dst: DevicePtr, bytes: usize) -> Result<()> {
270 if crate::launch_trace::on() {
271 crate::launch_trace::record(crate::launch_trace::Entry {
272 kind: "d2d",
273 func: 0,
274 grid: [0, 0, 0],
275 block: [0, 0, 0],
276 smem: 0,
277 args: vec![src.0, dst.0, bytes as u64],
278 });
279 }
280 AtlasCudaBackend::copy_d2d_impl(self, src, dst, bytes)
281 }
282
283 fn launch(
284 &self,
285 func: KernelHandle,
286 grid: [u32; 3],
287 block: [u32; 3],
288 shared_mem: u32,
289 stream: u64,
290 params: &mut [*mut c_void],
291 ) -> Result<()> {
292 let raw_func = RawCudaFunc(func.0 as *mut c_void);
293 let cfg = LaunchConfig {
294 grid_dim: (grid[0], grid[1], grid[2]),
295 block_dim: (block[0], block[1], block[2]),
296 shared_mem_bytes: shared_mem,
297 };
298 let registry = self.registry();
299 unsafe { registry.launch_on_stream(raw_func, cfg, stream, params) }.map_err(|e| {
300 super::fault_probe::note_failure("kernel launch", &e.to_string());
305 anyhow::anyhow!("Kernel launch failed: {e}")
306 })
307 }
308
309 fn stream_is_capturing(&self, stream: u64) -> bool {
310 #[cfg(atlas_scale)]
314 {
315 let _ = stream;
316 false
317 }
318 #[cfg(not(atlas_scale))]
319 {
320 let mut status: u32 = 0;
321 let rc = unsafe { super::cuStreamIsCapturing(stream, &mut status) };
324 rc != 0 || status != 0
325 }
326 }
327
328 fn synchronize(&self, stream: u64) -> Result<()> {
329 let status = unsafe { cuStreamSynchronize(stream) };
330 if status != 0 {
331 bail!("cuStreamSynchronize failed: {}", cuda_error_text(status));
332 }
333 Ok(())
334 }
335
336 fn default_stream(&self) -> u64 {
337 self.default_stream
338 }
339
340 fn op_cache(&self) -> &crate::op_cache::OpCache {
341 &self.op_cache
342 }
343
344 fn debug_sync_kernels(&self) -> bool {
345 AtlasCudaBackend::debug_sync_kernels(self)
346 }
347
348 fn kernel_registry(&self) -> Option<std::sync::Arc<atlas_core::registry::AtlasRegistry>> {
349 Some(self.registry().clone())
350 }
351
352 #[track_caller]
353 fn kernel(&self, module: &str, func_name: &str) -> Result<KernelHandle> {
354 let site = std::panic::Location::caller();
359 let cache: OnceLock<RawCudaFunc> = OnceLock::new();
362 let registry = self.registry();
363 match registry.raw_function_cached(&cache, module, func_name) {
364 Ok(raw) => {
365 crate::kernel_audit::record(module, func_name, true, site);
366 crate::launch_trace::name_kernel(raw.0 as u64, module, func_name);
367 Ok(KernelHandle(raw.0 as u64))
368 }
369 Err(e) => {
370 crate::kernel_audit::record(module, func_name, false, site);
373 Err(anyhow::anyhow!("Kernel lookup {module}::{func_name}: {e}"))
374 }
375 }
376 }
377
378 fn copy_h2d_async(&self, src: &[u8], dst: DevicePtr, stream: u64) -> Result<()> {
379 h2d_enqueue(src, dst, stream)?;
380 if crate::pinned_hosts::is_pinned(src) {
392 warn_pinned_transient_source();
393 let sync = unsafe { cuStreamSynchronize(stream) };
394 if sync != 0 {
395 bail!(
396 "cuStreamSynchronize after pinned-source H2D failed: {}",
397 cuda_error_text(sync)
398 );
399 }
400 }
401 Ok(())
402 }
403
404 fn copy_h2d_async_retained(&self, src: &[u8], dst: DevicePtr, stream: u64) -> Result<()> {
405 h2d_enqueue(src, dst, stream)
409 }
410
411 fn copy_d2d_async(
412 &self,
413 src: DevicePtr,
414 dst: DevicePtr,
415 bytes: usize,
416 stream: u64,
417 ) -> Result<()> {
418 let status = unsafe { cuMemcpyDtoDAsync_v2(dst.0, src.0, bytes, stream) };
419 if status != 0 {
420 tracing::error!(
423 "copy_d2d_async failed (status {status}) at:\n{}",
424 std::backtrace::Backtrace::force_capture()
425 );
426 bail!("cuMemcpyDtoDAsync_v2 (copy_d2d_async) failed: status {status}");
427 }
428 Ok(())
429 }
430
431 fn copy_d2d_2d_async(
432 &self,
433 src: DevicePtr,
434 src_pitch: usize,
435 dst: DevicePtr,
436 dst_pitch: usize,
437 width_bytes: usize,
438 height: usize,
439 stream: u64,
440 ) -> Result<()> {
441 unsafe extern "C" {
446 fn cudaMemcpy2DAsync(
447 dst: *mut c_void,
448 dpitch: usize,
449 src: *const c_void,
450 spitch: usize,
451 width: usize,
452 height: usize,
453 kind: i32,
454 stream: u64,
455 ) -> i32;
456 }
457 let status = unsafe {
458 cudaMemcpy2DAsync(
459 dst.0 as *mut c_void,
460 dst_pitch,
461 src.0 as *const c_void,
462 src_pitch,
463 width_bytes,
464 height,
465 3,
466 stream,
467 )
468 };
469 if status != 0 {
470 bail!("cudaMemcpy2DAsync failed: status {status}");
471 }
472 Ok(())
473 }
474
475 fn begin_capture(&self, stream: u64) -> Result<()> {
476 self.begin_capture_cu(stream)
477 }
478 fn end_capture(&self, stream: u64) -> Result<GraphHandle> {
479 self.end_capture_cu(stream)
480 }
481
482 fn abort_capture_if_active(&self, stream: u64) {
483 self.abort_capture_if_active_cu(stream)
484 }
485
486 fn launch_graph(&self, graph: GraphHandle, stream: u64) -> Result<()> {
487 self.launch_graph_cu(graph, stream)
488 }
489 fn destroy_graph(&self, graph: GraphHandle) -> Result<()> {
490 self.destroy_graph_cu(graph)
491 }
492 fn memset(&self, ptr: DevicePtr, value: u8, bytes: usize) -> Result<()> {
493 self.memset_cu(ptr, value, bytes)
494 }
495 fn memset_async(&self, ptr: DevicePtr, value: u8, bytes: usize, stream: u64) -> Result<()> {
496 if crate::launch_trace::on() {
497 crate::launch_trace::record(crate::launch_trace::Entry {
498 kind: "memset",
499 func: 0,
500 grid: [0, 0, 0],
501 block: [0, 0, 0],
502 smem: 0,
503 args: vec![ptr.0, value as u64, bytes as u64],
504 });
505 }
506 self.memset_async_cu(ptr, value, bytes, stream)
507 }
508 fn total_memory(&self) -> Result<usize> {
509 self.total_memory_cu()
510 }
511 fn free_memory(&self) -> Result<usize> {
512 self.free_memory_cu()
513 }
514 fn device_free_memory(&self) -> Result<usize> {
515 self.device_free_memory_cu()
516 }
517 fn live_alloc_count(&self) -> usize {
518 self.live_alloc_len()
519 }
520 fn sm_count(&self) -> Result<u32> {
521 self.sm_count_cu()
522 }
523 fn create_stream(&self) -> Result<u64> {
524 self.create_stream_cu()
525 }
526 fn bind_to_thread(&self) -> Result<()> {
527 self.bind_to_thread_cu()
528 }
529 fn create_event(&self) -> Result<u64> {
530 self.create_event_cu()
531 }
532 fn record_event(&self, event: u64, stream: u64) -> Result<()> {
533 self.record_event_cu(event, stream)
534 }
535 fn stream_wait_event(&self, stream: u64, event: u64) -> Result<()> {
536 self.stream_wait_event_cu(stream, event)
537 }
538 fn event_synchronize(&self, event: u64) -> Result<()> {
539 self.event_synchronize_cu(event)
540 }
541 fn destroy_event(&self, event: u64) -> Result<()> {
542 self.destroy_event_cu(event)
543 }
544 fn host_ptr_to_device(&self, host: *mut u8) -> Result<DevicePtr> {
545 let mut dptr: u64 = 0;
546 let status =
547 unsafe { super::cuMemHostGetDevicePointer_v2(&mut dptr, host as *mut c_void, 0) };
548 if status != 0 {
549 bail!("cuMemHostGetDevicePointer_v2 failed: status {status}");
550 }
551 Ok(DevicePtr(dptr))
552 }
553
554 fn alloc_host_pinned(&self, bytes: usize) -> Result<*mut u8> {
555 if bytes >= 32 * 1024 * 1024 {
556 tracing::debug!(
557 "alloc_host_pinned {:.1} MB",
558 bytes as f64 / (1024.0 * 1024.0)
559 );
560 }
561 self.alloc_host_pinned_cu(bytes)
562 }
563 fn free_host_pinned(&self, ptr: *mut u8, _bytes: usize) -> Result<()> {
564 self.free_host_pinned_cu(ptr, _bytes)
565 }
566}