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 = output_shape.last().copied().unwrap_or(1);
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 = output_shape.last().copied().unwrap_or(1);
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 = output_shape.last().copied().unwrap_or(1);
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(Error::from_cuda_error(s).into());
1415 break 'capture;
1416 }
1417 }
1418
1419 for launch_idx in 0..n_launches {
1420 let mut packer = CudaArgPacker::new();
1421 if n_launches == 1 {
1422 loaded.runtime_op.pack_args(
1423 &act_inputs,
1424 ¶m_ptrs,
1425 kernel_out_raw,
1426 output_shape,
1427 required_stride as i32,
1428 &mut packer,
1429 );
1430 } else {
1431 loaded.runtime_op.pack_args_for_launch(
1432 launch_idx,
1433 &act_inputs,
1434 ¶m_ptrs,
1435 kernel_out_raw,
1436 output_shape,
1437 required_stride as i32,
1438 &mut packer,
1439 );
1440 }
1441 packer.visit_ptr(scratch_ptr as *mut core::ffi::c_void);
1443 packer.visit_ptr(std::ptr::null_mut());
1444
1445 let grid = if n_launches == 1 {
1446 loaded.runtime_op.grid(output_shape)
1447 } else {
1448 loaded
1449 .runtime_op
1450 .grid_for_launch(launch_idx, &input_shapes_ref, output_shape)
1451 };
1452 let cfg = CudaLaunchConfig {
1453 grid,
1454 block,
1455 cluster,
1456 };
1457 if let Err(e) = device.launch_on_stream(&loaded.program, &cfg, &mut packer, stream)
1458 {
1459 capture_err = Some(e);
1460 break 'capture;
1461 }
1462 }
1463
1464 if let Some((padded_ptr, ns, rs, n_rows, eb)) = node_padded[idx] {
1468 let params = cuda::CUDA_MEMCPY2D {
1469 srcMemoryType: cuda::CUmemorytype_enum_CU_MEMORYTYPE_DEVICE,
1470 srcDevice: padded_ptr,
1471 srcPitch: rs * eb,
1472 dstMemoryType: cuda::CUmemorytype_enum_CU_MEMORYTYPE_DEVICE,
1473 dstDevice: tight_out_ptr,
1474 dstPitch: ns * eb,
1475 WidthInBytes: ns * eb,
1476 Height: n_rows,
1477 ..Default::default()
1478 };
1479 let s = unsafe { cuda::cuMemcpy2DAsync_v2(¶ms, stream) };
1480 if s != cuda::cudaError_enum_CUDA_SUCCESS {
1481 capture_err = Some(Error::from_cuda_error(s).into());
1482 break 'capture;
1483 }
1484 }
1485 }
1486
1487 let mut graph: cuda::CUgraph = std::ptr::null_mut();
1489 let end_s = unsafe { cuda::cuStreamEndCapture(stream, &mut graph) };
1490
1491 if let Some(err) = capture_err {
1492 unsafe {
1494 if !graph.is_null() {
1495 cuda::cuGraphDestroy(graph);
1496 }
1497 cuda::cuStreamDestroy_v2(stream);
1498 }
1499 for &p in &owned {
1500 let _ = mem::free(p);
1501 }
1502 return Err(err);
1503 }
1504 if end_s != cuda::cudaError_enum_CUDA_SUCCESS {
1505 unsafe {
1506 if !graph.is_null() {
1507 cuda::cuGraphDestroy(graph);
1508 }
1509 cuda::cuStreamDestroy_v2(stream);
1510 }
1511 for &p in &owned {
1512 let _ = mem::free(p);
1513 }
1514 return Err(Error::from_cuda_error(end_s).into());
1515 }
1516
1517 let mut graph_exec: cuda::CUgraphExec = std::ptr::null_mut();
1518 let inst_s = unsafe { cuda::cuGraphInstantiateWithFlags(&mut graph_exec, graph, 0) };
1519 unsafe { cuda::cuGraphDestroy(graph) };
1520 if inst_s != cuda::cudaError_enum_CUDA_SUCCESS {
1521 unsafe { cuda::cuStreamDestroy_v2(stream) };
1522 for &p in &owned {
1523 let _ = mem::free(p);
1524 }
1525 return Err(Error::from_cuda_error(inst_s).into());
1526 }
1527
1528 if output_node_indices.is_empty() {
1529 unsafe {
1530 cuda::cuStreamDestroy_v2(stream);
1531 }
1532 for &p in &owned {
1533 let _ = mem::free(p);
1534 }
1535 return Err(anyhow!(
1536 "capture_graph: output_node_indices must not be empty"
1537 ));
1538 }
1539 let mut output_bufs: Vec<(DevicePtr, usize)> =
1540 Vec::with_capacity(output_node_indices.len());
1541 let mut output_shapes: Vec<Vec<usize>> = Vec::with_capacity(output_node_indices.len());
1542 for &oi in output_node_indices {
1543 if oi >= self.nodes.len() || self.nodes[oi].is_none() {
1544 unsafe {
1545 cuda::cuStreamDestroy_v2(stream);
1546 }
1547 for &p in &owned {
1548 let _ = mem::free(p);
1549 }
1550 return Err(anyhow!(
1551 "capture_graph: output node {oi} is an Input or out of bounds"
1552 ));
1553 }
1554 let shape = concrete_shapes[oi].clone();
1555 let n_elems: usize = shape.iter().product();
1556 output_bufs.push((node_out_bufs[oi], n_elems));
1557 output_shapes.push(shape);
1558 }
1559
1560 let mut pinned_inputs: Vec<*mut f32> = Vec::with_capacity(input_bufs.len());
1565 for &(_, n_elems) in &input_bufs {
1566 match mem::alloc_host::<f32>(n_elems) {
1567 Ok(ptr) => pinned_inputs.push(ptr),
1568 Err(e) => {
1569 unsafe {
1570 cuda::cuStreamDestroy_v2(stream);
1571 }
1572 for &p in &owned {
1573 let _ = mem::free(p);
1574 }
1575 for &p in &pinned_inputs {
1576 unsafe {
1577 let _ = mem::free_host(p);
1578 }
1579 }
1580 return Err(e);
1581 }
1582 }
1583 }
1584 let mut pinned_outputs: Vec<*mut f32> = Vec::with_capacity(output_bufs.len());
1585 for &(_, n_elems) in &output_bufs {
1586 match mem::alloc_host::<f32>(n_elems) {
1587 Ok(ptr) => pinned_outputs.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 for &p in &pinned_outputs {
1601 unsafe {
1602 let _ = mem::free_host(p);
1603 }
1604 }
1605 return Err(e);
1606 }
1607 }
1608 }
1609
1610 Ok(CudaGraphModel {
1611 stream,
1612 graph_exec,
1613 input_bufs,
1614 output_bufs,
1615 output_shapes,
1616 pinned_inputs,
1617 pinned_outputs,
1618 _owned: owned,
1619 })
1620 }
1621
1622 fn topo_sort(&self) -> Vec<usize> {
1623 let n = self.nodes.len();
1624 let mut in_deg: Vec<usize> = (0..n).map(|i| self.parents[i].len()).collect();
1625 let mut dependents: Vec<Vec<usize>> = vec![vec![]; n];
1626 for i in 0..n {
1627 for &p in &self.parents[i] {
1628 dependents[p].push(i);
1629 }
1630 }
1631 let mut stack: Vec<usize> = (0..n).filter(|&i| in_deg[i] == 0).collect();
1632 let mut order = Vec::with_capacity(n);
1633 while let Some(id) = stack.pop() {
1634 order.push(id);
1635 for &dep in &dependents[id] {
1636 in_deg[dep] -= 1;
1637 if in_deg[dep] == 0 {
1638 stack.push(dep);
1639 }
1640 }
1641 }
1642 order
1643 }
1644}
1645
1646#[cfg(feature = "training")]
1650pub struct ActivationCache {
1651 pub tensors: Vec<Option<TensorRef>>,
1654}
1655
1656#[cfg(feature = "training")]
1657impl Drop for ActivationCache {
1658 fn drop(&mut self) {
1659 for tr in self.tensors.iter().flatten() {
1660 if let Err(e) = mem::free(tr.ptr) {
1661 eprintln!("ActivationCache: failed to free buffer: {e}");
1662 }
1663 }
1664 }
1665}
1666
1667pub struct CudaGraphModel {
1677 stream: cuda::CUstream,
1678 graph_exec: cuda::CUgraphExec,
1679 input_bufs: Vec<(DevicePtr, usize)>,
1681 output_bufs: Vec<(DevicePtr, usize)>,
1683 output_shapes: Vec<Vec<usize>>,
1684 pinned_inputs: Vec<*mut f32>,
1688 pinned_outputs: Vec<*mut f32>,
1690 _owned: Vec<DevicePtr>,
1692}
1693
1694unsafe impl Send for CudaGraphModel {}
1697
1698impl CudaGraphModel {
1699 pub fn output_shapes(&self) -> &[Vec<usize>] {
1701 &self.output_shapes
1702 }
1703
1704 pub fn run_timed(&self, inputs: &[&[f32]]) -> Result<(Vec<Vec<f32>>, f32)> {
1710 if inputs.len() != self.input_bufs.len() {
1711 return Err(anyhow!(
1712 "CudaGraphModel::run_timed: expected {} inputs, got {}",
1713 self.input_bufs.len(),
1714 inputs.len()
1715 ));
1716 }
1717 for (i, (&(ptr, n_elems), &data)) in self.input_bufs.iter().zip(inputs.iter()).enumerate() {
1718 if data.len() != n_elems {
1719 return Err(anyhow!(
1720 "CudaGraphModel::run_timed: input[{i}] has {} elements, expected {n_elems}",
1721 data.len()
1722 ));
1723 }
1724 unsafe { mem::copy_h_to_d(ptr, data.as_ptr(), n_elems) }?;
1725 }
1726
1727 let mut ev_start = cuda::CUevent::default();
1729 let mut ev_end = cuda::CUevent::default();
1730 let cu_event_default = 0u32;
1731 unsafe {
1732 cuda::cuEventCreate(&mut ev_start, cu_event_default);
1733 cuda::cuEventCreate(&mut ev_end, cu_event_default);
1734 cuda::cuEventRecord(ev_start, self.stream);
1735 }
1736
1737 let launch_s = unsafe { cuda::cuGraphLaunch(self.graph_exec, self.stream) };
1738 if launch_s != cuda::cudaError_enum_CUDA_SUCCESS {
1739 unsafe {
1740 cuda::cuEventDestroy_v2(ev_start);
1741 cuda::cuEventDestroy_v2(ev_end);
1742 }
1743 return Err(Error::from_cuda_error(launch_s).into());
1744 }
1745
1746 unsafe {
1747 cuda::cuEventRecord(ev_end, self.stream);
1748 }
1749
1750 let sync_s = unsafe { cuda::cuEventSynchronize(ev_end) };
1751 if sync_s != cuda::cudaError_enum_CUDA_SUCCESS {
1752 unsafe {
1753 cuda::cuEventDestroy_v2(ev_start);
1754 cuda::cuEventDestroy_v2(ev_end);
1755 }
1756 return Err(Error::from_cuda_error(sync_s).into());
1757 }
1758
1759 let mut gpu_ms = 0.0f32;
1760 unsafe {
1761 cuda::cudaEventElapsedTime(&mut gpu_ms, ev_start, ev_end);
1762 cuda::cuEventDestroy_v2(ev_start);
1763 cuda::cuEventDestroy_v2(ev_end);
1764 }
1765
1766 let mut result = Vec::with_capacity(self.output_bufs.len());
1767 for &(ptr, n_elems) in &self.output_bufs {
1768 let mut out = vec![0.0_f32; n_elems];
1769 unsafe { mem::copy_d_to_h(out.as_mut_ptr(), ptr, n_elems) }?;
1770 result.push(out);
1771 }
1772 Ok((result, gpu_ms))
1773 }
1774
1775 pub fn run(&self, inputs: &[&[f32]]) -> Result<Vec<Vec<f32>>> {
1780 if inputs.len() != self.input_bufs.len() {
1781 return Err(anyhow!(
1782 "CudaGraphModel::run: expected {} inputs, got {}",
1783 self.input_bufs.len(),
1784 inputs.len()
1785 ));
1786 }
1787 for (i, (&(ptr, n_elems), &data)) in self.input_bufs.iter().zip(inputs.iter()).enumerate() {
1788 if data.len() != n_elems {
1789 return Err(anyhow!(
1790 "CudaGraphModel::run: input[{i}] has {} elements, expected {n_elems}",
1791 data.len()
1792 ));
1793 }
1794 unsafe { mem::copy_h_to_d(ptr, data.as_ptr(), n_elems) }?;
1795 }
1796
1797 let launch_s = unsafe { cuda::cuGraphLaunch(self.graph_exec, self.stream) };
1798 if launch_s != cuda::cudaError_enum_CUDA_SUCCESS {
1799 return Err(Error::from_cuda_error(launch_s).into());
1800 }
1801 let sync_s = unsafe { cuda::cuStreamSynchronize(self.stream) };
1802 if sync_s != cuda::cudaError_enum_CUDA_SUCCESS {
1803 return Err(Error::from_cuda_error(sync_s).into());
1804 }
1805
1806 let mut result = Vec::with_capacity(self.output_bufs.len());
1807 for &(ptr, n_elems) in &self.output_bufs {
1808 let mut out = vec![0.0_f32; n_elems];
1809 unsafe { mem::copy_d_to_h(out.as_mut_ptr(), ptr, n_elems) }?;
1810 result.push(out);
1811 }
1812 Ok(result)
1813 }
1814
1815 pub fn input_slice_mut(&mut self, i: usize) -> &mut [f32] {
1824 let (_, n_elems) = self.input_bufs[i];
1825 unsafe { std::slice::from_raw_parts_mut(self.pinned_inputs[i], n_elems) }
1826 }
1827
1828 pub fn output_slice(&self, i: usize) -> &[f32] {
1831 let (_, n_elems) = self.output_bufs[i];
1832 unsafe { std::slice::from_raw_parts(self.pinned_outputs[i], n_elems) }
1833 }
1834
1835 pub fn run_inplace(&self) -> Result<()> {
1840 for (i, &(dev_ptr, n_elems)) in self.input_bufs.iter().enumerate() {
1841 unsafe { mem::copy_h_to_d(dev_ptr, self.pinned_inputs[i], n_elems) }?;
1842 }
1843 let launch_s = unsafe { cuda::cuGraphLaunch(self.graph_exec, self.stream) };
1844 if launch_s != cuda::cudaError_enum_CUDA_SUCCESS {
1845 return Err(Error::from_cuda_error(launch_s).into());
1846 }
1847 let sync_s = unsafe { cuda::cuStreamSynchronize(self.stream) };
1848 if sync_s != cuda::cudaError_enum_CUDA_SUCCESS {
1849 return Err(Error::from_cuda_error(sync_s).into());
1850 }
1851 for (i, &(dev_ptr, n_elems)) in self.output_bufs.iter().enumerate() {
1852 unsafe { mem::copy_d_to_h(self.pinned_outputs[i], dev_ptr, n_elems) }?;
1853 }
1854 Ok(())
1855 }
1856
1857 pub fn run_timed_inplace(&self) -> Result<f32> {
1859 for (i, &(dev_ptr, n_elems)) in self.input_bufs.iter().enumerate() {
1860 unsafe { mem::copy_h_to_d(dev_ptr, self.pinned_inputs[i], n_elems) }?;
1861 }
1862
1863 let mut ev_start = cuda::CUevent::default();
1864 let mut ev_end = cuda::CUevent::default();
1865 unsafe {
1866 cuda::cuEventCreate(&mut ev_start, 0);
1867 cuda::cuEventCreate(&mut ev_end, 0);
1868 cuda::cuEventRecord(ev_start, self.stream);
1869 }
1870
1871 let launch_s = unsafe { cuda::cuGraphLaunch(self.graph_exec, self.stream) };
1872 if launch_s != cuda::cudaError_enum_CUDA_SUCCESS {
1873 unsafe {
1874 cuda::cuEventDestroy_v2(ev_start);
1875 cuda::cuEventDestroy_v2(ev_end);
1876 }
1877 return Err(Error::from_cuda_error(launch_s).into());
1878 }
1879
1880 unsafe {
1881 cuda::cuEventRecord(ev_end, self.stream);
1882 }
1883
1884 let sync_s = unsafe { cuda::cuEventSynchronize(ev_end) };
1885 if sync_s != cuda::cudaError_enum_CUDA_SUCCESS {
1886 unsafe {
1887 cuda::cuEventDestroy_v2(ev_start);
1888 cuda::cuEventDestroy_v2(ev_end);
1889 }
1890 return Err(Error::from_cuda_error(sync_s).into());
1891 }
1892
1893 let mut gpu_ms = 0.0f32;
1894 unsafe {
1895 cuda::cudaEventElapsedTime(&mut gpu_ms, ev_start, ev_end);
1896 cuda::cuEventDestroy_v2(ev_start);
1897 cuda::cuEventDestroy_v2(ev_end);
1898 }
1899
1900 for (i, &(dev_ptr, n_elems)) in self.output_bufs.iter().enumerate() {
1901 unsafe { mem::copy_d_to_h(self.pinned_outputs[i], dev_ptr, n_elems) }?;
1902 }
1903 Ok(gpu_ms)
1904 }
1905}
1906
1907impl Drop for CudaGraphModel {
1908 fn drop(&mut self) {
1909 unsafe {
1910 cuda::cuGraphExecDestroy(self.graph_exec);
1911 cuda::cuStreamDestroy_v2(self.stream);
1912 }
1913 for &ptr in &self._owned {
1914 if ptr != 0 && mem::free(ptr).is_err() {
1915 eprintln!("CudaGraphModel::drop: failed to free buffer at {ptr:#x}");
1916 }
1917 }
1918 for &ptr in &self.pinned_inputs {
1919 if !ptr.is_null()
1920 && let Err(e) = unsafe { mem::free_host(ptr) }
1921 {
1922 eprintln!("CudaGraphModel::drop: failed to free pinned input: {e}");
1923 }
1924 }
1925 for &ptr in &self.pinned_outputs {
1926 if !ptr.is_null()
1927 && let Err(e) = unsafe { mem::free_host(ptr) }
1928 {
1929 eprintln!("CudaGraphModel::drop: failed to free pinned output: {e}");
1930 }
1931 }
1932 }
1933}
1934
1935#[cfg(feature = "training")]
1943pub struct AdamwKernel {
1944 pub(crate) program: CudaProgram<'static, ErasedKernel>,
1945}
1946
1947#[cfg(feature = "training")]
1948impl AdamwKernel {
1949 pub fn from_ptx(ptx: &[u8]) -> Result<Self> {
1951 let program = CudaProgram::<ErasedKernel>::try_from_ptx(ptx)?;
1952 Ok(Self { program })
1953 }
1954}