1#[cfg(feature = "cuda")]
17use std::sync::{Arc, Mutex, OnceLock};
18
19#[cfg(feature = "cuda")]
20use super::compute::QualiaCompute;
21#[cfg(feature = "cuda")]
22use super::memory::{BindingUsage, BufferView, MemoryTopology, QualiaSlabAllocator};
23#[cfg(feature = "cuda")]
24use super::oracle_ctx::OracleContext;
25#[cfg(feature = "cuda")]
26use crate::wgsl_forge::{
27 emit_shader, AdapterConstraints, AdapterIdentity, BufferAccess, BufferElement, BufferSpec,
28 ForgeError, KernelSpec, ScalarType, Schedule, TargetBackend,
29};
30#[cfg(feature = "cuda")]
31use cudarc::driver::{
32 CudaContext, CudaFunction, CudaGraph, CudaModule, CudaSlice, CudaStream, DevicePtr, DeviceRepr,
33 LaunchConfig, PushKernelArg,
34};
35
36#[cfg(feature = "cuda")]
43#[repr(C)]
44#[derive(Clone, Copy)]
45struct AffineParamsRaw {
46 bytes: [u8; 16],
47}
48
49#[cfg(feature = "cuda")]
50unsafe impl DeviceRepr for AffineParamsRaw {}
51
52#[cfg(feature = "cuda")]
53pub struct CudaComputeContext {
54 pub ctx: Arc<CudaContext>,
55 pub stream: Arc<CudaStream>,
56 pub prefetch_stream: Option<Arc<CudaStream>>,
60 pub module_cache:
65 Mutex<std::collections::HashMap<u64, (CudaFunction, Arc<CudaModule>, Arc<KernelSpec>)>>,
66 pub adapter: AdapterIdentity,
67 pub constraints: AdapterConstraints,
68 pub allocator: QualiaSlabAllocator,
69 pub slab: CudaSlice<u8>,
70}
71
72#[cfg(feature = "cuda")]
78pub struct CapturedCudaGraph {
79 graph: CudaGraph,
80}
81
82#[cfg(feature = "cuda")]
83impl CapturedCudaGraph {
84 pub fn node_count(&self) -> Result<usize, ForgeError> {
86 let mut count = 0usize;
87 let status = unsafe {
88 cudarc::driver::sys::cuGraphGetNodes(
89 self.graph.cu_graph(),
90 core::ptr::null_mut(),
91 &mut count,
92 )
93 };
94 if status == cudarc::driver::sys::CUresult::CUDA_SUCCESS {
95 Ok(count)
96 } else {
97 Err(ForgeError::GpuValidation(format!(
98 "CUDA graph node query failed: {status:?}"
99 )))
100 }
101 }
102}
103
104#[cfg(feature = "cuda")]
107unsafe impl Send for CapturedCudaGraph {}
108
109#[cfg(feature = "cuda")]
110impl CudaComputeContext {
111 pub fn new(capacity_bytes: usize) -> Result<Self, ForgeError> {
112 let ctx = CudaContext::new(0)
113 .map_err(|e| ForgeError::GpuUnavailable(format!("CUDA init failed: {:?}", e)))?;
114 unsafe {
121 ctx.disable_event_tracking();
122 }
123 let stream = ctx
126 .new_stream()
127 .map_err(|e| ForgeError::GpuUnavailable(format!("CUDA stream init failed: {e:?}")))?;
128
129 let adapter = AdapterIdentity {
130 name: "CUDA Device".to_string(),
131 vendor: 4318, device: 0,
133 device_type: "DiscreteGpu".to_string(),
134 backend: "CUDA".to_string(),
135 driver: "cudarc".to_string(),
136 driver_info: "0.19".to_string(),
137 };
138
139 let constraints = AdapterConstraints {
144 max_workgroup_size_x: 1024,
145 max_invocations_per_workgroup: 1024,
146 max_workgroups_per_dimension: 65_535,
147 supports_subgroups: true,
148 supports_coopmat: false,
151 supports_rt_cores: false,
152 warp_size: 32, };
154
155 let topology = MemoryTopology::Discrete {
156 staging_required: true,
157 };
158 let allocator = QualiaSlabAllocator::new(topology, capacity_bytes);
159
160 let slab = stream.alloc_zeros::<u8>(capacity_bytes).map_err(|e| {
163 ForgeError::GpuUnavailable(format!("Failed to allocate CUDA slab: {:?}", e))
164 })?;
165
166 Ok(Self {
167 ctx,
168 stream,
169 prefetch_stream: None,
170 module_cache: Mutex::new(std::collections::HashMap::new()),
171 adapter,
172 constraints,
173 allocator,
174 slab,
175 })
176 }
177
178 pub fn begin_graph_capture(&self) -> Result<(), ForgeError> {
180 self.stream.synchronize().map_err(|e| {
183 ForgeError::GpuValidation(format!("CUDA graph pre-capture sync: {e:?}"))
184 })?;
185 self.stream
186 .begin_capture(
187 cudarc::driver::sys::CUstreamCaptureMode::CU_STREAM_CAPTURE_MODE_THREAD_LOCAL,
188 )
189 .map_err(|e| ForgeError::GpuValidation(format!("CUDA graph begin capture: {e:?}")))
190 }
191
192 pub fn end_graph_capture(&self) -> Result<CapturedCudaGraph, ForgeError> {
194 let graph = self
195 .stream
196 .end_capture(
197 cudarc::driver::sys::CUgraphInstantiate_flags::CUDA_GRAPH_INSTANTIATE_FLAG_USE_NODE_PRIORITY,
198 )
199 .map_err(|e| ForgeError::GpuValidation(format!("CUDA graph end capture: {e:?}")))?
200 .ok_or_else(|| ForgeError::GpuValidation("CUDA capture produced no graph".into()))?;
201 Ok(CapturedCudaGraph { graph })
202 }
203
204 pub fn launch_graph(&self, graph: &CapturedCudaGraph) -> Result<(), ForgeError> {
206 graph
207 .graph
208 .launch()
209 .map_err(|e| ForgeError::GpuValidation(format!("CUDA graph launch: {e:?}")))
210 }
211
212 pub fn allocate_and_write(
213 &mut self,
214 data: &[u8],
215 binding: u32,
216 group: u32,
217 ) -> Result<BufferView, ForgeError> {
218 let view = self.allocator.allocate_transient(
221 data.len(),
222 binding,
223 group,
224 BindingUsage::StorageReadWrite,
225 )?;
226 if !data.is_empty() {
227 let mut dst = self
228 .slab
229 .slice_mut(view.offset..view.offset + view.length_bytes);
230 self.stream
231 .memcpy_htod(data, &mut dst)
232 .map_err(|e| ForgeError::GpuValidation(format!("H2D transfer failed: {:?}", e)))?;
233 }
234 Ok(view)
235 }
236
237 fn ensure_prefetch_stream(&mut self) -> Result<&Arc<CudaStream>, ForgeError> {
239 if self.prefetch_stream.is_none() {
240 let s = self
241 .ctx
242 .new_stream()
243 .map_err(|e| ForgeError::GpuUnavailable(format!("prefetch stream: {:?}", e)))?;
244 self.prefetch_stream = Some(s);
245 }
246 Ok(self.prefetch_stream.as_ref().unwrap())
247 }
248
249 pub fn write_view_prefetch(
253 &mut self,
254 view: &BufferView,
255 data: &[u8],
256 ) -> Result<(), ForgeError> {
257 if data.len() > view.length_bytes {
258 return Err(ForgeError::GpuValidation(format!(
259 "write_view_prefetch overflow: {} > {}",
260 data.len(),
261 view.length_bytes
262 )));
263 }
264 if data.is_empty() {
265 return Ok(());
266 }
267 let pf_stream = self.ensure_prefetch_stream()?.clone();
268 let mut dst = self.slab.slice_mut(view.offset..view.offset + data.len());
269 pf_stream
270 .memcpy_htod(data, &mut dst)
271 .map_err(|e| ForgeError::GpuValidation(format!("prefetch H2D: {:?}", e)))?;
272 Ok(())
273 }
274
275 pub fn join_prefetch(&self) -> Result<(), ForgeError> {
278 if let Some(ref pf) = self.prefetch_stream {
279 self.stream
280 .join(pf)
281 .map_err(|e| ForgeError::GpuValidation(format!("join_prefetch: {:?}", e)))?;
282 }
283 Ok(())
284 }
285
286 pub fn write_view(&mut self, view: &BufferView, data: &[u8]) -> Result<(), ForgeError> {
289 if data.len() > view.length_bytes {
290 return Err(ForgeError::GpuValidation(format!(
291 "write_view overflow: {} > {}",
292 data.len(),
293 view.length_bytes
294 )));
295 }
296 if data.is_empty() {
297 return Ok(());
298 }
299 let mut dst = self.slab.slice_mut(view.offset..view.offset + data.len());
300 self.stream
301 .memcpy_htod(data, &mut dst)
302 .map_err(|e| ForgeError::GpuValidation(format!("H2D write_view failed: {:?}", e)))?;
303 Ok(())
304 }
305
306 pub fn allocate_transient(
307 &mut self,
308 size_bytes: usize,
309 binding: u32,
310 group: u32,
311 ) -> Result<BufferView, ForgeError> {
312 self.allocator.allocate_transient(
313 size_bytes,
314 binding,
315 group,
316 BindingUsage::StorageReadWrite,
317 )
318 }
319
320 pub fn advance_read_head(&mut self, offset: usize) {
321 self.allocator.advance_read_head(offset);
322 }
323
324 pub fn clear_transient_allocations(&mut self) {
325 self.allocator.clear();
326 }
327
328 pub fn write_checkpoint(&self) -> u64 {
330 self.allocator.write_checkpoint()
331 }
332
333 pub fn restore_checkpoint(&mut self, write_count: u64) {
335 self.allocator.restore_checkpoint(write_count);
336 }
337
338 pub fn read_buffer_f32(&self, view: &BufferView) -> Result<Vec<f32>, ForgeError> {
339 let src = self
340 .slab
341 .slice(view.offset..view.offset + view.length_bytes);
342 let bytes = self
343 .stream
344 .clone_dtoh(&src)
345 .map_err(|e| ForgeError::GpuValidation(format!("D2H transfer failed: {:?}", e)))?;
346
347 let elements = view.length_bytes / std::mem::size_of::<f32>();
348 let output = bytemuck::cast_slice::<u8, f32>(&bytes)[..elements].to_vec();
349 Ok(output)
350 }
351
352 pub fn read_buffer_u32_into(
357 &self,
358 view: &BufferView,
359 output: &mut [u32],
360 ) -> Result<(), ForgeError> {
361 let bytes = bytemuck::cast_slice_mut(output);
362 if bytes.len() > view.length_bytes {
363 return Err(ForgeError::GpuValidation(format!(
364 "u32 readback overflow: {} > {}",
365 bytes.len(),
366 view.length_bytes
367 )));
368 }
369 let src = self.slab.slice(view.offset..view.offset + bytes.len());
370 self.stream
371 .memcpy_dtoh(&src, bytes)
372 .map_err(|e| ForgeError::GpuValidation(format!("D2H transfer failed: {e:?}")))
373 }
374
375 pub fn read_buffer_f64(&self, view: &BufferView) -> Result<Vec<f64>, ForgeError> {
379 let src = self
380 .slab
381 .slice(view.offset..view.offset + view.length_bytes);
382 let bytes = self
383 .stream
384 .clone_dtoh(&src)
385 .map_err(|e| ForgeError::GpuValidation(format!("D2H transfer failed: {:?}", e)))?;
386
387 let elements = view.length_bytes / std::mem::size_of::<f64>();
388 let output = bytemuck::cast_slice::<u8, f64>(&bytes)[..elements].to_vec();
389 Ok(output)
390 }
391}
392
393#[cfg(feature = "cuda")]
394pub struct CudaPipeline<'a> {
395 context: &'a CudaComputeContext,
396 func: CudaFunction,
397 spec: Arc<KernelSpec>,
398 _module: Arc<CudaModule>,
400}
401
402#[cfg(feature = "cuda")]
403impl<'a> CudaPipeline<'a> {
404 pub fn compile_cuda_c(
407 context: &'a CudaComputeContext,
408 kernel: &KernelSpec,
409 schedule: Schedule,
410 ) -> Result<Self, ForgeError> {
411 let generated = emit_shader(kernel, schedule, TargetBackend::CudaC)?;
412 Self::from_source(
413 context,
414 &generated.source,
415 &kernel.entry_point,
416 kernel.clone(),
417 )
418 }
419
420 pub fn compile_cuda_c_source(
427 context: &'a CudaComputeContext,
428 source: &str,
429 entry_point: &str,
430 storage_buffer_bindings: &[u32],
431 ) -> Result<Self, ForgeError> {
432 let buffers: Vec<BufferSpec> = storage_buffer_bindings
433 .iter()
434 .map(|&binding| BufferSpec {
435 group: 0,
436 binding,
437 name: format!("buf{binding}"),
438 element: BufferElement::Scalar(ScalarType::F32),
439 access: BufferAccess::StorageReadWrite,
440 })
441 .collect();
442 let spec = KernelSpec {
443 id: entry_point.to_string(),
444 semantic_version: 1,
445 entry_point: entry_point.to_string(),
446 description: "raw CUDA-C kernel".to_string(),
447 buffers,
448 ops: Vec::new(),
449 shared_memory: Vec::new(),
450 };
451 Self::from_source(context, source, entry_point, spec)
452 }
453
454 fn from_source(
455 context: &'a CudaComputeContext,
456 source: &str,
457 entry_point: &str,
458 spec: KernelSpec,
459 ) -> Result<Self, ForgeError> {
460 let ptx = nvrtc_compile_to_ptx_cached(context, source)?;
461 Self::from_ptx(context, &ptx, entry_point, spec)
462 }
463
464 pub fn from_ptx(
467 context: &'a CudaComputeContext,
468 ptx: &cudarc::nvrtc::Ptx,
469 entry_point: &str,
470 spec: KernelSpec,
471 ) -> Result<Self, ForgeError> {
472 let module = context
473 .ctx
474 .load_module(ptx.clone())
475 .map_err(|e| ForgeError::GpuValidation(format!("Failed to load module: {:?}", e)))?;
476 let func = module
477 .load_function(entry_point)
478 .map_err(|e| ForgeError::GpuValidation(format!("Entry point not found: {:?}", e)))?;
479
480 Ok(Self {
481 context,
482 func,
483 spec: Arc::new(spec),
484 _module: module,
485 })
486 }
487
488 pub fn compile_cuda_c_source_cached(
492 context: &'a CudaComputeContext,
493 source: &str,
494 entry_point: &str,
495 storage_buffer_bindings: &[u32],
496 ) -> Result<Self, ForgeError> {
497 let src_hash = fnv1a64_bytes(source.as_bytes());
498 let cache_key = src_hash ^ fnv1a64_bytes(entry_point.as_bytes()).rotate_left(1);
499
500 if let Ok(guard) = context.module_cache.lock() {
503 if let Some((func, module, spec)) = guard.get(&cache_key) {
504 return Ok(Self {
505 context,
506 func: func.clone(),
507 spec: spec.clone(),
508 _module: module.clone(),
509 });
510 }
511 }
512
513 let buffers: Vec<BufferSpec> = storage_buffer_bindings
515 .iter()
516 .map(|&binding| BufferSpec {
517 group: 0,
518 binding,
519 name: format!("buf{binding}"),
520 element: BufferElement::Scalar(ScalarType::F32),
521 access: BufferAccess::StorageReadWrite,
522 })
523 .collect();
524 let spec = KernelSpec {
525 id: entry_point.to_string(),
526 semantic_version: 1,
527 entry_point: entry_point.to_string(),
528 description: "raw CUDA-C kernel (cached PTX)".to_string(),
529 buffers,
530 ops: Vec::new(),
531 shared_memory: Vec::new(),
532 };
533 let ptx = nvrtc_compile_to_ptx_cached(context, source)?;
534 let pipe = Self::from_ptx(context, &ptx, entry_point, spec)?;
535
536 if let Ok(mut guard) = context.module_cache.lock() {
538 guard.insert(
539 cache_key,
540 (pipe.func.clone(), pipe._module.clone(), pipe.spec.clone()),
541 );
542 }
543 Ok(pipe)
544 }
545
546 pub fn compile_ptx(
555 context: &'a CudaComputeContext,
556 ptx_source: &str,
557 entry_point: &str,
558 storage_buffer_bindings: &[u32],
559 ) -> Result<Self, ForgeError> {
560 let src_hash = fnv1a64_bytes(ptx_source.as_bytes());
561 let cache_key = src_hash ^ fnv1a64_bytes(entry_point.as_bytes()).rotate_left(1);
562
563 if let Ok(guard) = context.module_cache.lock() {
565 if let Some((func, module, spec)) = guard.get(&cache_key) {
566 return Ok(Self {
567 context,
568 func: func.clone(),
569 spec: spec.clone(),
570 _module: module.clone(),
571 });
572 }
573 }
574
575 let buffers: Vec<BufferSpec> = storage_buffer_bindings
576 .iter()
577 .map(|&binding| BufferSpec {
578 group: 0,
579 binding,
580 name: format!("buf{binding}"),
581 element: BufferElement::Scalar(ScalarType::F32),
582 access: BufferAccess::StorageReadWrite,
583 })
584 .collect();
585 let spec = KernelSpec {
586 id: entry_point.to_string(),
587 semantic_version: 1,
588 entry_point: entry_point.to_string(),
589 description: "hand-emitted PTX kernel".to_string(),
590 buffers,
591 ops: Vec::new(),
592 shared_memory: Vec::new(),
593 };
594 let ptx = cudarc::nvrtc::Ptx::from_src(ptx_source.to_string());
595 let pipe = Self::from_ptx(context, &ptx, entry_point, spec)?;
596
597 if let Ok(mut guard) = context.module_cache.lock() {
598 guard.insert(
599 cache_key,
600 (pipe.func.clone(), pipe._module.clone(), pipe.spec.clone()),
601 );
602 }
603 Ok(pipe)
604 }
605}
606
607#[cfg(feature = "cuda")]
614static NVRTC_PTX_CACHE: OnceLock<Mutex<std::collections::HashMap<(u64, String), String>>> =
615 OnceLock::new();
616
617#[cfg(feature = "cuda")]
618fn nvrtc_ptx_cache() -> &'static Mutex<std::collections::HashMap<(u64, String), String>> {
619 NVRTC_PTX_CACHE.get_or_init(|| Mutex::new(std::collections::HashMap::new()))
620}
621
622#[cfg(feature = "cuda")]
624fn fnv1a64_bytes(data: &[u8]) -> u64 {
625 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
626 for &b in data {
627 h ^= b as u64;
628 h = h.wrapping_mul(0x0100_0000_01b3);
629 }
630 h
631}
632
633#[cfg(feature = "cuda")]
635pub(crate) fn nvrtc_compile_to_ptx_cached(
636 context: &CudaComputeContext,
637 source: &str,
638) -> Result<cudarc::nvrtc::Ptx, ForgeError> {
639 use cudarc::nvrtc::{compile_ptx_with_opts, CompileOptions};
640 let (major, minor) = context.ctx.compute_capability().map_err(|e| {
641 ForgeError::GpuUnavailable(format!("compute-capability query failed: {:?}", e))
642 })?;
643 let arch = arch_for_capability(major, minor).to_string();
644 let key = (fnv1a64_bytes(source.as_bytes()), arch.clone());
645
646 if let Ok(guard) = nvrtc_ptx_cache().lock() {
647 if let Some(text) = guard.get(&key) {
648 return Ok(cudarc::nvrtc::Ptx::from_src(text.clone()));
649 }
650 }
651
652 let mut include_paths = Vec::new();
653 if let Ok(cuda_path) = std::env::var("CUDA_PATH") {
654 include_paths.push(format!("{cuda_path}/include"));
655 }
656 let opts = CompileOptions {
657 arch: Some(arch_for_capability(major, minor)),
658 include_paths,
659 ..Default::default()
660 };
661 let compiled = compile_ptx_with_opts(source, opts)
662 .map_err(|e| ForgeError::GpuValidation(format!("NVRTC compile failed: {:?}", e)))?;
663 let text = downgrade_ptx_isa(&compiled.to_src());
667 if let Ok(mut guard) = nvrtc_ptx_cache().lock() {
668 guard.insert(key, text.clone());
669 log::info!(
670 "cuda_nvrtc|cache_store|arch={arch}|src_hash={:#x}|ptx_bytes={}",
671 fnv1a64_bytes(source.as_bytes()),
672 text.len()
673 );
674 }
675 Ok(cudarc::nvrtc::Ptx::from_src(text))
676}
677
678#[cfg(feature = "cuda")]
683fn arch_for_capability(major: i32, minor: i32) -> &'static str {
684 match (major, minor) {
685 (9, 0) => "compute_90",
686 (8, 9) => "compute_89",
687 (8, 7) => "compute_87",
688 (8, 6) => "compute_86",
689 (8, 0) => "compute_80",
690 (7, 5) => "compute_75",
691 (7, 2) => "compute_72",
692 (7, 0) => "compute_70",
693 _ => "compute_70",
694 }
695}
696
697#[cfg(feature = "cuda")]
702fn downgrade_ptx_isa(ptx: &str) -> String {
703 const TARGET_VERSION: &str = ".version 8.0";
704 let mut out = String::with_capacity(ptx.len());
705 let mut replaced = false;
706 for line in ptx.lines() {
707 if !replaced && line.trim_start().starts_with(".version") {
708 out.push_str(TARGET_VERSION);
709 replaced = true;
710 } else {
711 out.push_str(line);
712 }
713 out.push('\n');
714 }
715 out
716}
717
718#[cfg(feature = "cuda")]
719impl<'a> CudaPipeline<'a> {
720 pub fn dispatch_async(
724 &self,
725 buffers: &[BufferView],
726 schedule: &Schedule,
727 element_count: usize,
728 ) -> Result<(), ForgeError> {
729 self.launch_inner(buffers, schedule, element_count, false)
730 .map(|_| ())
731 }
732
733 pub fn dispatch_ptx(
737 &self,
738 buffers: &[BufferView],
739 grid: (u32, u32, u32),
740 block: (u32, u32, u32),
741 shared_mem_bytes: u32,
742 ) -> Result<(), ForgeError> {
743 let cfg = LaunchConfig {
744 grid_dim: grid,
745 block_dim: block,
746 shared_mem_bytes,
747 };
748
749 let (base, _guard) = self.context.slab.device_ptr(&self.context.stream);
750 let base = base as u64;
751
752 let mut ptr_args: [u64; 16] = [0; 16];
753 let n_bufs = buffers.len().min(16);
754 for i in 0..n_bufs {
755 ptr_args[i] = base + buffers[i].offset as u64;
756 }
757
758 let mut builder = self.context.stream.launch_builder(&self.func);
759 for i in 0..n_bufs {
760 builder.arg(&ptr_args[i]);
761 }
762 unsafe {
763 builder
764 .launch(cfg)
765 .map_err(|e| ForgeError::GpuValidation(format!("PTX launch failed: {:?}", e)))?;
766 }
767 Ok(())
768 }
769
770 pub fn dispatch_async_sorted(
776 &self,
777 buffers: &[BufferView],
778 schedule: &Schedule,
779 element_count: usize,
780 ) -> Result<(), ForgeError> {
781 let dispatch_x = schedule.dispatch_workgroups(element_count);
782 let cfg = LaunchConfig {
783 grid_dim: (dispatch_x, 1, 1),
784 block_dim: (schedule.workgroup_size, 1, 1),
785 shared_mem_bytes: 0,
786 };
787
788 let (base, _guard) = self.context.slab.device_ptr(&self.context.stream);
789 let base = base as u64;
790
791 let mut ptr_args: [u64; 16] = [0; 16];
794 let n_bufs = buffers.len().min(16);
795 for i in 0..n_bufs {
796 ptr_args[i] = base + buffers[i].offset as u64;
797 }
798
799 let mut builder = self.context.stream.launch_builder(&self.func);
800 for i in 0..n_bufs {
801 builder.arg(&ptr_args[i]);
802 }
803 unsafe {
804 builder
805 .launch(cfg)
806 .map_err(|e| ForgeError::GpuValidation(format!("CUDA launch failed: {:?}", e)))?;
807 }
808 Ok(())
809 }
810
811 pub fn dispatch_gpu_timed_ms_sorted(
818 &self,
819 buffers: &[BufferView],
820 schedule: &Schedule,
821 element_count: usize,
822 ) -> Result<f32, ForgeError> {
823 let dispatch_x = schedule.dispatch_workgroups(element_count);
824 let cfg = LaunchConfig {
825 grid_dim: (dispatch_x, 1, 1),
826 block_dim: (schedule.workgroup_size, 1, 1),
827 shared_mem_bytes: 0,
828 };
829
830 let (base, _guard) = self.context.slab.device_ptr(&self.context.stream);
831 let base = base as u64;
832 let mut ptr_args: [u64; 16] = [0; 16];
833 let n_bufs = buffers.len().min(16);
834 for index in 0..n_bufs {
835 ptr_args[index] = base + buffers[index].offset as u64;
836 }
837
838 let mut builder = self.context.stream.launch_builder(&self.func);
839 for ptr in ptr_args.iter().take(n_bufs) {
840 builder.arg(ptr);
841 }
842 builder.record_kernel_launch(cudarc::driver::sys::CUevent_flags::CU_EVENT_DEFAULT);
843 let events = unsafe {
844 builder.launch(cfg).map_err(|error| {
845 ForgeError::GpuValidation(format!("CUDA timed launch failed: {error:?}"))
846 })?
847 }
848 .ok_or_else(|| {
849 ForgeError::GpuValidation("CUDA timed launch returned no events".to_string())
850 })?;
851 events.0.elapsed_ms(&events.1).map_err(|error| {
852 ForgeError::GpuValidation(format!("CUDA event timing failed: {error:?}"))
853 })
854 }
855
856 fn launch_inner(
857 &self,
858 buffers: &[BufferView],
859 schedule: &Schedule,
860 element_count: usize,
861 sync: bool,
862 ) -> Result<u64, ForgeError> {
863 let dispatch_x = schedule.dispatch_workgroups(element_count);
864 let cfg = LaunchConfig {
865 grid_dim: (dispatch_x, 1, 1),
866 block_dim: (schedule.workgroup_size, 1, 1),
867 shared_mem_bytes: 0,
868 };
869
870 let (base, _guard) = self.context.slab.device_ptr(&self.context.stream);
874 let base = base as u64;
875
876 let mut sorted = self.spec.buffers.clone();
877 sorted.sort_by_key(|b| b.binding);
878
879 let mut ptr_args: Vec<u64> = Vec::with_capacity(sorted.len());
880 let mut params: Option<AffineParamsRaw> = None;
881 for bspec in &sorted {
882 let view = buffers
883 .iter()
884 .find(|b| b.binding == bspec.binding)
885 .ok_or_else(|| {
886 ForgeError::GpuValidation(format!(
887 "CUDA dispatch missing binding {}",
888 bspec.binding
889 ))
890 })?;
891 if bspec.access == BufferAccess::Uniform {
892 let host = self
893 .context
894 .stream
895 .clone_dtoh(&self.context.slab.slice(view.offset..view.offset + 16))
896 .map_err(|e| {
897 ForgeError::GpuValidation(format!("Failed to read params: {:?}", e))
898 })?;
899 let mut blob = AffineParamsRaw { bytes: [0u8; 16] };
900 blob.bytes.copy_from_slice(&host[..16]);
901 params = Some(blob);
902 } else {
903 ptr_args.push(base + view.offset as u64);
904 }
905 }
906
907 let start = std::time::Instant::now();
908 let mut builder = self.context.stream.launch_builder(&self.func);
909 for ptr in &ptr_args {
910 builder.arg(ptr);
911 }
912 if let Some(params) = ¶ms {
913 builder.arg(params);
914 }
915 unsafe {
918 builder
919 .launch(cfg)
920 .map_err(|e| ForgeError::GpuValidation(format!("CUDA launch failed: {:?}", e)))?;
921 }
922
923 if sync {
924 self.context
927 .stream
928 .synchronize()
929 .map_err(|e| ForgeError::DeviceLost(format!("CUDA sync failed: {:?}", e)))?;
930 }
931
932 Ok(start.elapsed().as_nanos().min(u64::MAX as u128) as u64)
933 }
934}
935
936#[cfg(feature = "cuda")]
937impl<'a> QualiaCompute for CudaPipeline<'a> {
938 fn dispatch(
939 &self,
940 buffers: &[BufferView],
941 schedule: &Schedule,
942 element_count: usize,
943 ) -> Result<u64, ForgeError> {
944 self.launch_inner(buffers, schedule, element_count, true)
945 }
946}
947
948#[cfg(feature = "cuda")]
949impl OracleContext for CudaComputeContext {
950 fn allocate_and_write(
951 &mut self,
952 data: &[u8],
953 binding: u32,
954 group: u32,
955 _usage: BindingUsage,
956 ) -> Result<BufferView, ForgeError> {
957 CudaComputeContext::allocate_and_write(self, data, binding, group)
960 }
961
962 fn allocate_transient(
963 &mut self,
964 size_bytes: usize,
965 binding: u32,
966 group: u32,
967 _usage: BindingUsage,
968 ) -> Result<BufferView, ForgeError> {
969 CudaComputeContext::allocate_transient(self, size_bytes, binding, group)
970 }
971
972 fn read_buffer_f32(&self, view: &BufferView) -> Result<Vec<f32>, ForgeError> {
973 CudaComputeContext::read_buffer_f32(self, view)
974 }
975
976 fn clear_transient_allocations(&mut self) {
977 CudaComputeContext::clear_transient_allocations(self);
978 }
979
980 fn adapter(&self) -> &AdapterIdentity {
981 &self.adapter
982 }
983
984 fn constraints(&self) -> &AdapterConstraints {
985 &self.constraints
986 }
987
988 fn timestamp_supported(&self) -> bool {
989 false
992 }
993
994 fn run_kernel(
1000 &mut self,
1001 kernel: &KernelSpec,
1002 schedule: &Schedule,
1003 buffers: &[BufferView],
1004 element_count: usize,
1005 warmups: usize,
1006 samples: usize,
1007 ) -> Result<Vec<u64>, ForgeError> {
1008 let pipeline = CudaPipeline::compile_cuda_c(self, kernel, *schedule)?;
1009
1010 for _ in 0..warmups {
1011 pipeline.dispatch(buffers, schedule, element_count)?;
1012 }
1013 let mut timing_samples = Vec::with_capacity(samples);
1014 for _ in 0..samples {
1015 timing_samples.push(pipeline.dispatch(buffers, schedule, element_count)?);
1016 }
1017 Ok(timing_samples)
1018 }
1019}
1020
1021#[cfg(all(test, feature = "cuda"))]
1022mod graph_tests {
1023 use super::*;
1024 use crate::specialized_libs::computational_geometry::allocation_counter::assert_zero_alloc;
1025
1026 #[test]
1027 fn captured_kernel_replays_without_host_redispatch() {
1028 let Ok(mut context) = CudaComputeContext::new(16 * 1024 * 1024) else {
1029 eprintln!("CUDA graph test skipped: CUDA context unavailable");
1030 return;
1031 };
1032 let Ok(mut value) = context.allocate_and_write(bytemuck::cast_slice(&[0u32; 1]), 0, 0)
1033 else {
1034 return;
1035 };
1036 let source = r#"
1037extern "C" __global__ void increment(unsigned *value) {
1038 if (blockIdx.x == 0u && threadIdx.x == 0u) value[0] += 1u;
1039}
1040"#;
1041 let Ok(pipeline) =
1042 CudaPipeline::compile_cuda_c_source_cached(&context, source, "increment", &[0])
1043 else {
1044 eprintln!("CUDA graph test skipped: NVRTC unavailable");
1045 return;
1046 };
1047 value.binding = 0;
1048 let schedule = Schedule {
1049 workgroup_size: 32,
1050 ..Default::default()
1051 };
1052 context.begin_graph_capture().unwrap();
1053 pipeline
1054 .dispatch_async_sorted(&[value], &schedule, 32)
1055 .unwrap();
1056 let graph = context.end_graph_capture().unwrap();
1057 assert_eq!(graph.node_count().unwrap(), 1);
1058 context.launch_graph(&graph).unwrap();
1059 context.launch_graph(&graph).unwrap();
1060 let mut output = [0u32; 1];
1061 context.read_buffer_u32_into(&value, &mut output).unwrap();
1062 assert_eq!(output[0], 2);
1063
1064 assert_zero_alloc("cuda_graph_dynamic_h2d", || {
1065 context
1066 .write_view(&value, bytemuck::cast_slice(&[0u32; 1]))
1067 .unwrap();
1068 });
1069 assert_zero_alloc("cuda_graph_launch", || {
1070 context.launch_graph(&graph).unwrap();
1071 });
1072 assert_zero_alloc("cuda_graph_token_d2h", || {
1073 context.read_buffer_u32_into(&value, &mut output).unwrap();
1074 });
1075 }
1076}