1use std::{collections::HashMap, marker::PhantomData, sync::Arc};
18
19use anyhow::anyhow;
20use teeny_core::{
21 device::program::ArgVisitor,
22 graph::{DtypeRepr, Shape},
23 model::{Model, RuntimeOp},
24 utils::dag::Dag,
25};
26
27use crate::{
28 cuda,
29 device::{
30 CudaArgPacker, CudaDevice, CudaLaunchConfig,
31 mem::{self, DevicePtr},
32 program::{CudaProgram, ErasedKernel},
33 },
34 errors::{Error, Result},
35};
36
37#[cfg(feature = "training")]
42const GRAD_ACCUM_F32_PTX: &[u8] = b"\
43// meta:name=grad_accum_f32\n\
44// meta:num_warps=4\n\
45.version 7.0\n\
46.target sm_60\n\
47.address_size 64\n\
48\n\
49.visible .entry grad_accum_f32(\n\
50 .param .u64 param0,\n\
51 .param .u64 param1,\n\
52 .param .u32 param2,\n\
53 .param .u64 param3,\n\
54 .param .u64 param4\n\
55)\n\
56{\n\
57 .reg .pred %p0;\n\
58 .reg .u32 %r<5>;\n\
59 .reg .u64 %rd<5>;\n\
60 .reg .f32 %f<3>;\n\
61 ld.param.u64 %rd0, [param0];\n\
62 ld.param.u64 %rd1, [param1];\n\
63 ld.param.u32 %r0, [param2];\n\
64 mov.u32 %r1, %ctaid.x;\n\
65 mov.u32 %r2, %ntid.x;\n\
66 mov.u32 %r3, %tid.x;\n\
67 mad.lo.u32 %r4, %r1, %r2, %r3;\n\
68 setp.ge.u32 %p0, %r4, %r0;\n\
69 @%p0 bra $L__return;\n\
70 mul.wide.u32 %rd2, %r4, 4;\n\
71 add.u64 %rd3, %rd0, %rd2;\n\
72 add.u64 %rd4, %rd1, %rd2;\n\
73 ld.global.f32 %f0, [%rd3];\n\
74 ld.global.f32 %f1, [%rd4];\n\
75 add.f32 %f2, %f0, %f1;\n\
76 st.global.f32 [%rd3], %f2;\n\
77$L__return:\n\
78 ret;\n\
79}\n\
80";
81
82#[derive(Clone, Debug)]
90pub struct TensorRef {
91 pub ptr: DevicePtr,
93 pub shape: Vec<usize>,
95}
96
97impl TensorRef {
98 pub fn new(ptr: DevicePtr, shape: Vec<usize>) -> Self {
100 Self { ptr, shape }
101 }
102
103 pub fn n_elements(&self) -> usize {
105 self.shape.iter().product()
106 }
107
108 pub fn from_host_f32(data: &[f32], shape: Vec<usize>) -> Result<Self> {
113 assert_eq!(
114 data.len(),
115 shape.iter().product::<usize>(),
116 "data length must match shape product"
117 );
118 let ptr = mem::alloc(std::mem::size_of_val(data))?;
119 unsafe { mem::copy_h_to_d(ptr, data.as_ptr(), data.len()) }?;
120 Ok(Self { ptr, shape })
121 }
122
123 pub fn to_host_f32(&self) -> Result<Vec<f32>> {
125 let n = self.n_elements();
126 let mut out = vec![0.0_f32; n];
127 unsafe { mem::copy_d_to_h(out.as_mut_ptr(), self.ptr, n) }?;
128 Ok(out)
129 }
130
131 pub fn free(self) -> Result<()> {
138 mem::free(self.ptr)
139 }
140}
141
142fn dtype_bytes(dtype: DtypeRepr) -> usize {
143 match dtype {
144 DtypeRepr::Bool | DtypeRepr::I8 | DtypeRepr::U8 => 1,
145 DtypeRepr::I16 | DtypeRepr::U16 | DtypeRepr::F16 | DtypeRepr::BF16 => 2,
146 DtypeRepr::I32 | DtypeRepr::U32 | DtypeRepr::F32 => 4,
147 DtypeRepr::I64 | DtypeRepr::U64 | DtypeRepr::F64 => 8,
148 }
149}
150
151fn resolve_shape(shape: &Shape, batch_size: usize) -> Vec<usize> {
152 shape.iter().map(|d| d.unwrap_or(batch_size)).collect()
153}
154
155pub struct CompiledNode {
162 pub ptx_path: String,
164 pub entry_point: String,
166 pub output_shape: Shape,
168 pub output_dtype: DtypeRepr,
170 pub runtime_op: Option<Arc<dyn RuntimeOp>>,
172 #[cfg(feature = "training")]
174 pub backward_ptx_path: Option<String>,
175 #[cfg(feature = "training")]
177 pub backward_entry_point: String,
178}
179
180pub struct CudaModel<'a> {
187 pub dag: Dag<CompiledNode>,
189 pub names: HashMap<usize, String>,
192 _marker: PhantomData<&'a ()>,
193}
194
195impl<'a> Model<'a> for CudaModel<'a> {
196 type Input = TensorRef;
197 type Output = TensorRef;
198
199 fn forward(&self, _input: Self::Input) -> teeny_core::errors::Result<Self::Output> {
200 Err(anyhow!(
201 "call CudaModel::load() first, then LoadedModel::forward()"
202 ))
203 }
204}
205
206impl<'a> CudaModel<'a> {
207 pub fn new(dag: Dag<CompiledNode>) -> Result<Self> {
209 Ok(Self {
210 dag,
211 names: HashMap::new(),
212 _marker: PhantomData,
213 })
214 }
215
216 pub fn with_names(dag: Dag<CompiledNode>, names: HashMap<usize, String>) -> Result<Self> {
218 Ok(Self {
219 dag,
220 names,
221 _marker: PhantomData,
222 })
223 }
224
225 pub fn load(self, _device: &CudaDevice<'_>, batch_size: usize) -> Result<LoadedModel> {
232 let names = self.names;
233 let n = self.dag.len();
234 let topo = self.dag.topological_sort();
235
236 let parents: Vec<Vec<usize>> = (0..n).map(|i| self.dag.node(i).parents.clone()).collect();
238
239 let compiled: Vec<CompiledNode> = self.dag.into_iter().map(|node| node.value).collect();
241
242 let mut loaded_nodes: Vec<Option<LoadedNode>> = (0..n).map(|_| None).collect();
243
244 let mut concrete_shapes: Vec<Vec<usize>> = compiled
248 .iter()
249 .map(|cn| resolve_shape(&cn.output_shape, batch_size))
250 .collect();
251
252 for &idx in &topo {
253 let cn = &compiled[idx];
254 let Some(rop) = cn.runtime_op.as_ref() else {
255 continue;
257 };
258
259 let parent_shapes: Vec<Vec<usize>> = parents[idx]
261 .iter()
262 .map(|&p| concrete_shapes[p].clone())
263 .collect();
264 let parent_shape_refs: Vec<&[usize]> =
265 parent_shapes.iter().map(|s| s.as_slice()).collect();
266 let raw_output_shape = resolve_shape(&cn.output_shape, batch_size);
267 let output_shape =
268 rop.compute_concrete_output_shape(&parent_shape_refs, &raw_output_shape);
269 concrete_shapes[idx] = output_shape.clone();
270
271 let p_shapes = rop.param_shapes(&parent_shape_refs, &output_shape);
273 let mut param_bufs: Vec<DevicePtr> = Vec::with_capacity(p_shapes.len());
274 for (pi, ps) in p_shapes.iter().enumerate() {
275 let n_elems: usize = ps.iter().product();
276 let byte_size = n_elems * dtype_bytes(cn.output_dtype);
277 let ptr = mem::alloc(byte_size)?;
278 unsafe { cuda::cuMemsetD8_v2(ptr, 0, byte_size) };
279 if let Some(cpu_bytes) = rop.param_init_data(pi) {
280 unsafe { mem::copy_h_to_d::<u8>(ptr, cpu_bytes.as_ptr(), cpu_bytes.len())? };
281 }
282 param_bufs.push(ptr);
283 }
284
285 let ptx = std::fs::read(&cn.ptx_path)
287 .map_err(|e| anyhow!("failed to read PTX for node {idx}: {e}"))?;
288 let program = CudaProgram::<ErasedKernel>::try_from_ptx(&ptx)?;
289
290 #[cfg(feature = "training")]
291 let backward_program = if let Some(ref bwd_path) = cn.backward_ptx_path {
292 let bwd_ptx = std::fs::read(bwd_path)
293 .map_err(|e| anyhow!("failed to read backward PTX for node {idx}: {e}"))?;
294 Some(CudaProgram::<ErasedKernel>::try_from_ptx(&bwd_ptx)?)
295 } else {
296 None
297 };
298
299 #[cfg(feature = "training")]
301 let (grad_param_bufs, optim_m_bufs, optim_v_bufs) = {
302 let mut grads = Vec::with_capacity(p_shapes.len());
303 let mut ms = Vec::with_capacity(p_shapes.len());
304 let mut vs = Vec::with_capacity(p_shapes.len());
305 for ps in &p_shapes {
306 let n_elems: usize = ps.iter().product();
307 let byte_size = n_elems * dtype_bytes(cn.output_dtype);
308 let gp = mem::alloc(byte_size)?;
309 let mp = mem::alloc(byte_size)?;
310 let vp = mem::alloc(byte_size)?;
311 unsafe {
312 cuda::cuMemsetD8_v2(gp, 0, byte_size);
313 cuda::cuMemsetD8_v2(mp, 0, byte_size);
314 cuda::cuMemsetD8_v2(vp, 0, byte_size);
315 }
316 grads.push(gp);
317 ms.push(mp);
318 vs.push(vp);
319 }
320 (grads, ms, vs)
321 };
322
323 loaded_nodes[idx] = Some(LoadedNode {
324 program,
325 output_shape: cn.output_shape.clone(),
326 output_dtype: cn.output_dtype,
327 runtime_op: Arc::clone(rop),
328 param_bufs,
329 param_shapes: p_shapes,
330 #[cfg(feature = "training")]
331 backward_program,
332 #[cfg(feature = "training")]
333 grad_param_bufs,
334 #[cfg(feature = "training")]
335 optim_m_bufs,
336 #[cfg(feature = "training")]
337 optim_v_bufs,
338 });
339 }
340
341 Ok(LoadedModel {
342 nodes: loaded_nodes,
343 parents,
344 names,
345 #[cfg(feature = "training")]
346 optim_step: 0,
347 #[cfg(feature = "training")]
348 accum_program: None,
349 })
350 }
351}
352
353struct LoadedNode {
358 program: CudaProgram<'static, ErasedKernel>,
359 output_shape: Shape,
360 output_dtype: DtypeRepr,
361 runtime_op: Arc<dyn RuntimeOp>,
362 param_bufs: Vec<DevicePtr>,
364 param_shapes: Vec<Vec<usize>>,
366 #[cfg(feature = "training")]
368 backward_program: Option<CudaProgram<'static, ErasedKernel>>,
369 #[cfg(feature = "training")]
371 grad_param_bufs: Vec<DevicePtr>,
372 #[cfg(feature = "training")]
374 optim_m_bufs: Vec<DevicePtr>,
375 #[cfg(feature = "training")]
377 optim_v_bufs: Vec<DevicePtr>,
378}
379
380impl Drop for LoadedNode {
381 fn drop(&mut self) {
382 for &ptr in &self.param_bufs {
383 if let Err(e) = mem::free(ptr) {
384 eprintln!("LoadedNode: failed to free param buffer: {e}");
385 }
386 }
387 #[cfg(feature = "training")]
388 for &ptr in &self.grad_param_bufs {
389 if let Err(e) = mem::free(ptr) {
390 eprintln!("LoadedNode: failed to free grad param buffer: {e}");
391 }
392 }
393 #[cfg(feature = "training")]
394 for &ptr in &self.optim_m_bufs {
395 if let Err(e) = mem::free(ptr) {
396 eprintln!("LoadedNode: failed to free optim m buffer: {e}");
397 }
398 }
399 #[cfg(feature = "training")]
400 for &ptr in &self.optim_v_bufs {
401 if let Err(e) = mem::free(ptr) {
402 eprintln!("LoadedNode: failed to free optim v buffer: {e}");
403 }
404 }
405 }
406}
407
408pub struct LoadedModel {
415 nodes: Vec<Option<LoadedNode>>,
417 parents: Vec<Vec<usize>>,
419 names: HashMap<usize, String>,
421 #[cfg(feature = "training")]
423 optim_step: u32,
424 #[cfg(feature = "training")]
426 accum_program: Option<CudaProgram<'static, ErasedKernel>>,
427}
428
429impl LoadedModel {
430 pub fn param_info(&self) -> impl Iterator<Item = (usize, &[Vec<usize>])> {
436 self.nodes.iter().enumerate().filter_map(|(idx, node)| {
437 node.as_ref()
438 .filter(|n| !n.param_shapes.is_empty())
439 .map(|n| (idx, n.param_shapes.as_slice()))
440 })
441 }
442
443 pub fn param_info_named(&self) -> impl Iterator<Item = (String, usize, usize)> + '_ {
450 self.nodes
451 .iter()
452 .enumerate()
453 .filter_map(|(node_idx, node)| {
454 let n = node.as_ref().filter(|n| !n.param_shapes.is_empty())?;
455 let node_name = self.names.get(&node_idx)?;
456 let slot_names = n.runtime_op.param_names();
457 Some((node_idx, node_name, n, slot_names))
458 })
459 .flat_map(|(node_idx, node_name, n, slot_names)| {
460 (0..n.param_shapes.len()).filter_map(move |param_idx| {
461 let slot = slot_names.get(param_idx).copied().unwrap_or("");
462 if slot.is_empty() {
463 return None;
464 }
465 let key = format!("{node_name}.{slot}");
466 Some((key, node_idx, param_idx))
467 })
468 })
469 }
470
471 pub fn load_param_f32(
477 &mut self,
478 node_idx: usize,
479 param_idx: usize,
480 data: &[f32],
481 ) -> Result<()> {
482 let node = self.nodes[node_idx]
483 .as_ref()
484 .ok_or_else(|| anyhow!("node {node_idx} is an Input placeholder"))?;
485 let ptr = *node
486 .param_bufs
487 .get(param_idx)
488 .ok_or_else(|| anyhow!("node {node_idx} has no param at index {param_idx}"))?;
489 unsafe { mem::copy_h_to_d(ptr, data.as_ptr(), data.len()) }
490 }
491
492 #[cfg(feature = "training")]
496 pub fn read_param_grad_f32(&self, node_idx: usize, param_idx: usize) -> Result<Vec<f32>> {
497 let node = self.nodes[node_idx]
498 .as_ref()
499 .ok_or_else(|| anyhow!("node {node_idx} is an Input placeholder"))?;
500 let &ptr = node
501 .grad_param_bufs
502 .get(param_idx)
503 .ok_or_else(|| anyhow!("node {node_idx} has no grad param at index {param_idx}"))?;
504 let n_elems: usize = node.param_shapes[param_idx].iter().product();
505 let mut out = vec![0.0_f32; n_elems];
506 unsafe { mem::copy_d_to_h(out.as_mut_ptr(), ptr, n_elems) }?;
507 Ok(out)
508 }
509
510 pub fn node_parents(&self, idx: usize) -> &[usize] {
512 self.parents.get(idx).map(|v| v.as_slice()).unwrap_or(&[])
513 }
514
515 pub fn forward(
525 &self,
526 device: &CudaDevice<'_>,
527 batch_size: usize,
528 inputs: &[TensorRef],
529 ) -> Result<TensorRef> {
530 let n = self.nodes.len();
531 let topo = self.topo_sort();
532
533 let mut ctx: Vec<Option<TensorRef>> = vec![None; n];
535 let mut intermediate_ptrs: Vec<DevicePtr> = Vec::new();
537 let mut input_cursor = 0usize;
538
539 for &idx in &topo {
540 if self.nodes[idx].is_none() {
541 let tr = inputs
543 .get(input_cursor)
544 .ok_or_else(|| anyhow!("too few inputs: needed >{input_cursor}"))?
545 .clone();
546 ctx[idx] = Some(tr);
547 input_cursor += 1;
548 continue;
549 }
550
551 let loaded = self.nodes[idx].as_ref().unwrap();
552
553 let parent_refs: Vec<&TensorRef> = self.parents[idx]
555 .iter()
556 .map(|&p| {
557 ctx[p]
558 .as_ref()
559 .expect("parent must be computed before child")
560 })
561 .collect();
562 let act_input_shapes: Vec<&[usize]> =
563 parent_refs.iter().map(|tr| tr.shape.as_slice()).collect();
564 let raw_output_shape = resolve_shape(&loaded.output_shape, batch_size);
565 let output_shape = loaded
566 .runtime_op
567 .compute_concrete_output_shape(&act_input_shapes, &raw_output_shape);
568
569 let n_elems: usize = output_shape.iter().product();
571 let elem_bytes = dtype_bytes(loaded.output_dtype);
572 let byte_size = n_elems * elem_bytes;
573 let out_ptr = mem::alloc(byte_size)?;
574 intermediate_ptrs.push(out_ptr);
575
576 let natural_stride = loaded.runtime_op.forward_output_row_elems(&output_shape);
579 let required_stride = loaded.runtime_op.forward_output_row_stride(&output_shape);
580 let n_rows = output_shape.iter().product::<usize>() / natural_stride.max(1);
581
582 let (kernel_out_ptr, padded_out) = if required_stride > natural_stride {
583 let padded_bytes = n_rows * required_stride * elem_bytes;
584 let padded = mem::alloc(padded_bytes)?;
585 unsafe {
586 cuda::cuMemsetD8_v2(padded, 0, padded_bytes);
587 }
588 (padded, Some(padded))
589 } else {
590 (out_ptr, None)
591 };
592
593 let act_inputs: Vec<(teeny_core::model::RawPtr, &[usize])> = parent_refs
595 .iter()
596 .map(|tr| (tr.ptr as *mut core::ffi::c_void, tr.shape.as_slice()))
597 .collect();
598
599 let param_ptrs: Vec<teeny_core::model::RawPtr> = loaded
600 .param_bufs
601 .iter()
602 .map(|&p| p as *mut core::ffi::c_void)
603 .collect();
604
605 let n_launches = loaded.runtime_op.n_launches();
608 let input_shapes: Vec<&[usize]> = act_inputs.iter().map(|(_, s)| *s).collect();
609 let block = [loaded.program.metadata.threads_per_block(), 1, 1];
610 let cluster = [loaded.program.metadata.num_ctas, 1, 1];
611 let out_raw = kernel_out_ptr as *mut core::ffi::c_void;
612
613 let mut last_result = Ok(());
614 for launch_idx in 0..n_launches {
615 let mut packer = CudaArgPacker::new();
616 if n_launches == 1 {
617 loaded.runtime_op.pack_args(
618 &act_inputs,
619 ¶m_ptrs,
620 out_raw,
621 &output_shape,
622 required_stride as i32,
623 &mut packer,
624 );
625 } else {
626 loaded.runtime_op.pack_args_for_launch(
627 launch_idx,
628 &act_inputs,
629 ¶m_ptrs,
630 out_raw,
631 &output_shape,
632 required_stride as i32,
633 &mut packer,
634 );
635 }
636 let grid = if n_launches == 1 {
637 loaded.runtime_op.grid(&output_shape)
638 } else {
639 loaded
640 .runtime_op
641 .grid_for_launch(launch_idx, &input_shapes, &output_shape)
642 };
643 last_result = device.launch_with_packer(
644 &loaded.program,
645 &CudaLaunchConfig {
646 grid,
647 block,
648 cluster,
649 },
650 &mut packer,
651 );
652 if last_result.is_err() {
653 break;
654 }
655 }
656
657 if let Some(padded) = padded_out {
659 if last_result.is_ok() {
660 mem::copy_rows_d_to_d(
661 out_ptr,
662 natural_stride * elem_bytes,
663 padded,
664 required_stride * elem_bytes,
665 natural_stride * elem_bytes,
666 n_rows,
667 )?;
668 }
669 mem::free(padded).ok();
670 }
671 last_result?;
672
673 ctx[idx] = Some(TensorRef::new(out_ptr, output_shape));
674 }
675
676 let last_idx = *topo.last().ok_or_else(|| anyhow!("empty model"))?;
677 let result = ctx[last_idx]
678 .clone()
679 .ok_or_else(|| anyhow!("last node produced no output"))?;
680
681 for ptr in intermediate_ptrs {
684 if ptr != result.ptr {
685 let _ = mem::free(ptr).map_err(|e| {
686 eprintln!("LoadedModel::forward: failed to free intermediate buffer: {e}");
687 });
688 }
689 }
690
691 Ok(result)
692 }
693
694 #[cfg(feature = "training")]
702 pub fn forward_train(
703 &self,
704 device: &CudaDevice<'_>,
705 batch_size: usize,
706 inputs: &[TensorRef],
707 ) -> Result<(TensorRef, ActivationCache)> {
708 let n = self.nodes.len();
709 let topo = self.topo_sort();
710
711 let mut ctx: Vec<Option<TensorRef>> = vec![None; n];
712 let mut input_cursor = 0usize;
713
714 for &idx in &topo {
715 if self.nodes[idx].is_none() {
716 let tr = inputs
717 .get(input_cursor)
718 .ok_or_else(|| anyhow!("too few inputs: needed >{input_cursor}"))?
719 .clone();
720 ctx[idx] = Some(tr);
721 input_cursor += 1;
722 continue;
723 }
724
725 let loaded = self.nodes[idx].as_ref().unwrap();
726
727 let parent_refs: Vec<&TensorRef> = self.parents[idx]
728 .iter()
729 .map(|&p| {
730 ctx[p]
731 .as_ref()
732 .expect("parent must be computed before child")
733 })
734 .collect();
735 let act_inputs: Vec<(teeny_core::model::RawPtr, &[usize])> = parent_refs
736 .iter()
737 .map(|tr| (tr.ptr as *mut core::ffi::c_void, tr.shape.as_slice()))
738 .collect();
739 let param_ptrs: Vec<teeny_core::model::RawPtr> = loaded
740 .param_bufs
741 .iter()
742 .map(|&p| p as *mut core::ffi::c_void)
743 .collect();
744
745 let input_shapes: Vec<&[usize]> = act_inputs.iter().map(|(_, s)| *s).collect();
746 let raw_output_shape = resolve_shape(&loaded.output_shape, batch_size);
747 let output_shape = loaded
748 .runtime_op
749 .compute_concrete_output_shape(&input_shapes, &raw_output_shape);
750
751 let n_elems: usize = output_shape.iter().product();
752 let elem_bytes = dtype_bytes(loaded.output_dtype);
753 let byte_size = n_elems * elem_bytes;
754 let out_ptr = mem::alloc(byte_size)?;
755
756 let natural_stride = loaded.runtime_op.forward_output_row_elems(&output_shape);
758 let required_stride = loaded.runtime_op.forward_output_row_stride(&output_shape);
759 let n_rows = output_shape.iter().product::<usize>() / natural_stride.max(1);
760
761 let (kernel_out_ptr, padded_out) = if required_stride > natural_stride {
762 let padded_bytes = n_rows * required_stride * elem_bytes;
763 let padded = mem::alloc(padded_bytes)?;
764 unsafe {
765 cuda::cuMemsetD8_v2(padded, 0, padded_bytes);
766 }
767 (padded, Some(padded))
768 } else {
769 (out_ptr, None)
770 };
771
772 let n_launches = loaded.runtime_op.n_launches();
773 let block = [loaded.program.metadata.threads_per_block(), 1, 1];
774 let cluster = [loaded.program.metadata.num_ctas, 1, 1];
775 let out_raw = kernel_out_ptr as *mut core::ffi::c_void;
776
777 let mut launch_result = Ok(());
778 for launch_idx in 0..n_launches {
779 let mut packer = CudaArgPacker::new();
780 if n_launches == 1 {
781 loaded.runtime_op.pack_args(
782 &act_inputs,
783 ¶m_ptrs,
784 out_raw,
785 &output_shape,
786 required_stride as i32,
787 &mut packer,
788 );
789 } else {
790 loaded.runtime_op.pack_args_for_launch(
791 launch_idx,
792 &act_inputs,
793 ¶m_ptrs,
794 out_raw,
795 &output_shape,
796 required_stride as i32,
797 &mut packer,
798 );
799 }
800 let grid = if n_launches == 1 {
801 loaded.runtime_op.grid(&output_shape)
802 } else {
803 loaded
804 .runtime_op
805 .grid_for_launch(launch_idx, &input_shapes, &output_shape)
806 };
807 launch_result = device.launch_with_packer(
808 &loaded.program,
809 &CudaLaunchConfig {
810 grid,
811 block,
812 cluster,
813 },
814 &mut packer,
815 );
816 if launch_result.is_err() {
817 break;
818 }
819 }
820
821 if let Some(padded) = padded_out {
822 if launch_result.is_ok() {
823 mem::copy_rows_d_to_d(
824 out_ptr,
825 natural_stride * elem_bytes,
826 padded,
827 required_stride * elem_bytes,
828 natural_stride * elem_bytes,
829 n_rows,
830 )?;
831 }
832 mem::free(padded).ok();
833 }
834 launch_result?;
835
836 ctx[idx] = Some(TensorRef::new(out_ptr, output_shape));
837 }
838
839 let last_idx = *topo.last().ok_or_else(|| anyhow!("empty model"))?;
840 let output = ctx[last_idx]
841 .clone()
842 .ok_or_else(|| anyhow!("last node produced no output"))?;
843
844 Ok((output, ActivationCache { tensors: ctx }))
845 }
846
847 #[cfg(feature = "training")]
849 pub fn zero_grad(&mut self) {
850 for node in self.nodes.iter().flatten() {
851 for (&gp, ps) in node.grad_param_bufs.iter().zip(node.param_shapes.iter()) {
852 let byte_size = ps.iter().product::<usize>() * dtype_bytes(node.output_dtype);
853 unsafe {
854 cuda::cuMemsetD8_v2(gp, 0, byte_size);
855 }
856 }
857 }
858 }
859
860 #[cfg(feature = "training")]
867 #[allow(clippy::too_many_arguments)]
868 pub fn adamw_step(
869 &mut self,
870 device: &CudaDevice<'_>,
871 kernel: &AdamwKernel,
872 lr: f32,
873 beta1: f32,
874 beta2: f32,
875 eps: f32,
876 weight_decay: f32,
877 ) -> Result<()> {
878 self.optim_step += 1;
879 let bias_correction1 = 1.0_f32 - beta1.powi(self.optim_step as i32);
880 let bias_correction2 = 1.0_f32 - beta2.powi(self.optim_step as i32);
881 let step_size = lr / bias_correction1;
882 let bias_corr2_sqrt = bias_correction2.sqrt();
883
884 for node in self.nodes.iter().flatten() {
885 if node.param_bufs.is_empty() {
886 continue;
887 }
888 for i in 0..node.param_bufs.len() {
889 let n_elems: usize = node.param_shapes[i].iter().product();
890 let mut packer = CudaArgPacker::new();
891 packer.visit_ptr(node.param_bufs[i] as *mut core::ffi::c_void); packer.visit_ptr(node.grad_param_bufs[i] as *mut core::ffi::c_void); packer.visit_ptr(node.optim_m_bufs[i] as *mut core::ffi::c_void); packer.visit_ptr(node.optim_v_bufs[i] as *mut core::ffi::c_void); packer.visit_i32(n_elems as i32); packer.visit_f32(step_size); packer.visit_f32(bias_corr2_sqrt); packer.visit_f32(beta1); packer.visit_f32(beta2); packer.visit_f32(eps); packer.visit_f32(weight_decay); packer.visit_f32(lr); let threads = kernel.program.metadata.threads_per_block();
905 let grid = [n_elems.div_ceil(threads as usize) as u32, 1, 1];
906 let block = [threads, 1, 1];
907 device.launch_with_packer(
908 &kernel.program,
909 &CudaLaunchConfig {
910 grid,
911 block,
912 cluster: [1, 1, 1],
913 },
914 &mut packer,
915 )?;
916 }
917 }
918 Ok(())
919 }
920
921 pub fn terminal_node_indices(&self) -> Vec<usize> {
926 let n = self.nodes.len();
927 let mut has_child = vec![false; n];
928 for i in 0..n {
929 for &p in &self.parents[i] {
930 has_child[p] = true;
931 }
932 }
933 (0..n).filter(|&i| !has_child[i]).collect()
934 }
935
936 pub fn node_name(&self, idx: usize) -> Option<&str> {
943 self.names.get(&idx).map(|s| s.as_str())
944 }
945
946 pub fn terminal_node_indices_sorted_by_size(&self) -> Vec<usize> {
948 let mut terminals = self.terminal_node_indices();
949 terminals.sort_by_key(|&i| {
950 self.nodes[i]
951 .as_ref()
952 .map(|n| n.output_shape.iter().filter_map(|&d| d).product::<usize>())
953 .unwrap_or(0)
954 });
955 terminals
956 }
957
958 #[cfg(feature = "training")]
963 pub fn backward(
964 &mut self,
965 device: &CudaDevice<'_>,
966 batch_size: usize,
967 grad_output: TensorRef,
968 cache: &ActivationCache,
969 ) -> Result<()> {
970 let topo = self.topo_sort();
971 let last_idx = *topo.last().ok_or_else(|| anyhow!("empty model"))?;
972 self.backward_multi(device, batch_size, &[(last_idx, grad_output)], cache)
973 }
974
975 #[cfg(feature = "training")]
980 pub fn backward_multi(
981 &mut self,
982 device: &CudaDevice<'_>,
983 batch_size: usize,
984 seed_grads: &[(usize, TensorRef)],
985 cache: &ActivationCache,
986 ) -> Result<()> {
987 if self.accum_program.is_none() {
989 self.accum_program = Some(CudaProgram::<ErasedKernel>::try_from_ptx(
990 GRAD_ACCUM_F32_PTX,
991 )?);
992 }
993
994 let n = self.nodes.len();
995 let topo = self.topo_sort();
996
997 let mut grad_ctx: Vec<Option<DevicePtr>> = vec![None; n];
999 let mut owned_grad_ptrs: Vec<DevicePtr> = Vec::new();
1001
1002 for (node_idx, grad) in seed_grads {
1003 grad_ctx[*node_idx] = Some(grad.ptr);
1004 }
1005
1006 for &idx in topo.iter().rev() {
1007 let grad_in_ptr = match grad_ctx[idx] {
1008 Some(p) => p,
1009 None => continue,
1010 };
1011
1012 let parent_indices: Vec<usize> = self.parents[idx].clone();
1014
1015 {
1016 let loaded = match self.nodes[idx].as_ref() {
1017 Some(n) => n,
1018 None => continue,
1019 };
1020 let bwd_prog = match loaded.backward_program.as_ref() {
1021 Some(p) => p,
1022 None => continue,
1023 };
1024
1025 let output_shape = resolve_shape(&loaded.output_shape, batch_size);
1026 let node_out_ptr = cache.tensors[idx]
1027 .as_ref()
1028 .ok_or_else(|| anyhow!("activation cache missing for node {idx}"))?
1029 .ptr;
1030
1031 let parent_trs: Vec<&TensorRef> = parent_indices
1033 .iter()
1034 .map(|&p| {
1035 cache.tensors[p]
1036 .as_ref()
1037 .expect("activation cache must have parent activation")
1038 })
1039 .collect();
1040
1041 let act_inputs: Vec<(teeny_core::model::RawPtr, &[usize])> = parent_trs
1042 .iter()
1043 .map(|tr| (tr.ptr as *mut core::ffi::c_void, tr.shape.as_slice()))
1044 .collect();
1045 let param_ptrs: Vec<teeny_core::model::RawPtr> = loaded
1046 .param_bufs
1047 .iter()
1048 .map(|&p| p as *mut core::ffi::c_void)
1049 .collect();
1050 let grad_param_rawptrs: Vec<teeny_core::model::RawPtr> = loaded
1051 .grad_param_bufs
1052 .iter()
1053 .map(|&p| p as *mut core::ffi::c_void)
1054 .collect();
1055
1056 let mut grad_input_ptrs: Vec<DevicePtr> = Vec::with_capacity(parent_trs.len());
1058 for tr in &parent_trs {
1059 let n_elems: usize = tr.shape.iter().product();
1060 let byte_size = n_elems * dtype_bytes(loaded.output_dtype);
1061 let gptr = mem::alloc(byte_size)?;
1062 unsafe {
1063 cuda::cuMemsetD8_v2(gptr, 0, byte_size);
1064 }
1065 grad_input_ptrs.push(gptr);
1066 owned_grad_ptrs.push(gptr);
1067 }
1068
1069 let grad_input_rawptrs: Vec<teeny_core::model::RawPtr> = grad_input_ptrs
1070 .iter()
1071 .map(|&p| p as *mut core::ffi::c_void)
1072 .collect();
1073
1074 let input_shapes: Vec<&[usize]> =
1075 parent_trs.iter().map(|tr| tr.shape.as_slice()).collect();
1076
1077 let natural_stride = output_shape.last().copied().unwrap_or(1);
1081 let required_stride = loaded
1082 .runtime_op
1083 .backward_grad_output_row_stride(&output_shape);
1084 let elem_bytes = dtype_bytes(loaded.output_dtype);
1085 let n_rows = output_shape.iter().product::<usize>() / natural_stride.max(1);
1086
1087 let (dy_ptr, padded_dy) = if required_stride > natural_stride {
1088 let padded_bytes = n_rows * required_stride * elem_bytes;
1089 let padded = mem::alloc(padded_bytes)?;
1090 unsafe {
1091 cuda::cuMemsetD8_v2(padded, 0, padded_bytes);
1092 }
1093 mem::copy_rows_d_to_d(
1094 padded,
1095 required_stride * elem_bytes,
1096 grad_in_ptr,
1097 natural_stride * elem_bytes,
1098 natural_stride * elem_bytes,
1099 n_rows,
1100 )?;
1101 (padded, Some(padded))
1102 } else {
1103 (grad_in_ptr, None)
1104 };
1105
1106 let bwd_block = [bwd_prog.metadata.threads_per_block(), 1, 1];
1107 let bwd_cluster = [bwd_prog.metadata.num_ctas, 1, 1];
1108 let n_bwd_launches = loaded.runtime_op.n_backward_launches();
1109 let node_out_raw = node_out_ptr as teeny_core::model::RawPtr;
1110 let dy_raw = dy_ptr as teeny_core::model::RawPtr;
1111
1112 let mut bwd_result = Ok(());
1113 for launch_idx in 0..n_bwd_launches {
1114 let mut packer = CudaArgPacker::new();
1115 if n_bwd_launches == 1 {
1116 loaded.runtime_op.pack_backward_args(
1117 &act_inputs,
1118 ¶m_ptrs,
1119 node_out_raw,
1120 &output_shape,
1121 dy_raw,
1122 required_stride as i32,
1123 &grad_input_rawptrs,
1124 &grad_param_rawptrs,
1125 &mut packer,
1126 );
1127 } else {
1128 loaded.runtime_op.pack_backward_args_for_launch(
1129 launch_idx,
1130 &act_inputs,
1131 ¶m_ptrs,
1132 node_out_raw,
1133 &output_shape,
1134 dy_raw,
1135 required_stride as i32,
1136 &grad_input_rawptrs,
1137 &grad_param_rawptrs,
1138 &mut packer,
1139 );
1140 }
1141 let grid = if n_bwd_launches == 1 {
1142 loaded
1143 .runtime_op
1144 .backward_grid(&input_shapes, &output_shape)
1145 } else {
1146 loaded.runtime_op.backward_grid_for_launch(
1147 launch_idx,
1148 &input_shapes,
1149 &output_shape,
1150 )
1151 };
1152 bwd_result = device.launch_with_packer(
1153 bwd_prog,
1154 &CudaLaunchConfig {
1155 grid,
1156 block: bwd_block,
1157 cluster: bwd_cluster,
1158 },
1159 &mut packer,
1160 );
1161 if bwd_result.is_err() {
1162 break;
1163 }
1164 }
1165
1166 if let Some(padded) = padded_dy {
1168 mem::free(padded).ok();
1169 }
1170
1171 bwd_result?;
1172
1173 for (i, &pidx) in parent_indices.iter().enumerate() {
1176 if let Some(existing) = grad_ctx[pidx] {
1177 let n_elems: usize = parent_trs[i].shape.iter().product();
1178 self.accum_grad_f32(device, existing, grad_input_ptrs[i], n_elems)?;
1179 } else {
1180 grad_ctx[pidx] = Some(grad_input_ptrs[i]);
1181 }
1182 }
1183 }
1184 }
1185
1186 for ptr in owned_grad_ptrs {
1188 let _ = mem::free(ptr).map_err(|e| {
1189 eprintln!("LoadedModel::backward_multi: failed to free grad buffer: {e}");
1190 });
1191 }
1192
1193 Ok(())
1194 }
1195
1196 #[cfg(feature = "training")]
1198 fn accum_grad_f32(
1199 &self,
1200 device: &CudaDevice<'_>,
1201 dst: DevicePtr,
1202 src: DevicePtr,
1203 n_elems: usize,
1204 ) -> Result<()> {
1205 let prog = self
1206 .accum_program
1207 .as_ref()
1208 .expect("accum_program must be initialised before calling accum_grad_f32");
1209 let threads: u32 = prog.metadata.threads_per_block();
1210 let grid = [n_elems.div_ceil(threads as usize) as u32, 1, 1];
1211 let block = [threads, 1, 1];
1212 let mut packer = CudaArgPacker::new();
1213 packer.visit_ptr(dst as *mut core::ffi::c_void);
1214 packer.visit_ptr(src as *mut core::ffi::c_void);
1215 packer.visit_i32(n_elems as i32);
1216 device.launch_with_packer(
1217 prog,
1218 &CudaLaunchConfig {
1219 grid,
1220 block,
1221 cluster: [1, 1, 1],
1222 },
1223 &mut packer,
1224 )
1225 }
1226
1227 pub fn capture_graph(
1246 &self,
1247 device: &CudaDevice<'_>,
1248 batch_size: usize,
1249 input_shapes: &[Vec<usize>],
1250 output_node_indices: &[usize],
1251 ) -> Result<CudaGraphModel> {
1252 let n = self.nodes.len();
1253 let topo = self.topo_sort();
1254
1255 let mut concrete_shapes: Vec<Vec<usize>> = vec![vec![]; n];
1257 let mut input_cursor = 0usize;
1258 for &idx in &topo {
1259 if self.nodes[idx].is_none() {
1260 let shape = input_shapes
1261 .get(input_cursor)
1262 .ok_or_else(|| anyhow!("capture_graph: input_shapes[{input_cursor}] missing"))?
1263 .clone();
1264 concrete_shapes[idx] = shape;
1265 input_cursor += 1;
1266 } else {
1267 let loaded = self.nodes[idx].as_ref().unwrap();
1268 let parent_shapes: Vec<&[usize]> = self.parents[idx]
1269 .iter()
1270 .map(|&p| concrete_shapes[p].as_slice())
1271 .collect();
1272 let raw = resolve_shape(&loaded.output_shape, batch_size);
1273 concrete_shapes[idx] = loaded
1274 .runtime_op
1275 .compute_concrete_output_shape(&parent_shapes, &raw);
1276 }
1277 }
1278
1279 let mut owned: Vec<DevicePtr> = Vec::new();
1282 let mut node_out_bufs: Vec<DevicePtr> = vec![0; n];
1283 let mut node_scratch_bufs: Vec<DevicePtr> = vec![0; n];
1284 type PaddedEntry = Option<(DevicePtr, usize, usize, usize, usize)>;
1285 let mut node_padded: Vec<PaddedEntry> = vec![None; n];
1287 let mut input_bufs: Vec<(DevicePtr, usize)> = Vec::new();
1288
1289 for &idx in &topo {
1290 if self.nodes[idx].is_none() {
1291 let n_elems: usize = concrete_shapes[idx].iter().product();
1293 let ptr = mem::alloc(n_elems * 4)?;
1294 owned.push(ptr);
1295 node_out_bufs[idx] = ptr;
1296 input_bufs.push((ptr, n_elems));
1297 continue;
1298 }
1299
1300 let loaded = self.nodes[idx].as_ref().unwrap();
1301 let output_shape = &concrete_shapes[idx];
1302 let n_elems: usize = output_shape.iter().product();
1303 let elem_bytes = dtype_bytes(loaded.output_dtype);
1304
1305 let out_ptr = mem::alloc(n_elems * elem_bytes)?;
1307 owned.push(out_ptr);
1308 node_out_bufs[idx] = out_ptr;
1309
1310 let natural_stride = loaded.runtime_op.forward_output_row_elems(output_shape);
1312 let required_stride = loaded.runtime_op.forward_output_row_stride(output_shape);
1313 if required_stride > natural_stride {
1314 let n_rows = n_elems / natural_stride.max(1);
1315 let padded_bytes = n_rows * required_stride * elem_bytes;
1316 let padded_ptr = mem::alloc(padded_bytes)?;
1317 unsafe { cuda::cuMemsetD8_v2(padded_ptr, 0, padded_bytes) };
1318 owned.push(padded_ptr);
1319 node_padded[idx] = Some((
1320 padded_ptr,
1321 natural_stride,
1322 required_stride,
1323 n_rows,
1324 elem_bytes,
1325 ));
1326 }
1327
1328 let grid = loaded.runtime_op.grid(output_shape);
1330 let num_ctas = (grid[0] * grid[1] * grid[2]) as u64;
1331 let scratch_total = loaded.program.metadata.global_scratch_size * num_ctas;
1332 if scratch_total > 0 {
1333 let scratch_ptr = mem::alloc(scratch_total as usize)?;
1334 unsafe { cuda::cuMemsetD8_v2(scratch_ptr, 0, scratch_total as usize) };
1335 owned.push(scratch_ptr);
1336 node_scratch_bufs[idx] = scratch_ptr;
1337 }
1338 }
1339
1340 let mut stream: cuda::CUstream = std::ptr::null_mut();
1342 {
1343 let s = unsafe { cuda::cuStreamCreate(&mut stream, 0) };
1344 if s != cuda::cudaError_enum_CUDA_SUCCESS {
1345 for &p in &owned {
1346 let _ = mem::free(p);
1347 }
1348 return Err(Error::from_cuda_error(s).into());
1349 }
1350 }
1351
1352 {
1353 let s = unsafe {
1354 cuda::cuStreamBeginCapture_v2(
1355 stream,
1356 cuda::CUstreamCaptureMode_enum_CU_STREAM_CAPTURE_MODE_GLOBAL,
1357 )
1358 };
1359 if s != cuda::cudaError_enum_CUDA_SUCCESS {
1360 unsafe { cuda::cuStreamDestroy_v2(stream) };
1361 for &p in &owned {
1362 let _ = mem::free(p);
1363 }
1364 return Err(Error::from_cuda_error(s).into());
1365 }
1366 }
1367
1368 let mut capture_err: Option<anyhow::Error> = None;
1370 'capture: for &idx in &topo {
1371 let Some(loaded) = self.nodes[idx].as_ref() else {
1372 continue;
1373 };
1374 let output_shape = &concrete_shapes[idx];
1375
1376 let act_inputs: Vec<(teeny_core::model::RawPtr, &[usize])> = self.parents[idx]
1377 .iter()
1378 .map(|&p| {
1379 (
1380 node_out_bufs[p] as *mut core::ffi::c_void,
1381 concrete_shapes[p].as_slice(),
1382 )
1383 })
1384 .collect();
1385 let param_ptrs: Vec<teeny_core::model::RawPtr> = loaded
1386 .param_bufs
1387 .iter()
1388 .map(|&p| p as *mut core::ffi::c_void)
1389 .collect();
1390 let input_shapes_ref: Vec<&[usize]> = act_inputs.iter().map(|(_, s)| *s).collect();
1391
1392 let required_stride = loaded.runtime_op.forward_output_row_stride(output_shape);
1393 let scratch_ptr = node_scratch_bufs[idx];
1394 let (kernel_out_raw, tight_out_ptr) = if let Some((padded_ptr, ..)) = node_padded[idx] {
1395 (padded_ptr as *mut core::ffi::c_void, node_out_bufs[idx])
1396 } else {
1397 let op = node_out_bufs[idx];
1398 (op as *mut core::ffi::c_void, op)
1399 };
1400
1401 let block = [loaded.program.metadata.threads_per_block(), 1, 1];
1402 let cluster = [loaded.program.metadata.num_ctas, 1, 1];
1403 let n_launches = loaded.runtime_op.n_launches();
1404
1405 if scratch_ptr != 0 {
1407 let grid = loaded.runtime_op.grid(output_shape);
1408 let num_ctas = (grid[0] * grid[1] * grid[2]) as u64;
1409 let scratch_total = loaded.program.metadata.global_scratch_size * num_ctas;
1410 let s = unsafe {
1411 cuda::cuMemsetD8Async(scratch_ptr, 0, scratch_total as usize, stream)
1412 };
1413 if s != cuda::cudaError_enum_CUDA_SUCCESS {
1414 capture_err = Some(anyhow::anyhow!(
1415 "capture_graph: scratch memset failed at node {idx} \
1416 (shape={output_shape:?}, scratch_total={scratch_total}): {}",
1417 Error::from_cuda_error(s),
1418 ));
1419 break 'capture;
1420 }
1421 }
1422
1423 for launch_idx in 0..n_launches {
1424 let mut packer = CudaArgPacker::new();
1425 if n_launches == 1 {
1426 loaded.runtime_op.pack_args(
1427 &act_inputs,
1428 ¶m_ptrs,
1429 kernel_out_raw,
1430 output_shape,
1431 required_stride as i32,
1432 &mut packer,
1433 );
1434 } else {
1435 loaded.runtime_op.pack_args_for_launch(
1436 launch_idx,
1437 &act_inputs,
1438 ¶m_ptrs,
1439 kernel_out_raw,
1440 output_shape,
1441 required_stride as i32,
1442 &mut packer,
1443 );
1444 }
1445 packer.visit_ptr(scratch_ptr as *mut core::ffi::c_void);
1447 packer.visit_ptr(std::ptr::null_mut());
1448
1449 let grid = if n_launches == 1 {
1450 loaded.runtime_op.grid(output_shape)
1451 } else {
1452 loaded
1453 .runtime_op
1454 .grid_for_launch(launch_idx, &input_shapes_ref, output_shape)
1455 };
1456 let cfg = CudaLaunchConfig {
1457 grid,
1458 block,
1459 cluster,
1460 };
1461 if let Err(e) = device.launch_on_stream(&loaded.program, &cfg, &mut packer, stream)
1462 {
1463 capture_err = Some(anyhow::anyhow!(
1464 "capture_graph: launch failed at node {idx} \
1465 (grid={grid:?}, block={block:?}, cluster={cluster:?}, \
1466 shape={output_shape:?}, row_stride={required_stride}, \
1467 scratch={}): {e:#}",
1468 loaded.program.metadata.global_scratch_size,
1469 ));
1470 break 'capture;
1471 }
1472 }
1473
1474 if let Some((padded_ptr, ns, rs, n_rows, eb)) = node_padded[idx] {
1478 let params = cuda::CUDA_MEMCPY2D {
1479 srcMemoryType: cuda::CUmemorytype_enum_CU_MEMORYTYPE_DEVICE,
1480 srcDevice: padded_ptr,
1481 srcPitch: rs * eb,
1482 dstMemoryType: cuda::CUmemorytype_enum_CU_MEMORYTYPE_DEVICE,
1483 dstDevice: tight_out_ptr,
1484 dstPitch: ns * eb,
1485 WidthInBytes: ns * eb,
1486 Height: n_rows,
1487 ..Default::default()
1488 };
1489 let s = unsafe { cuda::cuMemcpy2DAsync_v2(¶ms, stream) };
1490 if s != cuda::cudaError_enum_CUDA_SUCCESS {
1491 capture_err = Some(anyhow::anyhow!(
1492 "capture_graph: cuMemcpy2DAsync depad failed at node {idx} \
1493 (ns={ns}, rs={rs}, n_rows={n_rows}, eb={eb}): {}",
1494 Error::from_cuda_error(s),
1495 ));
1496 break 'capture;
1497 }
1498 }
1499 }
1500
1501 let mut graph: cuda::CUgraph = std::ptr::null_mut();
1503 let end_s = unsafe { cuda::cuStreamEndCapture(stream, &mut graph) };
1504
1505 if let Some(err) = capture_err {
1506 unsafe {
1508 if !graph.is_null() {
1509 cuda::cuGraphDestroy(graph);
1510 }
1511 cuda::cuStreamDestroy_v2(stream);
1512 }
1513 for &p in &owned {
1514 let _ = mem::free(p);
1515 }
1516 return Err(err);
1517 }
1518 if end_s != cuda::cudaError_enum_CUDA_SUCCESS {
1519 unsafe {
1520 if !graph.is_null() {
1521 cuda::cuGraphDestroy(graph);
1522 }
1523 cuda::cuStreamDestroy_v2(stream);
1524 }
1525 for &p in &owned {
1526 let _ = mem::free(p);
1527 }
1528 return Err(anyhow::anyhow!(
1529 "capture_graph: cuStreamEndCapture failed: {}",
1530 Error::from_cuda_error(end_s)
1531 ));
1532 }
1533
1534 let mut graph_exec: cuda::CUgraphExec = std::ptr::null_mut();
1535 let inst_s = unsafe { cuda::cuGraphInstantiateWithFlags(&mut graph_exec, graph, 0) };
1536 unsafe { cuda::cuGraphDestroy(graph) };
1537 if inst_s != cuda::cudaError_enum_CUDA_SUCCESS {
1538 unsafe { cuda::cuStreamDestroy_v2(stream) };
1539 for &p in &owned {
1540 let _ = mem::free(p);
1541 }
1542 return Err(anyhow::anyhow!(
1543 "capture_graph: cuGraphInstantiateWithFlags failed: {}",
1544 Error::from_cuda_error(inst_s)
1545 ));
1546 }
1547
1548 if output_node_indices.is_empty() {
1549 unsafe {
1550 cuda::cuStreamDestroy_v2(stream);
1551 }
1552 for &p in &owned {
1553 let _ = mem::free(p);
1554 }
1555 return Err(anyhow!(
1556 "capture_graph: output_node_indices must not be empty"
1557 ));
1558 }
1559 let mut output_bufs: Vec<(DevicePtr, usize)> =
1560 Vec::with_capacity(output_node_indices.len());
1561 let mut output_shapes: Vec<Vec<usize>> = Vec::with_capacity(output_node_indices.len());
1562 for &oi in output_node_indices {
1563 if oi >= self.nodes.len() || self.nodes[oi].is_none() {
1564 unsafe {
1565 cuda::cuStreamDestroy_v2(stream);
1566 }
1567 for &p in &owned {
1568 let _ = mem::free(p);
1569 }
1570 return Err(anyhow!(
1571 "capture_graph: output node {oi} is an Input or out of bounds"
1572 ));
1573 }
1574 let shape = concrete_shapes[oi].clone();
1575 let n_elems: usize = shape.iter().product();
1576 output_bufs.push((node_out_bufs[oi], n_elems));
1577 output_shapes.push(shape);
1578 }
1579
1580 let mut pinned_inputs: Vec<*mut f32> = Vec::with_capacity(input_bufs.len());
1585 for &(_, n_elems) in &input_bufs {
1586 match mem::alloc_host::<f32>(n_elems) {
1587 Ok(ptr) => pinned_inputs.push(ptr),
1588 Err(e) => {
1589 unsafe {
1590 cuda::cuStreamDestroy_v2(stream);
1591 }
1592 for &p in &owned {
1593 let _ = mem::free(p);
1594 }
1595 for &p in &pinned_inputs {
1596 unsafe {
1597 let _ = mem::free_host(p);
1598 }
1599 }
1600 return Err(e);
1601 }
1602 }
1603 }
1604 let mut pinned_outputs: Vec<*mut f32> = Vec::with_capacity(output_bufs.len());
1605 for &(_, n_elems) in &output_bufs {
1606 match mem::alloc_host::<f32>(n_elems) {
1607 Ok(ptr) => pinned_outputs.push(ptr),
1608 Err(e) => {
1609 unsafe {
1610 cuda::cuStreamDestroy_v2(stream);
1611 }
1612 for &p in &owned {
1613 let _ = mem::free(p);
1614 }
1615 for &p in &pinned_inputs {
1616 unsafe {
1617 let _ = mem::free_host(p);
1618 }
1619 }
1620 for &p in &pinned_outputs {
1621 unsafe {
1622 let _ = mem::free_host(p);
1623 }
1624 }
1625 return Err(e);
1626 }
1627 }
1628 }
1629
1630 Ok(CudaGraphModel {
1631 stream,
1632 graph_exec,
1633 input_bufs,
1634 output_bufs,
1635 output_shapes,
1636 pinned_inputs,
1637 pinned_outputs,
1638 _owned: owned,
1639 })
1640 }
1641
1642 fn topo_sort(&self) -> Vec<usize> {
1643 let n = self.nodes.len();
1644 let mut in_deg: Vec<usize> = (0..n).map(|i| self.parents[i].len()).collect();
1645 let mut dependents: Vec<Vec<usize>> = vec![vec![]; n];
1646 for i in 0..n {
1647 for &p in &self.parents[i] {
1648 dependents[p].push(i);
1649 }
1650 }
1651 let mut stack: Vec<usize> = (0..n).filter(|&i| in_deg[i] == 0).collect();
1652 let mut order = Vec::with_capacity(n);
1653 while let Some(id) = stack.pop() {
1654 order.push(id);
1655 for &dep in &dependents[id] {
1656 in_deg[dep] -= 1;
1657 if in_deg[dep] == 0 {
1658 stack.push(dep);
1659 }
1660 }
1661 }
1662 order
1663 }
1664}
1665
1666#[cfg(feature = "training")]
1670pub struct ActivationCache {
1671 pub tensors: Vec<Option<TensorRef>>,
1674}
1675
1676#[cfg(feature = "training")]
1677impl Drop for ActivationCache {
1678 fn drop(&mut self) {
1679 for tr in self.tensors.iter().flatten() {
1680 if let Err(e) = mem::free(tr.ptr) {
1681 eprintln!("ActivationCache: failed to free buffer: {e}");
1682 }
1683 }
1684 }
1685}
1686
1687pub struct CudaGraphModel {
1697 stream: cuda::CUstream,
1698 graph_exec: cuda::CUgraphExec,
1699 input_bufs: Vec<(DevicePtr, usize)>,
1701 output_bufs: Vec<(DevicePtr, usize)>,
1703 output_shapes: Vec<Vec<usize>>,
1704 pinned_inputs: Vec<*mut f32>,
1708 pinned_outputs: Vec<*mut f32>,
1710 _owned: Vec<DevicePtr>,
1712}
1713
1714unsafe impl Send for CudaGraphModel {}
1717
1718impl CudaGraphModel {
1719 pub fn output_shapes(&self) -> &[Vec<usize>] {
1721 &self.output_shapes
1722 }
1723
1724 pub fn run_timed(&self, inputs: &[&[f32]]) -> Result<(Vec<Vec<f32>>, f32)> {
1730 if inputs.len() != self.input_bufs.len() {
1731 return Err(anyhow!(
1732 "CudaGraphModel::run_timed: expected {} inputs, got {}",
1733 self.input_bufs.len(),
1734 inputs.len()
1735 ));
1736 }
1737 for (i, (&(ptr, n_elems), &data)) in self.input_bufs.iter().zip(inputs.iter()).enumerate() {
1738 if data.len() != n_elems {
1739 return Err(anyhow!(
1740 "CudaGraphModel::run_timed: input[{i}] has {} elements, expected {n_elems}",
1741 data.len()
1742 ));
1743 }
1744 unsafe { mem::copy_h_to_d(ptr, data.as_ptr(), n_elems) }?;
1745 }
1746
1747 let mut ev_start = cuda::CUevent::default();
1749 let mut ev_end = cuda::CUevent::default();
1750 let cu_event_default = 0u32;
1751 unsafe {
1752 cuda::cuEventCreate(&mut ev_start, cu_event_default);
1753 cuda::cuEventCreate(&mut ev_end, cu_event_default);
1754 cuda::cuEventRecord(ev_start, self.stream);
1755 }
1756
1757 let launch_s = unsafe { cuda::cuGraphLaunch(self.graph_exec, self.stream) };
1758 if launch_s != cuda::cudaError_enum_CUDA_SUCCESS {
1759 unsafe {
1760 cuda::cuEventDestroy_v2(ev_start);
1761 cuda::cuEventDestroy_v2(ev_end);
1762 }
1763 return Err(Error::from_cuda_error(launch_s).into());
1764 }
1765
1766 unsafe {
1767 cuda::cuEventRecord(ev_end, self.stream);
1768 }
1769
1770 let sync_s = unsafe { cuda::cuEventSynchronize(ev_end) };
1771 if sync_s != cuda::cudaError_enum_CUDA_SUCCESS {
1772 unsafe {
1773 cuda::cuEventDestroy_v2(ev_start);
1774 cuda::cuEventDestroy_v2(ev_end);
1775 }
1776 return Err(Error::from_cuda_error(sync_s).into());
1777 }
1778
1779 let mut gpu_ms = 0.0f32;
1780 unsafe {
1781 cuda::cudaEventElapsedTime(&mut gpu_ms, ev_start, ev_end);
1782 cuda::cuEventDestroy_v2(ev_start);
1783 cuda::cuEventDestroy_v2(ev_end);
1784 }
1785
1786 let mut result = Vec::with_capacity(self.output_bufs.len());
1787 for &(ptr, n_elems) in &self.output_bufs {
1788 let mut out = vec![0.0_f32; n_elems];
1789 unsafe { mem::copy_d_to_h(out.as_mut_ptr(), ptr, n_elems) }?;
1790 result.push(out);
1791 }
1792 Ok((result, gpu_ms))
1793 }
1794
1795 pub fn run(&self, inputs: &[&[f32]]) -> Result<Vec<Vec<f32>>> {
1800 if inputs.len() != self.input_bufs.len() {
1801 return Err(anyhow!(
1802 "CudaGraphModel::run: expected {} inputs, got {}",
1803 self.input_bufs.len(),
1804 inputs.len()
1805 ));
1806 }
1807 for (i, (&(ptr, n_elems), &data)) in self.input_bufs.iter().zip(inputs.iter()).enumerate() {
1808 if data.len() != n_elems {
1809 return Err(anyhow!(
1810 "CudaGraphModel::run: input[{i}] has {} elements, expected {n_elems}",
1811 data.len()
1812 ));
1813 }
1814 unsafe { mem::copy_h_to_d(ptr, data.as_ptr(), n_elems) }?;
1815 }
1816
1817 let launch_s = unsafe { cuda::cuGraphLaunch(self.graph_exec, self.stream) };
1818 if launch_s != cuda::cudaError_enum_CUDA_SUCCESS {
1819 return Err(Error::from_cuda_error(launch_s).into());
1820 }
1821 let sync_s = unsafe { cuda::cuStreamSynchronize(self.stream) };
1822 if sync_s != cuda::cudaError_enum_CUDA_SUCCESS {
1823 return Err(Error::from_cuda_error(sync_s).into());
1824 }
1825
1826 let mut result = Vec::with_capacity(self.output_bufs.len());
1827 for &(ptr, n_elems) in &self.output_bufs {
1828 let mut out = vec![0.0_f32; n_elems];
1829 unsafe { mem::copy_d_to_h(out.as_mut_ptr(), ptr, n_elems) }?;
1830 result.push(out);
1831 }
1832 Ok(result)
1833 }
1834
1835 pub fn input_slice_mut(&mut self, i: usize) -> &mut [f32] {
1844 let (_, n_elems) = self.input_bufs[i];
1845 unsafe { std::slice::from_raw_parts_mut(self.pinned_inputs[i], n_elems) }
1846 }
1847
1848 pub fn output_slice(&self, i: usize) -> &[f32] {
1851 let (_, n_elems) = self.output_bufs[i];
1852 unsafe { std::slice::from_raw_parts(self.pinned_outputs[i], n_elems) }
1853 }
1854
1855 pub fn run_inplace(&self) -> Result<()> {
1860 for (i, &(dev_ptr, n_elems)) in self.input_bufs.iter().enumerate() {
1861 unsafe { mem::copy_h_to_d(dev_ptr, self.pinned_inputs[i], n_elems) }?;
1862 }
1863 let launch_s = unsafe { cuda::cuGraphLaunch(self.graph_exec, self.stream) };
1864 if launch_s != cuda::cudaError_enum_CUDA_SUCCESS {
1865 return Err(Error::from_cuda_error(launch_s).into());
1866 }
1867 let sync_s = unsafe { cuda::cuStreamSynchronize(self.stream) };
1868 if sync_s != cuda::cudaError_enum_CUDA_SUCCESS {
1869 return Err(Error::from_cuda_error(sync_s).into());
1870 }
1871 for (i, &(dev_ptr, n_elems)) in self.output_bufs.iter().enumerate() {
1872 unsafe { mem::copy_d_to_h(self.pinned_outputs[i], dev_ptr, n_elems) }?;
1873 }
1874 Ok(())
1875 }
1876
1877 pub fn run_timed_inplace(&self) -> Result<f32> {
1879 for (i, &(dev_ptr, n_elems)) in self.input_bufs.iter().enumerate() {
1880 unsafe { mem::copy_h_to_d(dev_ptr, self.pinned_inputs[i], n_elems) }?;
1881 }
1882
1883 let mut ev_start = cuda::CUevent::default();
1884 let mut ev_end = cuda::CUevent::default();
1885 unsafe {
1886 cuda::cuEventCreate(&mut ev_start, 0);
1887 cuda::cuEventCreate(&mut ev_end, 0);
1888 cuda::cuEventRecord(ev_start, self.stream);
1889 }
1890
1891 let launch_s = unsafe { cuda::cuGraphLaunch(self.graph_exec, self.stream) };
1892 if launch_s != cuda::cudaError_enum_CUDA_SUCCESS {
1893 unsafe {
1894 cuda::cuEventDestroy_v2(ev_start);
1895 cuda::cuEventDestroy_v2(ev_end);
1896 }
1897 return Err(Error::from_cuda_error(launch_s).into());
1898 }
1899
1900 unsafe {
1901 cuda::cuEventRecord(ev_end, self.stream);
1902 }
1903
1904 let sync_s = unsafe { cuda::cuEventSynchronize(ev_end) };
1905 if sync_s != cuda::cudaError_enum_CUDA_SUCCESS {
1906 unsafe {
1907 cuda::cuEventDestroy_v2(ev_start);
1908 cuda::cuEventDestroy_v2(ev_end);
1909 }
1910 return Err(Error::from_cuda_error(sync_s).into());
1911 }
1912
1913 let mut gpu_ms = 0.0f32;
1914 unsafe {
1915 cuda::cudaEventElapsedTime(&mut gpu_ms, ev_start, ev_end);
1916 cuda::cuEventDestroy_v2(ev_start);
1917 cuda::cuEventDestroy_v2(ev_end);
1918 }
1919
1920 for (i, &(dev_ptr, n_elems)) in self.output_bufs.iter().enumerate() {
1921 unsafe { mem::copy_d_to_h(self.pinned_outputs[i], dev_ptr, n_elems) }?;
1922 }
1923 Ok(gpu_ms)
1924 }
1925}
1926
1927impl Drop for CudaGraphModel {
1928 fn drop(&mut self) {
1929 unsafe {
1930 cuda::cuGraphExecDestroy(self.graph_exec);
1931 cuda::cuStreamDestroy_v2(self.stream);
1932 }
1933 for &ptr in &self._owned {
1934 if ptr != 0 && mem::free(ptr).is_err() {
1935 eprintln!("CudaGraphModel::drop: failed to free buffer at {ptr:#x}");
1936 }
1937 }
1938 for &ptr in &self.pinned_inputs {
1939 if !ptr.is_null()
1940 && let Err(e) = unsafe { mem::free_host(ptr) }
1941 {
1942 eprintln!("CudaGraphModel::drop: failed to free pinned input: {e}");
1943 }
1944 }
1945 for &ptr in &self.pinned_outputs {
1946 if !ptr.is_null()
1947 && let Err(e) = unsafe { mem::free_host(ptr) }
1948 {
1949 eprintln!("CudaGraphModel::drop: failed to free pinned output: {e}");
1950 }
1951 }
1952 }
1953}
1954
1955#[cfg(feature = "training")]
1963pub struct AdamwKernel {
1964 pub(crate) program: CudaProgram<'static, ErasedKernel>,
1965}
1966
1967#[cfg(feature = "training")]
1968impl AdamwKernel {
1969 pub fn from_ptx(ptx: &[u8]) -> Result<Self> {
1971 let program = CudaProgram::<ErasedKernel>::try_from_ptx(ptx)?;
1972 Ok(Self { program })
1973 }
1974}