1use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
47use std::sync::Arc;
48use std::time::{Duration, Instant};
49
50use onnx_runtime_ep_api::{
51 CaptureRegionShapeStatus, DeviceBuffer, DevicePtr, DevicePtrMut, EpError, ExecutionProvider,
52 ExternalMmapRegion, Kernel, KernelInput, KernelMatch, LazyWeight, LazyWeightBoundary,
53 ResidentWeight, StructuralCaptureDecline, TensorBacking, TensorMut, TensorView, WeightHandle,
54};
55
56type OptionalTensorSpecs = Vec<Option<(DataType, Vec<usize>)>>;
57use onnx_runtime_ep_cpu::CpuExecutionProvider;
58use onnx_runtime_ep_cpu::strided::view_in_bounds;
59use onnx_runtime_ir::Attribute;
60use onnx_runtime_ir::{
61 DataType, DeviceType, Dim, Graph, Node, NodeId, Shape, SymbolId, TensorLayout, ValueId,
62 WeightRef, as_static_shape, broadcast_shapes, compute_contiguous_strides,
63};
64use onnx_runtime_loader::WeightStore;
65use onnx_runtime_optimizer::InitializerResolver;
66use onnx_runtime_shape_inference::{
67 DimExpr, InferenceRegistry, MAX_SHAPE_DATA_ELEMS, MergePolicy, NodeIo, ShapeData,
68 SymbolInterner, TypeInfo,
69};
70use onnx_runtime_tracer::{TraceContext, annotate_current_span_with};
71
72use crate::SessionOutput;
73use crate::error::{Result, SessionError};
74use crate::sequence::{
75 ConcatPlan, SeqTensor, SequenceError, SequenceValue, SplitSpec, split_tensor, stack_new_axis,
76};
77use crate::tensor::{DeviceIoBinding, SharedTensorBuffer, Tensor};
78
79fn profile_ops_enabled() -> bool {
80 static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
81 *ENABLED.get_or_init(|| {
82 std::env::var("ONNX_GENAI_PROFILE_OPS")
83 .is_ok_and(|value| value == "1" || value.eq_ignore_ascii_case("true"))
84 })
85}
86
87mod phase_profile {
96 use std::collections::BTreeMap;
97 use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
98 use std::sync::{Mutex, OnceLock};
99 use std::time::Instant;
100
101 static STATE: AtomicU8 = AtomicU8::new(0); pub fn enabled() -> bool {
104 match STATE.load(Ordering::Relaxed) {
105 1 => false,
106 2 => true,
107 _ => {
108 let on = std::env::var("NXRT_EXEC_PHASE_PROFILE")
109 .is_ok_and(|v| v == "1" || v.eq_ignore_ascii_case("true"));
110 STATE.store(if on { 2 } else { 1 }, Ordering::Relaxed);
111 on
112 }
113 }
114 }
115
116 #[cfg(test)]
118 pub(super) fn force_enabled(on: bool) {
119 STATE.store(if on { 2 } else { 1 }, Ordering::Relaxed);
120 }
121
122 #[cfg(test)]
124 pub(super) fn snapshot(phase: &'static str) -> Option<(u128, u64)> {
125 registry()
126 .lock()
127 .ok()
128 .and_then(|reg| reg.get(phase).map(|s| (s.total_ns, s.count)))
129 }
130
131 #[derive(Default, Clone, Copy)]
132 struct PhaseStat {
133 total_ns: u128,
134 count: u64,
135 }
136
137 fn registry() -> &'static Mutex<BTreeMap<&'static str, PhaseStat>> {
138 static REGISTRY: OnceLock<Mutex<BTreeMap<&'static str, PhaseStat>>> = OnceLock::new();
139 REGISTRY.get_or_init(|| Mutex::new(BTreeMap::new()))
140 }
141
142 pub fn record(phase: &'static str, nanos: u128) {
144 if !enabled() {
145 return;
146 }
147 if let Ok(mut reg) = registry().lock() {
148 let entry = reg.entry(phase).or_default();
149 entry.total_ns += nanos;
150 entry.count += 1;
151 }
152 }
153
154 pub struct PhaseSpan {
156 phase: &'static str,
157 start: Option<Instant>,
158 }
159
160 impl PhaseSpan {
161 pub fn new(phase: &'static str) -> Self {
162 let active = enabled();
163 Self {
164 phase,
165 start: if active { Some(Instant::now()) } else { None },
167 }
168 }
169 }
170
171 impl Drop for PhaseSpan {
172 fn drop(&mut self) {
173 if let Some(start) = self.start {
174 record(self.phase, start.elapsed().as_nanos());
175 }
176 }
177 }
178
179 pub fn report_to_stderr() {
182 if !enabled() {
183 return;
184 }
185 let rows: Vec<(&'static str, PhaseStat)> = match registry().lock() {
186 Ok(reg) => reg.iter().map(|(n, s)| (*n, *s)).collect(),
187 Err(_) => return,
188 };
189 static PRINTED: AtomicBool = AtomicBool::new(false);
190 if PRINTED.swap(true, Ordering::Relaxed) {
191 return;
192 }
193 let mut rows = rows;
194 rows.sort_by_key(|r| std::cmp::Reverse(r.1.total_ns));
195 eprintln!("[nxrt-phase] phase,total_ms,calls,us/call");
196 for (name, stat) in &rows {
197 if name.ends_with("_bytes") {
198 continue;
199 }
200 let total_ms = stat.total_ns as f64 / 1_000_000.0;
201 let us_per_call = if stat.count > 0 {
202 (stat.total_ns as f64 / 1_000.0) / stat.count as f64
203 } else {
204 0.0
205 };
206 eprintln!(
207 "[nxrt-phase] {name},{total_ms:.3},{},{us_per_call:.2}",
208 stat.count
209 );
210 }
211 for (name, stat) in &rows {
213 if !name.ends_with("_bytes") {
214 continue;
215 }
216 let total_mb = stat.total_ns as f64 / (1024.0 * 1024.0);
217 let mb_per_call = if stat.count > 0 {
218 total_mb / stat.count as f64
219 } else {
220 0.0
221 };
222 eprintln!(
223 "[nxrt-phase] {name},total_mb={total_mb:.1},calls={},mb/call={mb_per_call:.3}",
224 stat.count
225 );
226 }
227 }
228}
229
230macro_rules! phase_span {
232 ($phase:expr) => {
233 phase_profile::PhaseSpan::new($phase)
234 };
235}
236
237pub fn print_exec_phase_profile() {
239 phase_profile::report_to_stderr();
240}
241
242fn host_dtype_alignment(dtype: DataType) -> usize {
243 match dtype {
244 DataType::Float16 | DataType::BFloat16 | DataType::Int16 | DataType::Uint16 => 2,
245 DataType::Float32 | DataType::Int32 | DataType::Uint32 | DataType::Complex64 => 4,
246 DataType::Float64 | DataType::Int64 | DataType::Uint64 | DataType::Complex128 => 8,
247 _ => 1,
248 }
249}
250
251fn print_op_profile(total: Duration, timings: HashMap<String, (Duration, usize)>) {
252 let mut timings = timings.into_iter().collect::<Vec<_>>();
253 timings.sort_unstable_by_key(|entry| std::cmp::Reverse(entry.1.0));
254 let total_ms = total.as_secs_f64() * 1_000.0;
255 eprintln!("[onnx-genai-profile] node execution: {total_ms:.3} ms");
256 eprintln!("[onnx-genai-profile] op_type,total_ms,percent,calls");
257 for (op_type, (elapsed, calls)) in timings {
258 let elapsed_ms = elapsed.as_secs_f64() * 1_000.0;
259 let percent = if total_ms == 0.0 {
260 0.0
261 } else {
262 elapsed_ms / total_ms * 100.0
263 };
264 eprintln!("[onnx-genai-profile] {op_type},{elapsed_ms:.3},{percent:.2},{calls}");
265 }
266}
267
268fn log_capture_segmentation(schedule: &CaptureSchedule) {
272 let captured = schedule.captured_segments();
273 let seams = schedule.segments.len() - captured;
274 eprintln!(
275 "[onnx-genai-capture] segmented CUDA graph: {captured} captured segment(s), \
276 {seams} eager seam(s)"
277 );
278 for boundary in &schedule.boundaries {
279 match boundary.node_id {
280 Some(id) => {
281 let seam_label = boundary
282 .seam_reason
283 .map(SeamReason::label)
284 .unwrap_or("unclassified-seam");
285 eprintln!(
286 "[onnx-genai-capture] seam node {id} ({}::{}) [{seam_label}] ran eagerly: {}",
287 boundary.domain, boundary.op_type, boundary.reason
288 );
289 }
290 None => eprintln!(
291 "[onnx-genai-capture] seam ({}): {}",
292 boundary.op_type, boundary.reason
293 ),
294 }
295 }
296}
297
298#[derive(Debug)]
302pub(crate) struct NodePlan {
303 pub node_id: NodeId,
304 pub inputs: Vec<Option<ValueId>>,
310 pub outputs: Vec<ValueId>,
312 pub input_dtypes: Vec<DataType>,
316 pub output_dtypes: Vec<DataType>,
318}
319
320#[derive(Clone, PartialEq, Eq, Hash, Debug)]
327struct KernelKey {
328 node: u32,
329 shapes: Vec<Vec<usize>>,
330}
331
332#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
334pub struct CacheStats {
335 pub entries: usize,
337 pub hits: u64,
339 pub misses: u64,
341}
342
343#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
346pub struct ControlFlowStats {
347 pub subgraph_builds: u64,
349 pub subgraph_runs: u64,
351}
352
353#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
354pub struct DeviceAllocationCounts {
355 pub allocations: u64,
356 pub frees: u64,
357}
358
359#[derive(Clone, Copy, Debug, PartialEq, Eq)]
361pub enum CapturePathKind {
362 CaptureRegion,
364 EagerDeviceSeam,
366 HostSeam,
368}
369
370impl CapturePathKind {
371 pub const fn label(self) -> &'static str {
373 match self {
374 Self::CaptureRegion => "capture-region",
375 Self::EagerDeviceSeam => "eager-device-seam",
376 Self::HostSeam => "host-seam",
377 }
378 }
379}
380
381#[derive(Clone, Copy, Debug, PartialEq, Eq)]
383pub enum SeamReason {
384 HostControlFlowOrSequence,
386 UnresolvedOutputShape,
388 UnresolvedInputShape,
390 KernelNotWarmed,
392 KernelCaptureUnsupported,
394 CaptureRecordingFailed,
399}
400
401impl SeamReason {
402 pub const fn path_kind(self) -> CapturePathKind {
404 match self {
405 Self::HostControlFlowOrSequence => CapturePathKind::HostSeam,
406 Self::UnresolvedOutputShape
407 | Self::UnresolvedInputShape
408 | Self::KernelNotWarmed
409 | Self::CaptureRecordingFailed
410 | Self::KernelCaptureUnsupported => CapturePathKind::EagerDeviceSeam,
411 }
412 }
413
414 pub const fn label(self) -> &'static str {
416 self.path_kind().label()
417 }
418}
419
420#[derive(Clone, Debug, PartialEq, Eq)]
421pub struct CaptureDecline {
423 pub node_id: Option<u32>,
425 pub op_type: String,
427 pub domain: String,
429 pub reason: String,
431 pub seam_reason: Option<SeamReason>,
433}
434
435impl CaptureDecline {
436 fn node(
437 node_id: NodeId,
438 node: &Node,
439 seam_reason: SeamReason,
440 reason: impl Into<String>,
441 ) -> Self {
442 Self {
443 node_id: Some(node_id.0),
444 op_type: node.op_type.clone(),
445 domain: canonical_domain(node),
446 reason: reason.into(),
447 seam_reason: Some(seam_reason),
448 }
449 }
450
451 fn graph(reason: impl Into<String>) -> Self {
452 Self {
453 node_id: None,
454 op_type: "<graph>".to_string(),
455 domain: "nxrt".to_string(),
456 reason: reason.into(),
457 seam_reason: None,
458 }
459 }
460}
461
462#[derive(Clone, Debug, Default, PartialEq, Eq)]
463pub struct CaptureDeclineReport {
465 pub entries: Vec<CaptureDecline>,
467}
468
469impl CaptureDeclineReport {
470 fn one(decline: CaptureDecline) -> Self {
471 Self {
472 entries: vec![decline],
473 }
474 }
475
476 pub fn is_empty(&self) -> bool {
478 self.entries.is_empty()
479 }
480}
481
482#[derive(Clone, Debug, PartialEq, Eq)]
484pub struct ExecutionProviderDecline {
485 pub node: String,
487 pub domain: String,
489 pub op_type: String,
491 pub reason: String,
493}
494
495#[derive(Clone, Debug, PartialEq, Eq)]
497pub struct ExecutionProviderFallbackReport {
498 pub requested_provider: String,
500 pub fallback_provider: String,
502 pub assigned_node_count: usize,
504 pub assigned_ops: Vec<String>,
506 pub declines: Vec<ExecutionProviderDecline>,
508}
509
510impl std::fmt::Display for ExecutionProviderFallbackReport {
511 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
512 write!(
513 f,
514 "{} nodes assigned to CPU (ops: {}) — GPU EP {} did not claim {} node(s): {}. \
515 Heterogeneous CUDA+CPU placement is unavailable, so the whole session uses {}",
516 self.assigned_node_count,
517 self.assigned_ops.join(", "),
518 self.requested_provider,
519 self.declines.len(),
520 format_cuda_coverage_issues(&self.declines),
521 self.fallback_provider,
522 )
523 }
524}
525
526impl std::fmt::Display for CaptureDeclineReport {
527 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
528 write!(f, "CUDA graph capture rejected")?;
529 for (index, decline) in self.entries.iter().enumerate() {
530 if index == 0 {
531 write!(f, ": ")?;
532 } else {
533 write!(f, "; ")?;
534 }
535 match decline.node_id {
536 Some(node_id) => write!(
537 f,
538 "node {node_id} ({}::{}) — {}",
539 decline.domain, decline.op_type, decline.reason
540 )?,
541 None => write!(f, "{} — {}", decline.op_type, decline.reason)?,
542 }
543 }
544 Ok(())
545 }
546}
547
548pub enum DeviceGraphCaptureResult {
549 Captured(Vec<Option<Tensor>>),
550 NotCapturable(CaptureDeclineReport),
551}
552
553enum ScopedRunResult {
554 Executed(Vec<Option<SessionOutput>>),
555 NotCapturable(CaptureDeclineReport),
556}
557
558fn kernel_capture_decline(
559 node_id: NodeId,
560 node: &Node,
561 kernel: &dyn Kernel,
562) -> Option<CaptureDecline> {
563 kernel.capture_support().reason().map(|reason| {
564 CaptureDecline::node(node_id, node, SeamReason::KernelCaptureUnsupported, reason)
565 })
566}
567
568fn structural_capture_decline(
569 node_id: NodeId,
570 node: &Node,
571 decline: StructuralCaptureDecline,
572) -> CaptureDecline {
573 let seam_reason = match decline {
574 StructuralCaptureDecline::HostControlFlowOrSequence => {
575 SeamReason::HostControlFlowOrSequence
576 }
577 StructuralCaptureDecline::UnresolvedOutputShape => SeamReason::UnresolvedOutputShape,
578 StructuralCaptureDecline::UnresolvedInputShape => SeamReason::UnresolvedInputShape,
579 };
580 CaptureDecline::node(node_id, node, seam_reason, decline.reason())
581}
582
583fn capture_segmentation_logging_enabled() -> bool {
588 static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
589 *ENABLED.get_or_init(|| {
590 std::env::var("ONNX_GENAI_LOG_CAPTURE_SEGMENTS")
591 .is_ok_and(|value| value == "1" || value.eq_ignore_ascii_case("true"))
592 })
593}
594
595#[derive(Clone, Copy, PartialEq, Eq, Debug)]
597enum RunMode {
598 Eager,
600 Capture,
604 Replay,
607}
608
609#[derive(Clone, Copy)]
613enum OpCaptureTrace<'a> {
614 Eager,
616 Captured,
618 Rejected(&'a str),
621}
622
623const ARG_CAPTURE_STATUS: &str = "capture_status";
625const ARG_CAPTURE_REASON: &str = "capture_reason";
627
628impl OpCaptureTrace<'_> {
629 fn annotate(self) {
633 match self {
634 OpCaptureTrace::Eager => {}
635 OpCaptureTrace::Captured => {
636 annotate_current_span_with(|| {
637 onnx_runtime_tracer::Args::new().with(ARG_CAPTURE_STATUS, "captured")
638 });
639 }
640 OpCaptureTrace::Rejected(reason) => {
641 annotate_current_span_with(|| {
642 onnx_runtime_tracer::Args::new()
643 .with(ARG_CAPTURE_STATUS, "rejected")
644 .with(ARG_CAPTURE_REASON, reason)
645 });
646 }
647 }
648 }
649}
650
651struct SegmentCaptureGuard<'a> {
667 ep: &'a dyn ExecutionProvider,
668 armed: bool,
669}
670
671impl<'a> SegmentCaptureGuard<'a> {
672 fn arm(ep: &'a dyn ExecutionProvider) -> Self {
673 Self { ep, armed: true }
674 }
675
676 fn disarm(&mut self) {
677 self.armed = false;
678 }
679}
680
681impl Drop for SegmentCaptureGuard<'_> {
682 fn drop(&mut self) {
683 if self.armed {
684 let _ = self.ep.abort_device_graph_capture();
687 }
688 }
689}
690
691#[derive(Clone, Debug, PartialEq, Eq)]
694struct ScheduledSegment {
695 start: usize,
697 end: usize,
699 captured: bool,
702 graph_index: usize,
705}
706
707#[derive(Clone, Debug, Default, PartialEq, Eq)]
713struct CaptureSchedule {
714 segments: Vec<ScheduledSegment>,
715 boundaries: Vec<CaptureDecline>,
718}
719
720impl CaptureSchedule {
721 fn captured_segments(&self) -> usize {
723 self.segments.iter().filter(|seg| seg.captured).count()
724 }
725
726 fn is_single_graph(&self) -> bool {
728 self.segments.len() == 1 && self.segments[0].captured
729 }
730}
731
732#[derive(Clone, Debug, PartialEq, Eq)]
733struct DeviceBindingSignature {
734 input_name: String,
735 binds_input: bool,
736 output_name: Option<String>,
737 dtype: DataType,
738 physical_shape: Vec<usize>,
739 device_ptr: usize,
740}
741
742#[derive(Default)]
744pub(crate) struct KernelCache {
745 entries: HashMap<KernelKey, Box<dyn onnx_runtime_ep_api::Kernel>>,
746 hits: u64,
747 misses: u64,
748}
749
750impl KernelCache {
751 fn stats(&self) -> CacheStats {
752 CacheStats {
753 entries: self.entries.len(),
754 hits: self.hits,
755 misses: self.misses,
756 }
757 }
758
759 fn get_or_create(
764 &mut self,
765 node_id: NodeId,
766 node: &Node,
767 input_shapes: &[Vec<usize>],
768 input_dtypes: &[DataType],
769 constant_inputs: &[bool],
770 opset: u64,
771 ep: &dyn ExecutionProvider,
772 ) -> Result<&dyn onnx_runtime_ep_api::Kernel> {
773 let key = KernelKey {
774 node: node_id.0,
775 shapes: input_shapes.to_vec(),
776 };
777 if self.entries.contains_key(&key) {
778 self.hits += 1;
779 } else {
780 let shape_dims: Vec<Shape> = input_shapes
783 .iter()
784 .map(|s| s.iter().map(|&d| Dim::Static(d)).collect())
785 .collect();
786 let layouts = vec![TensorLayout::contiguous(); input_shapes.len()];
787 if let KernelMatch::Unsupported { reason } =
788 ep.supports_op(node, opset, &shape_dims, input_dtypes, &layouts)
789 {
790 return Err(SessionError::unsupported_op(
791 node,
792 node_id,
793 opset,
794 ep.name(),
795 reason,
796 ));
797 }
798 let mut kernel = match ep.get_kernel(node, input_shapes, opset) {
799 Ok(kernel) => kernel,
800 Err(EpError::NoEpForOp {
801 domain,
802 op_type,
803 opset,
804 }) => {
805 return Err(SessionError::unsupported_op(
808 node,
809 node_id,
810 opset,
811 ep.name(),
812 format!(
813 "no handler for {domain}::{op_type} at opset {opset} — add a claim+handler"
814 ),
815 ));
816 }
817 Err(error) => return Err(error.into()),
818 };
819 kernel.set_constant_inputs(constant_inputs);
820 self.entries.insert(key.clone(), kernel);
821 self.misses += 1;
822 }
823 Ok(self.entries.get(&key).expect("just inserted").as_ref())
824 }
825}
826
827pub(crate) struct Executor {
830 graph: Graph,
831 weights: Arc<WeightStore>,
841 ep: Arc<dyn ExecutionProvider>,
842 weight_handles: HashMap<ValueId, WeightHandle>,
845 buffers: HashMap<ValueId, DeviceBuffer>,
849 buffer_shapes: HashMap<ValueId, Vec<usize>>,
852 value_shapes: HashMap<ValueId, Shape>,
854 value_dtypes: HashMap<ValueId, DataType>,
856 plan: Vec<NodePlan>,
858 input_index: HashMap<String, ValueId>,
860 required_inputs: Vec<ValueId>,
862 has_symbols: bool,
866 cache: KernelCache,
867 name_index: HashMap<String, ValueId>,
871 subgraph_execs: HashMap<(NodeId, String), ChildExecutor>,
880 control_flow_stats: ControlFlowStats,
881 if_last_predicate: HashMap<NodeId, bool>,
890 device_graph_signature: Option<Vec<DeviceBindingSignature>>,
891 capture_schedule: Option<CaptureSchedule>,
895 capture_segmentation: Vec<CaptureDecline>,
898 control_flow_output_values: HashSet<ValueId>,
908 capture_cf_shapes: HashMap<ValueId, Vec<usize>>,
915 capture_warm_signature: Option<Vec<ExternalCaptureSig>>,
921 capture_warm_shapes: HashMap<ValueId, Vec<usize>>,
927 capture_warm_seeded: HashMap<ValueId, Vec<usize>>,
932 capture_quarantine_ops: HashSet<(String, String)>,
942 last_capture_failed_node: Option<NodeId>,
946 views: HashMap<ValueId, ValueView>,
951 pinned: HashSet<ValueId>,
956 sequence_values: HashSet<ValueId>,
962 shared_buffers: HashMap<ValueId, Arc<SharedTensorBuffer>>,
968 sequences: HashMap<ValueId, SequenceValue>,
974 seq_elem_values: HashMap<ValueId, SeqTensor>,
981 execution_provider_fallback_report: Option<ExecutionProviderFallbackReport>,
982 trace: TraceContext,
988 scratch_input_shapes: Vec<Vec<usize>>,
996 decode_memo_enabled: bool,
1000 decode_memo_verify: bool,
1004 decode_memo: Option<DecodePlanMemo>,
1007 decode_memo_prev_bindings: Option<HashMap<SymbolId, usize>>,
1010 decode_memo_last_action: DecodeMemoAction,
1013 decode_memo_resolved: HashMap<ValueId, Vec<usize>>,
1022 decode_memo_primed_count: u64,
1027 decode_memo_rebuilt_count: u64,
1028 decode_memo_replayed_count: u64,
1029 decode_memo_ineligible_count: u64,
1033 decode_view_plan: Option<DecodeViewPlan>,
1042 decode_views_reused_count: u64,
1046 decode_dispatch_elided_count: u64,
1047 decode_view_plan_sig_mismatch_streak: u32,
1053 decode_view_plan_disabled: bool,
1055}
1056
1057const STAGE2_SIG_MISMATCH_LIMIT: u32 = 2;
1060
1061#[derive(Clone, Debug)]
1067struct ValueView {
1068 source: ValueId,
1069 shape: Vec<usize>,
1070 strides: Vec<i64>,
1071 byte_offset: usize,
1072}
1073
1074struct DecodePlanMemo {
1100 reference_bindings: HashMap<SymbolId, usize>,
1102 decode_varying: HashSet<SymbolId>,
1105 invariant_shapes: HashMap<ValueId, Vec<usize>>,
1108 variant_values: Vec<ValueId>,
1111 canonical: HashSet<ValueId>,
1116 reference_external_sig: Vec<DecodeBindingSig>,
1121}
1122
1123#[derive(Clone, PartialEq, Eq)]
1139struct DecodeBindingSig {
1140 vid: ValueId,
1141 is_input: bool,
1142 dtype: DataType,
1143 decl_shape: Shape,
1144}
1145
1146impl DecodePlanMemo {
1147 fn matches(&self, bindings: &HashMap<SymbolId, usize>, external_sig: &[DecodeBindingSig]) -> bool {
1152 if external_sig != self.reference_external_sig {
1153 return false;
1154 }
1155 if bindings.len() != self.reference_bindings.len() {
1156 return false;
1157 }
1158 bindings.iter().all(|(sym, &val)| {
1159 match self.reference_bindings.get(sym) {
1160 Some(&ref_val) => val == ref_val || self.decode_varying.contains(sym),
1161 None => false,
1163 }
1164 })
1165 }
1166}
1167
1168struct DecodeViewPlan {
1212 elided_nodes: HashSet<usize>,
1216 retained_views: Vec<(ValueId, ValueView)>,
1219 pinned_sources: Vec<ValueId>,
1222 source_buffer_sig: Vec<(ValueId, usize, usize)>,
1225 validated: bool,
1229}
1230
1231#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1234enum DecodeMemoAction {
1235 Disabled,
1237 Primed,
1240 Rebuilt,
1243 Replayed,
1246}
1247
1248fn same_symbol_keys(a: &HashMap<SymbolId, usize>, b: &HashMap<SymbolId, usize>) -> bool {
1250 a.len() == b.len() && a.keys().all(|k| b.contains_key(k))
1251}
1252
1253fn is_decode_growth_transition(
1263 prev: &HashMap<SymbolId, usize>,
1264 cur: &HashMap<SymbolId, usize>,
1265) -> bool {
1266 if !same_symbol_keys(prev, cur) {
1267 return false;
1268 }
1269 let mut any_grew = false;
1270 for (sym, &c) in cur {
1271 let p = prev[sym];
1272 if c > p {
1273 any_grew = true;
1274 } else if c < p {
1275 return false; }
1277 }
1278 any_grew
1279}
1280
1281fn shape_references_any(shape: &Shape, symbols: &HashSet<SymbolId>) -> bool {
1283 shape
1284 .iter()
1285 .any(|d| matches!(d, Dim::Symbolic(s) if symbols.contains(s)))
1286}
1287
1288fn decode_memo_env_enabled() -> bool {
1299 match std::env::var("ONNX_GENAI_DECODE_MEMO") {
1300 Ok(value) => !matches!(
1301 value.trim().to_ascii_lowercase().as_str(),
1302 "0" | "false" | "off"
1303 ),
1304 Err(_) => true,
1305 }
1306}
1307
1308fn decode_memo_verify_env_enabled() -> bool {
1311 matches!(
1312 std::env::var("ONNX_GENAI_DECODE_MEMO_VERIFY").ok().as_deref(),
1313 Some("1") | Some("true") | Some("on")
1314 )
1315}
1316
1317struct InInfo {
1324 present: bool,
1325 dtype: DataType,
1326 shape: Vec<usize>,
1327 strides: Vec<i64>,
1328 byte_offset: usize,
1329 base_ptr: *const std::ffi::c_void,
1330 device: onnx_runtime_ir::DeviceId,
1331 backing: TensorBacking,
1332 root_len: usize,
1334}
1335
1336#[derive(Clone)]
1337struct ExternalValue {
1338 dtype: DataType,
1339 shape: Vec<usize>,
1340 accepts_subshape: bool,
1341 ptr: *mut std::ffi::c_void,
1342 len: usize,
1343 alignment: usize,
1344 device: onnx_runtime_ir::DeviceId,
1345}
1346
1347impl ExternalValue {
1348 fn accepts_output(&self, dtype: DataType, shape: &[usize], bytes: usize) -> bool {
1349 self.dtype == dtype
1350 && self.len >= bytes
1351 && if self.accepts_subshape {
1352 shape.len() == self.shape.len()
1353 && shape
1354 .iter()
1355 .zip(&self.shape)
1356 .all(|(&required, &capacity)| required <= capacity)
1357 } else {
1358 self.shape == shape
1359 }
1360 }
1361
1362 fn writable_buffer(&self) -> Result<DeviceBuffer> {
1363 unsafe {
1368 DeviceBuffer::from_borrowed_mut_parts(self.ptr, self.device, self.len, self.alignment)
1369 }
1370 .ok_or_else(|| SessionError::Internal("external output binding has a null pointer".into()))
1371 }
1372}
1373
1374#[derive(Default)]
1375struct ExternalBindings {
1376 inputs: HashMap<ValueId, ExternalValue>,
1377 outputs: HashMap<ValueId, ExternalValue>,
1378}
1379
1380#[derive(Clone, PartialEq, Eq)]
1387struct ExternalCaptureSig {
1388 vid: ValueId,
1389 is_input: bool,
1390 dtype: DataType,
1391 shape: Vec<usize>,
1392 ptr: usize,
1393 len: usize,
1394}
1395
1396impl ExternalBindings {
1397 fn seed_capture_shapes(&self, resolved: &mut HashMap<ValueId, Vec<usize>>) {
1398 for (&vid, value) in self.inputs.iter().chain(&self.outputs) {
1399 resolved.entry(vid).or_insert_with(|| value.shape.clone());
1400 }
1401 }
1402
1403 fn capture_signature(&self) -> Vec<ExternalCaptureSig> {
1408 let mut sig: Vec<ExternalCaptureSig> = self
1409 .inputs
1410 .iter()
1411 .map(|(&vid, v)| (vid, true, v))
1412 .chain(self.outputs.iter().map(|(&vid, v)| (vid, false, v)))
1413 .map(|(vid, is_input, v)| ExternalCaptureSig {
1414 vid,
1415 is_input,
1416 dtype: v.dtype,
1417 shape: v.shape.clone(),
1418 ptr: v.ptr as usize,
1419 len: v.len,
1420 })
1421 .collect();
1422 sig.sort_by_key(|a| (a.vid.0, a.is_input));
1423 sig
1424 }
1425}
1426
1427struct CompiledChildPlan {
1429 exec: Executor,
1430 signature: Vec<ChildInputSignature>,
1431}
1432
1433const CHILD_EXECUTOR_CACHE_CAPACITY: usize = 4;
1436
1437#[derive(Clone, Debug, PartialEq, Eq)]
1438struct ChildInputSignature {
1439 dtype: DataType,
1440 shape: Vec<usize>,
1441}
1442
1443pub(crate) struct ChildExecutor {
1450 name: String,
1451 template: Graph,
1452 inherited_opsets: HashMap<String, u64>,
1453 weights: Arc<WeightStore>,
1454 ep: Arc<dyn ExecutionProvider>,
1455 formal_names: Vec<String>,
1456 capture_names: Vec<String>,
1457 input_names: Vec<String>,
1458 compiled: Vec<CompiledChildPlan>,
1459 builds: u64,
1460 runs: u64,
1461 trace: TraceContext,
1463}
1464
1465#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1466pub(crate) struct ChildExecutorStats {
1467 pub builds: u64,
1468 pub runs: u64,
1469}
1470
1471struct PreparedSubgraph {
1475 key: (NodeId, String),
1476 captures: HashMap<String, Tensor>,
1478}
1479
1480fn view_bounds(
1485 shape: &[usize],
1486 strides: &[i64],
1487 byte_offset: usize,
1488 dtype: DataType,
1489 buffer_len: usize,
1490) -> Result<()> {
1491 let esize = dtype.byte_size();
1492 if esize == 0 {
1493 let numel: usize = shape.iter().product();
1495 let need = byte_offset + dtype.storage_bytes(numel);
1496 if need > buffer_len {
1497 return Err(SessionError::from(
1498 onnx_runtime_ep_api::EpError::InvalidTensorView {
1499 reason: format!(
1500 "sub-byte view needs {need} bytes but backing allocation is {buffer_len}"
1501 ),
1502 },
1503 ));
1504 }
1505 return Ok(());
1506 }
1507 view_in_bounds(shape, strides, byte_offset, esize, buffer_len)?;
1508 Ok(())
1509}
1510
1511fn gather_view(
1518 src: &[u8],
1519 shape: &[usize],
1520 strides: &[i64],
1521 byte_offset: usize,
1522 esize: usize,
1523) -> Vec<u8> {
1524 let n: usize = shape.iter().product();
1525 let mut out = vec![0u8; n * esize];
1526 if n == 0 {
1527 return out;
1528 }
1529 let rank = shape.len();
1530 let mut idx = vec![0usize; rank];
1531 let mut w = 0usize;
1532 loop {
1533 let mut off = byte_offset as i64;
1534 for d in 0..rank {
1535 off += strides[d] * idx[d] as i64 * esize as i64;
1536 }
1537 let s = off as usize;
1538 out[w..w + esize].copy_from_slice(&src[s..s + esize]);
1539 w += esize;
1540 let mut carried = true;
1542 for axis in (0..rank).rev() {
1543 idx[axis] += 1;
1544 if idx[axis] < shape[axis] {
1545 carried = false;
1546 break;
1547 }
1548 idx[axis] = 0;
1549 }
1550 if carried {
1551 break;
1552 }
1553 }
1554 out
1555}
1556
1557fn checked_numel(dims: &[usize], value: impl FnOnce() -> String) -> Result<usize> {
1562 let mut acc = 1usize;
1563 for &d in dims {
1564 acc = match acc.checked_mul(d) {
1565 Some(n) => n,
1566 None => {
1567 return Err(SessionError::ShapeOverflow {
1568 value: value(),
1569 dims: dims.to_vec(),
1570 });
1571 }
1572 };
1573 }
1574 Ok(acc)
1575}
1576
1577fn checked_storage_bytes(
1583 dtype: DataType,
1584 numel: usize,
1585 value: impl FnOnce() -> String,
1586 dims: &[usize],
1587) -> Result<usize> {
1588 dtype
1589 .checked_storage_bytes(numel)
1590 .ok_or_else(|| SessionError::ShapeOverflow {
1591 value: value(),
1592 dims: dims.to_vec(),
1593 })
1594}
1595
1596fn effective_opset(graph: &Graph, node: &Node) -> u64 {
1601 graph
1602 .opset_imports
1603 .get(node.domain.as_str())
1604 .copied()
1605 .unwrap_or_else(|| {
1606 unreachable!(
1607 "internal invariant violated: node #{} ({}::{}) has no opset import",
1608 node.id.0,
1609 if node.domain.is_empty() {
1610 "ai.onnx"
1611 } else {
1612 &node.domain
1613 },
1614 node.op_type
1615 )
1616 })
1617}
1618
1619fn substitute(shape: &Shape, bindings: &HashMap<SymbolId, usize>) -> Option<Vec<usize>> {
1622 shape
1623 .iter()
1624 .map(|d| match d {
1625 Dim::Static(n) => Some(*n),
1626 Dim::Symbolic(s) => bindings.get(s).copied(),
1627 })
1628 .collect()
1629}
1630
1631fn substitute_into(
1636 shape: &Shape,
1637 bindings: &HashMap<SymbolId, usize>,
1638 out: &mut Vec<usize>,
1639) -> bool {
1640 out.clear();
1641 for d in shape {
1642 match d {
1643 Dim::Static(n) => out.push(*n),
1644 Dim::Symbolic(s) => match bindings.get(s) {
1645 Some(&v) => out.push(v),
1646 None => {
1647 out.clear();
1648 return false;
1649 }
1650 },
1651 }
1652 }
1653 true
1654}
1655
1656fn bytes_as_i64(bytes: &[u8], dtype: DataType) -> Option<Vec<i64>> {
1660 match dtype {
1661 DataType::Int64 => Some(
1662 bytes
1663 .chunks_exact(8)
1664 .map(|c| i64::from_le_bytes(c.try_into().unwrap()))
1665 .collect(),
1666 ),
1667 DataType::Int32 => Some(
1668 bytes
1669 .chunks_exact(4)
1670 .map(|c| i32::from_le_bytes(c.try_into().unwrap()) as i64)
1671 .collect(),
1672 ),
1673 _ => None,
1674 }
1675}
1676
1677fn bytes_as_f64(bytes: &[u8], dtype: DataType) -> Option<Vec<f64>> {
1678 match dtype {
1679 DataType::Float32 => Some(
1680 bytes
1681 .chunks_exact(4)
1682 .map(|c| f32::from_le_bytes(c.try_into().unwrap()) as f64)
1683 .collect(),
1684 ),
1685 DataType::Float64 => Some(
1686 bytes
1687 .chunks_exact(8)
1688 .map(|c| f64::from_le_bytes(c.try_into().unwrap()))
1689 .collect(),
1690 ),
1691 _ => None,
1692 }
1693}
1694
1695fn bounded_shape_input(dtype: DataType, shape: &[usize]) -> bool {
1699 if !matches!(dtype, DataType::Int32 | DataType::Int64) {
1700 return false;
1701 }
1702 if shape.len() > 1 {
1703 return false;
1704 }
1705 shape
1706 .iter()
1707 .try_fold(1usize, |count, &dim| count.checked_mul(dim))
1708 .is_some_and(|count| count <= MAX_SHAPE_DATA_ELEMS)
1709}
1710
1711fn reads_float_shape_input(node: &Node, input_index: usize, opset: u64) -> bool {
1715 node.is_default_domain()
1716 && ((node.op_type == "Resize" && input_index == if opset == 10 { 1 } else { 2 })
1717 || (node.op_type == "NonMaxSuppression" && matches!(input_index, 0 | 1 | 3 | 4)))
1718}
1719
1720fn kernel_input_uses_physical_capacity(node: &Node, input_index: usize) -> bool {
1721 if node.domain == "com.microsoft"
1725 && node.op_type == "GroupQueryAttention"
1726 && matches!(input_index, 3 | 4)
1727 {
1728 return true;
1729 }
1730 node.is_default_domain()
1741 && node.op_type == "Attention"
1742 && matches!(input_index, 4 | 5)
1743 && node.inputs.get(3).is_some_and(Option::is_some)
1744 && node
1745 .attr("is_causal")
1746 .and_then(|attr| attr.as_int())
1747 .unwrap_or(0)
1748 == 0
1749 || (
1750 node.domain == "pkg.nxrt"
1761 && node.op_type == "IndexShare"
1762 && matches!(input_index, 3 | 4)
1763 && node.outputs.len() == 3
1764 && node.inputs.get(6).is_some_and(Option::is_some)
1765 )
1766}
1767
1768fn kernel_input_uses_padded_capacity(node: &Node, input_index: usize) -> bool {
1769 node.is_default_domain()
1774 && input_index == 0
1775 && matches!(node.op_type.as_str(), "Shape" | "ReduceSum")
1776}
1777
1778fn runtime_elementwise_output_shape(
1782 node: &Node,
1783 input_shapes: &[Vec<usize>],
1784) -> Option<std::result::Result<Vec<usize>, onnx_runtime_ir::IrError>> {
1785 if !node.is_default_domain() {
1786 return None;
1787 }
1788
1789 let input_count = match node.op_type.as_str() {
1790 "Add" | "Sub" | "Mul" | "Div" | "Pow" | "Mod" | "BitShift" | "Less" | "Greater"
1791 | "Equal" | "And" | "Or" | "Xor" | "LessOrEqual" | "GreaterOrEqual" => 2,
1792 "Where" => 3,
1793 "Min" | "Max" | "Sum" | "Mean" => input_shapes.len(),
1794 _ => return None,
1795 };
1796 if input_count == 0 || input_shapes.len() < input_count {
1797 return None;
1798 }
1799
1800 let mut shape = input_shapes[0].clone();
1801 for input in &input_shapes[1..input_count] {
1802 shape = match broadcast_shapes(&shape, input) {
1803 Ok(shape) => shape,
1804 Err(error) => return Some(Err(error)),
1805 };
1806 }
1807 Some(Ok(shape))
1808}
1809
1810fn dynamic_output_shapes(
1820 node: &Node,
1821 input_shapes: &[Vec<usize>],
1822 input_dtypes: &[DataType],
1823 input_values: &[Option<Vec<i64>>],
1824 input_float_values: &[Option<Vec<f64>>],
1825 opset: u64,
1826) -> Option<Vec<Vec<usize>>> {
1827 match node.op_type.as_str() {
1828 "Resize" if node.is_default_domain() => {
1829 let input = input_shapes.first()?;
1830 let rank = input.len();
1831 let axes = if let Some(raw) = node.attr("axes").and_then(Attribute::as_ints) {
1832 let mut axes = Vec::with_capacity(raw.len());
1833 for &axis in raw {
1834 let axis = if axis < 0 { axis + rank as i64 } else { axis };
1835 let axis = usize::try_from(axis).ok()?;
1836 if axis >= rank || axes.contains(&axis) {
1837 return None;
1838 }
1839 axes.push(axis);
1840 }
1841 if axes.is_empty() {
1842 (0..rank).collect()
1843 } else {
1844 axes
1845 }
1846 } else {
1847 (0..rank).collect()
1848 };
1849 let scales_index = if opset == 10 { 1 } else { 2 };
1850 let scales = input_float_values
1851 .get(scales_index)
1852 .and_then(|values| values.as_deref())
1853 .filter(|values| !values.is_empty());
1854 let sizes = (opset >= 11)
1855 .then(|| input_values.get(3).and_then(|values| values.as_deref()))
1856 .flatten()
1857 .filter(|values| !values.is_empty());
1858 if scales.is_some() == sizes.is_some() {
1859 return None;
1860 }
1861 let mut output = input.clone();
1862 if let Some(scales) = scales {
1863 if scales.len() != axes.len()
1864 || node
1865 .attr("keep_aspect_ratio_policy")
1866 .and_then(Attribute::as_str)
1867 .is_some_and(|policy| policy != "stretch")
1868 {
1869 return None;
1870 }
1871 for (&axis, &scale) in axes.iter().zip(scales) {
1872 if !scale.is_finite() || scale <= 0.0 {
1873 return None;
1874 }
1875 let extent = input[axis] as f64 * scale;
1876 if extent > usize::MAX as f64 {
1877 return None;
1878 }
1879 output[axis] = extent.floor() as usize;
1880 }
1881 } else {
1882 let sizes = sizes?;
1883 if sizes.len() != axes.len() {
1884 return None;
1885 }
1886 let requested = sizes
1887 .iter()
1888 .map(|&size| usize::try_from(size).ok().filter(|&size| size > 0))
1889 .collect::<Option<Vec<_>>>()?;
1890 match node
1891 .attr("keep_aspect_ratio_policy")
1892 .and_then(Attribute::as_str)
1893 .unwrap_or("stretch")
1894 {
1895 "stretch" => {
1896 for (&axis, &size) in axes.iter().zip(&requested) {
1897 output[axis] = size;
1898 }
1899 }
1900 policy @ ("not_larger" | "not_smaller") => {
1901 if axes.iter().any(|&axis| input[axis] == 0) {
1902 return None;
1903 }
1904 let (numerator, denominator) = axes
1905 .iter()
1906 .zip(&requested)
1907 .map(|(&axis, &size)| (size, input[axis]))
1908 .reduce(|left, right| {
1909 let order = (left.0 as u128 * right.1 as u128)
1910 .cmp(&(right.0 as u128 * left.1 as u128));
1911 if (policy == "not_larger" && order.is_le())
1912 || (policy == "not_smaller" && order.is_ge())
1913 {
1914 left
1915 } else {
1916 right
1917 }
1918 })?;
1919 if denominator == 0 {
1920 return None;
1921 }
1922 for &axis in &axes {
1923 let product = (input[axis] as u128).checked_mul(numerator as u128)?;
1924 output[axis] = usize::try_from(
1925 (product + denominator as u128 / 2) / denominator as u128,
1926 )
1927 .ok()?;
1928 }
1929 }
1930 _ => return None,
1931 }
1932 }
1933 Some(vec![output])
1934 }
1935 "Slice" if node.is_default_domain() => {
1940 let data_shape = input_shapes.first()?;
1941 let starts = input_values.get(1)?.as_ref()?;
1942 let ends = input_values.get(2)?.as_ref()?;
1943 let (axes, steps) = onnx_runtime_ep_cpu::slice_axes_steps(
1944 starts.len(),
1945 input_values.get(3).and_then(|v| v.as_deref()),
1946 input_values.get(4).and_then(|v| v.as_deref()),
1947 );
1948 let plan =
1952 onnx_runtime_ep_cpu::slice_plan(data_shape, starts, ends, &axes, &steps).ok()?;
1953 let count: Vec<usize> = plan.iter().map(|p| p.count).collect();
1954 Some(vec![count])
1955 }
1956 "NonMaxSuppression" if node.is_default_domain() => {
1957 let boxes_shape = input_shapes.first()?;
1958 let scores_shape = input_shapes.get(1)?;
1959 let boxes = input_float_values.first()?.as_ref()?;
1960 let scores = input_float_values.get(1)?.as_ref()?;
1961 let max_output_boxes_per_class = input_values
1962 .get(2)
1963 .and_then(|value| value.as_ref())
1964 .filter(|value| value.len() == 1)
1965 .map(|value| value[0])
1966 .unwrap_or(0);
1967 let iou_threshold = input_float_values
1968 .get(3)
1969 .and_then(|value| value.as_ref())
1970 .filter(|value| value.len() == 1)
1971 .map(|value| value[0] as f32)
1972 .unwrap_or(0.0);
1973 let score_threshold = input_float_values
1974 .get(4)
1975 .and_then(|value| value.as_ref())
1976 .filter(|value| value.len() == 1)
1977 .map(|value| value[0] as f32)
1978 .unwrap_or(f32::NEG_INFINITY);
1979 let center_point_box = node
1980 .attr("center_point_box")
1981 .and_then(Attribute::as_int)
1982 .unwrap_or(0);
1983 let boxes = boxes.iter().map(|&value| value as f32).collect::<Vec<_>>();
1984 let scores = scores.iter().map(|&value| value as f32).collect::<Vec<_>>();
1985 let selected = onnx_runtime_ep_cpu::non_max_suppression(
1986 &boxes,
1987 boxes_shape,
1988 &scores,
1989 scores_shape,
1990 max_output_boxes_per_class,
1991 iou_threshold,
1992 score_threshold,
1993 center_point_box,
1994 )
1995 .ok()?;
1996 Some(vec![vec![selected.len(), 3]])
1997 }
1998 "GroupQueryAttention" if node.domain == "com.microsoft" => {
1999 let query = input_shapes.first()?;
2000 let past_key = input_shapes.get(3)?;
2001 if query.len() != 3 || past_key.len() != 4 {
2002 return None;
2003 }
2004 let num_heads = usize::try_from(node.attr("num_heads")?.as_int()?).ok()?;
2005 let kv_heads = usize::try_from(node.attr("kv_num_heads")?.as_int()?).ok()?;
2006 if num_heads == 0 || kv_heads == 0 {
2007 return None;
2008 }
2009 let (output, head_dim) = if node.inputs.get(1).and_then(|input| *input).is_some() {
2010 let key = input_shapes.get(1)?;
2011 if key.len() != 3 || !key[2].is_multiple_of(kv_heads) {
2012 return None;
2013 }
2014 (query.clone(), key[2] / kv_heads)
2015 } else {
2016 let packed_heads = num_heads.checked_add(kv_heads.checked_mul(2)?)?;
2017 if !query[2].is_multiple_of(packed_heads) {
2018 return None;
2019 }
2020 let head_dim = query[2] / packed_heads;
2021 (
2022 vec![query[0], query[1], head_dim.checked_mul(num_heads)?],
2023 head_dim,
2024 )
2025 };
2026 let total_sequence_values = input_values.get(6)?.as_ref()?;
2027 if total_sequence_values.len() != 1 {
2028 return None;
2029 }
2030 let total_sequence = usize::try_from(total_sequence_values[0]).ok()?;
2031 let present_sequence = past_key[2].max(total_sequence);
2032 let present = vec![query[0], kv_heads, present_sequence, head_dim];
2033 let mut shapes = vec![output];
2034 if node.outputs.len() >= 2 {
2035 shapes.push(present.clone());
2036 }
2037 if node.outputs.len() >= 3 {
2038 shapes.push(present);
2039 }
2040 Some(shapes)
2041 }
2042 _ => {
2043 let inputs = node
2049 .inputs
2050 .iter()
2051 .enumerate()
2052 .map(|(i, input)| {
2053 if input.is_none() {
2054 return Some(NodeIo::default());
2055 }
2056 let shape = input_shapes
2057 .get(i)?
2058 .iter()
2059 .map(|&dim| i64::try_from(dim).ok().map(DimExpr::constant))
2060 .collect::<Option<Vec<_>>>()?;
2061 let dtype = *input_dtypes.get(i)?;
2062 let shape_data = input_values.get(i)?.as_ref().and_then(|values| {
2063 let elems = values
2064 .iter()
2065 .copied()
2066 .map(DimExpr::constant)
2067 .collect::<Vec<_>>();
2068 match input_shapes[i].as_slice() {
2069 [] if elems.len() == 1 => {
2070 Some(ShapeData::scalar(dtype, elems[0].clone()))
2071 }
2072 [len] if *len == elems.len() => Some(ShapeData::vector(dtype, elems)),
2073 _ => None,
2074 }
2075 });
2076 Some(NodeIo {
2077 type_info: Some(TypeInfo::new(dtype, shape)),
2078 shape_data,
2079 })
2080 })
2081 .collect::<Option<Vec<_>>>()?;
2082 let mut imports = HashMap::new();
2083 imports.insert(node.domain.clone(), opset);
2084 let mut interner = SymbolInterner::new(0x8000_0000);
2085 static REGISTRY: std::sync::OnceLock<InferenceRegistry> = std::sync::OnceLock::new();
2086 REGISTRY
2087 .get_or_init(InferenceRegistry::default_registry)
2088 .infer_node(node, &imports, inputs, MergePolicy::Strict, &mut interner)
2089 .ok()?
2090 .into_iter()
2091 .map(|output| {
2092 output
2093 .type_info?
2094 .shape
2095 .into_iter()
2096 .map(|dim| usize::try_from(dim.as_const()?).ok())
2097 .collect()
2098 })
2099 .collect()
2100 }
2101 }
2102}
2103
2104fn fuse_silu_patterns(graph: &mut Graph) -> usize {
2109 let sigmoid_ids: Vec<NodeId> = graph
2110 .nodes
2111 .iter()
2112 .filter_map(|(id, node)| {
2113 (node.op_type == "Sigmoid"
2114 && node.is_default_domain()
2115 && node.inputs.len() == 1
2116 && node.outputs.len() == 1)
2117 .then_some(id)
2118 })
2119 .collect();
2120 let mut fused = 0;
2121
2122 for sigmoid_id in sigmoid_ids {
2123 let Some(sigmoid) = graph.try_node(sigmoid_id) else {
2124 continue;
2125 };
2126 let Some(x) = sigmoid.inputs[0] else {
2127 continue;
2128 };
2129 let sigmoid_output = sigmoid.outputs[0];
2130 if graph.outputs.contains(&sigmoid_output) {
2131 continue;
2132 }
2133 let consumers = graph.consumers(sigmoid_output);
2134 if consumers.len() != 1 {
2135 continue;
2136 }
2137 let mul_id = consumers[0];
2138 let mul = graph.node(mul_id);
2139 if mul.op_type != "Mul"
2140 || !mul.is_default_domain()
2141 || mul.inputs.len() != 2
2142 || mul.outputs.len() != 1
2143 || !((mul.inputs[0] == Some(x) && mul.inputs[1] == Some(sigmoid_output))
2144 || (mul.inputs[1] == Some(x) && mul.inputs[0] == Some(sigmoid_output)))
2145 {
2146 continue;
2147 }
2148
2149 let mut silu = mul.clone();
2150 silu.op_type = "Silu".to_string();
2151 silu.domain = "com.microsoft".to_string();
2152 silu.inputs = vec![Some(x)];
2153 silu.attributes.clear();
2154 graph.replace_node(mul_id, silu);
2155 graph.remove_node(sigmoid_id);
2156 fused += 1;
2157 }
2158
2159 if fused != 0 {
2160 graph
2161 .opset_imports
2162 .entry("com.microsoft".to_string())
2163 .or_insert(1);
2164 }
2165 fused
2166}
2167
2168struct WeightStoreInitializerResolver(Arc<WeightStore>);
2169
2170impl InitializerResolver for WeightStoreInitializerResolver {
2171 fn bytes<'a>(&'a self, weight: &'a onnx_runtime_ir::WeightRef) -> Option<&'a [u8]> {
2172 self.0.bytes(weight)
2173 }
2174}
2175
2176fn run_ep_scoped_passes(
2177 graph: &mut Graph,
2178 weights: &Arc<WeightStore>,
2179 ep: &dyn ExecutionProvider,
2180) -> Result<()> {
2181 let passes = ep.custom_passes();
2182 if passes.is_empty() {
2183 return Ok(());
2184 }
2185
2186 let resolver = Arc::new(WeightStoreInitializerResolver(Arc::clone(weights)));
2187 let context = onnx_runtime_optimizer::PassContext::new().with_initializer_resolver(resolver);
2188 onnx_runtime_optimizer::run_passes(graph, &passes, &context)?;
2189
2190 let registry = InferenceRegistry::default_registry();
2199 let opset_imports = graph.opset_imports.clone();
2200 let mut refreshed = graph.clone();
2201 if registry
2202 .infer_graph(&mut refreshed, &opset_imports, MergePolicy::Permissive)
2203 .is_ok()
2204 {
2205 *graph = refreshed;
2206 }
2207 Ok(())
2208}
2209
2210fn validate_if_branch_outputs(graph: &Graph, node: &Node) -> Result<()> {
2211 let Some(then_branch) = graph.subgraphs.get(&(node.id, "then_branch".to_string())) else {
2212 return Ok(());
2213 };
2214 let Some(else_branch) = graph.subgraphs.get(&(node.id, "else_branch".to_string())) else {
2215 return Ok(());
2216 };
2217
2218 if then_branch.outputs.len() != else_branch.outputs.len() {
2219 return Err(SessionError::ControlFlow {
2220 op: "If".to_string(),
2221 reason: format!(
2222 "branches declare different output counts: then_branch has {}, \
2223 else_branch has {}",
2224 then_branch.outputs.len(),
2225 else_branch.outputs.len()
2226 ),
2227 });
2228 }
2229 if then_branch.outputs.len() != node.outputs.len() {
2230 return Err(SessionError::ControlFlow {
2231 op: "If".to_string(),
2232 reason: format!(
2233 "node declares {} output(s), but each branch declares {}",
2234 node.outputs.len(),
2235 then_branch.outputs.len()
2236 ),
2237 });
2238 }
2239 for (index, (&then_output, &else_output)) in then_branch
2240 .outputs
2241 .iter()
2242 .zip(&else_branch.outputs)
2243 .enumerate()
2244 {
2245 if then_branch.value_type_is_known(then_output)
2246 && else_branch.value_type_is_known(else_output)
2247 {
2248 let then_dtype = then_branch.value(then_output).dtype;
2249 let else_dtype = else_branch.value(else_output).dtype;
2250 if then_dtype != else_dtype {
2251 return Err(SessionError::ControlFlow {
2252 op: "If".to_string(),
2253 reason: format!(
2254 "branches declare different dtypes for output {index}: \
2255 then_branch is {then_dtype:?}, else_branch is {else_dtype:?}"
2256 ),
2257 });
2258 }
2259 }
2260 }
2261 Ok(())
2262}
2263
2264fn validate_control_flow_signatures(graph: &Graph) -> Result<()> {
2265 for (_, node) in graph.nodes.iter() {
2266 if node.op_type == "If" && matches!(node.domain.as_str(), "" | "ai.onnx") {
2267 validate_if_branch_outputs(graph, node)?;
2268 }
2269 }
2270 for subgraph in graph.subgraphs.values() {
2271 validate_control_flow_signatures(subgraph)?;
2272 }
2273 Ok(())
2274}
2275
2276fn reject_unsupported_operators(graph: &Graph, ep: &dyn ExecutionProvider) -> Result<()> {
2289 if ep.device_type() == DeviceType::Cuda {
2290 return Ok(());
2291 }
2292 for (node_id, node) in graph.nodes.iter() {
2293 if onnx_runtime_loader::is_ep_context_op(&node.op_type, &node.domain)
2294 || is_control_flow_op(&node.op_type, &node.domain)
2295 || is_sequence_op(&node.op_type, &node.domain)
2296 {
2297 continue;
2298 }
2299
2300 let shapes = node
2301 .inputs
2302 .iter()
2303 .map(|input| {
2304 input
2305 .map(|value| graph.value(value).shape.clone())
2306 .unwrap_or_default()
2307 })
2308 .collect::<Vec<_>>();
2309 if !shapes.iter().all(|shape| as_static_shape(shape).is_some()) {
2312 continue;
2313 }
2314 let input_dtypes = node
2315 .inputs
2316 .iter()
2317 .map(|input| {
2318 input
2319 .map(|value| graph.value(value).dtype)
2320 .unwrap_or(DataType::Undefined)
2321 })
2322 .collect::<Vec<_>>();
2323 let layouts = vec![TensorLayout::contiguous(); shapes.len()];
2324 let opset = effective_opset(graph, node);
2325 if let KernelMatch::Unsupported { reason } =
2326 ep.supports_op(node, opset, &shapes, &input_dtypes, &layouts)
2327 {
2328 return Err(SessionError::unsupported_op(
2329 node,
2330 node_id,
2331 opset,
2332 ep.name(),
2333 reason,
2334 ));
2335 }
2336 }
2337 Ok(())
2338}
2339
2340fn cuda_fallback_report(
2341 graph: &Graph,
2342 ep: &dyn ExecutionProvider,
2343) -> Option<ExecutionProviderFallbackReport> {
2344 if ep.device_type() != DeviceType::Cuda {
2345 return None;
2346 }
2347
2348 let mut issues = Vec::new();
2349 collect_cuda_coverage_issues(graph, graph, ep, "graph", &mut issues);
2350 if issues.is_empty() {
2351 return None;
2352 }
2353
2354 let mut assigned_ops = BTreeSet::new();
2355 let assigned_node_count = collect_executable_ops(graph, &mut assigned_ops);
2356 Some(ExecutionProviderFallbackReport {
2357 requested_provider: ep.name().to_string(),
2358 fallback_provider: "cpu_ep".to_string(),
2359 assigned_node_count,
2360 assigned_ops: assigned_ops.into_iter().collect(),
2361 declines: issues,
2362 })
2363}
2364
2365fn collect_executable_ops(graph: &Graph, ops: &mut BTreeSet<String>) -> usize {
2366 let mut count = 0;
2367 for (_, node) in graph.nodes.iter() {
2368 if !onnx_runtime_loader::is_ep_context_op(&node.op_type, &node.domain) {
2369 count += 1;
2370 ops.insert(format!("{}::{}", canonical_domain(node), node.op_type));
2371 }
2372 }
2373 for subgraph in graph.subgraphs.values() {
2374 count += collect_executable_ops(subgraph, ops);
2375 }
2376 count
2377}
2378
2379fn format_cuda_coverage_issues(issues: &[ExecutionProviderDecline]) -> String {
2380 const MAX_EXAMPLES_PER_CLASS: usize = 3;
2381
2382 let mut groups: BTreeMap<(String, String, String), Vec<String>> = BTreeMap::new();
2383 for issue in issues {
2384 groups
2385 .entry((
2386 issue.domain.clone(),
2387 issue.op_type.clone(),
2388 issue.reason.clone(),
2389 ))
2390 .or_default()
2391 .push(issue.node.clone());
2392 }
2393
2394 groups
2395 .into_iter()
2396 .map(|((domain, op_type, reason), mut nodes)| {
2397 nodes.sort();
2398 let count = nodes.len();
2399 nodes.truncate(MAX_EXAMPLES_PER_CLASS);
2400 format!(
2401 "{domain}::{op_type}: {reason} [count={count}; examples: {}]",
2402 nodes.join(", ")
2403 )
2404 })
2405 .collect::<Vec<_>>()
2406 .join("; ")
2407}
2408
2409fn collect_cuda_coverage_issues(
2410 graph: &Graph,
2411 opset_graph: &Graph,
2412 ep: &dyn ExecutionProvider,
2413 scope: &str,
2414 issues: &mut Vec<ExecutionProviderDecline>,
2415) {
2416 for (node_id, node) in graph.nodes.iter() {
2417 if onnx_runtime_loader::is_ep_context_op(&node.op_type, &node.domain)
2418 || is_control_flow_op(&node.op_type, &node.domain)
2419 || is_sequence_op(&node.op_type, &node.domain)
2420 {
2421 continue;
2422 }
2423
2424 let shapes = node
2425 .inputs
2426 .iter()
2427 .map(|input| {
2428 input
2429 .map(|value| graph.value(value).shape.clone())
2430 .unwrap_or_default()
2431 })
2432 .collect::<Vec<_>>();
2433 let layouts = node
2434 .inputs
2435 .iter()
2436 .map(|input| {
2437 input
2438 .map(|value| graph.value(value).layout.clone())
2439 .unwrap_or_else(TensorLayout::contiguous)
2440 })
2441 .collect::<Vec<_>>();
2442 let input_dtypes = node
2443 .inputs
2444 .iter()
2445 .map(|input| {
2446 input
2447 .map(|value| graph.value(value).dtype)
2448 .unwrap_or(DataType::Undefined)
2449 })
2450 .collect::<Vec<_>>();
2451
2452 let opset = effective_opset(opset_graph, node);
2453 if let KernelMatch::Unsupported { reason } =
2454 ep.supports_op(node, opset, &shapes, &input_dtypes, &layouts)
2455 {
2456 issues.push(ExecutionProviderDecline {
2457 node: format_node_identity(scope, node_id, node),
2458 domain: canonical_domain(node),
2459 op_type: node.op_type.clone(),
2460 reason: reason.into_owned(),
2461 });
2462 continue;
2463 }
2464
2465 let Some(concrete_shapes) = shapes
2466 .iter()
2467 .map(|shape| as_static_shape(shape))
2468 .collect::<Option<Vec<_>>>()
2469 else {
2470 continue;
2471 };
2472 if let Err(error) = ep.get_kernel(node, &concrete_shapes, opset) {
2473 issues.push(ExecutionProviderDecline {
2474 node: format_node_identity(scope, node_id, node),
2475 domain: canonical_domain(node),
2476 op_type: node.op_type.clone(),
2477 reason: format!("kernel creation failed: {error}"),
2478 });
2479 }
2480 }
2481
2482 for ((node_id, attribute), subgraph) in &graph.subgraphs {
2483 let sub_scope = format!("{scope}/node#{}/{}", node_id.0, attribute);
2484 collect_cuda_coverage_issues(subgraph, opset_graph, ep, &sub_scope, issues);
2485 }
2486}
2487
2488fn canonical_domain(node: &Node) -> String {
2489 if node.domain.is_empty() {
2490 "ai.onnx".to_string()
2491 } else {
2492 node.domain.clone()
2493 }
2494}
2495
2496fn format_node_identity(scope: &str, node_id: NodeId, node: &Node) -> String {
2497 if node.name.is_empty() {
2498 format!("{scope}/node#{}", node_id.0)
2499 } else {
2500 format!("{scope}/node#{} {:?}", node_id.0, node.name)
2501 }
2502}
2503
2504fn build_lazy_weight_handles(
2505 graph: &Graph,
2506 weights: &Arc<WeightStore>,
2507 ep: &dyn ExecutionProvider,
2508) -> Result<HashMap<ValueId, WeightHandle>> {
2509 let capabilities = ep.capabilities();
2510 if !capabilities.advertises(onnx_runtime_ep_api::NXRT_WEIGHT_PAGING_CAPABILITY) {
2511 return Ok(HashMap::new());
2512 }
2513
2514 let boundary = LazyWeightBoundary::BlockQuantizedMoe;
2515 let mut handles = HashMap::new();
2516 for (&value, weight) in &graph.initializers {
2517 let graph_value = graph.value(value);
2518 let consumers = graph.consumers(value);
2519 let lazy_only = graph_value.producer.is_none()
2520 && !graph.outputs.contains(&value)
2521 && !consumers.is_empty()
2522 && consumers.into_iter().all(|consumer| {
2523 let node = graph.node(consumer);
2524 boundary.matches(&node.domain, &node.op_type)
2525 });
2526 if !lazy_only {
2527 continue;
2528 }
2529 let Some((mapping_id, offset, len)) = weights.external_mmap_provenance(weight) else {
2530 continue;
2531 };
2532 let region = ExternalMmapRegion {
2533 mapping_id,
2534 offset,
2535 len,
2536 };
2537 let dtype = weight.dtype();
2538 let shape = weight.dims().to_vec();
2539 let weight = weight.clone();
2540 let store = Arc::clone(weights);
2541 let lazy = LazyWeight::block_quantized_moe(vec![region], move || {
2542 let bytes = store.bytes(&weight).ok_or_else(|| {
2543 onnx_runtime_ep_api::WeightHandleError::InvalidResident(
2544 "external weight bytes are no longer available".into(),
2545 )
2546 })?;
2547 ResidentWeight::new(dtype, shape.clone(), Arc::<[u8]>::from(bytes))
2548 })
2549 .map_err(|error| {
2550 SessionError::Internal(format!(
2551 "cannot create lazy weight handle for value#{}: {error}",
2552 value.0
2553 ))
2554 })?;
2555 handles.insert(value, WeightHandle::Lazy(lazy));
2556 }
2557 Ok(handles)
2558}
2559
2560impl Executor {
2561 pub(crate) fn build(
2563 graph: Graph,
2564 weights: Arc<WeightStore>,
2565 ep: Arc<dyn ExecutionProvider>,
2566 ) -> Result<Self> {
2567 Self::build_with_cuda_requirement(
2568 graph,
2569 weights,
2570 ep,
2571 onnx_genai_runtime_config::runtime_config().require_cuda,
2572 )
2573 }
2574
2575 fn build_with_cuda_requirement(
2576 mut graph: Graph,
2577 weights: Arc<WeightStore>,
2578 mut ep: Arc<dyn ExecutionProvider>,
2579 require_cuda: bool,
2580 ) -> Result<Self> {
2581 validate_control_flow_signatures(&graph)?;
2586 graph.topological_order()?;
2593 reject_unsupported_operators(&graph, ep.as_ref())?;
2594 fuse_silu_patterns(&mut graph);
2595 let graph_before_ep_passes = graph.clone();
2596 run_ep_scoped_passes(&mut graph, &weights, ep.as_ref())?;
2597 let mut execution_provider_fallback_report = cuda_fallback_report(&graph, ep.as_ref());
2598 if let Some(report) = &mut execution_provider_fallback_report {
2599 if require_cuda {
2600 return Err(SessionError::HeterogeneousPlacementRequired {
2601 unsupported_nodes: report.to_string(),
2602 });
2603 }
2604 graph = graph_before_ep_passes;
2605 ep = auto_detect_cpu_ep()?;
2606 run_ep_scoped_passes(&mut graph, &weights, ep.as_ref())?;
2607 let mut assigned_ops = BTreeSet::new();
2608 report.assigned_node_count = collect_executable_ops(&graph, &mut assigned_ops);
2609 report.assigned_ops = assigned_ops.into_iter().collect();
2610 eprintln!(
2611 "[onnx-genai-warning] {report}. Set ONNX_GENAI_REQUIRE_CUDA=1 to reject this fallback"
2612 );
2613 }
2614 let order = graph.topological_order()?;
2616 let weight_handles = build_lazy_weight_handles(&graph, &weights, ep.as_ref())?;
2617
2618 let mut value_shapes: HashMap<ValueId, Shape> = HashMap::new();
2619 let mut value_dtypes: HashMap<ValueId, DataType> = HashMap::new();
2620 let mut buffers: HashMap<ValueId, DeviceBuffer> = HashMap::new();
2621 let mut buffer_shapes: HashMap<ValueId, Vec<usize>> = HashMap::new();
2622
2623 let init_align = TensorLayout::contiguous().alignment;
2631 for (&vid, weight) in &graph.initializers {
2632 let dtype = weight.dtype();
2633 let dims = weight.dims().to_vec();
2634 value_dtypes.insert(vid, dtype);
2635 value_shapes.insert(vid, dims.iter().map(|&d| Dim::Static(d)).collect());
2636 if !ep.device_id().is_host_accessible() && weight_handles.contains_key(&vid) {
2637 continue;
2638 }
2639 let bytes = weights.bytes(weight).ok_or_else(|| {
2640 SessionError::Internal(format!("weight bytes unavailable for value#{}", vid.0))
2641 })?;
2642 let producer_less = graph.value(vid).producer.is_none();
2651 let borrow_align = if matches!(weight, WeightRef::External { .. }) {
2652 host_dtype_alignment(dtype)
2653 } else {
2654 init_align
2655 };
2656 let buf = if ep.device_id().is_host_accessible()
2657 && producer_less
2658 && !bytes.is_empty()
2659 && (bytes.as_ptr() as usize).is_multiple_of(borrow_align)
2660 {
2661 unsafe {
2669 DeviceBuffer::from_borrowed_parts(
2670 bytes.as_ptr() as *mut std::ffi::c_void,
2671 ep.device_id(),
2672 bytes.len(),
2673 borrow_align,
2674 )
2675 }
2676 } else {
2677 let mut owned = ep.allocate(bytes.len().max(1), init_align)?;
2678 ep.copy_from_host(bytes, &mut owned)?;
2679 owned
2680 };
2681 buffer_shapes.insert(vid, dims);
2682 buffers.insert(vid, buf);
2683 }
2684
2685 for &vid in &graph.inputs {
2689 value_shapes
2690 .entry(vid)
2691 .or_insert_with(|| graph.value(vid).shape.clone());
2692 value_dtypes.entry(vid).or_insert(graph.value(vid).dtype);
2693 }
2694 for &nid in &order {
2695 for &out in &graph.node(nid).outputs {
2696 value_shapes
2697 .entry(out)
2698 .or_insert_with(|| graph.value(out).shape.clone());
2699 value_dtypes.entry(out).or_insert(graph.value(out).dtype);
2700 }
2701 }
2702
2703 let has_symbols = value_shapes.values().any(|s| as_static_shape(s).is_none());
2704
2705 let mut sequence_values: HashSet<ValueId> = HashSet::new();
2710 for &nid in &order {
2711 let node = graph.node(nid);
2712 if produces_sequence_output(&node.op_type, &node.domain) {
2713 for &out in &node.outputs {
2714 sequence_values.insert(out);
2715 }
2716 }
2717 }
2718
2719 let mut control_flow_output_values: HashSet<ValueId> = HashSet::new();
2723 for &nid in &order {
2724 let node = graph.node(nid);
2725 if is_control_flow_op(&node.op_type, &node.domain) {
2726 for &out in &node.outputs {
2727 control_flow_output_values.insert(out);
2728 }
2729 }
2730 }
2731
2732 let mut plan = Vec::with_capacity(order.len());
2734 for &nid in &order {
2735 let node = graph.node(nid);
2736 if onnx_runtime_loader::is_ep_context_op(&node.op_type, &node.domain) {
2742 continue;
2743 }
2744 let mut slots: Vec<Option<ValueId>> = node.inputs.clone();
2749 while matches!(slots.last(), Some(None)) {
2750 slots.pop();
2751 }
2752 let inputs = slots;
2753 let outputs: Vec<ValueId> = node.outputs.clone();
2754 let input_dtypes: Vec<DataType> = inputs
2755 .iter()
2756 .map(|v| {
2757 v.map(|vid| value_dtypes[&vid])
2758 .unwrap_or(DataType::Undefined)
2759 })
2760 .collect();
2761 let output_dtypes: Vec<DataType> = outputs.iter().map(|v| value_dtypes[v]).collect();
2762 plan.push(NodePlan {
2763 node_id: nid,
2764 inputs,
2765 outputs,
2766 input_dtypes,
2767 output_dtypes,
2768 });
2769 }
2770
2771 let mut input_index = HashMap::new();
2773 let mut required_inputs = Vec::new();
2774 for &vid in &graph.inputs {
2775 if graph.initializers.contains_key(&vid) {
2776 continue; }
2778 required_inputs.push(vid);
2779 if let Some(name) = &graph.value(vid).name {
2780 input_index.insert(name.clone(), vid);
2781 }
2782 }
2783
2784 let mut name_index = HashMap::new();
2787 for (vid, value) in graph.values.iter() {
2788 if let Some(name) = &value.name {
2789 name_index.insert(name.clone(), vid);
2790 }
2791 }
2792
2793 let mut exec = Self {
2794 graph,
2795 weights,
2796 ep,
2797 weight_handles,
2798 buffers,
2799 buffer_shapes,
2800 value_shapes,
2801 value_dtypes,
2802 plan,
2803 input_index,
2804 required_inputs,
2805 has_symbols,
2806 cache: KernelCache::default(),
2807 name_index,
2808 subgraph_execs: HashMap::new(),
2809 control_flow_stats: ControlFlowStats::default(),
2810 if_last_predicate: HashMap::new(),
2811 device_graph_signature: None,
2812 capture_schedule: None,
2813 capture_segmentation: Vec::new(),
2814 control_flow_output_values,
2815 capture_cf_shapes: HashMap::new(),
2816 capture_warm_signature: None,
2817 capture_warm_shapes: HashMap::new(),
2818 capture_warm_seeded: HashMap::new(),
2819 capture_quarantine_ops: HashSet::new(),
2820 last_capture_failed_node: None,
2821 views: HashMap::new(),
2822 pinned: HashSet::new(),
2823 sequence_values,
2824 shared_buffers: HashMap::new(),
2825 sequences: HashMap::new(),
2826 seq_elem_values: HashMap::new(),
2827 execution_provider_fallback_report,
2828 trace: TraceContext::noop(),
2829 scratch_input_shapes: Vec::new(),
2830 decode_memo_enabled: decode_memo_env_enabled(),
2831 decode_memo_verify: cfg!(debug_assertions) || decode_memo_verify_env_enabled(),
2832 decode_memo: None,
2833 decode_memo_prev_bindings: None,
2834 decode_memo_last_action: DecodeMemoAction::Disabled,
2835 decode_memo_resolved: HashMap::new(),
2836 decode_memo_primed_count: 0,
2837 decode_memo_rebuilt_count: 0,
2838 decode_memo_replayed_count: 0,
2839 decode_memo_ineligible_count: 0,
2840 decode_view_plan: None,
2841 decode_views_reused_count: 0,
2842 decode_dispatch_elided_count: 0,
2843 decode_view_plan_sig_mismatch_streak: 0,
2844 decode_view_plan_disabled: false,
2845 };
2846
2847 if !exec.has_symbols {
2852 let empty = HashMap::new();
2853 let resolved = exec.resolve_all(&empty)?;
2854 exec.size_buffers(&resolved)?;
2855 exec.compile_all(&resolved)?;
2856 }
2857 Ok(exec)
2858 }
2859
2860 fn ensure_buffer(&mut self, vid: ValueId, dtype: DataType, dims: &[usize]) -> Result<()> {
2863 if self.buffer_shapes.get(&vid).map(|s| s.as_slice()) == Some(dims) {
2864 return Ok(()); }
2866 if let Some(old) = self.buffers.remove(&vid) {
2867 self.ep.deallocate(old)?;
2868 }
2869 self.shared_buffers.remove(&vid);
2870 let numel = checked_numel(dims, || format!("value#{}", vid.0))?;
2871 let size = checked_storage_bytes(dtype, numel, || format!("value#{}", vid.0), dims)?;
2872 let buf = self
2873 .ep
2874 .allocate(size.max(1), TensorLayout::contiguous().alignment)?;
2875 self.buffers.insert(vid, buf);
2876 self.buffer_shapes.insert(vid, dims.to_vec());
2877 Ok(())
2878 }
2879
2880 fn resolve_all(
2884 &self,
2885 bindings: &HashMap<SymbolId, usize>,
2886 ) -> Result<HashMap<ValueId, Vec<usize>>> {
2887 let mut resolved = HashMap::with_capacity(self.value_shapes.len());
2888 for (&vid, shape) in &self.value_shapes {
2889 if self.sequence_values.contains(&vid) {
2893 continue;
2894 }
2895 match substitute(shape, bindings) {
2896 Some(dims) => {
2897 resolved.insert(vid, dims);
2898 }
2899 None => {
2900 let value = self.graph.value(vid);
2901 let name = value
2902 .name
2903 .clone()
2904 .unwrap_or_else(|| format!("value#{}", vid.0));
2905 let op = value
2906 .producer
2907 .map(|nid| self.graph.node(nid).op_type.clone())
2908 .unwrap_or_else(|| "<graph input>".to_string());
2909 return Err(SessionError::UnresolvedShape { value: name, op });
2910 }
2911 }
2912 }
2913 Ok(resolved)
2914 }
2915
2916 fn resolve_soft(&self, bindings: &HashMap<SymbolId, usize>) -> HashMap<ValueId, Vec<usize>> {
2921 let mut resolved = HashMap::with_capacity(self.value_shapes.len());
2922 for (&vid, shape) in &self.value_shapes {
2923 if let Some(dims) = substitute(shape, bindings) {
2924 resolved.insert(vid, dims);
2925 }
2926 }
2927 resolved
2928 }
2929
2930 fn resolve_soft_decode_memo(
2940 &mut self,
2941 bindings: &HashMap<SymbolId, usize>,
2942 external: &ExternalBindings,
2943 ) -> HashMap<ValueId, Vec<usize>> {
2944 let external_sig = self.decode_external_signature(external);
2947 if self
2954 .decode_memo
2955 .as_ref()
2956 .is_some_and(|memo| memo.matches(bindings, &external_sig))
2957 {
2958 let memo = self.decode_memo.take().unwrap();
2961 let mut resolved = std::mem::take(&mut self.decode_memo_resolved);
2962 resolved.retain(|vid, _| memo.canonical.contains(vid));
2965 for (&vid, dims) in &memo.invariant_shapes {
2971 resolved.entry(vid).or_insert_with(|| dims.clone());
2972 }
2973 for &vid in &memo.variant_values {
2975 let shape = &self.value_shapes[&vid];
2976 match resolved.get_mut(&vid) {
2977 Some(slot) => {
2978 if !substitute_into(shape, bindings, slot) {
2979 resolved.remove(&vid);
2980 }
2981 }
2982 None => {
2983 if let Some(dims) = substitute(shape, bindings) {
2984 resolved.insert(vid, dims);
2985 }
2986 }
2987 }
2988 }
2989 if self.decode_memo_verify {
2990 let fresh = self.resolve_soft(bindings);
2992 assert_eq!(
2993 resolved, fresh,
2994 "decode-plan memo replay diverged from resolve_soft (unsound invariant \
2995 classification)"
2996 );
2997 }
2998 self.decode_memo = Some(memo);
2999 self.decode_memo_last_action = DecodeMemoAction::Replayed;
3000 self.decode_memo_replayed_count += 1;
3001 self.decode_memo_prev_bindings = Some(bindings.clone());
3002 return resolved;
3003 }
3004
3005 self.decode_memo_resolved.clear();
3017 self.decode_view_plan = None;
3023 let resolved = self.resolve_soft(bindings);
3024 match self.decode_memo_prev_bindings.take() {
3025 Some(prev) if is_decode_growth_transition(&prev, bindings) => {
3026 let decode_varying: HashSet<SymbolId> = bindings
3027 .iter()
3028 .filter(|(sym, val)| prev.get(*sym) != Some(*val))
3029 .map(|(&sym, _)| sym)
3030 .collect();
3031 let mut invariant_shapes = HashMap::with_capacity(resolved.len());
3032 let mut variant_values = Vec::new();
3033 let mut canonical = HashSet::with_capacity(resolved.len());
3034 for (&vid, dims) in &resolved {
3035 canonical.insert(vid);
3036 if shape_references_any(&self.value_shapes[&vid], &decode_varying) {
3037 variant_values.push(vid);
3038 } else {
3039 invariant_shapes.insert(vid, dims.clone());
3040 }
3041 }
3042 self.decode_memo = Some(DecodePlanMemo {
3043 reference_bindings: bindings.clone(),
3044 decode_varying,
3045 invariant_shapes,
3046 variant_values,
3047 canonical,
3048 reference_external_sig: external_sig,
3049 });
3050 self.decode_memo_last_action = DecodeMemoAction::Rebuilt;
3051 self.decode_memo_rebuilt_count += 1;
3052 }
3053 _ => {
3054 self.decode_memo = None;
3058 self.decode_memo_last_action = DecodeMemoAction::Primed;
3059 self.decode_memo_primed_count += 1;
3060 }
3061 }
3062 self.decode_memo_prev_bindings = Some(bindings.clone());
3063 resolved
3064 }
3065
3066 fn decode_external_signature(&self, external: &ExternalBindings) -> Vec<DecodeBindingSig> {
3072 let mut sig: Vec<DecodeBindingSig> = external
3073 .inputs
3074 .keys()
3075 .map(|&vid| (vid, true))
3076 .chain(external.outputs.keys().map(|&vid| (vid, false)))
3077 .map(|(vid, is_input)| DecodeBindingSig {
3078 vid,
3079 is_input,
3080 dtype: self.value_dtypes[&vid],
3081 decl_shape: self.value_shapes[&vid].clone(),
3082 })
3083 .collect();
3084 sig.sort_by_key(|s| (s.vid.0, s.is_input));
3085 sig
3086 }
3087
3088 #[cfg(test)]
3089 fn set_decode_memo_enabled(&mut self, enabled: bool) {
3090 self.decode_memo_enabled = enabled;
3091 self.decode_memo_verify = true;
3092 self.decode_memo = None;
3093 self.decode_memo_prev_bindings = None;
3094 self.decode_memo_resolved.clear();
3095 self.decode_memo_last_action = DecodeMemoAction::Disabled;
3096 self.decode_memo_primed_count = 0;
3097 self.decode_memo_rebuilt_count = 0;
3098 self.decode_memo_replayed_count = 0;
3099 self.decode_memo_ineligible_count = 0;
3100 self.decode_view_plan = None;
3101 self.decode_views_reused_count = 0;
3102 self.decode_dispatch_elided_count = 0;
3103 self.decode_view_plan_sig_mismatch_streak = 0;
3104 self.decode_view_plan_disabled = false;
3105 }
3106
3107 #[cfg(test)]
3108 fn decode_memo_action(&self) -> DecodeMemoAction {
3109 self.decode_memo_last_action
3110 }
3111
3112 pub(crate) fn decode_memo_counts(&self) -> (u64, u64, u64, u64) {
3117 (
3118 self.decode_memo_primed_count,
3119 self.decode_memo_rebuilt_count,
3120 self.decode_memo_replayed_count,
3121 self.decode_memo_ineligible_count,
3122 )
3123 }
3124
3125 pub(crate) fn decode_view_plan_counts(&self) -> (u64, u64) {
3130 (
3131 self.decode_views_reused_count,
3132 self.decode_dispatch_elided_count,
3133 )
3134 }
3135
3136 fn stage2_buffer_sig_matches(&self, plan: &DecodeViewPlan) -> bool {
3143 plan.source_buffer_sig.iter().all(|(vid, ptr, cap)| {
3144 self.buffers
3145 .get(vid)
3146 .is_some_and(|buf| buf.as_ptr() as usize == *ptr && buf.len() == *cap)
3147 })
3148 }
3149
3150 fn build_decode_view_plan(&self) -> Option<DecodeViewPlan> {
3161 let memo = self.decode_memo.as_ref()?;
3162 let invariant = |vid: &ValueId| memo.invariant_shapes.contains_key(vid);
3163 let mut elided_nodes = HashSet::new();
3164 let mut retained_views = Vec::new();
3165 let mut sources: HashSet<ValueId> = HashSet::new();
3166 for pi in 0..self.plan.len() {
3167 let outputs = &self.plan[pi].outputs;
3168 if outputs.is_empty() {
3169 continue;
3170 }
3171 let all_view_invariant = outputs
3175 .iter()
3176 .all(|ovid| invariant(ovid) && self.views.contains_key(ovid));
3177 if !all_view_invariant {
3178 continue;
3179 }
3180 elided_nodes.insert(pi);
3181 for ovid in outputs {
3182 let view = self.views[ovid].clone();
3183 sources.insert(view.source);
3184 retained_views.push((*ovid, view));
3185 }
3186 }
3187 if elided_nodes.is_empty() {
3188 return None;
3189 }
3190 let mut source_buffer_sig = Vec::with_capacity(sources.len());
3192 for &src in &sources {
3193 let buf = self.buffers.get(&src)?;
3194 source_buffer_sig.push((src, buf.as_ptr() as usize, buf.len()));
3195 }
3196 Some(DecodeViewPlan {
3197 elided_nodes,
3198 retained_views,
3199 pinned_sources: sources.into_iter().collect(),
3200 source_buffer_sig,
3201 validated: false,
3202 })
3203 }
3204
3205 fn validate_decode_view_plan(&self, mut plan: DecodeViewPlan) -> Option<DecodeViewPlan> {
3215 let view_matches = |a: &ValueView, b: &ValueView| {
3216 a.source == b.source
3217 && a.shape == b.shape
3218 && a.strides == b.strides
3219 && a.byte_offset == b.byte_offset
3220 };
3221 let mut surviving_nodes: HashSet<usize> = HashSet::new();
3224 let node_outputs = |pi: usize| self.plan[pi].outputs.clone();
3225 for &pi in &plan.elided_nodes {
3226 let ok = node_outputs(pi).iter().all(|ovid| {
3227 match (
3228 plan.retained_views.iter().find(|(v, _)| v == ovid),
3229 self.views.get(ovid),
3230 ) {
3231 (Some((_, cached)), Some(fresh)) => view_matches(cached, fresh),
3232 _ => false,
3233 }
3234 });
3235 if ok {
3236 surviving_nodes.insert(pi);
3237 }
3238 }
3239 if surviving_nodes.is_empty() {
3240 return None;
3241 }
3242 let surviving_outputs: HashSet<ValueId> = surviving_nodes
3245 .iter()
3246 .flat_map(|&pi| self.plan[pi].outputs.clone())
3247 .collect();
3248 let mut retained_views = Vec::new();
3249 let mut sources: HashSet<ValueId> = HashSet::new();
3250 for ovid in surviving_outputs {
3251 let view = self.views.get(&ovid)?.clone();
3252 sources.insert(view.source);
3253 retained_views.push((ovid, view));
3254 }
3255 let mut source_buffer_sig = Vec::with_capacity(sources.len());
3256 for &src in &sources {
3257 let buf = self.buffers.get(&src)?;
3258 source_buffer_sig.push((src, buf.as_ptr() as usize, buf.len()));
3259 }
3260 plan.elided_nodes = surviving_nodes;
3261 plan.retained_views = retained_views;
3262 plan.pinned_sources = sources.into_iter().collect();
3263 plan.source_buffer_sig = source_buffer_sig;
3264 plan.validated = true;
3265 Some(plan)
3266 }
3267
3268 fn size_buffers(&mut self, resolved: &HashMap<ValueId, Vec<usize>>) -> Result<()> {
3274 self.size_buffers_excluding(resolved, &HashSet::new())
3275 }
3276
3277 fn size_buffers_excluding(
3278 &mut self,
3279 resolved: &HashMap<ValueId, Vec<usize>>,
3280 excluded: &HashSet<ValueId>,
3281 ) -> Result<()> {
3282 let vids: Vec<ValueId> = self.value_shapes.keys().copied().collect();
3283 for vid in vids {
3284 if self.graph.initializers.contains_key(&vid) || excluded.contains(&vid) {
3285 continue;
3286 }
3287 if self.sequence_values.contains(&vid) {
3290 continue;
3291 }
3292 let dtype = self.value_dtypes[&vid];
3293 let Some(dims) = resolved.get(&vid).cloned() else {
3294 continue;
3295 };
3296 self.ensure_buffer(vid, dtype, &dims)?;
3297 }
3298 Ok(())
3299 }
3300
3301 fn node_input_shapes(
3305 plan: &NodePlan,
3306 resolved: &HashMap<ValueId, Vec<usize>>,
3307 ) -> Vec<Vec<usize>> {
3308 plan.inputs
3309 .iter()
3310 .map(|v| v.map(|vid| resolved[&vid].clone()).unwrap_or_default())
3311 .collect()
3312 }
3313
3314 fn compile_all(&mut self, resolved: &HashMap<ValueId, Vec<usize>>) -> Result<()> {
3316 for i in 0..self.plan.len() {
3317 let node_id = self.plan[i].node_id;
3318 let node = self.graph.node(node_id);
3319 if is_control_flow_op(&node.op_type, &node.domain) {
3323 continue;
3324 }
3325 if is_sequence_op(&node.op_type, &node.domain) {
3329 continue;
3330 }
3331 let input_shapes = Self::node_input_shapes(&self.plan[i], resolved);
3332 let input_dtypes = self.plan[i].input_dtypes.clone();
3333 let constant_inputs: Vec<bool> = self.plan[i]
3334 .inputs
3335 .iter()
3336 .map(|input| input.is_some_and(|vid| self.graph.initializers.contains_key(&vid)))
3337 .collect();
3338 let node = self.graph.node(node_id);
3339 let opset = effective_opset(&self.graph, node);
3340 self.cache.get_or_create(
3341 node_id,
3342 node,
3343 &input_shapes,
3344 &input_dtypes,
3345 &constant_inputs,
3346 opset,
3347 self.ep.as_ref(),
3348 )?;
3349 }
3350 Ok(())
3351 }
3352
3353 pub(crate) fn cache_stats(&self) -> CacheStats {
3354 self.cache.stats()
3355 }
3356
3357 pub(crate) fn control_flow_stats(&self) -> ControlFlowStats {
3358 self.control_flow_stats
3359 }
3360
3361 pub(crate) fn device_id(&self) -> onnx_runtime_ir::DeviceId {
3362 self.ep.device_id()
3363 }
3364
3365 pub(crate) fn allocate_device_binding(
3366 &self,
3367 input_name: String,
3368 output_name: Option<String>,
3369 dtype: DataType,
3370 physical_shape: Vec<usize>,
3371 logical_shape: Vec<usize>,
3372 ) -> Result<DeviceIoBinding> {
3373 let expose_logical_input_shape = self.input_index.get(&input_name).is_none_or(|&vid| {
3374 if output_name.is_some() {
3375 !self.binding_consumers_use_physical_capacity(vid)
3376 } else {
3377 !self.binding_consumers_use_padded_capacity(vid)
3378 }
3379 });
3380 DeviceIoBinding::allocate(
3381 self.ep.clone(),
3382 input_name,
3383 true,
3384 output_name,
3385 dtype,
3386 physical_shape,
3387 logical_shape,
3388 expose_logical_input_shape,
3389 )
3390 }
3391
3392 pub(crate) fn allocate_device_output_binding(
3393 &self,
3394 output_name: String,
3395 dtype: DataType,
3396 physical_shape: Vec<usize>,
3397 logical_shape: Vec<usize>,
3398 ) -> Result<DeviceIoBinding> {
3399 DeviceIoBinding::allocate(
3400 self.ep.clone(),
3401 String::new(),
3402 false,
3403 Some(output_name),
3404 dtype,
3405 physical_shape,
3406 logical_shape,
3407 false,
3408 )
3409 }
3410
3411 fn binding_consumers_use_physical_capacity(&self, input: ValueId) -> bool {
3412 let mut found = false;
3413 for plan in &self.plan {
3414 for (slot, value) in plan.inputs.iter().enumerate() {
3415 if *value != Some(input) {
3416 continue;
3417 }
3418 found = true;
3419 if !kernel_input_uses_physical_capacity(self.graph.node(plan.node_id), slot) {
3420 return false;
3421 }
3422 }
3423 }
3424 found
3425 }
3426
3427 fn binding_consumers_use_padded_capacity(&self, input: ValueId) -> bool {
3428 let mut found = false;
3429 for plan in &self.plan {
3430 for (slot, value) in plan.inputs.iter().enumerate() {
3431 if *value != Some(input) {
3432 continue;
3433 }
3434 found = true;
3435 if !kernel_input_uses_padded_capacity(self.graph.node(plan.node_id), slot) {
3436 return false;
3437 }
3438 }
3439 }
3440 found
3441 }
3442
3443 pub(crate) fn graph(&self) -> &Graph {
3447 &self.graph
3448 }
3449
3450 pub(crate) fn execution_provider_fallback_report(
3451 &self,
3452 ) -> Option<&ExecutionProviderFallbackReport> {
3453 self.execution_provider_fallback_report.as_ref()
3454 }
3455
3456 pub(crate) fn set_trace_context(&mut self, trace: TraceContext) {
3461 for child in self.subgraph_execs.values_mut() {
3462 child.set_trace_context(trace.clone());
3463 }
3464 self.trace = trace;
3465 }
3466
3467 pub(crate) fn weights(&self) -> &Arc<WeightStore> {
3470 &self.weights
3471 }
3472
3473 pub(crate) fn warmup(&mut self) -> Result<()> {
3478 if self.has_symbols {
3479 return Ok(());
3480 }
3481 let empty = HashMap::new();
3482 let resolved = self.resolve_all(&empty)?;
3483 self.compile_all(&resolved)
3484 }
3485
3486 fn bind_symbols(
3489 &self,
3490 inputs: &[(&str, &Tensor)],
3491 external: &ExternalBindings,
3492 ) -> Result<HashMap<SymbolId, usize>> {
3493 let mut bindings: HashMap<SymbolId, usize> = HashMap::new();
3494 for (name, tensor) in inputs {
3495 let vid = *self
3496 .input_index
3497 .get(*name)
3498 .ok_or_else(|| SessionError::InputNotFound {
3499 name: (*name).to_string(),
3500 })?;
3501 self.bind_input_shape(name, vid, tensor.dtype, &tensor.shape, &mut bindings)?;
3502 }
3503 for (&vid, value) in &external.inputs {
3504 let name = self.graph.value(vid).name.as_deref().unwrap_or("<unnamed>");
3505 self.bind_input_shape(name, vid, value.dtype, &value.shape, &mut bindings)?;
3506 }
3507 Ok(bindings)
3508 }
3509
3510 fn bind_input_shape(
3511 &self,
3512 name: &str,
3513 vid: ValueId,
3514 dtype: DataType,
3515 shape: &[usize],
3516 bindings: &mut HashMap<SymbolId, usize>,
3517 ) -> Result<()> {
3518 let want_dtype = self.value_dtypes[&vid];
3519 if dtype != want_dtype {
3520 return Err(SessionError::DtypeMismatch {
3521 name: name.to_string(),
3522 expected: format!("{want_dtype:?}"),
3523 got: format!("{dtype:?}"),
3524 });
3525 }
3526 let decl = &self.value_shapes[&vid];
3527 if decl.len() != shape.len() {
3528 return Err(SessionError::RankMismatch {
3529 name: name.to_string(),
3530 expected: decl.len(),
3531 got: shape.len(),
3532 });
3533 }
3534 for (dim, &actual) in decl.iter().zip(shape) {
3535 match dim {
3536 Dim::Static(n) if *n != actual => {
3537 return Err(SessionError::ShapeMismatch {
3538 name: name.to_string(),
3539 expected: as_static_shape(decl).unwrap_or_default(),
3540 got: shape.to_vec(),
3541 });
3542 }
3543 Dim::Static(_) => {}
3544 Dim::Symbolic(s) => {
3545 if let Some(&prev) = bindings.get(s) {
3546 if prev != actual {
3547 let sym = self
3548 .symbol_name(*s)
3549 .unwrap_or_else(|| format!("symbol#{}", s.0));
3550 return Err(SessionError::SymbolConflict {
3551 symbol: sym,
3552 first: prev,
3553 second: actual,
3554 });
3555 }
3556 } else {
3557 bindings.insert(*s, actual);
3558 }
3559 }
3560 }
3561 }
3562 Ok(())
3563 }
3564
3565 fn symbol_name(&self, s: SymbolId) -> Option<String> {
3567 self.graph
3568 .symbol_constraints
3569 .get(&s)
3570 .and_then(|c| c.name.clone())
3571 }
3572
3573 pub(crate) fn run(&mut self, inputs: &[(&str, &Tensor)]) -> Result<Vec<Tensor>> {
3575 self.run_outputs(inputs)?
3576 .into_iter()
3577 .map(|output| {
3578 match output {
3579 SessionOutput::Tensor(tensor) => Ok(tensor),
3580 SessionOutput::Sequence(_) => Err(SessionError::SequenceOp {
3581 op: "<graph output>".to_string(),
3582 reason: "the tensor-only run API received a Sequence graph output; use InferenceSession::run_outputs to preserve sequence values".to_string(),
3583 }),
3584 }
3585 })
3586 .collect()
3587 }
3588
3589 pub(crate) fn run_outputs(&mut self, inputs: &[(&str, &Tensor)]) -> Result<Vec<SessionOutput>> {
3590 self.run_scoped(inputs, &HashMap::new(), &ExternalBindings::default())?
3591 .into_iter()
3592 .map(|output| {
3593 output.ok_or_else(|| {
3594 SessionError::Internal(
3595 "ordinary run unexpectedly suppressed a bound graph output".into(),
3596 )
3597 })
3598 })
3599 .collect()
3600 }
3601
3602 pub(crate) fn run_with_device_bindings(
3603 &mut self,
3604 inputs: &[(&str, &Tensor)],
3605 bindings: &mut [DeviceIoBinding],
3606 ) -> Result<Vec<Option<Tensor>>> {
3607 let external = self.prepare_external_bindings(bindings)?;
3608 self.run_scoped(inputs, &HashMap::new(), &external)?
3609 .into_iter()
3610 .map(|output| match output {
3611 None => Ok(None),
3612 Some(SessionOutput::Tensor(tensor)) => Ok(Some(tensor)),
3613 Some(SessionOutput::Sequence(_)) => Err(SessionError::SequenceOp {
3614 op: "<graph output>".to_string(),
3615 reason: "run_with_device_bindings cannot return an unbound Sequence graph output; use run_outputs without tensor device bindings".to_string(),
3616 }),
3617 })
3618 .collect()
3619 }
3620
3621 pub(crate) fn try_capture_with_device_bindings(
3622 &mut self,
3623 inputs: &[(&str, &Tensor)],
3624 bindings: &mut [DeviceIoBinding],
3625 ) -> Result<DeviceGraphCaptureResult> {
3626 let external = self.prepare_external_bindings(bindings)?;
3627 match self.run_scoped_mode(inputs, &HashMap::new(), &external, RunMode::Capture)? {
3628 ScopedRunResult::Executed(outputs) => {
3629 let mut tensors = Vec::with_capacity(outputs.len());
3630 for output in outputs {
3631 match output {
3632 None => tensors.push(None),
3633 Some(SessionOutput::Tensor(tensor)) => tensors.push(Some(tensor)),
3634 Some(SessionOutput::Sequence(_)) => {
3635 self.reset_device_graph()?;
3636 return Ok(DeviceGraphCaptureResult::NotCapturable(
3637 CaptureDeclineReport::one(CaptureDecline::graph(
3638 "device graph capture cannot return a Sequence graph output",
3639 )),
3640 ));
3641 }
3642 }
3643 }
3644 self.device_graph_signature = Some(Self::binding_signature(bindings));
3645 Ok(DeviceGraphCaptureResult::Captured(tensors))
3646 }
3647 ScopedRunResult::NotCapturable(reason) => {
3648 Ok(DeviceGraphCaptureResult::NotCapturable(reason))
3649 }
3650 }
3651 }
3652
3653 pub(crate) fn replay_device_graph(&mut self, bindings: &mut [DeviceIoBinding]) -> Result<bool> {
3658 let external = self.prepare_external_bindings(bindings)?;
3659 let signature = Self::binding_signature(bindings);
3660 if self.device_graph_signature.as_ref() != Some(&signature) {
3661 self.reset_device_graph()?;
3662 return Err(SessionError::Internal(
3663 "device graph replay bindings changed shape, address, or I/O identity; graph was invalidated"
3664 .into(),
3665 ));
3666 }
3667 let single_graph = self
3673 .capture_schedule
3674 .as_ref()
3675 .is_none_or(CaptureSchedule::is_single_graph);
3676 if single_graph {
3677 self.ep.replay_device_graph()?;
3678 return Ok(true);
3679 }
3680 match self.run_scoped_mode(&[], &HashMap::new(), &external, RunMode::Replay)? {
3681 ScopedRunResult::Executed(_) => Ok(self.capture_schedule.is_some()),
3684 ScopedRunResult::NotCapturable(reason) => {
3685 self.reset_device_graph()?;
3686 Err(SessionError::Internal(format!(
3687 "segmented device graph replay lost its schedule: {reason}"
3688 )))
3689 }
3690 }
3691 }
3692
3693 pub(crate) fn reset_device_graph(&mut self) -> Result<bool> {
3694 self.device_graph_signature = None;
3695 self.capture_schedule = None;
3696 self.capture_cf_shapes.clear();
3697 self.capture_warm_seeded.clear();
3698 Ok(self.ep.reset_device_graph()?)
3699 }
3700
3701 pub(crate) fn capture_segmentation(&self) -> &[CaptureDecline] {
3705 &self.capture_segmentation
3706 }
3707
3708 pub(crate) fn captured_segment_count(&self) -> usize {
3711 self.capture_schedule
3712 .as_ref()
3713 .map(CaptureSchedule::captured_segments)
3714 .unwrap_or(0)
3715 }
3716
3717 pub(crate) fn check_device_capture_error(&self) -> Result<u32> {
3718 Ok(self.ep.check_device_capture_error()?)
3719 }
3720
3721 pub(crate) fn device_allocation_counts(&self) -> Option<DeviceAllocationCounts> {
3722 self.ep
3723 .device_allocation_counts()
3724 .map(|(allocations, frees)| DeviceAllocationCounts { allocations, frees })
3725 }
3726
3727 fn binding_signature(bindings: &[DeviceIoBinding]) -> Vec<DeviceBindingSignature> {
3728 bindings
3729 .iter()
3730 .map(|binding| DeviceBindingSignature {
3731 input_name: binding.input_name().to_string(),
3732 binds_input: binding.binds_input(),
3733 output_name: binding.output_name().map(str::to_string),
3734 dtype: binding.dtype,
3735 physical_shape: binding.physical_shape().to_vec(),
3736 device_ptr: binding.device_ptr() as usize,
3737 })
3738 .collect()
3739 }
3740
3741 fn prepare_external_bindings(
3742 &self,
3743 bindings: &mut [DeviceIoBinding],
3744 ) -> Result<ExternalBindings> {
3745 let mut external = ExternalBindings::default();
3746 for binding in bindings {
3747 let input_name = binding.input_name().to_string();
3748 let bind_input = binding.binds_input();
3749 let output_name = binding.output_name().map(str::to_string);
3750 let dtype = binding.dtype;
3751 let len = binding.buffer().len();
3752 let alignment = binding.buffer().alignment();
3753 let device = binding.buffer().device();
3754 if device != self.ep.device_id() {
3755 return Err(SessionError::Internal(format!(
3756 "device binding '{input_name}' is on {device:?}, session is on {:?}",
3757 self.ep.device_id()
3758 )));
3759 }
3760 let physical_shape = binding.physical_shape();
3761 let required = dtype.storage_bytes(physical_shape.iter().product());
3762 if required > len {
3763 return Err(SessionError::Internal(format!(
3764 "device binding '{input_name}' needs {required} bytes for {physical_shape:?}, allocation has {len}"
3765 )));
3766 }
3767 let ptr = binding.buffer_mut().as_mut_ptr();
3768 if bind_input {
3769 let input_vid = *self.input_index.get(&input_name).ok_or_else(|| {
3770 SessionError::InputNotFound {
3771 name: input_name.clone(),
3772 }
3773 })?;
3774 let value = ExternalValue {
3775 dtype,
3776 shape: binding.kernel_input_shape().to_vec(),
3777 accepts_subshape: false,
3778 ptr,
3779 len,
3780 alignment,
3781 device,
3782 };
3783 if external.inputs.insert(input_vid, value).is_some() {
3784 return Err(SessionError::Internal(format!(
3785 "duplicate device input binding '{input_name}'"
3786 )));
3787 }
3788 }
3789 if let Some(output_name) = output_name {
3790 let output_vid = self
3791 .graph
3792 .outputs
3793 .iter()
3794 .copied()
3795 .find(|&vid| {
3796 self.graph.value(vid).name.as_deref() == Some(output_name.as_str())
3797 })
3798 .ok_or_else(|| {
3799 SessionError::Internal(format!(
3800 "device binding output not found: {output_name}"
3801 ))
3802 })?;
3803 if self.sequence_values.contains(&output_vid) {
3804 return Err(SessionError::SequenceOp {
3805 op: "<graph output binding>".to_string(),
3806 reason: format!(
3807 "graph output '{output_name}' is a Sequence value and cannot be bound to tensor device storage"
3808 ),
3809 });
3810 }
3811 if self.value_dtypes[&output_vid] != dtype {
3812 return Err(SessionError::DtypeMismatch {
3813 name: output_name.clone(),
3814 expected: format!("{:?}", self.value_dtypes[&output_vid]),
3815 got: format!("{dtype:?}"),
3816 });
3817 }
3818 let value = ExternalValue {
3819 dtype,
3820 shape: binding.physical_shape().to_vec(),
3821 accepts_subshape: bind_input
3822 && binding.logical_shape() != binding.physical_shape(),
3823 ptr,
3824 len,
3825 alignment,
3826 device,
3827 };
3828 if external.outputs.insert(output_vid, value).is_some() {
3829 return Err(SessionError::Internal(format!(
3830 "duplicate device output binding '{output_name}'"
3831 )));
3832 }
3833 }
3834 }
3835 Ok(external)
3836 }
3837
3838 fn run_scoped(
3844 &mut self,
3845 inputs: &[(&str, &Tensor)],
3846 outer_scope: &HashMap<String, Tensor>,
3847 external: &ExternalBindings,
3848 ) -> Result<Vec<Option<SessionOutput>>> {
3849 match self.run_scoped_mode(inputs, outer_scope, external, RunMode::Eager)? {
3850 ScopedRunResult::Executed(outputs) => Ok(outputs),
3851 ScopedRunResult::NotCapturable(_) => unreachable!("eager runs are always executed"),
3852 }
3853 }
3854
3855 fn run_scoped_mode(
3856 &mut self,
3857 inputs: &[(&str, &Tensor)],
3858 outer_scope: &HashMap<String, Tensor>,
3859 external: &ExternalBindings,
3860 mode: RunMode,
3861 ) -> Result<ScopedRunResult> {
3862 thread_local! {
3866 static RUN_DEPTH: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
3867 }
3868 let depth = RUN_DEPTH.with(|d| {
3869 let cur = d.get();
3870 d.set(cur + 1);
3871 cur
3872 });
3873 struct DepthGuard;
3874 impl Drop for DepthGuard {
3875 fn drop(&mut self) {
3876 RUN_DEPTH.with(|d| d.set(d.get().saturating_sub(1)));
3877 }
3878 }
3879 let _depth_guard = DepthGuard;
3880 let nested = depth > 0;
3881 self.views.clear();
3884 self.pinned.clear();
3885 self.sequences.clear();
3888 self.seq_elem_values.clear();
3889 self.restore_shared_buffers()?;
3890
3891 let _phase_setup = phase_span!(if nested {
3893 "run_scoped.setup_total.child"
3894 } else {
3895 "run_scoped.setup_total.top"
3896 });
3897 let bindings = self.bind_symbols(inputs, external)?;
3898
3899 for (name, _) in inputs {
3900 let vid = self.input_index[*name];
3901 if external.inputs.contains_key(&vid) {
3902 return Err(SessionError::Internal(format!(
3903 "input '{name}' is bound both as a host tensor and a persistent device buffer"
3904 )));
3905 }
3906 }
3907
3908 let mut provided: HashSet<ValueId> = inputs
3910 .iter()
3911 .filter_map(|(name, _)| self.input_index.get(*name).copied())
3912 .collect();
3913 provided.extend(external.inputs.keys().copied());
3914 for &vid in &self.required_inputs {
3915 if !provided.contains(&vid) {
3916 let name = self
3917 .graph
3918 .value(vid)
3919 .name
3920 .clone()
3921 .unwrap_or_else(|| format!("value#{}", vid.0));
3922 return Err(SessionError::InputNotFound { name });
3923 }
3924 }
3925
3926 let decode_memo_eligible = self.decode_memo_enabled
3949 && mode == RunMode::Eager
3950 && !nested
3951 && self.ep.device_type() != DeviceType::Cuda;
3952 let mut resolved = {
3953 let _s = phase_span!("run_scoped.resolve_soft");
3954 if decode_memo_eligible {
3955 self.resolve_soft_decode_memo(&bindings, external)
3956 } else {
3957 if self.decode_memo_enabled && !nested {
3962 self.decode_memo_ineligible_count += 1;
3963 }
3964 let mut resolved = self.resolve_soft(&bindings);
3965 if mode != RunMode::Eager {
3966 external.seed_capture_shapes(&mut resolved);
3970 self.seed_control_flow_capture_shapes(&mut resolved);
3975 self.seed_warm_decode_capture_shapes(&mut resolved, external);
3983 }
3984 resolved
3985 }
3986 };
3987 let mut stage2_plan: Option<DecodeViewPlan> = None;
3995 let mut stage2_candidate: Option<DecodeViewPlan> = None;
3996 let mut stage2_excluded: Option<HashSet<ValueId>> = None;
3997 if decode_memo_eligible
3998 && !self.decode_view_plan_disabled
3999 && self.decode_memo_last_action == DecodeMemoAction::Replayed
4000 && let Some(plan) = self.decode_view_plan.take()
4001 {
4002 if !plan.validated {
4003 stage2_candidate = Some(plan);
4008 } else if self.stage2_buffer_sig_matches(&plan) {
4009 self.decode_view_plan_sig_mismatch_streak = 0;
4010 for (vid, view) in &plan.retained_views {
4018 self.views.insert(*vid, view.clone());
4019 resolved.insert(*vid, view.shape.clone());
4020 }
4021 for &src in &plan.pinned_sources {
4022 self.pinned.insert(src);
4023 }
4024 self.decode_views_reused_count += plan.retained_views.len() as u64;
4025 self.decode_dispatch_elided_count += plan.elided_nodes.len() as u64;
4026 if let Some(memo) = self.decode_memo.as_ref() {
4032 stage2_excluded = Some(memo.invariant_shapes.keys().copied().collect());
4033 }
4034 stage2_plan = Some(plan);
4035 } else {
4036 self.decode_view_plan_sig_mismatch_streak += 1;
4041 if self.decode_view_plan_sig_mismatch_streak >= STAGE2_SIG_MISMATCH_LIMIT {
4042 self.decode_view_plan_disabled = true;
4043 }
4044 }
4045 }
4046 let external_values = external
4047 .inputs
4048 .keys()
4049 .chain(external.outputs.keys())
4050 .copied()
4051 .collect::<HashSet<_>>();
4052 for &vid in &external_values {
4053 if let Some(old) = self.buffers.remove(&vid) {
4054 self.ep.deallocate(old)?;
4055 }
4056 self.shared_buffers.remove(&vid);
4057 self.buffer_shapes.remove(&vid);
4058 }
4059 {
4060 let _s = phase_span!("run_scoped.size_buffers");
4061 match &stage2_excluded {
4062 Some(invariant) => {
4066 let mut excluded = external_values.clone();
4067 excluded.extend(invariant.iter().copied());
4068 self.size_buffers_excluding(&resolved, &excluded)?;
4069 }
4070 None => {
4071 self.size_buffers_excluding(&resolved, &external_values)?;
4072 }
4073 }
4074 }
4075
4076 for (name, tensor) in inputs {
4078 let vid = self.input_index[*name];
4079 let buf = self
4080 .buffers
4081 .get_mut(&vid)
4082 .expect("input value has a buffer");
4083 self.ep.copy_from_host(tensor.as_bytes(), buf)?;
4084 }
4085 drop(_phase_setup);
4086
4087 match mode {
4092 RunMode::Eager => {
4093 let _s = phase_span!(if nested {
4094 "run_scoped.plan_eager.child"
4095 } else {
4096 "run_scoped.plan_eager.top"
4097 });
4098 let verify_stage2 = self.decode_memo_verify && stage2_plan.is_some();
4103 let verify_snapshot: Option<Vec<(ValueId, ValueView)>> = if verify_stage2 {
4104 stage2_plan.as_ref().map(|p| p.retained_views.clone())
4105 } else {
4106 None
4107 };
4108 let elided = if verify_stage2 {
4109 None
4110 } else {
4111 stage2_plan.as_ref().map(|p| &p.elided_nodes)
4112 };
4113 self.run_plan_eager(&mut resolved, outer_scope, external, elided)?;
4114 if let (Some(snapshot), Some(plan)) = (&verify_snapshot, &stage2_plan) {
4115 for (vid, cached) in snapshot {
4116 let fresh = self.views.get(vid).unwrap_or_else(|| {
4117 panic!(
4118 "F5 Stage 2 verify: elided view value#{} was not rebuilt by a \
4119 full dispatch",
4120 vid.0
4121 )
4122 });
4123 assert!(
4124 fresh.source == cached.source
4125 && fresh.shape == cached.shape
4126 && fresh.strides == cached.strides
4127 && fresh.byte_offset == cached.byte_offset,
4128 "F5 Stage 2 verify: cached view for value#{} ({cached:?}) diverged \
4129 from a freshly built one ({fresh:?}) — invariant view reuse is unsound",
4130 vid.0
4131 );
4132 }
4133 assert!(
4134 self.stage2_buffer_sig_matches(plan),
4135 "F5 Stage 2 verify: a cached view source buffer moved during the step"
4136 );
4137 }
4138 if decode_memo_eligible {
4143 match self.decode_memo_last_action {
4144 DecodeMemoAction::Rebuilt => {
4145 self.decode_view_plan = self.build_decode_view_plan();
4146 }
4147 DecodeMemoAction::Replayed => {
4148 if let Some(cand) = stage2_candidate.take() {
4149 self.decode_view_plan = self.validate_decode_view_plan(cand);
4154 } else if let Some(plan) = stage2_plan.take() {
4155 self.decode_view_plan = Some(plan);
4156 }
4157 }
4158 _ => {}
4159 }
4160 }
4161 if !decode_memo_eligible {
4171 self.capture_warm_shapes = resolved.clone();
4172 self.capture_warm_signature = Some(external.capture_signature());
4173 }
4174 }
4175 RunMode::Capture => {
4176 self.if_last_predicate.clear();
4182 let max_capture_attempts = self.plan.len() + 1;
4198 let schedule = 'capture: loop {
4199 let schedule = match self.plan_capture_segments(&resolved, external) {
4200 Ok(schedule) => schedule,
4201 Err(report) => return Ok(ScopedRunResult::NotCapturable(report)),
4202 };
4203 self.last_capture_failed_node = None;
4204 match self.run_plan_segmented(
4205 &schedule,
4206 RunMode::Capture,
4207 &mut resolved,
4208 outer_scope,
4209 external,
4210 ) {
4211 Ok(_) => break 'capture schedule,
4212 Err(error) => {
4213 let _ = self.ep.reset_device_graph();
4214 let quarantined =
4219 self.last_capture_failed_node.take().and_then(|node_id| {
4220 let node = self.graph.node(node_id);
4221 let key = (canonical_domain(node), node.op_type.clone());
4222 self.capture_quarantine_ops.insert(key).then_some(())
4223 });
4224 if quarantined.is_some()
4225 && self.capture_quarantine_ops.len() < max_capture_attempts
4226 {
4227 self.if_last_predicate.clear();
4229 continue 'capture;
4230 }
4231 self.capture_schedule = None;
4232 self.capture_segmentation.clear();
4233 self.capture_cf_shapes.clear();
4234 self.capture_warm_seeded.clear();
4235 return Ok(ScopedRunResult::NotCapturable(CaptureDeclineReport::one(
4236 CaptureDecline::graph(format!(
4237 "segmented CUDA graph capture failed: {error}"
4238 )),
4239 )));
4240 }
4241 }
4242 };
4243 if let Some((vid, seeded)) = self
4251 .capture_warm_seeded
4252 .iter()
4253 .find(|(vid, seeded)| resolved.get(vid) != Some(*seeded))
4254 .map(|(vid, seeded)| (*vid, seeded.clone()))
4255 {
4256 let current = resolved.get(&vid).cloned();
4257 let _ = self.ep.reset_device_graph();
4258 self.capture_schedule = None;
4259 self.capture_segmentation.clear();
4260 self.capture_cf_shapes.clear();
4261 self.capture_warm_seeded.clear();
4262 return Ok(ScopedRunResult::NotCapturable(CaptureDeclineReport::one(
4263 CaptureDecline::graph(format!(
4264 "warm decode shape seed for value#{} ({seeded:?}) diverged from the \
4265 captured shape ({current:?}); recapturing",
4266 vid.0
4267 )),
4268 )));
4269 }
4270 self.capture_cf_shapes = self
4274 .control_flow_output_values
4275 .iter()
4276 .filter_map(|vid| resolved.get(vid).map(|shape| (*vid, shape.clone())))
4277 .collect();
4278 self.capture_segmentation = schedule.boundaries.clone();
4279 if capture_segmentation_logging_enabled() {
4280 log_capture_segmentation(&schedule);
4281 }
4282 self.capture_schedule = Some(schedule);
4283 }
4284 RunMode::Replay => {
4285 let Some(schedule) = self.capture_schedule.take() else {
4288 return Ok(ScopedRunResult::NotCapturable(CaptureDeclineReport::one(
4289 CaptureDecline::graph(
4290 "segmented device graph replay requested without a capture schedule",
4291 ),
4292 )));
4293 };
4294 let still_valid = self.run_plan_segmented(
4295 &schedule,
4296 RunMode::Replay,
4297 &mut resolved,
4298 outer_scope,
4299 external,
4300 )?;
4301 if still_valid {
4302 self.capture_schedule = Some(schedule);
4303 } else {
4304 self.capture_segmentation.clear();
4310 self.capture_cf_shapes.clear();
4311 self.device_graph_signature = None;
4312 self.ep.reset_device_graph()?;
4313 }
4314 }
4315 }
4316
4317 let _phase_collect = phase_span!(if nested {
4322 "run_scoped.collect_outputs.child"
4323 } else {
4324 "run_scoped.collect_outputs.top"
4325 });
4326 let mut results = Vec::with_capacity(self.graph.outputs.len());
4327 let mut host_output_bytes = 0usize;
4328 let output_vids: Vec<ValueId> = self.graph.outputs.clone();
4329 for vid in output_vids {
4330 if external.outputs.contains_key(&vid) {
4331 results.push(None);
4332 continue;
4333 }
4334 if self.sequence_values.contains(&vid) {
4335 let sequence = self.sequences.get(&vid).cloned().ok_or_else(|| {
4336 SessionError::Internal(format!(
4337 "sequence graph output value#{} has no live runtime value",
4338 vid.0
4339 ))
4340 })?;
4341 results.push(Some(SessionOutput::Sequence(sequence)));
4342 continue;
4343 }
4344
4345 let dtype = self.value_dtypes[&vid];
4346 let shape = resolved[&vid].clone();
4347 if !nested && let Some(tensor) = self.try_move_host_output(vid, &shape, dtype)? {
4352 results.push(Some(SessionOutput::Tensor(tensor)));
4353 continue;
4354 }
4355 let bytes = self.contiguous_bytes(vid, &shape, dtype)?;
4356 host_output_bytes += bytes.len();
4357 results.push(Some(SessionOutput::Tensor(Tensor::from_raw(
4358 dtype, shape, &bytes,
4359 )?)));
4360 }
4361 if !nested {
4367 phase_profile::record("collect_outputs.top_host_bytes", host_output_bytes as u128);
4368 }
4369 if decode_memo_eligible {
4376 self.decode_memo_resolved = std::mem::take(&mut resolved);
4377 }
4378 Ok(ScopedRunResult::Executed(results))
4379 }
4380
4381 fn seed_control_flow_capture_shapes(&self, resolved: &mut HashMap<ValueId, Vec<usize>>) {
4402 for &vid in &self.control_flow_output_values {
4403 if resolved.contains_key(&vid) {
4404 continue;
4405 }
4406 if let Some(shape) = self.buffer_shapes.get(&vid) {
4407 resolved.insert(vid, shape.clone());
4408 }
4409 }
4410 }
4411
4412 fn seed_warm_decode_capture_shapes(
4434 &mut self,
4435 resolved: &mut HashMap<ValueId, Vec<usize>>,
4436 external: &ExternalBindings,
4437 ) {
4438 self.capture_warm_seeded.clear();
4439 if self.capture_warm_signature.as_ref() != Some(&external.capture_signature()) {
4442 return;
4443 }
4444 let external_values: HashSet<ValueId> = external
4445 .inputs
4446 .keys()
4447 .chain(external.outputs.keys())
4448 .copied()
4449 .collect();
4450 let warm: Vec<(ValueId, Vec<usize>)> = self
4451 .capture_warm_shapes
4452 .iter()
4453 .map(|(&vid, shape)| (vid, shape.clone()))
4454 .collect();
4455 for (vid, shape) in warm {
4456 if resolved.contains_key(&vid)
4457 || external_values.contains(&vid)
4458 || self.graph.initializers.contains_key(&vid)
4459 || self.sequence_values.contains(&vid)
4460 {
4461 continue;
4462 }
4463 self.capture_warm_seeded.insert(vid, shape.clone());
4464 resolved.insert(vid, shape);
4465 }
4466 }
4467
4468 fn control_flow_seam_invalidated(
4474 &self,
4475 pi: usize,
4476 resolved: &HashMap<ValueId, Vec<usize>>,
4477 ) -> bool {
4478 let node = self.graph.node(self.plan[pi].node_id);
4479 if !is_control_flow_op(&node.op_type, &node.domain) {
4480 return false;
4481 }
4482 self.plan[pi].outputs.iter().any(|out| {
4483 match (self.capture_cf_shapes.get(out), resolved.get(out)) {
4484 (Some(captured), Some(current)) => captured != current,
4485 (Some(_), None) => true,
4486 _ => false,
4487 }
4488 })
4489 }
4490
4491 fn node_capture_reason(
4492 &self,
4493 plan: &NodePlan,
4494 resolved: &HashMap<ValueId, Vec<usize>>,
4495 ) -> Option<CaptureDecline> {
4496 let node = self.graph.node(plan.node_id);
4497 if self
4502 .capture_quarantine_ops
4503 .contains(&(canonical_domain(node), node.op_type.clone()))
4504 {
4505 return Some(CaptureDecline::node(
4506 plan.node_id,
4507 node,
4508 SeamReason::CaptureRecordingFailed,
4509 "kernel aborted device-graph recording on a prior capture pass; \
4510 quarantined to an eager seam",
4511 ));
4512 }
4513 let outputs_resolved = plan
4514 .outputs
4515 .iter()
4516 .all(|output| resolved.contains_key(output));
4517 let inputs_resolved = plan.inputs.iter().all(|input| match input {
4518 Some(value) => resolved.contains_key(value),
4519 None => true,
4520 });
4521 if let Some(decline) = self.ep.plan_capture_region(
4522 node,
4523 CaptureRegionShapeStatus {
4524 inputs_resolved,
4525 outputs_resolved,
4526 },
4527 ) {
4528 return Some(structural_capture_decline(plan.node_id, node, decline));
4529 }
4530 assert!(
4531 inputs_resolved && outputs_resolved,
4532 "EP capture-region policy admitted a node with unresolved shapes"
4533 );
4534 let input_shapes = plan
4535 .inputs
4536 .iter()
4537 .map(|input| {
4538 input.map_or_else(Vec::new, |value| {
4539 resolved
4540 .get(&value)
4541 .cloned()
4542 .expect("resolved input shape checked above")
4543 })
4544 })
4545 .collect();
4546 let key = KernelKey {
4547 node: plan.node_id.0,
4548 shapes: input_shapes,
4549 };
4550 let Some(kernel) = self.cache.entries.get(&key) else {
4551 return Some(CaptureDecline::node(
4552 plan.node_id,
4553 node,
4554 SeamReason::KernelNotWarmed,
4555 "kernel has not been warmed for the requested capture shape",
4556 ));
4557 };
4558 kernel_capture_decline(plan.node_id, node, kernel.as_ref())
4559 }
4560
4561 fn plan_capture_segments(
4571 &self,
4572 resolved: &HashMap<ValueId, Vec<usize>>,
4573 external: &ExternalBindings,
4574 ) -> std::result::Result<CaptureSchedule, CaptureDeclineReport> {
4575 if self
4576 .graph
4577 .outputs
4578 .iter()
4579 .any(|output| !external.outputs.contains_key(output))
4580 {
4581 return Err(CaptureDeclineReport::one(CaptureDecline::graph(
4582 "every graph output must use a persistent device binding during capture",
4583 )));
4584 }
4585
4586 let declines: Vec<Option<CaptureDecline>> = self
4587 .plan
4588 .iter()
4589 .map(|plan| self.node_capture_reason(plan, resolved))
4590 .collect();
4591
4592 let mut segments: Vec<ScheduledSegment> = Vec::new();
4593 let mut boundaries: Vec<CaptureDecline> = Vec::new();
4594 let mut next_graph_index = 0usize;
4595 let mut pi = 0usize;
4596 while pi < declines.len() {
4597 let captured = declines[pi].is_none();
4598 let start = pi;
4599 while pi < declines.len() && declines[pi].is_none() == captured {
4600 if let Some(decline) = &declines[pi] {
4601 boundaries.push(decline.clone());
4602 }
4603 pi += 1;
4604 }
4605 let graph_index = if captured {
4606 let index = next_graph_index;
4607 next_graph_index += 1;
4608 index
4609 } else {
4610 0
4611 };
4612 segments.push(ScheduledSegment {
4613 start,
4614 end: pi,
4615 captured,
4616 graph_index,
4617 });
4618 }
4619
4620 if next_graph_index == 0 {
4621 return Err(CaptureDeclineReport {
4622 entries: boundaries,
4623 });
4624 }
4625
4626 Ok(CaptureSchedule {
4627 segments,
4628 boundaries,
4629 })
4630 }
4631
4632 fn collect_segment_kernels(
4635 &self,
4636 seg: &ScheduledSegment,
4637 resolved: &HashMap<ValueId, Vec<usize>>,
4638 ) -> Result<Vec<&dyn onnx_runtime_ep_api::Kernel>> {
4639 let mut kernels = Vec::with_capacity(seg.end - seg.start);
4640 for pi in seg.start..seg.end {
4641 let plan = &self.plan[pi];
4642 let input_shapes = plan
4643 .inputs
4644 .iter()
4645 .map(|input| {
4646 input
4647 .map(|value| resolved.get(&value).cloned())
4648 .unwrap_or(Some(Vec::new()))
4649 })
4650 .collect::<Option<Vec<_>>>()
4651 .ok_or_else(|| {
4652 SessionError::Internal(format!(
4653 "segment kernel node {} lost its resolved input shape before capture",
4654 plan.node_id.0
4655 ))
4656 })?;
4657 let key = KernelKey {
4658 node: plan.node_id.0,
4659 shapes: input_shapes,
4660 };
4661 let kernel = self.cache.entries.get(&key).ok_or_else(|| {
4662 SessionError::Internal(format!(
4663 "segment kernel node {} was not warmed before capture",
4664 plan.node_id.0
4665 ))
4666 })?;
4667 kernels.push(kernel.as_ref());
4668 }
4669 Ok(kernels)
4670 }
4671
4672 fn exec_plan_node(
4681 &mut self,
4682 pi: usize,
4683 resolved: &mut HashMap<ValueId, Vec<usize>>,
4684 outer_scope: &HashMap<String, Tensor>,
4685 external: &ExternalBindings,
4686 capture: OpCaptureTrace<'_>,
4687 ) -> Result<()> {
4688 let (is_control_flow, is_sequence, _span) = {
4696 let node = self.graph.node(self.plan[pi].node_id);
4697 let is_control_flow = is_control_flow_op(&node.op_type, &node.domain);
4698 let is_sequence = is_sequence_op(&node.op_type, &node.domain);
4699 let span = self.trace.is_enabled().then(|| {
4702 let span = self.trace.span(node.op_type.clone(), "op");
4703 capture.annotate();
4706 span
4707 });
4708 (is_control_flow, is_sequence, span)
4709 };
4710 if is_control_flow {
4711 self.exec_control_flow(pi, resolved, outer_scope)
4712 } else if is_sequence {
4713 self.exec_sequence_node(pi, resolved, external)
4714 } else {
4715 self.exec_kernel_node(pi, resolved, external)
4716 }
4717 }
4718
4719 fn run_plan_eager(
4727 &mut self,
4728 resolved: &mut HashMap<ValueId, Vec<usize>>,
4729 outer_scope: &HashMap<String, Tensor>,
4730 external: &ExternalBindings,
4731 elided: Option<&HashSet<usize>>,
4732 ) -> Result<()> {
4733 let elided = elided.filter(|set| !set.is_empty());
4734 if profile_ops_enabled() {
4735 let run_start = Instant::now();
4736 let mut timings: HashMap<String, (Duration, usize)> = HashMap::new();
4737 for pi in 0..self.plan.len() {
4738 if elided.is_some_and(|set| set.contains(&pi)) {
4739 continue;
4740 }
4741 let op_type = self.graph.node(self.plan[pi].node_id).op_type.clone();
4742 let start = Instant::now();
4743 let result =
4744 self.exec_plan_node(pi, resolved, outer_scope, external, OpCaptureTrace::Eager);
4745 let elapsed = start.elapsed();
4746 let entry = timings.entry(op_type).or_insert((Duration::ZERO, 0));
4747 entry.0 += elapsed;
4748 entry.1 += 1;
4749 result?;
4750 }
4751 print_op_profile(run_start.elapsed(), timings);
4752 } else {
4753 for pi in 0..self.plan.len() {
4754 if elided.is_some_and(|set| set.contains(&pi)) {
4755 continue;
4756 }
4757 self.exec_plan_node(pi, resolved, outer_scope, external, OpCaptureTrace::Eager)?;
4758 }
4759 }
4760 Ok(())
4761 }
4762
4763 fn run_plan_segmented(
4778 &mut self,
4779 schedule: &CaptureSchedule,
4780 mode: RunMode,
4781 resolved: &mut HashMap<ValueId, Vec<usize>>,
4782 outer_scope: &HashMap<String, Tensor>,
4783 external: &ExternalBindings,
4784 ) -> Result<bool> {
4785 let ep = Arc::clone(&self.ep);
4786 let mut invalidated = false;
4791 for seg in &schedule.segments {
4792 if invalidated {
4793 for pi in seg.start..seg.end {
4796 self.exec_plan_node(
4797 pi,
4798 resolved,
4799 outer_scope,
4800 external,
4801 OpCaptureTrace::Eager,
4802 )?;
4803 }
4804 continue;
4805 }
4806 if seg.captured {
4807 match mode {
4808 RunMode::Capture => {
4809 {
4810 let kernels = self.collect_segment_kernels(seg, resolved)?;
4811 ep.begin_device_graph_capture(&kernels)?;
4812 }
4813 let mut capture_guard = SegmentCaptureGuard::arm(ep.as_ref());
4821 for pi in seg.start..seg.end {
4822 let node_id = self.plan[pi].node_id;
4823 if let Err(error) = self.exec_plan_node(
4824 pi,
4825 resolved,
4826 outer_scope,
4827 external,
4828 OpCaptureTrace::Captured,
4829 ) {
4830 self.last_capture_failed_node = Some(node_id);
4835 return Err(error);
4836 }
4837 }
4838 capture_guard.disarm();
4839 ep.end_device_graph_capture()?;
4840 ep.replay_device_graph_segment(seg.graph_index)?;
4841 }
4842 RunMode::Replay => {
4843 ep.replay_device_graph_segment(seg.graph_index)?;
4844 }
4845 RunMode::Eager => {
4846 unreachable!("eager runs never build a segment schedule")
4847 }
4848 }
4849 } else {
4850 for pi in seg.start..seg.end {
4851 let node_id = self.plan[pi].node_id.0;
4854 let reason = schedule
4855 .boundaries
4856 .iter()
4857 .find(|decline| decline.node_id == Some(node_id))
4858 .map(|decline| decline.reason.as_str())
4859 .unwrap_or("non-capturable seam node (no recorded reason)");
4860 self.exec_plan_node(
4861 pi,
4862 resolved,
4863 outer_scope,
4864 external,
4865 OpCaptureTrace::Rejected(reason),
4866 )?;
4867 if mode == RunMode::Replay && self.control_flow_seam_invalidated(pi, resolved) {
4872 invalidated = true;
4873 }
4874 }
4875 }
4876 }
4877 Ok(!invalidated)
4878 }
4879
4880 fn refill_input_shapes(&mut self, pi: usize, resolved: &HashMap<ValueId, Vec<usize>>) {
4893 let inputs = &self.plan[pi].inputs;
4894 let scratch = &mut self.scratch_input_shapes;
4895 scratch.truncate(inputs.len());
4896 for (i, slot) in inputs.iter().enumerate() {
4897 if i < scratch.len() {
4898 scratch[i].clear();
4899 } else {
4900 scratch.push(Vec::new());
4901 }
4902 if let Some(vid) = slot {
4903 scratch[i].extend_from_slice(&resolved[vid]);
4904 }
4905 }
4906 }
4907
4908 fn exec_kernel_node(
4912 &mut self,
4913 pi: usize,
4914 resolved: &mut HashMap<ValueId, Vec<usize>>,
4915 external: &ExternalBindings,
4916 ) -> Result<()> {
4917 let _node_span = phase_span!("exec_kernel.node");
4921 let node_id = self.plan[pi].node_id;
4927 self.refill_input_shapes(pi, resolved);
4931 let inputs = &self.plan[pi].inputs;
4932 let outputs = &self.plan[pi].outputs;
4933 let input_dtypes = &self.plan[pi].input_dtypes;
4934 let output_dtypes = &self.plan[pi].output_dtypes;
4935 let input_shapes = &self.scratch_input_shapes;
4936
4937 let node = self.graph.node(node_id);
4938 if let Some(output_shape) = runtime_elementwise_output_shape(node, input_shapes) {
4939 let output_shape = output_shape.map_err(|_| {
4940 let node_name = if node.name.is_empty() {
4941 format!("<unnamed node #{}>", node_id.0)
4942 } else {
4943 format!("{:?}", node.name)
4944 };
4945 SessionError::RuntimeBroadcastIncompatible {
4946 node: node_name,
4947 domain: canonical_domain(node),
4948 op_type: node.op_type.clone(),
4949 input_shapes: input_shapes.to_vec(),
4950 }
4951 })?;
4952 if outputs.len() != 1 {
4953 return Err(SessionError::OutputShapeCountMismatch {
4954 op: node.op_type.clone(),
4955 expected: outputs.len(),
4956 got: 1,
4957 });
4958 }
4959 resolved.insert(outputs[0], output_shape);
4960 }
4961
4962 if outputs.iter().any(|v| !resolved.contains_key(v)) {
4967 let opset = effective_opset(&self.graph, node);
4968 let input_values: Vec<Option<Vec<i64>>> = inputs
4969 .iter()
4970 .enumerate()
4971 .map(|(i, v)| {
4972 v.and_then(|vid| self.shape_input_i64(vid, &input_shapes[i], input_dtypes[i]))
4973 })
4974 .collect();
4975 let input_float_values: Vec<Option<Vec<f64>>> = inputs
4983 .iter()
4984 .enumerate()
4985 .map(|(i, v)| {
4986 if !reads_float_shape_input(node, i, opset) {
4987 return None;
4988 }
4989 v.and_then(|vid| {
4990 if node.is_default_domain() && node.op_type == "NonMaxSuppression" {
4991 self.nms_input_f64(vid, &input_shapes[i], input_dtypes[i])
4992 } else {
4993 self.shape_input_f64(vid, &input_shapes[i], input_dtypes[i])
4994 }
4995 })
4996 })
4997 .collect();
4998 let out_shapes = dynamic_output_shapes(
4999 node,
5000 input_shapes,
5001 input_dtypes,
5002 &input_values,
5003 &input_float_values,
5004 opset,
5005 )
5006 .ok_or_else(|| {
5007 let vid = outputs
5008 .iter()
5009 .find(|v| !resolved.contains_key(v))
5010 .copied()
5011 .unwrap_or(outputs[0]);
5012 let value = self.graph.value(vid);
5013 SessionError::UnresolvedShape {
5014 value: value
5015 .name
5016 .clone()
5017 .unwrap_or_else(|| format!("value#{}", vid.0)),
5018 op: node.op_type.clone(),
5019 }
5020 })?;
5021 if out_shapes.len() != outputs.len() {
5022 return Err(SessionError::OutputShapeCountMismatch {
5023 op: self.graph.node(node_id).op_type.clone(),
5024 expected: outputs.len(),
5025 got: out_shapes.len(),
5026 });
5027 }
5028 for (oi, &ovid) in outputs.iter().enumerate() {
5029 resolved.insert(ovid, out_shapes[oi].clone());
5030 }
5031 }
5032 let mut output_shapes: Vec<Vec<usize>> =
5033 outputs.iter().map(|v| resolved[v].clone()).collect();
5034 {
5045 let node = self.graph.node(node_id);
5046 if node.is_default_domain() && node.op_type == "Attention" {
5047 let kv_capacity_bound = kernel_input_uses_physical_capacity(node, 4)
5058 && kernel_input_uses_physical_capacity(node, 5);
5059 for (oi, &ovid) in outputs.iter().enumerate() {
5060 if oi == 0 {
5061 continue;
5062 }
5063 if let Some(value) = external.outputs.get(&ovid)
5064 && value.accepts_subshape
5065 && value.shape.len() == output_shapes[oi].len()
5066 && value
5067 .shape
5068 .iter()
5069 .zip(&output_shapes[oi])
5070 .enumerate()
5071 .all(|(axis, (&physical, &logical))| axis == 2 || physical == logical)
5072 && (kv_capacity_bound
5073 || value
5074 .shape
5075 .get(2)
5076 .zip(output_shapes[oi].get(2))
5077 .is_some_and(|(&physical, &logical)| physical >= logical))
5078 {
5079 output_shapes[oi] = value.shape.clone();
5080 }
5081 }
5082 }
5083 }
5084 let capabilities = self.ep.capabilities();
5085 let accepts_lazy_weights =
5086 LazyWeightBoundary::BlockQuantizedMoe.matches(&node.domain, &node.op_type);
5087 let has_lazy_inputs = accepts_lazy_weights
5088 && inputs.iter().any(|input| {
5089 input
5090 .and_then(|value| self.weight_handles.get(&value))
5091 .is_some_and(|handle| handle.is_lazy_for(&capabilities))
5092 });
5093
5094 let mut in_infos: Vec<InInfo> = Vec::with_capacity(inputs.len());
5097 let _build_inputs_span = phase_span!("exec_kernel.build_inputs");
5098 for (i, slot) in inputs.iter().enumerate() {
5099 let Some(vid) = *slot else {
5100 in_infos.push(InInfo {
5101 present: false,
5102 dtype: input_dtypes[i],
5103 shape: Vec::new(),
5104 strides: Vec::new(),
5105 byte_offset: 0,
5106 base_ptr: std::ptr::null(),
5107 device: self.ep.device_id(),
5108 backing: TensorBacking::Opaque,
5109 root_len: 0,
5110 });
5111 continue;
5112 };
5113 if let Some(value) = external
5114 .inputs
5115 .get(&vid)
5116 .or_else(|| external.outputs.get(&vid))
5117 {
5118 let strides = compute_contiguous_strides(&value.shape);
5119 view_bounds(&value.shape, &strides, 0, value.dtype, value.len)?;
5120 in_infos.push(InInfo {
5121 present: true,
5122 dtype: value.dtype,
5123 shape: value.shape.clone(),
5124 strides,
5125 byte_offset: 0,
5126 base_ptr: value.ptr.cast_const(),
5127 device: value.device,
5128 backing: TensorBacking::Opaque,
5129 root_len: value.len,
5130 });
5131 continue;
5132 }
5133 if let Some(elem) = self.seq_elem_values.get(&vid) {
5137 let shape = input_shapes[i].clone();
5138 let strides = elem.layout.resolved_strides(&shape);
5139 let root_len = elem.root_len();
5140 let base_ptr = elem.as_ptr();
5141 view_bounds(
5142 &shape,
5143 &strides,
5144 elem.byte_offset(),
5145 input_dtypes[i],
5146 root_len,
5147 )?;
5148 in_infos.push(InInfo {
5149 present: true,
5150 dtype: input_dtypes[i],
5151 shape,
5152 strides,
5153 byte_offset: elem.byte_offset(),
5154 base_ptr,
5155 device: elem.device(),
5156 backing: TensorBacking::Opaque,
5157 root_len,
5158 });
5159 continue;
5160 }
5161 if accepts_lazy_weights
5162 && self
5163 .weight_handles
5164 .get(&vid)
5165 .is_some_and(|handle| handle.is_lazy_for(&capabilities))
5166 {
5167 in_infos.push(InInfo {
5168 present: false,
5169 dtype: input_dtypes[i],
5170 shape: input_shapes[i].clone(),
5171 strides: compute_contiguous_strides(&input_shapes[i]),
5172 byte_offset: 0,
5173 base_ptr: std::ptr::null(),
5174 device: self.ep.device_id(),
5175 backing: TensorBacking::Opaque,
5176 root_len: 0,
5177 });
5178 continue;
5179 }
5180 let root = self.root_of(vid);
5181 let buf = self.buffers.get(&root).ok_or_else(|| {
5182 SessionError::Internal(format!("missing buffer for input value#{}", vid.0))
5183 })?;
5184 let root_len = buf.len();
5185 let base_ptr = buf.as_ptr();
5186 let (shape, strides, byte_offset) = match self.views.get(&vid) {
5187 Some(view) => (view.shape.clone(), view.strides.clone(), view.byte_offset),
5188 None => {
5189 let shape = input_shapes[i].clone();
5190 let strides = compute_contiguous_strides(&shape);
5191 (shape, strides, 0)
5192 }
5193 };
5194 view_bounds(&shape, &strides, byte_offset, input_dtypes[i], root_len)?;
5195 let backing = self
5196 .graph
5197 .initializers
5198 .get(&root)
5199 .filter(|_| buf.is_borrowed())
5200 .and_then(|weight| self.weights.external_mmap_provenance(weight))
5201 .map(|(mapping_id, offset, len)| {
5202 TensorBacking::ExternalMmap(ExternalMmapRegion {
5203 mapping_id,
5204 offset,
5205 len,
5206 })
5207 })
5208 .unwrap_or(TensorBacking::Opaque);
5209 in_infos.push(InInfo {
5210 present: true,
5211 dtype: input_dtypes[i],
5212 shape,
5213 strides,
5214 byte_offset,
5215 base_ptr,
5216 device: buf.device(),
5217 backing,
5218 root_len,
5219 });
5220 }
5221 drop(_build_inputs_span);
5222
5223 let ep = self.ep.clone();
5224
5225 let graph = &self.graph;
5228 let cache = &mut self.cache;
5229 let weight_handles = &self.weight_handles;
5230 let buffers = &mut self.buffers;
5231 let buffer_shapes = &mut self.buffer_shapes;
5232 let shared_buffers = &mut self.shared_buffers;
5233 let views_meta = &mut self.views;
5234 let pinned = &mut self.pinned;
5235
5236 let mut views: Vec<TensorView> = Vec::with_capacity(in_infos.len());
5239 for info in &in_infos {
5240 if !info.present {
5241 views.push(TensorView::absent(info.dtype));
5242 continue;
5243 }
5244 views.push(
5245 TensorView::new(
5246 DevicePtr(info.base_ptr),
5247 info.dtype,
5248 &info.shape,
5249 &info.strides,
5250 info.device,
5251 )
5252 .with_byte_offset(info.byte_offset)
5253 .with_backing(info.backing),
5254 );
5255 }
5256
5257 let opset = effective_opset(graph, node);
5258 let constant_inputs: Vec<bool> = inputs
5259 .iter()
5260 .map(|input| {
5261 input.is_some_and(|vid| {
5262 graph.initializers.contains_key(&vid)
5263 || views_meta
5264 .get(&vid)
5265 .is_some_and(|view| graph.initializers.contains_key(&view.source))
5266 })
5267 })
5268 .collect();
5269 let kernel = {
5270 let _s = phase_span!("exec_kernel.get_kernel");
5271 cache.get_or_create(
5272 node_id,
5273 node,
5274 input_shapes,
5275 input_dtypes,
5276 &constant_inputs,
5277 opset,
5278 ep.as_ref(),
5279 )?
5280 };
5281 if !has_lazy_inputs && let Some(specs) = kernel.view_outputs(&views, outputs.len()) {
5286 if outputs
5287 .iter()
5288 .any(|output| external.outputs.contains_key(output))
5289 {
5290 return Err(SessionError::Internal(format!(
5291 "op '{}' cannot bind a zero-copy view output to external storage",
5292 node.op_type
5293 )));
5294 }
5295 drop(views);
5296 if specs.len() != outputs.len() {
5297 return Err(SessionError::Internal(format!(
5298 "op '{}' returned {} view outputs for {} outputs",
5299 node.op_type,
5300 specs.len(),
5301 outputs.len()
5302 )));
5303 }
5304 for (oi, spec) in specs.into_iter().enumerate() {
5305 let ovid = outputs[oi];
5306 let Some(in_vid) = inputs.get(spec.input_index).copied().flatten() else {
5307 return Err(SessionError::Internal(format!(
5308 "op '{}' view output {} references invalid input index {}",
5309 node.op_type, oi, spec.input_index
5310 )));
5311 };
5312 let root = match views_meta.get(&in_vid) {
5313 Some(v) => v.source,
5314 None => in_vid,
5315 };
5316 let root_len = buffers.get(&root).map(|b| b.len()).ok_or_else(|| {
5317 SessionError::Internal(format!("view source value#{} has no buffer", root.0))
5318 })?;
5319 view_bounds(
5321 &spec.shape,
5322 &spec.strides,
5323 spec.byte_offset,
5324 output_dtypes[oi],
5325 root_len,
5326 )?;
5327 debug_assert!(
5334 !pinned.contains(&ovid),
5335 "value#{} is pinned as a live view source yet is being reproduced",
5336 ovid.0
5337 );
5338 if let Some(old) = buffers.remove(&ovid) {
5339 ep.deallocate(old)?;
5340 }
5341 shared_buffers.remove(&ovid);
5342 buffer_shapes.remove(&ovid);
5343 views_meta.insert(
5344 ovid,
5345 ValueView {
5346 source: root,
5347 shape: spec.shape.clone(),
5348 strides: spec.strides,
5349 byte_offset: spec.byte_offset,
5350 },
5351 );
5352 pinned.insert(root);
5353 resolved.insert(ovid, spec.shape);
5354 }
5355 return Ok(());
5356 }
5357
5358 for (oi, &ovid) in outputs.iter().enumerate() {
5363 let dims = &output_shapes[oi];
5364 let numel = checked_numel(dims, || format!("value#{}", ovid.0))?;
5365 let need = checked_storage_bytes(
5366 output_dtypes[oi],
5367 numel,
5368 || format!("value#{}", ovid.0),
5369 dims,
5370 )?
5371 .max(1);
5372 if let Some(value) = external.outputs.get(&ovid) {
5373 if !value.accepts_output(output_dtypes[oi], dims, need) {
5374 let name = graph.value(ovid).name.as_deref().unwrap_or("<unnamed>");
5375 return Err(SessionError::Internal(format!(
5376 "external output '{name}' has {:?} {:?} ({} bytes), kernel requires {:?} {:?} ({need} bytes)",
5377 value.dtype, value.shape, value.len, output_dtypes[oi], dims
5378 )));
5379 }
5380 continue;
5381 }
5382 let fits = buffers.get(&ovid).map(|b| b.len() == need).unwrap_or(false);
5383 if !fits {
5384 debug_assert!(
5387 !pinned.contains(&ovid),
5388 "value#{} is pinned as a live view source yet is being resized",
5389 ovid.0
5390 );
5391 if let Some(old) = buffers.remove(&ovid) {
5392 ep.deallocate(old)?;
5393 }
5394 shared_buffers.remove(&ovid);
5395 let buf = ep.allocate(need, TensorLayout::contiguous().alignment)?;
5396 buffers.insert(ovid, buf);
5397 }
5398 }
5399
5400 let mut mat: Vec<Option<(Vec<u8>, Vec<i64>)>> = Vec::with_capacity(in_infos.len());
5405 for (i, info) in in_infos.iter().enumerate() {
5406 if !info.present {
5407 mat.push(None);
5408 continue;
5409 }
5410 let contiguous = onnx_runtime_ir::is_contiguous(&info.shape, &info.strides);
5411 if contiguous || kernel.supports_strided_input(i) {
5412 mat.push(None);
5413 continue;
5414 }
5415 if !info.device.is_host_accessible() {
5416 return Err(SessionError::Internal(format!(
5417 "op '{}' requires host-only strided materialization for CUDA input {i}",
5418 node.op_type
5419 )));
5420 }
5421 let esize = info.dtype.byte_size();
5422 if esize == 0 {
5423 return Err(SessionError::from(
5424 onnx_runtime_ep_api::EpError::InvalidTensorView {
5425 reason: format!(
5426 "cannot materialize sub-byte strided input {i} of op '{}'",
5427 node.op_type
5428 ),
5429 },
5430 ));
5431 }
5432 let src =
5433 unsafe { std::slice::from_raw_parts(info.base_ptr as *const u8, info.root_len) };
5434 let gathered = gather_view(src, &info.shape, &info.strides, info.byte_offset, esize);
5435 let strides = compute_contiguous_strides(&info.shape);
5436 mat.push(Some((gathered, strides)));
5437 }
5438
5439 drop(views);
5442 let mut views: Vec<TensorView> = Vec::with_capacity(in_infos.len());
5443 for (i, info) in in_infos.iter().enumerate() {
5444 if !info.present {
5445 views.push(TensorView::absent(info.dtype));
5446 continue;
5447 }
5448 match &mat[i] {
5449 Some((buf, strides)) => views.push(TensorView::new(
5450 DevicePtr(buf.as_ptr() as *const std::ffi::c_void),
5451 info.dtype,
5452 &info.shape,
5453 strides,
5454 onnx_runtime_ir::DeviceId::cpu(),
5455 )),
5456 None => views.push(
5457 TensorView::new(
5458 DevicePtr(info.base_ptr),
5459 info.dtype,
5460 &info.shape,
5461 &info.strides,
5462 info.device,
5463 )
5464 .with_byte_offset(info.byte_offset)
5465 .with_backing(info.backing),
5466 ),
5467 }
5468 }
5469
5470 let out_strides: Vec<Vec<i64>> = output_shapes
5473 .iter()
5474 .map(|s| compute_contiguous_strides(s))
5475 .collect();
5476 struct OutBacking {
5477 vid: ValueId,
5478 internal: Option<DeviceBuffer>,
5479 ptr: *mut std::ffi::c_void,
5480 len: usize,
5481 device: onnx_runtime_ir::DeviceId,
5482 }
5483 let mut out_bufs: Vec<OutBacking> = Vec::with_capacity(outputs.len());
5484 for &vid in outputs {
5485 if let Some(value) = external.outputs.get(&vid) {
5486 out_bufs.push(OutBacking {
5487 vid,
5488 internal: None,
5489 ptr: value.ptr,
5490 len: value.len,
5491 device: value.device,
5492 });
5493 } else {
5494 let mut buf = buffers.remove(&vid).ok_or_else(|| {
5495 SessionError::Internal(format!("missing buffer for output value#{}", vid.0))
5496 })?;
5497 let ptr = buf.as_mut_ptr();
5498 out_bufs.push(OutBacking {
5499 vid,
5500 ptr,
5501 len: buf.len(),
5502 device: buf.device(),
5503 internal: Some(buf),
5504 });
5505 }
5506 }
5507 let mut outs: Vec<TensorMut> = Vec::with_capacity(out_bufs.len());
5508 for (i, backing) in out_bufs.iter_mut().enumerate() {
5509 view_bounds(
5510 &output_shapes[i],
5511 &out_strides[i],
5512 0,
5513 output_dtypes[i],
5514 backing.len,
5515 )?;
5516 outs.push(TensorMut::new(
5517 DevicePtrMut(backing.ptr),
5518 output_dtypes[i],
5519 &output_shapes[i],
5520 &out_strides[i],
5521 backing.device,
5522 ));
5523 }
5524
5525 let kernel_inputs = has_lazy_inputs.then(|| {
5526 inputs
5527 .iter()
5528 .zip(views.iter().copied())
5529 .map(|(value, view)| {
5530 value
5531 .and_then(|value| weight_handles.get(&value))
5532 .filter(|handle| handle.is_lazy_for(&capabilities))
5533 .map(KernelInput::Weight)
5534 .unwrap_or(KernelInput::Tensor(view))
5535 })
5536 .collect::<Vec<_>>()
5537 });
5538 let execution = {
5539 let _s = phase_span!("exec_kernel.compute");
5540 match &kernel_inputs {
5541 Some(inputs) => kernel.execute_with_inputs(inputs, &mut outs),
5542 None => kernel.execute(&views, &mut outs),
5543 }
5544 };
5545 execution.map_err(|error| {
5546 let input_types = views.iter().map(|view| view.dtype).collect::<Vec<_>>();
5547 let output_types = outs.iter().map(|output| output.dtype).collect::<Vec<_>>();
5548 let input_shapes = views
5549 .iter()
5550 .map(|view| view.shape.to_vec())
5551 .collect::<Vec<_>>();
5552 let output_shapes = outs
5553 .iter()
5554 .map(|output| output.shape.to_vec())
5555 .collect::<Vec<_>>();
5556 let input_names = inputs
5557 .iter()
5558 .map(|input| {
5559 input
5560 .map(|value| {
5561 self.graph.value(value).name.as_deref().unwrap_or("<unnamed>")
5562 })
5563 .unwrap_or("<absent>")
5564 })
5565 .collect::<Vec<_>>();
5566 let output_names = outputs
5567 .iter()
5568 .map(|&value| {
5569 self.graph.value(value).name.as_deref().unwrap_or("<unnamed>")
5570 })
5571 .collect::<Vec<_>>();
5572 SessionError::Internal(format!(
5573 "node {} ({:?}, op '{}::{}', inputs {input_names:?} {input_types:?} {input_shapes:?}, outputs {output_names:?} {output_types:?} {output_shapes:?}) failed: {error}",
5574 node.id.0, node.name, node.domain, node.op_type,
5575 ))
5576 })?;
5577
5578 drop(kernel_inputs);
5579 drop(views);
5580 drop(outs);
5581 for backing in out_bufs {
5582 if let Some(buf) = backing.internal {
5583 buffers.insert(backing.vid, buf);
5584 }
5585 }
5586 Ok(())
5587 }
5588
5589 fn input_i64(&self, vid: ValueId, shape: &[usize], dtype: DataType) -> Option<Vec<i64>> {
5594 let bytes = self.contiguous_bytes(vid, shape, dtype).ok()?;
5595 bytes_as_i64(&bytes, dtype)
5596 }
5597
5598 fn shape_input_i64(&self, vid: ValueId, shape: &[usize], dtype: DataType) -> Option<Vec<i64>> {
5602 if !bounded_shape_input(dtype, shape) {
5603 return None;
5604 }
5605 let max_bytes = MAX_SHAPE_DATA_ELEMS.checked_mul(dtype.byte_size())?;
5606 if let Some(view) = self.views.get(&vid) {
5607 let source = self.buffers.get(&view.source)?;
5608 if source.len() > max_bytes {
5609 return None;
5610 }
5611 }
5612 if self
5613 .seq_elem_values
5614 .get(&vid)
5615 .is_some_and(|elem| elem.root_len() > max_bytes)
5616 {
5617 return None;
5618 }
5619 self.input_i64(vid, shape, dtype)
5620 }
5621
5622 fn shape_input_f64(&self, vid: ValueId, shape: &[usize], dtype: DataType) -> Option<Vec<f64>> {
5623 if !matches!(dtype, DataType::Float32 | DataType::Float64)
5624 || shape.len() > 1
5625 || shape
5626 .iter()
5627 .try_fold(1usize, |count, &dim| count.checked_mul(dim))
5628 .is_none_or(|count| count > MAX_SHAPE_DATA_ELEMS)
5629 {
5630 return None;
5631 }
5632 let max_bytes = MAX_SHAPE_DATA_ELEMS.checked_mul(dtype.byte_size())?;
5633 if let Some(view) = self.views.get(&vid) {
5634 let source = self.buffers.get(&view.source)?;
5635 if source.len() > max_bytes {
5636 return None;
5637 }
5638 }
5639 if self
5640 .seq_elem_values
5641 .get(&vid)
5642 .is_some_and(|elem| elem.root_len() > max_bytes)
5643 {
5644 return None;
5645 }
5646 let bytes = self.contiguous_bytes(vid, shape, dtype).ok()?;
5647 bytes_as_f64(&bytes, dtype)
5648 }
5649
5650 fn nms_input_f64(&self, vid: ValueId, shape: &[usize], dtype: DataType) -> Option<Vec<f64>> {
5655 if dtype != DataType::Float32 {
5656 return None;
5657 }
5658 let bytes = self.contiguous_bytes(vid, shape, dtype).ok()?;
5659 bytes_as_f64(&bytes, dtype)
5660 }
5661}
5662
5663impl Executor {
5691 fn exec_sequence_node(
5693 &mut self,
5694 pi: usize,
5695 resolved: &mut HashMap<ValueId, Vec<usize>>,
5696 external: &ExternalBindings,
5697 ) -> Result<()> {
5698 let node_id = self.plan[pi].node_id;
5699 let inputs = self.plan[pi].inputs.clone();
5700 let outputs = self.plan[pi].outputs.clone();
5701 let op = self.graph.node(node_id).op_type.clone();
5702
5703 match op.as_str() {
5704 "SequenceEmpty" => {
5705 let dtype_attr = self
5706 .graph
5707 .node(node_id)
5708 .attr("dtype")
5709 .and_then(|a| a.as_int());
5710 let dtype = match dtype_attr {
5711 None => DataType::Float32, Some(raw) => i32::try_from(raw)
5713 .ok()
5714 .and_then(DataType::from_onnx)
5715 .ok_or_else(|| SessionError::SequenceOp {
5716 op: op.clone(),
5717 reason: format!(
5718 "attribute 'dtype' = {raw} is not a known ONNX \
5719 TensorProto.DataType. To fix: use a valid element \
5720 dtype id (e.g. 1=float32, 7=int64)"
5721 ),
5722 })?,
5723 };
5724 self.sequences
5725 .insert(outputs[0], SequenceValue::empty(dtype));
5726 Ok(())
5727 }
5728 "SequenceConstruct" => {
5729 let mut items = Vec::with_capacity(inputs.len());
5730 for slot in &inputs {
5731 let vid = slot.ok_or_else(|| self.seq_missing_input(&op))?;
5732 items.push(self.read_seq_element(vid, resolved)?);
5733 }
5734 let seq = SequenceValue::construct(items).map_err(seq_err)?;
5735 self.sequences.insert(outputs[0], seq);
5736 Ok(())
5737 }
5738 "SequenceInsert" => {
5739 let seq = self.get_sequence(inputs.first().copied().flatten(), &op)?;
5740 let tvid = inputs
5741 .get(1)
5742 .copied()
5743 .flatten()
5744 .ok_or_else(|| self.seq_missing_input(&op))?;
5745 let tensor = self.read_seq_element(tvid, resolved)?;
5746 let position = match inputs.get(2).copied().flatten() {
5747 Some(pvid) => Some(self.read_scalar_i64(pvid, resolved, &op)?),
5748 None => None,
5749 };
5750 let out = seq.insert(tensor, position).map_err(seq_err)?;
5751 self.sequences.insert(outputs[0], out);
5752 Ok(())
5753 }
5754 "SequenceErase" => {
5755 let seq = self.get_sequence(inputs.first().copied().flatten(), &op)?;
5756 let position = match inputs.get(1).copied().flatten() {
5757 Some(pvid) => Some(self.read_scalar_i64(pvid, resolved, &op)?),
5758 None => None,
5759 };
5760 let out = seq.erase(position).map_err(seq_err)?;
5761 self.sequences.insert(outputs[0], out);
5762 Ok(())
5763 }
5764 "SequenceAt" => {
5765 let seq = self.get_sequence(inputs.first().copied().flatten(), &op)?;
5766 let pvid =
5767 inputs
5768 .get(1)
5769 .copied()
5770 .flatten()
5771 .ok_or_else(|| SessionError::SequenceOp {
5772 op: op.clone(),
5773 reason: "requires a 'position' input. To fix: supply the \
5774 index tensor of the element to read"
5775 .to_string(),
5776 })?;
5777 let pos = self.read_scalar_i64(pvid, resolved, &op)?;
5778 let elem = seq.at(pos).map_err(seq_err)?;
5779 self.store_seq_element_output(outputs[0], elem, resolved, external)
5780 }
5781 "SequenceLength" => {
5782 let seq = self.get_sequence(inputs.first().copied().flatten(), &op)?;
5783 let len = i64::try_from(seq.length()).map_err(|_| {
5784 seq_err(SequenceError::LengthOverflow {
5785 op: "SequenceLength",
5786 len: seq.length(),
5787 })
5788 })?;
5789 self.store_raw_tensor_output(
5790 outputs[0],
5791 DataType::Int64,
5792 Vec::new(),
5793 &len.to_le_bytes(),
5794 resolved,
5795 external,
5796 )
5797 }
5798 "SplitToSequence" => {
5799 self.exec_split_to_sequence(node_id, &op, &inputs, &outputs, resolved)
5800 }
5801 "ConcatFromSequence" => {
5802 self.exec_concat_from_sequence(node_id, &op, &inputs, &outputs, resolved, external)
5803 }
5804 other => Err(SessionError::SequenceOp {
5805 op: other.to_string(),
5806 reason: "unrecognized Sequence op (executor routing bug)".to_string(),
5807 }),
5808 }
5809 }
5810
5811 fn exec_split_to_sequence(
5813 &mut self,
5814 node_id: NodeId,
5815 op: &str,
5816 inputs: &[Option<ValueId>],
5817 outputs: &[ValueId],
5818 resolved: &mut HashMap<ValueId, Vec<usize>>,
5819 ) -> Result<()> {
5820 let (axis_attr, keepdims) = {
5821 let node = self.graph.node(node_id);
5822 (
5823 node.attr("axis").and_then(|a| a.as_int()).unwrap_or(0),
5824 node.attr("keepdims").and_then(|a| a.as_int()).unwrap_or(1) != 0,
5825 )
5826 };
5827
5828 let ivid = inputs
5829 .first()
5830 .copied()
5831 .flatten()
5832 .ok_or_else(|| self.seq_missing_input(op))?;
5833 let input = self.read_seq_element(ivid, resolved)?;
5834
5835 let split_input = match inputs.get(1).copied().flatten() {
5836 None => None,
5837 Some(svid) => {
5838 let split_shape = resolved
5839 .get(&svid)
5840 .cloned()
5841 .ok_or_else(|| self.seq_unresolved(op, svid))?;
5842 let values = self.read_i64_vec(svid, &split_shape, op)?;
5843 Some((split_shape, values))
5844 }
5845 };
5846 let split_spec = match split_input.as_ref() {
5847 None => SplitSpec::Each,
5848 Some((split_shape, values)) if split_shape.is_empty() => {
5849 let [chunk] = values.as_slice() else {
5850 return Err(SessionError::SequenceOp {
5851 op: op.to_string(),
5852 reason: format!(
5853 "scalar 'split' input contains {} values, expected exactly one",
5854 values.len()
5855 ),
5856 });
5857 };
5858 SplitSpec::Chunk(*chunk)
5859 }
5860 Some((split_shape, values)) if split_shape.len() == 1 => SplitSpec::Sizes(values),
5861 Some((split_shape, _)) => {
5862 return Err(SessionError::SequenceOp {
5863 op: op.to_string(),
5864 reason: format!(
5865 "'split' input must be rank 0 (chunk size) or rank 1 (explicit sizes), \
5866 got rank {} with shape {split_shape:?}",
5867 split_shape.len()
5868 ),
5869 });
5870 }
5871 };
5872 let sequence = split_tensor(&input, axis_attr, split_spec, keepdims).map_err(seq_err)?;
5873 self.sequences.insert(outputs[0], sequence);
5874 Ok(())
5875 }
5876
5877 fn exec_concat_from_sequence(
5880 &mut self,
5881 node_id: NodeId,
5882 op: &str,
5883 inputs: &[Option<ValueId>],
5884 outputs: &[ValueId],
5885 resolved: &mut HashMap<ValueId, Vec<usize>>,
5886 external: &ExternalBindings,
5887 ) -> Result<()> {
5888 let node = self.graph.node(node_id);
5889 let axis_attr =
5890 node.attr("axis")
5891 .and_then(|a| a.as_int())
5892 .ok_or_else(|| SessionError::SequenceOp {
5893 op: op.to_string(),
5894 reason: "requires the mandatory 'axis' attribute. To fix: set 'axis'"
5895 .to_string(),
5896 })?;
5897 let new_axis = node.attr("new_axis").and_then(|a| a.as_int()).unwrap_or(0) != 0;
5898
5899 let seq = self.get_sequence(inputs.first().copied().flatten(), op)?;
5900 let plan = ConcatPlan::new(&seq, axis_attr, new_axis).map_err(seq_err)?;
5901 self.prepare_tensor_output(
5902 outputs[0],
5903 plan.dtype,
5904 plan.shape.clone(),
5905 plan.bytes,
5906 resolved,
5907 external,
5908 )?;
5909 let ep = Arc::clone(&self.ep);
5910 if let Some(value) = external.outputs.get(&outputs[0]) {
5911 let mut buffer = value.writable_buffer()?;
5912 plan.write(&seq, |offset, bytes| {
5913 ep.copy_from_host_at(bytes, &mut buffer, offset)?;
5914 Ok(())
5915 })?;
5916 } else {
5917 let buffer = self.buffers.get_mut(&outputs[0]).ok_or_else(|| {
5918 SessionError::Internal(format!(
5919 "missing ConcatFromSequence output buffer for value#{}",
5920 outputs[0].0
5921 ))
5922 })?;
5923 plan.write(&seq, |offset, bytes| {
5924 ep.copy_from_host_at(bytes, buffer, offset)?;
5925 Ok(())
5926 })?;
5927 }
5928 Ok(())
5929 }
5930
5931 fn read_seq_element(
5936 &mut self,
5937 vid: ValueId,
5938 resolved: &HashMap<ValueId, Vec<usize>>,
5939 ) -> Result<SeqTensor> {
5940 if self.sequence_values.contains(&vid) {
5941 return Err(SessionError::SequenceOp {
5942 op: "Sequence".to_string(),
5943 reason: format!(
5944 "input value#{} is a Sequence value, expected a tensor element",
5945 vid.0
5946 ),
5947 });
5948 }
5949 if let Some(elem) = self.seq_elem_values.get(&vid) {
5950 return Ok(elem.clone()); }
5952 let dtype = self.value_dtypes[&vid];
5953 let shape = resolved
5954 .get(&vid)
5955 .cloned()
5956 .ok_or_else(|| self.seq_unresolved("Sequence", vid))?;
5957 let (root, layout, byte_offset) = match self.views.get(&vid) {
5958 Some(view) => (
5959 view.source,
5960 TensorLayout::strided(view.strides.clone()),
5961 view.byte_offset,
5962 ),
5963 None => (vid, TensorLayout::contiguous(), 0),
5964 };
5965 if !self.shared_buffers.contains_key(&root) {
5966 let buffer = self
5967 .buffers
5968 .remove(&root)
5969 .ok_or_else(|| SessionError::SequenceOp {
5970 op: "Sequence".to_string(),
5971 reason: format!("tensor value#{} has no live backing buffer", vid.0),
5972 })?;
5973 let storage = SharedTensorBuffer::new(Arc::clone(&self.ep), buffer);
5974 self.buffers.insert(root, storage.alias());
5975 self.shared_buffers.insert(root, storage);
5976 }
5977 self.pinned.insert(root);
5978 SeqTensor::from_shared(
5979 Arc::clone(&self.shared_buffers[&root]),
5980 dtype,
5981 shape,
5982 layout,
5983 byte_offset,
5984 )
5985 .map_err(SessionError::from)
5986 }
5987
5988 fn restore_shared_buffers(&mut self) -> Result<()> {
5989 let mut retained = Vec::new();
5990 for (vid, storage) in self.shared_buffers.drain() {
5991 if let Some(alias) = self.buffers.remove(&vid) {
5992 self.ep.deallocate(alias)?;
5993 }
5994 match Arc::try_unwrap(storage) {
5995 Ok(storage) => {
5996 self.buffers.insert(vid, storage.into_buffer());
5997 }
5998 Err(storage) if self.graph.initializers.contains_key(&vid) => {
5999 self.buffers.insert(vid, storage.alias());
6000 retained.push((vid, storage));
6001 }
6002 Err(storage) => {
6003 let replacement = self
6004 .ep
6005 .allocate(storage.buffer().len(), storage.buffer().alignment())?;
6006 self.buffers.insert(vid, replacement);
6007 }
6008 }
6009 }
6010 for (vid, storage) in retained {
6011 self.shared_buffers.insert(vid, storage);
6012 }
6013 Ok(())
6014 }
6015
6016 fn get_sequence(&self, vid: Option<ValueId>, op: &str) -> Result<SequenceValue> {
6019 let vid = vid.ok_or_else(|| self.seq_missing_input(op))?;
6020 self.sequences
6021 .get(&vid)
6022 .cloned()
6023 .ok_or_else(|| SessionError::SequenceOp {
6024 op: op.to_string(),
6025 reason: format!(
6026 "input value#{} is not a live sequence. To fix: ensure it is produced \
6027 by a Sequence-producing op (SequenceEmpty/Construct/Insert/Erase/\
6028 SplitToSequence)",
6029 vid.0
6030 ),
6031 })
6032 }
6033
6034 fn read_scalar_i64(
6036 &self,
6037 vid: ValueId,
6038 resolved: &HashMap<ValueId, Vec<usize>>,
6039 op: &str,
6040 ) -> Result<i64> {
6041 let shape = resolved.get(&vid).cloned().unwrap_or_default();
6042 if !shape.is_empty() {
6043 return Err(SessionError::SequenceOp {
6044 op: op.to_string(),
6045 reason: format!(
6046 "position input must be a rank-0 scalar, got rank {} with shape {shape:?}",
6047 shape.len()
6048 ),
6049 });
6050 }
6051 let dtype = self.value_dtypes[&vid];
6052 let vals = self
6053 .input_i64(vid, &shape, dtype)
6054 .ok_or_else(|| SessionError::SequenceOp {
6055 op: op.to_string(),
6056 reason: format!(
6057 "position input has dtype {dtype:?}, expected an integer (int32/int64). \
6058 To fix: provide an int64 scalar index"
6059 ),
6060 })?;
6061 let [value] = vals.as_slice() else {
6062 return Err(SessionError::SequenceOp {
6063 op: op.to_string(),
6064 reason: format!(
6065 "position input contains {} values; expected exactly one scalar index",
6066 vals.len()
6067 ),
6068 });
6069 };
6070 Ok(*value)
6071 }
6072
6073 fn read_i64_vec(&self, vid: ValueId, shape: &[usize], op: &str) -> Result<Vec<i64>> {
6076 let dtype = self.value_dtypes[&vid];
6077 self.input_i64(vid, shape, dtype)
6078 .ok_or_else(|| SessionError::SequenceOp {
6079 op: op.to_string(),
6080 reason: format!(
6081 "'split' input has dtype {dtype:?}, expected int32/int64. To fix: \
6082 provide integer split sizes"
6083 ),
6084 })
6085 }
6086
6087 fn store_seq_element_output(
6090 &mut self,
6091 vid: ValueId,
6092 elem: SeqTensor,
6093 resolved: &mut HashMap<ValueId, Vec<usize>>,
6094 external: &ExternalBindings,
6095 ) -> Result<()> {
6096 if elem.device() != self.ep.device_id() {
6097 return Err(SessionError::SequenceOp {
6098 op: "SequenceAt".to_string(),
6099 reason: format!(
6100 "sequence element is on {:?}, but the active execution provider is on {:?}",
6101 elem.device(),
6102 self.ep.device_id()
6103 ),
6104 });
6105 }
6106 if external.outputs.contains_key(&vid) {
6107 let dtype = elem.dtype;
6108 let shape = elem.shape.clone();
6109 let bytes = elem.contiguous_bytes().map_err(seq_err)?;
6110 return self.store_raw_tensor_output(vid, dtype, shape, &bytes, resolved, external);
6111 }
6112 if let Some(old) = self.buffers.remove(&vid) {
6113 self.ep.deallocate(old)?;
6114 }
6115 self.shared_buffers.remove(&vid);
6116 self.buffer_shapes.remove(&vid);
6117 self.views.remove(&vid);
6118 resolved.insert(vid, elem.shape.clone());
6119 self.value_dtypes.insert(vid, elem.dtype);
6120 self.seq_elem_values.insert(vid, elem);
6121 Ok(())
6122 }
6123
6124 fn store_raw_tensor_output(
6128 &mut self,
6129 vid: ValueId,
6130 dtype: DataType,
6131 dims: Vec<usize>,
6132 bytes: &[u8],
6133 resolved: &mut HashMap<ValueId, Vec<usize>>,
6134 external: &ExternalBindings,
6135 ) -> Result<()> {
6136 self.prepare_tensor_output(vid, dtype, dims, bytes.len(), resolved, external)?;
6137 if let Some(value) = external.outputs.get(&vid) {
6138 let mut buffer = value.writable_buffer()?;
6139 self.ep.copy_from_host(bytes, &mut buffer)?;
6140 } else {
6141 let buffer = self.buffers.get_mut(&vid).ok_or_else(|| {
6142 SessionError::Internal(format!("missing tensor output buffer for value#{}", vid.0))
6143 })?;
6144 self.ep.copy_from_host(bytes, buffer)?;
6145 }
6146 Ok(())
6147 }
6148
6149 fn prepare_tensor_output(
6150 &mut self,
6151 vid: ValueId,
6152 dtype: DataType,
6153 dims: Vec<usize>,
6154 bytes: usize,
6155 resolved: &mut HashMap<ValueId, Vec<usize>>,
6156 external: &ExternalBindings,
6157 ) -> Result<()> {
6158 self.seq_elem_values.remove(&vid);
6159 self.views.remove(&vid);
6160 let need = bytes.max(1);
6161 if let Some(value) = external.outputs.get(&vid) {
6162 if !value.accepts_output(dtype, &dims, need) {
6163 let name = self.graph.value(vid).name.as_deref().unwrap_or("<unnamed>");
6164 return Err(SessionError::Internal(format!(
6165 "external output '{name}' has {:?} {:?} ({} bytes), sequence op requires {:?} {:?} ({need} bytes)",
6166 value.dtype, value.shape, value.len, dtype, dims
6167 )));
6168 }
6169 } else {
6170 let fits = self
6171 .buffers
6172 .get(&vid)
6173 .map(|buffer| buffer.len() == need)
6174 .unwrap_or(false);
6175 if !fits {
6176 if let Some(old) = self.buffers.remove(&vid) {
6177 self.ep.deallocate(old)?;
6178 }
6179 self.shared_buffers.remove(&vid);
6180 let buffer = self
6181 .ep
6182 .allocate(need, TensorLayout::contiguous().alignment)?;
6183 self.buffers.insert(vid, buffer);
6184 }
6185 self.buffer_shapes.insert(vid, dims.clone());
6186 }
6187 self.value_dtypes.insert(vid, dtype);
6188 resolved.insert(vid, dims);
6189 Ok(())
6190 }
6191
6192 fn seq_missing_input(&self, op: &str) -> SessionError {
6193 SessionError::SequenceOp {
6194 op: op.to_string(),
6195 reason: "a required input is missing (omitted None slot). To fix: connect \
6196 all required inputs of this Sequence op"
6197 .to_string(),
6198 }
6199 }
6200
6201 fn seq_unresolved(&self, op: &str, vid: ValueId) -> SessionError {
6202 let name = self
6203 .graph
6204 .try_value(vid)
6205 .and_then(|v| v.name.clone())
6206 .unwrap_or_else(|| format!("value#{}", vid.0));
6207 SessionError::SequenceOp {
6208 op: op.to_string(),
6209 reason: format!(
6210 "input {name} has no resolved shape yet. To fix: ensure its producer \
6211 runs before this Sequence op"
6212 ),
6213 }
6214 }
6215}
6216
6217fn seq_err(e: crate::sequence::SequenceError) -> SessionError {
6219 e.into()
6220}
6221
6222fn normalize_axis(axis: i64, rank: usize) -> Option<usize> {
6225 let r = rank as i64;
6226 let a = if axis < 0 { axis + r } else { axis };
6227 if a < 0 || a >= r {
6228 None
6229 } else {
6230 Some(a as usize)
6231 }
6232}
6233
6234fn scan_list_attr(node: &Node, name: &str, count: usize, default: i64) -> Result<Vec<i64>> {
6235 match node.attr(name) {
6236 None => Ok(vec![default; count]),
6237 Some(attr) => {
6238 let values = attr.as_ints().ok_or_else(|| SessionError::ControlFlow {
6239 op: "Scan".to_string(),
6240 reason: format!("attribute '{name}' must be an INTS list"),
6241 })?;
6242 if values.len() != count {
6243 return Err(SessionError::ControlFlow {
6244 op: "Scan".to_string(),
6245 reason: format!(
6246 "attribute '{name}' has {} value(s), expected {count}",
6247 values.len()
6248 ),
6249 });
6250 }
6251 Ok(values.to_vec())
6252 }
6253 }
6254}
6255
6256fn is_control_flow_op(op_type: &str, domain: &str) -> bool {
6261 domain.is_empty() && matches!(op_type, "If" | "Loop" | "Scan")
6262}
6263
6264fn is_sequence_op(op_type: &str, domain: &str) -> bool {
6271 domain.is_empty()
6272 && matches!(
6273 op_type,
6274 "SequenceEmpty"
6275 | "SequenceConstruct"
6276 | "SequenceInsert"
6277 | "SequenceErase"
6278 | "SequenceAt"
6279 | "SequenceLength"
6280 | "SplitToSequence"
6281 | "ConcatFromSequence"
6282 )
6283}
6284
6285fn produces_sequence_output(op_type: &str, domain: &str) -> bool {
6288 domain.is_empty()
6289 && matches!(
6290 op_type,
6291 "SequenceEmpty"
6292 | "SequenceConstruct"
6293 | "SequenceInsert"
6294 | "SequenceErase"
6295 | "SplitToSequence"
6296 )
6297}
6298
6299fn tensor_scalar_i64(t: &Tensor) -> Option<i64> {
6301 if t.dtype != DataType::Int64 || t.numel() != 1 {
6302 return None;
6303 }
6304 t.as_bytes()
6305 .get(..8)
6306 .map(|c| i64::from_le_bytes(c.try_into().unwrap()))
6307}
6308
6309fn tensor_scalar_bool(t: &Tensor) -> Option<bool> {
6312 if t.dtype != DataType::Bool || t.numel() != 1 {
6313 return None;
6314 }
6315 t.as_bytes().first().map(|&b| b != 0)
6316}
6317
6318fn scalar_i64_tensor(v: i64) -> Result<Tensor> {
6320 Tensor::from_raw(DataType::Int64, vec![], &v.to_le_bytes())
6321}
6322
6323fn scalar_bool_tensor(v: bool) -> Result<Tensor> {
6325 Tensor::from_raw(DataType::Bool, vec![], &[u8::from(v)])
6326}
6327
6328fn missing_capture_error(attr_key: &str, name: &str) -> SessionError {
6329 SessionError::Internal(format!(
6330 "control-flow body '{attr_key}' captures free variable '{name}', but it is not \
6331 available in the enclosing scope. RULES #1: a subgraph may only reference outer \
6332 values that are graph inputs, initializers, or produced by an upstream node in an \
6333 enclosing graph; '{name}' matches none of these"
6334 ))
6335}
6336
6337fn required_outer_names(graph: &Graph) -> HashSet<String> {
6341 let formal_set: HashSet<ValueId> = graph.inputs.iter().copied().collect();
6342 let local_names: HashSet<&str> = graph
6343 .values
6344 .iter()
6345 .filter_map(|(_, value)| value.name.as_deref())
6346 .collect();
6347 let mut required = HashSet::new();
6348 for (vid, value) in graph.values.iter() {
6349 if value.producer.is_none()
6350 && !formal_set.contains(&vid)
6351 && !graph.initializers.contains_key(&vid)
6352 && let Some(name) = &value.name
6353 {
6354 required.insert(name.clone());
6355 }
6356 }
6357 for nested in graph.subgraphs.values() {
6358 for name in required_outer_names(nested) {
6359 if !local_names.contains(name.as_str()) {
6360 required.insert(name);
6361 }
6362 }
6363 }
6364 required
6365}
6366
6367impl ChildExecutor {
6368 pub(crate) fn new(
6374 name: impl Into<String>,
6375 body: Graph,
6376 inherited_opsets: HashMap<String, u64>,
6377 weights: Arc<WeightStore>,
6378 ep: Arc<dyn ExecutionProvider>,
6379 ) -> Result<Self> {
6380 let name = name.into();
6381 let formal_names = body
6382 .inputs
6383 .iter()
6384 .map(|&vid| {
6385 body.value(vid).name.clone().ok_or_else(|| {
6386 SessionError::Internal(format!(
6387 "subgraph '{name}' has an unnamed formal input value#{}",
6388 vid.0
6389 ))
6390 })
6391 })
6392 .collect::<Result<Vec<_>>>()?;
6393 let formal_set: HashSet<ValueId> = body.inputs.iter().copied().collect();
6394 let mut capture_names = body
6395 .values
6396 .iter()
6397 .filter_map(|(vid, value)| {
6398 (value.producer.is_none()
6399 && !formal_set.contains(&vid)
6400 && !body.initializers.contains_key(&vid))
6401 .then(|| value.name.clone())
6402 .flatten()
6403 })
6404 .collect::<Vec<_>>();
6405 capture_names.sort();
6406 let input_names = formal_names
6407 .iter()
6408 .chain(capture_names.iter())
6409 .cloned()
6410 .collect();
6411
6412 Ok(Self {
6413 name,
6414 template: body,
6415 inherited_opsets,
6416 weights,
6417 ep,
6418 formal_names,
6419 capture_names,
6420 input_names,
6421 compiled: Vec::new(),
6422 builds: 0,
6423 runs: 0,
6424 trace: TraceContext::noop(),
6425 })
6426 }
6427
6428 pub(crate) fn stats(&self) -> ChildExecutorStats {
6429 ChildExecutorStats {
6430 builds: self.builds,
6431 runs: self.runs,
6432 }
6433 }
6434
6435 pub(crate) fn set_trace_context(&mut self, trace: TraceContext) {
6438 for plan in &mut self.compiled {
6439 plan.exec.set_trace_context(trace.clone());
6440 }
6441 self.trace = trace;
6442 }
6443
6444 fn compile(&self, externals: &[&Tensor]) -> Result<CompiledChildPlan> {
6445 let mut graph = self.template.clone();
6446 graph.opset_imports = self.inherited_opsets.clone();
6449
6450 let body_names = graph
6451 .values
6452 .iter()
6453 .filter_map(|(vid, value)| value.name.clone().map(|name| (name, vid)))
6454 .collect::<HashMap<_, _>>();
6455
6456 for name in &self.capture_names {
6459 let vid = *body_names.get(name).ok_or_else(|| {
6460 SessionError::Internal(format!(
6461 "subgraph '{}' lost captured value '{name}'",
6462 self.name
6463 ))
6464 })?;
6465 if !graph.inputs.contains(&vid) {
6466 graph.add_input(vid);
6467 }
6468 }
6469
6470 for (name, tensor) in self.input_names.iter().zip(externals) {
6471 let vid = *body_names.get(name).ok_or_else(|| {
6472 SessionError::Internal(format!(
6473 "subgraph '{}' is missing bound input '{name}'",
6474 self.name
6475 ))
6476 })?;
6477 let value = graph.value_mut(vid);
6478 value.dtype = tensor.dtype;
6479 value.shape = tensor.shape.iter().map(|&dim| Dim::Static(dim)).collect();
6480 }
6481
6482 let registry = InferenceRegistry::default_registry();
6485 registry.infer_graph(&mut graph, &self.inherited_opsets, MergePolicy::Permissive)?;
6486
6487 Ok(CompiledChildPlan {
6488 exec: {
6489 let mut exec = Executor::build(graph, self.weights.clone(), self.ep.clone())?;
6490 exec.set_trace_context(self.trace.clone());
6491 exec
6492 },
6493 signature: externals
6494 .iter()
6495 .map(|tensor| ChildInputSignature {
6496 dtype: tensor.dtype,
6497 shape: tensor.shape.clone(),
6498 })
6499 .collect(),
6500 })
6501 }
6502
6503 pub(crate) fn run(
6506 &mut self,
6507 formal_inputs: &[&Tensor],
6508 outer_scope: &HashMap<String, Tensor>,
6509 ) -> Result<Vec<Tensor>> {
6510 if self.formal_names.len() != formal_inputs.len() {
6511 return Err(SessionError::Internal(format!(
6512 "subgraph '{}' expects {} formal input(s) but {} were supplied",
6513 self.name,
6514 self.formal_names.len(),
6515 formal_inputs.len()
6516 )));
6517 }
6518
6519 let mut externals = Vec::with_capacity(formal_inputs.len() + self.capture_names.len());
6520 externals.extend_from_slice(formal_inputs);
6521 for name in &self.capture_names {
6522 externals.push(
6523 outer_scope
6524 .get(name)
6525 .ok_or_else(|| missing_capture_error(&self.name, name))?,
6526 );
6527 }
6528
6529 let signature = externals
6530 .iter()
6531 .map(|tensor| ChildInputSignature {
6532 dtype: tensor.dtype,
6533 shape: tensor.shape.clone(),
6534 })
6535 .collect::<Vec<_>>();
6536 let cache_index = if let Some(index) = self
6537 .compiled
6538 .iter()
6539 .position(|compiled| compiled.signature == signature)
6540 {
6541 let compiled = self.compiled.remove(index);
6542 self.compiled.push(compiled);
6543 self.compiled.len() - 1
6544 } else {
6545 let compiled = self.compile(&externals)?;
6546 if self.compiled.len() == CHILD_EXECUTOR_CACHE_CAPACITY {
6547 self.compiled.remove(0);
6548 }
6549 self.compiled.push(compiled);
6550 self.builds += 1;
6551 self.compiled.len() - 1
6552 };
6553
6554 self.runs += 1;
6555 let inputs = self
6556 .input_names
6557 .iter()
6558 .map(String::as_str)
6559 .zip(externals)
6560 .collect::<Vec<_>>();
6561 self.compiled[cache_index]
6562 .exec
6563 .run_scoped(&inputs, outer_scope, &ExternalBindings::default())?
6564 .into_iter()
6565 .map(|output| {
6566 let output = output.ok_or_else(|| {
6567 SessionError::Internal(format!(
6568 "subgraph '{}' unexpectedly suppressed an output",
6569 self.name
6570 ))
6571 })?;
6572 match output {
6573 SessionOutput::Tensor(tensor) => Ok(tensor),
6574 SessionOutput::Sequence(_) => Err(SessionError::SequenceOp {
6575 op: "<control-flow output>".to_string(),
6576 reason: format!(
6577 "subgraph '{}' produced a Sequence output where this control-flow path requires a tensor",
6578 self.name
6579 ),
6580 }),
6581 }
6582 })
6583 .collect()
6584 }
6585}
6586
6587impl Executor {
6595 fn value_tensor(
6598 &self,
6599 vid: ValueId,
6600 resolved: &HashMap<ValueId, Vec<usize>>,
6601 ) -> Result<Tensor> {
6602 let dtype = self.value_dtypes[&vid];
6603 let shape = resolved.get(&vid).cloned().ok_or_else(|| {
6604 let name = self
6605 .graph
6606 .try_value(vid)
6607 .and_then(|v| v.name.clone())
6608 .unwrap_or_else(|| format!("value#{}", vid.0));
6609 SessionError::UnresolvedShape {
6610 value: name,
6611 op: "<control-flow input>".to_string(),
6612 }
6613 })?;
6614 let bytes = self.contiguous_bytes(vid, &shape, dtype)?;
6616 Tensor::from_raw(dtype, shape, &bytes)
6617 }
6618
6619 fn root_of(&self, vid: ValueId) -> ValueId {
6623 match self.views.get(&vid) {
6624 Some(v) => v.source,
6625 None => vid,
6626 }
6627 }
6628
6629 fn try_move_host_output(
6643 &mut self,
6644 vid: ValueId,
6645 shape: &[usize],
6646 dtype: DataType,
6647 ) -> Result<Option<Tensor>> {
6648 if self.views.contains_key(&vid)
6652 || self.seq_elem_values.contains_key(&vid)
6653 || self.shared_buffers.contains_key(&vid)
6654 || self.pinned.contains(&vid)
6655 {
6656 return Ok(None);
6657 }
6658 if self
6662 .graph
6663 .try_value(vid)
6664 .is_none_or(|value| value.producer.is_none())
6665 {
6666 return Ok(None);
6667 }
6668 if let Some(producer) = self.graph.try_value(vid).and_then(|value| value.producer)
6674 && self.if_last_predicate.contains_key(&producer)
6675 {
6676 return Ok(None);
6677 }
6678 if self.graph.outputs.iter().filter(|&&o| o == vid).count() != 1 {
6680 return Ok(None);
6681 }
6682 let value_name = || format!("value#{}", vid.0);
6683 let numel = checked_numel(shape, value_name)?;
6684 let n = checked_storage_bytes(dtype, numel, value_name, shape)?;
6685 let movable = self.buffers.get(&vid).is_some_and(|buf| {
6686 buf.device().is_host_accessible() && !buf.is_borrowed() && buf.len() == n
6687 });
6688 if !movable {
6689 return Ok(None);
6690 }
6691 let buffer = self
6692 .buffers
6693 .remove(&vid)
6694 .expect("buffer presence checked above");
6695 self.buffer_shapes.remove(&vid);
6698 Ok(Some(Tensor::from_owned_buffer(
6699 self.ep.clone(),
6700 dtype,
6701 shape.to_vec(),
6702 buffer,
6703 )))
6704 }
6705
6706 fn contiguous_bytes(&self, vid: ValueId, shape: &[usize], dtype: DataType) -> Result<Vec<u8>> {
6711 let value_name = || {
6712 self.graph
6713 .try_value(vid)
6714 .and_then(|value| value.name.clone())
6715 .unwrap_or_else(|| format!("value#{}", vid.0))
6716 };
6717 let numel = checked_numel(shape, value_name)?;
6718 let n = checked_storage_bytes(dtype, numel, value_name, shape)?;
6719 if let Some(elem) = self.seq_elem_values.get(&vid) {
6724 let bytes = elem.contiguous_bytes().map_err(SessionError::from)?;
6725 return Ok(bytes[..n.min(bytes.len())].to_vec());
6726 }
6727 if let Some(view) = self.views.get(&vid) {
6728 let buf = self.buffers.get(&view.source).ok_or_else(|| {
6729 SessionError::Internal(format!(
6730 "view value#{} aliases missing source buffer value#{}",
6731 vid.0, view.source.0
6732 ))
6733 })?;
6734 let esize = dtype.byte_size();
6735 if esize == 0 {
6736 return Err(SessionError::Internal(format!(
6739 "cannot materialize sub-byte view value#{}",
6740 vid.0
6741 )));
6742 }
6743 let mut host = vec![0u8; buf.len()];
6744 self.ep.copy_to_host(buf, &mut host)?;
6745 Ok(gather_view(
6746 &host,
6747 &view.shape,
6748 &view.strides,
6749 view.byte_offset,
6750 esize,
6751 ))
6752 } else {
6753 let buf = self
6754 .buffers
6755 .get(&vid)
6756 .ok_or_else(|| SessionError::Internal(format!("value#{} not produced", vid.0)))?;
6757 let mut host = vec![0u8; n];
6758 self.ep.copy_to_host(buf, &mut host)?;
6759 Ok(host)
6760 }
6761 }
6762
6763 fn store_output_tensor(
6770 &mut self,
6771 vid: ValueId,
6772 tensor: &Tensor,
6773 resolved: &mut HashMap<ValueId, Vec<usize>>,
6774 ) -> Result<()> {
6775 self.store_output_bytes(
6776 vid,
6777 tensor.dtype,
6778 tensor.shape.clone(),
6779 tensor.as_bytes(),
6780 resolved,
6781 )
6782 }
6783
6784 fn store_output_bytes(
6785 &mut self,
6786 vid: ValueId,
6787 dtype: DataType,
6788 dims: Vec<usize>,
6789 bytes: &[u8],
6790 resolved: &mut HashMap<ValueId, Vec<usize>>,
6791 ) -> Result<()> {
6792 let numel = checked_numel(&dims, || format!("value#{}", vid.0))?;
6793 let need =
6794 checked_storage_bytes(dtype, numel, || format!("value#{}", vid.0), &dims)?.max(1);
6795 let fits = self
6796 .buffers
6797 .get(&vid)
6798 .map(|b| b.len() == need)
6799 .unwrap_or(false);
6800 if !fits {
6801 if let Some(old) = self.buffers.remove(&vid) {
6802 self.ep.deallocate(old)?;
6803 }
6804 self.shared_buffers.remove(&vid);
6805 let buf = self
6806 .ep
6807 .allocate(need, TensorLayout::contiguous().alignment)?;
6808 self.buffers.insert(vid, buf);
6809 }
6810 let buf = self.buffers.get_mut(&vid).expect("just ensured");
6811 self.ep.copy_from_host(bytes, buf)?;
6812 self.value_dtypes.insert(vid, dtype);
6813 self.buffer_shapes.insert(vid, dims.clone());
6814 resolved.insert(vid, dims);
6815 Ok(())
6816 }
6817
6818 fn prepare_subgraph(
6823 &self,
6824 node_id: NodeId,
6825 attr_key: &str,
6826 resolved: &HashMap<ValueId, Vec<usize>>,
6827 outer_scope: &HashMap<String, Tensor>,
6828 ) -> Result<PreparedSubgraph> {
6829 let key = (node_id, attr_key.to_string());
6830 let body = self.graph.subgraphs.get(&key).ok_or_else(|| {
6831 SessionError::Internal(format!(
6832 "control-flow node #{} references missing subgraph '{attr_key}'",
6833 node_id.0
6834 ))
6835 })?;
6836
6837 let mut scope_names = required_outer_names(body).into_iter().collect::<Vec<_>>();
6838 scope_names.sort();
6839 let mut captures = HashMap::with_capacity(scope_names.len());
6840 for name in scope_names {
6841 let tensor = if let Some(&vid) = self.name_index.get(&name) {
6842 let materialized = self.buffers.contains_key(&vid)
6843 || self.views.contains_key(&vid)
6844 || self.seq_elem_values.contains_key(&vid);
6845 if resolved.contains_key(&vid) && materialized {
6846 self.value_tensor(vid, resolved)?
6847 } else {
6848 outer_scope
6849 .get(&name)
6850 .cloned()
6851 .ok_or_else(|| missing_capture_error(attr_key, &name))?
6852 }
6853 } else {
6854 outer_scope
6855 .get(&name)
6856 .cloned()
6857 .ok_or_else(|| missing_capture_error(attr_key, &name))?
6858 };
6859 captures.insert(name, tensor);
6860 }
6861
6862 Ok(PreparedSubgraph { key, captures })
6863 }
6864
6865 fn run_subgraph(
6869 &mut self,
6870 prepared: &PreparedSubgraph,
6871 formal_inputs: &[&Tensor],
6872 ) -> Result<Vec<Tensor>> {
6873 if !self.subgraph_execs.contains_key(&prepared.key) {
6874 let body = self
6875 .graph
6876 .subgraphs
6877 .get(&prepared.key)
6878 .cloned()
6879 .ok_or_else(|| {
6880 SessionError::Internal(format!(
6881 "control-flow node #{} has no registered subgraph '{}'",
6882 prepared.key.0.0, prepared.key.1
6883 ))
6884 })?;
6885 let mut child = ChildExecutor::new(
6886 format!("node#{}/{}", prepared.key.0.0, prepared.key.1),
6887 body,
6888 self.graph.opset_imports.clone(),
6889 self.weights.clone(),
6890 self.ep.clone(),
6891 )?;
6892 child.set_trace_context(self.trace.clone());
6893 self.subgraph_execs.insert(prepared.key.clone(), child);
6894 }
6895
6896 let child = self
6897 .subgraph_execs
6898 .get_mut(&prepared.key)
6899 .expect("child present");
6900 let before = child.stats();
6901 let result = child.run(formal_inputs, &prepared.captures);
6902 let after = child.stats();
6903 self.control_flow_stats.subgraph_builds += after.builds - before.builds;
6904 self.control_flow_stats.subgraph_runs += after.runs - before.runs;
6905 result
6906 }
6907
6908 fn exec_control_flow(
6910 &mut self,
6911 pi: usize,
6912 resolved: &mut HashMap<ValueId, Vec<usize>>,
6913 outer_scope: &HashMap<String, Tensor>,
6914 ) -> Result<()> {
6915 let node = self.graph.node(self.plan[pi].node_id).clone();
6916 match node.op_type.as_str() {
6917 "If" => self.exec_if(&node, resolved, outer_scope),
6918 "Loop" => self.exec_loop(&node, resolved, outer_scope),
6919 "Scan" => self.exec_scan(&node, resolved, outer_scope),
6920 other => Err(SessionError::Internal(format!(
6921 "exec_control_flow reached non-control-flow op {other:?}"
6922 ))),
6923 }
6924 }
6925
6926 fn exec_if(
6929 &mut self,
6930 node: &Node,
6931 resolved: &mut HashMap<ValueId, Vec<usize>>,
6932 outer_scope: &HashMap<String, Tensor>,
6933 ) -> Result<()> {
6934 {
6935 let then_branch = self
6936 .graph
6937 .subgraphs
6938 .get(&(node.id, "then_branch".to_string()))
6939 .ok_or_else(|| SessionError::ControlFlow {
6940 op: "If".to_string(),
6941 reason: "missing required 'then_branch' subgraph".to_string(),
6942 })?;
6943 let else_branch = self
6944 .graph
6945 .subgraphs
6946 .get(&(node.id, "else_branch".to_string()))
6947 .ok_or_else(|| SessionError::ControlFlow {
6948 op: "If".to_string(),
6949 reason: "missing required 'else_branch' subgraph".to_string(),
6950 })?;
6951
6952 if !then_branch.inputs.is_empty() || !else_branch.inputs.is_empty() {
6953 return Err(SessionError::ControlFlow {
6954 op: "If".to_string(),
6955 reason: format!(
6956 "branch subgraphs must declare zero formal inputs, but then_branch has {} \
6957 and else_branch has {}",
6958 then_branch.inputs.len(),
6959 else_branch.inputs.len()
6960 ),
6961 });
6962 }
6963 validate_if_branch_outputs(&self.graph, node)?;
6964 }
6965
6966 let cond_vid =
6967 node.inputs
6968 .first()
6969 .and_then(|s| *s)
6970 .ok_or_else(|| SessionError::ControlFlow {
6971 op: "If".to_string(),
6972 reason: "missing required 'cond' input".to_string(),
6973 })?;
6974 let cond_t = self.value_tensor(cond_vid, resolved)?;
6975 if cond_t.dtype != DataType::Bool {
6976 return Err(SessionError::DtypeMismatch {
6977 name: "If cond".to_string(),
6978 expected: format!("{:?}", DataType::Bool),
6979 got: format!("{:?}", cond_t.dtype),
6980 });
6981 }
6982 let cond = tensor_scalar_bool(&cond_t).ok_or_else(|| SessionError::ControlFlow {
6983 op: "If".to_string(),
6984 reason: format!(
6985 "'cond' must be a BOOL scalar or single-element tensor, got shape {:?}",
6986 cond_t.shape
6987 ),
6988 })?;
6989
6990 if self.if_last_predicate.get(&node.id) == Some(&cond)
7001 && node.outputs.iter().all(|v| resolved.contains_key(v))
7002 {
7003 return Ok(());
7004 }
7005
7006 let attr_key = if cond { "then_branch" } else { "else_branch" };
7007 let taken_branch_is_invariant = self
7010 .graph
7011 .subgraphs
7012 .get(&(node.id, attr_key.to_string()))
7013 .map(|body| required_outer_names(body).is_empty())
7014 .unwrap_or(false);
7015 let prepared = {
7016 let _s = phase_span!("execif.prepare_subgraph");
7017 self.prepare_subgraph(node.id, attr_key, resolved, outer_scope)?
7018 };
7019 let outs = {
7020 let _s = phase_span!("execif.run_subgraph");
7021 self.run_subgraph(&prepared, &[])?
7022 };
7023
7024 if outs.len() != node.outputs.len() {
7025 return Err(SessionError::OutputShapeCountMismatch {
7026 op: format!("If/{attr_key}"),
7027 expected: node.outputs.len(),
7028 got: outs.len(),
7029 });
7030 }
7031 {
7032 let _s = phase_span!("execif.store_output");
7033 for (vid, t) in node.outputs.iter().zip(outs.iter()) {
7034 self.store_output_tensor(*vid, t, resolved)?;
7035 }
7036 }
7037 if taken_branch_is_invariant {
7040 self.if_last_predicate.insert(node.id, cond);
7041 } else {
7042 self.if_last_predicate.remove(&node.id);
7043 }
7044 Ok(())
7045 }
7046
7047 fn loop_body_scan_specs(
7050 &self,
7051 node: &Node,
7052 carried: &[Tensor],
7053 num_scan: usize,
7054 resolved: &HashMap<ValueId, Vec<usize>>,
7055 ) -> Result<OptionalTensorSpecs> {
7056 let body = self
7057 .graph
7058 .subgraphs
7059 .get(&(node.id, "body".to_string()))
7060 .ok_or_else(|| SessionError::ControlFlow {
7061 op: "Loop".to_string(),
7062 reason: "missing required 'body' subgraph".to_string(),
7063 })?;
7064 let expected_inputs = 2 + carried.len();
7065 if body.inputs.len() != expected_inputs {
7066 return Err(SessionError::ControlFlow {
7067 op: "Loop".to_string(),
7068 reason: format!(
7069 "body declares {} formal input(s), expected {expected_inputs}",
7070 body.inputs.len()
7071 ),
7072 });
7073 }
7074 let expected_outputs = 1 + carried.len() + num_scan;
7075 if body.outputs.len() != expected_outputs {
7076 return Err(SessionError::ControlFlow {
7077 op: "Loop".to_string(),
7078 reason: format!(
7079 "body declares {} output(s), expected {expected_outputs}",
7080 body.outputs.len()
7081 ),
7082 });
7083 }
7084
7085 for (index, expected) in [(0, DataType::Int64), (1, DataType::Bool)] {
7086 let input = body.inputs[index];
7087 if body.value_type_is_known(input) && body.value(input).dtype != expected {
7088 return Err(SessionError::ControlFlow {
7089 op: "Loop".to_string(),
7090 reason: format!(
7091 "body formal input {index} must be {expected:?}, got {:?}",
7092 body.value(input).dtype
7093 ),
7094 });
7095 }
7096 }
7097 let cond_out = body.outputs[0];
7098 if body.value_type_is_known(cond_out) && body.value(cond_out).dtype != DataType::Bool {
7099 return Err(SessionError::ControlFlow {
7100 op: "Loop".to_string(),
7101 reason: format!(
7102 "body output 0 ('cond_out') must be Bool, got {:?}",
7103 body.value(cond_out).dtype
7104 ),
7105 });
7106 }
7107
7108 for (index, initial) in carried.iter().enumerate() {
7109 for (kind, value) in [
7110 ("formal input", body.inputs[2 + index]),
7111 ("output", body.outputs[1 + index]),
7112 ] {
7113 if body.value_type_is_known(value) && body.value(value).dtype != initial.dtype {
7114 return Err(SessionError::ControlFlow {
7115 op: "Loop".to_string(),
7116 reason: format!(
7117 "loop-carried {kind} {index} has dtype {:?}, but its initial value has \
7118 dtype {:?}",
7119 body.value(value).dtype,
7120 initial.dtype
7121 ),
7122 });
7123 }
7124 }
7125 }
7126
7127 body.outputs
7128 .iter()
7129 .skip(1 + carried.len())
7130 .zip(node.outputs.iter().skip(carried.len()))
7131 .enumerate()
7132 .map(|(index, (&body_output, &node_output))| {
7133 let body_value = body.value(body_output);
7134 let node_dtype = self.value_dtypes[&node_output];
7135 let dtype = if body.value_type_is_known(body_output) {
7136 if self.graph.value_type_is_known(node_output)
7137 && body_value.dtype != node_dtype
7138 {
7139 return Err(SessionError::ControlFlow {
7140 op: "Loop".to_string(),
7141 reason: format!(
7142 "scan output {index} has body dtype {:?}, but the Loop node declares \
7143 {node_dtype:?}",
7144 body_value.dtype
7145 ),
7146 });
7147 }
7148 body_value.dtype
7149 } else {
7150 node_dtype
7151 };
7152 let elem_shape = body
7153 .value_shape_is_known(body_output)
7154 .then(|| as_static_shape(&body_value.shape))
7155 .flatten()
7156 .or_else(|| {
7157 resolved
7158 .get(&node_output)
7159 .and_then(|shape| shape.get(1..).map(<[_]>::to_vec))
7160 });
7161 Ok(elem_shape.map(|shape| (dtype, shape)))
7162 })
7163 .collect()
7164 }
7165
7166 fn exec_loop(
7172 &mut self,
7173 node: &Node,
7174 resolved: &mut HashMap<ValueId, Vec<usize>>,
7175 outer_scope: &HashMap<String, Tensor>,
7176 ) -> Result<()> {
7177 let m: Option<i64> = match node.inputs.first().and_then(|s| *s) {
7180 Some(vid) => {
7181 let t = self.value_tensor(vid, resolved)?;
7182 if t.dtype != DataType::Int64 {
7183 return Err(SessionError::DtypeMismatch {
7184 name: "Loop M".to_string(),
7185 expected: format!("{:?}", DataType::Int64),
7186 got: format!("{:?}", t.dtype),
7187 });
7188 }
7189 let m = tensor_scalar_i64(&t).ok_or_else(|| SessionError::ControlFlow {
7190 op: "Loop".to_string(),
7191 reason: format!(
7192 "'M' must be an INT64 scalar or single-element tensor, got shape {:?}",
7193 t.shape
7194 ),
7195 })?;
7196 Some(m)
7197 }
7198 None => None,
7199 };
7200 let mut cond: Option<bool> =
7201 match node.inputs.get(1).and_then(|s| *s) {
7202 Some(vid) => {
7203 let t = self.value_tensor(vid, resolved)?;
7204 if t.dtype != DataType::Bool {
7205 return Err(SessionError::DtypeMismatch {
7206 name: "Loop cond".to_string(),
7207 expected: format!("{:?}", DataType::Bool),
7208 got: format!("{:?}", t.dtype),
7209 });
7210 }
7211 Some(tensor_scalar_bool(&t).ok_or_else(|| SessionError::ControlFlow {
7212 op: "Loop".to_string(),
7213 reason: format!(
7214 "'cond' must be a BOOL scalar or single-element tensor, got shape {:?}",
7215 t.shape
7216 ),
7217 })?)
7218 }
7219 None => None,
7220 };
7221
7222 let mut carried: Vec<Tensor> = Vec::new();
7224 for slot in node.inputs.iter().skip(2) {
7225 let vid = slot.ok_or_else(|| {
7226 SessionError::Internal(
7227 "Loop: an interior loop-carried input is omitted (empty), which ONNX does not \
7228 allow — every v_initial must be provided"
7229 .to_string(),
7230 )
7231 })?;
7232 carried.push(self.value_tensor(vid, resolved)?);
7233 }
7234 let num_carried = carried.len();
7235 let carried_invariants: Vec<(DataType, Vec<usize>)> = carried
7236 .iter()
7237 .map(|tensor| (tensor.dtype, tensor.shape.clone()))
7238 .collect();
7239 let num_outputs = node.outputs.len();
7242 if num_outputs < num_carried {
7243 return Err(SessionError::Internal(format!(
7244 "Loop: node declares {num_outputs} output(s) but has {num_carried} loop-carried \
7245 dependency(ies); outputs must be carried-finals followed by scan-outputs"
7246 )));
7247 }
7248 let num_scan = num_outputs - num_carried;
7249 let empty_scan_specs = self.loop_body_scan_specs(node, &carried, num_scan, resolved)?;
7250 let mut scan_acc: Vec<TensorStackAccumulator> = (0..num_scan)
7251 .map(|_| TensorStackAccumulator::new())
7252 .collect();
7253 let prepared = self.prepare_subgraph(node.id, "body", resolved, outer_scope)?;
7254 let mut iter_tensor = scalar_i64_tensor(0)?;
7255 let mut cond_tensor = scalar_bool_tensor(cond.unwrap_or(true))?;
7256
7257 let mut iter: i64 = 0;
7258 loop {
7259 if let Some(m) = m
7260 && iter >= m
7261 {
7262 break;
7263 }
7264 if cond == Some(false) {
7265 break;
7266 }
7267
7268 iter_tensor.overwrite_bytes(&iter.to_le_bytes())?;
7269 cond_tensor.overwrite_bytes(&[u8::from(cond.unwrap_or(true))])?;
7270 let mut formal: Vec<&Tensor> = Vec::with_capacity(2 + num_carried);
7271 formal.push(&iter_tensor);
7272 formal.push(&cond_tensor);
7273 formal.extend(carried.iter());
7274
7275 let outs = self.run_subgraph(&prepared, &formal)?;
7276 drop(formal);
7277 let expected = 1 + num_carried + num_scan;
7279 if outs.len() != expected {
7280 return Err(SessionError::OutputShapeCountMismatch {
7281 op: "Loop/body".to_string(),
7282 expected,
7283 got: outs.len(),
7284 });
7285 }
7286 let mut it = outs.into_iter();
7287 let cond_out = it.next().expect("cond_out present");
7288 cond = Some(tensor_scalar_bool(&cond_out).ok_or_else(|| {
7289 SessionError::Internal(format!(
7290 "Loop: body's first output 'cond_out' must be a BOOL scalar, got dtype {:?}",
7291 cond_out.dtype
7292 ))
7293 })?);
7294 let next_carried: Vec<Tensor> = (&mut it).take(num_carried).collect();
7295 for (index, (tensor, (expected_dtype, expected_shape))) in
7296 next_carried.iter().zip(&carried_invariants).enumerate()
7297 {
7298 if tensor.dtype != *expected_dtype {
7299 return Err(SessionError::ControlFlow {
7300 op: "Loop".to_string(),
7301 reason: format!(
7302 "loop-carried output {index} dtype mismatch: expected \
7303 {expected_dtype:?}, got {:?}",
7304 tensor.dtype
7305 ),
7306 });
7307 }
7308 if tensor.shape != *expected_shape {
7309 return Err(SessionError::ControlFlow {
7310 op: "Loop".to_string(),
7311 reason: format!(
7312 "loop-carried output {index} shape mismatch: expected \
7313 {expected_shape:?}, got {:?}",
7314 tensor.shape
7315 ),
7316 });
7317 }
7318 }
7319 carried = next_carried;
7320 for acc in scan_acc.iter_mut() {
7321 acc.push(it.next().expect("scan output present"))?;
7322 }
7323
7324 iter = iter
7325 .checked_add(1)
7326 .ok_or_else(|| SessionError::ControlFlow {
7327 op: "Loop".to_string(),
7328 reason: "iteration counter overflowed INT64".to_string(),
7329 })?;
7330 }
7331
7332 for (i, t) in carried.iter().enumerate() {
7334 self.store_output_tensor(node.outputs[i], t, resolved)?;
7335 }
7336 for (s, (acc, empty_spec)) in scan_acc.into_iter().zip(empty_scan_specs).enumerate() {
7337 let (dtype, shape, bytes) = acc.finish_with_empty(empty_spec, s)?;
7338 self.store_output_bytes(
7339 node.outputs[num_carried + s],
7340 dtype,
7341 shape,
7342 &bytes,
7343 resolved,
7344 )?;
7345 }
7346 Ok(())
7347 }
7348
7349 fn scan_body_specs(
7350 &self,
7351 node: &Node,
7352 state: &[Tensor],
7353 scan_inputs: &[Tensor],
7354 input_axes: &[usize],
7355 num_scan_outputs: usize,
7356 output_axes: &[i64],
7357 resolved: &HashMap<ValueId, Vec<usize>>,
7358 ) -> Result<OptionalTensorSpecs> {
7359 let body = self
7360 .graph
7361 .subgraphs
7362 .get(&(node.id, "body".to_string()))
7363 .ok_or_else(|| SessionError::ControlFlow {
7364 op: "Scan".to_string(),
7365 reason: "missing required 'body' subgraph".to_string(),
7366 })?;
7367 let expected_inputs = state.len() + scan_inputs.len();
7368 if body.inputs.len() != expected_inputs {
7369 return Err(SessionError::ControlFlow {
7370 op: "Scan".to_string(),
7371 reason: format!(
7372 "body declares {} formal input(s), expected {expected_inputs}",
7373 body.inputs.len()
7374 ),
7375 });
7376 }
7377 let expected_outputs = state.len() + num_scan_outputs;
7378 if body.outputs.len() != expected_outputs {
7379 return Err(SessionError::ControlFlow {
7380 op: "Scan".to_string(),
7381 reason: format!(
7382 "body declares {} output(s), expected {expected_outputs}",
7383 body.outputs.len()
7384 ),
7385 });
7386 }
7387
7388 for (index, initial) in state.iter().enumerate() {
7389 for (kind, value) in [
7390 ("formal input", body.inputs[index]),
7391 ("output", body.outputs[index]),
7392 ] {
7393 if body.value_type_is_known(value) && body.value(value).dtype != initial.dtype {
7394 return Err(SessionError::ControlFlow {
7395 op: "Scan".to_string(),
7396 reason: format!(
7397 "state {kind} {index} has dtype {:?}, but its initial value has dtype {:?}",
7398 body.value(value).dtype,
7399 initial.dtype
7400 ),
7401 });
7402 }
7403 }
7404 }
7405 for (index, ((input, &axis), &formal)) in scan_inputs
7406 .iter()
7407 .zip(input_axes)
7408 .zip(body.inputs.iter().skip(state.len()))
7409 .enumerate()
7410 {
7411 if body.value_type_is_known(formal) && body.value(formal).dtype != input.dtype {
7412 return Err(SessionError::ControlFlow {
7413 op: "Scan".to_string(),
7414 reason: format!(
7415 "scan formal input {index} has dtype {:?}, but scan input {index} has dtype {:?}",
7416 body.value(formal).dtype,
7417 input.dtype
7418 ),
7419 });
7420 }
7421 let mut slice_shape = input.shape.clone();
7422 slice_shape.remove(axis);
7423 if body.value_shape_is_known(formal)
7424 && let Some(shape) = as_static_shape(&body.value(formal).shape)
7425 && shape != slice_shape
7426 {
7427 return Err(SessionError::ControlFlow {
7428 op: "Scan".to_string(),
7429 reason: format!(
7430 "scan formal input {index} has shape {shape:?}, but slicing input shape {:?} \
7431 along axis {axis} produces {slice_shape:?}",
7432 input.shape
7433 ),
7434 });
7435 }
7436 }
7437
7438 body.outputs
7439 .iter()
7440 .skip(state.len())
7441 .zip(node.outputs.iter().skip(state.len()))
7442 .zip(output_axes)
7443 .enumerate()
7444 .map(|(index, ((&body_output, &node_output), &axis))| {
7445 let body_value = body.value(body_output);
7446 let node_dtype = self.value_dtypes[&node_output];
7447 let dtype = if body.value_type_is_known(body_output) {
7448 if self.graph.value_type_is_known(node_output)
7449 && body_value.dtype != node_dtype
7450 {
7451 return Err(SessionError::ControlFlow {
7452 op: "Scan".to_string(),
7453 reason: format!(
7454 "scan output {index} has body dtype {:?}, but the Scan node declares \
7455 {node_dtype:?}",
7456 body_value.dtype
7457 ),
7458 });
7459 }
7460 body_value.dtype
7461 } else {
7462 node_dtype
7463 };
7464 let elem_shape = body
7465 .value_shape_is_known(body_output)
7466 .then(|| as_static_shape(&body_value.shape))
7467 .flatten()
7468 .or_else(|| {
7469 resolved.get(&node_output).and_then(|shape| {
7470 normalize_axis(axis, shape.len()).map(|axis| {
7471 let mut elem_shape = shape.clone();
7472 elem_shape.remove(axis);
7473 elem_shape
7474 })
7475 })
7476 });
7477 if let Some(shape) = &elem_shape
7478 && normalize_axis(axis, shape.len() + 1).is_none()
7479 {
7480 return Err(SessionError::ControlFlow {
7481 op: "Scan".to_string(),
7482 reason: format!(
7483 "scan_output_axes[{index}]={axis} is out of range for output rank {}",
7484 shape.len() + 1
7485 ),
7486 });
7487 }
7488 Ok(elem_shape.map(|shape| (dtype, shape)))
7489 })
7490 .collect()
7491 }
7492
7493 fn exec_scan(
7496 &mut self,
7497 node: &Node,
7498 resolved: &mut HashMap<ValueId, Vec<usize>>,
7499 outer_scope: &HashMap<String, Tensor>,
7500 ) -> Result<()> {
7501 let raw_num_scan_inputs = node
7502 .attr("num_scan_inputs")
7503 .and_then(|a| a.as_int())
7504 .ok_or_else(|| SessionError::ControlFlow {
7505 op: "Scan".to_string(),
7506 reason: "required attribute 'num_scan_inputs' is missing or not an INT".to_string(),
7507 })?;
7508 let num_scan_inputs = usize::try_from(raw_num_scan_inputs)
7509 .ok()
7510 .filter(|&count| count != 0)
7511 .ok_or_else(|| SessionError::ControlFlow {
7512 op: "Scan".to_string(),
7513 reason: format!(
7514 "'num_scan_inputs' must be a positive INT, got {raw_num_scan_inputs}"
7515 ),
7516 })?;
7517
7518 let total_inputs = node.inputs.len();
7519 if total_inputs < num_scan_inputs {
7520 return Err(SessionError::ControlFlow {
7521 op: "Scan".to_string(),
7522 reason: format!(
7523 "node has {total_inputs} input(s) but num_scan_inputs={num_scan_inputs}"
7524 ),
7525 });
7526 }
7527 let num_state = total_inputs - num_scan_inputs;
7528 if node.outputs.len() < num_state {
7529 return Err(SessionError::ControlFlow {
7530 op: "Scan".to_string(),
7531 reason: format!(
7532 "declares {} output(s) but has {num_state} state variable(s)",
7533 node.outputs.len()
7534 ),
7535 });
7536 }
7537 let num_scan_outputs = node.outputs.len() - num_state;
7538 let input_axes_raw = scan_list_attr(node, "scan_input_axes", num_scan_inputs, 0)?;
7539 let input_directions = scan_list_attr(node, "scan_input_directions", num_scan_inputs, 0)?;
7540 let output_axes = scan_list_attr(node, "scan_output_axes", num_scan_outputs, 0)?;
7541 let output_directions =
7542 scan_list_attr(node, "scan_output_directions", num_scan_outputs, 0)?;
7543 for (name, values) in [
7544 ("scan_input_directions", &input_directions),
7545 ("scan_output_directions", &output_directions),
7546 ] {
7547 for (index, &value) in values.iter().enumerate() {
7548 if !matches!(value, 0 | 1) {
7549 return Err(SessionError::ControlFlow {
7550 op: "Scan".to_string(),
7551 reason: format!(
7552 "{name}[{index}] must be 0 (forward) or 1 (reverse), got {value}"
7553 ),
7554 });
7555 }
7556 }
7557 }
7558
7559 let mut state: Vec<Tensor> = Vec::with_capacity(num_state);
7560 for slot in node.inputs.iter().take(num_state) {
7561 let vid = slot.ok_or_else(|| SessionError::ControlFlow {
7562 op: "Scan".to_string(),
7563 reason: "an initial-state input is omitted (empty), which ONNX does not allow"
7564 .to_string(),
7565 })?;
7566 state.push(self.value_tensor(vid, resolved)?);
7567 }
7568 let mut scan_inputs: Vec<Tensor> = Vec::with_capacity(num_scan_inputs);
7569 for slot in node.inputs.iter().skip(num_state) {
7570 let vid = slot.ok_or_else(|| SessionError::ControlFlow {
7571 op: "Scan".to_string(),
7572 reason: "a scan input is omitted (empty), which ONNX does not allow".to_string(),
7573 })?;
7574 scan_inputs.push(self.value_tensor(vid, resolved)?);
7575 }
7576
7577 let mut input_axes = Vec::with_capacity(num_scan_inputs);
7578 for (index, (input, &raw_axis)) in scan_inputs.iter().zip(&input_axes_raw).enumerate() {
7579 let axis = normalize_axis(raw_axis, input.shape.len()).ok_or_else(|| {
7580 SessionError::ControlFlow {
7581 op: "Scan".to_string(),
7582 reason: format!(
7583 "scan_input_axes[{index}]={raw_axis} is out of range for input rank {}",
7584 input.shape.len()
7585 ),
7586 }
7587 })?;
7588 input_axes.push(axis);
7589 }
7590 let trip_count = scan_inputs[0].shape[input_axes[0]];
7591 for (index, (input, &axis)) in scan_inputs.iter().zip(&input_axes).enumerate() {
7592 let length = input.shape[axis];
7593 if length != trip_count {
7594 return Err(SessionError::ControlFlow {
7595 op: "Scan".to_string(),
7596 reason: format!(
7597 "scan input {index} has scan-axis length {length}, but the first scan input \
7598 has {trip_count}; all scan inputs must agree"
7599 ),
7600 });
7601 }
7602 }
7603
7604 let state_specs: Vec<(DataType, Vec<usize>)> = state
7605 .iter()
7606 .map(|tensor| (tensor.dtype, tensor.shape.clone()))
7607 .collect();
7608 let empty_specs = self.scan_body_specs(
7609 node,
7610 &state,
7611 &scan_inputs,
7612 &input_axes,
7613 num_scan_outputs,
7614 &output_axes,
7615 resolved,
7616 )?;
7617 let mut scan_acc: Vec<TensorStackAccumulator> = (0..num_scan_outputs)
7618 .map(|_| TensorStackAccumulator::new())
7619 .collect();
7620 let prepared = self.prepare_subgraph(node.id, "body", resolved, outer_scope)?;
7621 let mut scan_slices = Vec::with_capacity(num_scan_inputs);
7622 if trip_count != 0 {
7623 for (index, ((input, &axis), &direction)) in scan_inputs
7624 .iter()
7625 .zip(&input_axes)
7626 .zip(&input_directions)
7627 .enumerate()
7628 {
7629 let source_index = if direction == 0 { 0 } else { trip_count - 1 };
7630 let (shape, bytes) = scan_slice(input, axis, source_index, index)?;
7631 scan_slices.push(Tensor::from_raw(input.dtype, shape, &bytes)?);
7632 }
7633 }
7634 for step in 0..trip_count {
7635 if step != 0 {
7636 for (index, (((input, &axis), &direction), slice)) in scan_inputs
7637 .iter()
7638 .zip(&input_axes)
7639 .zip(&input_directions)
7640 .zip(scan_slices.iter_mut())
7641 .enumerate()
7642 {
7643 let source_index = if direction == 0 {
7644 step
7645 } else {
7646 trip_count - 1 - step
7647 };
7648 let (_, bytes) = scan_slice(input, axis, source_index, index)?;
7649 slice.overwrite_bytes(&bytes)?;
7650 }
7651 }
7652 let mut formal: Vec<&Tensor> = Vec::with_capacity(num_state + num_scan_inputs);
7653 formal.extend(state.iter());
7654 formal.extend(scan_slices.iter());
7655
7656 let outs = self.run_subgraph(&prepared, &formal)?;
7657 drop(formal);
7658 let expected = num_state + num_scan_outputs;
7659 if outs.len() != expected {
7660 return Err(SessionError::OutputShapeCountMismatch {
7661 op: "Scan/body".to_string(),
7662 expected,
7663 got: outs.len(),
7664 });
7665 }
7666 let mut it = outs.into_iter();
7667 let next_state: Vec<Tensor> = (&mut it).take(num_state).collect();
7668 for (index, (tensor, (expected_dtype, expected_shape))) in
7669 next_state.iter().zip(&state_specs).enumerate()
7670 {
7671 if tensor.dtype != *expected_dtype {
7672 return Err(SessionError::ControlFlow {
7673 op: "Scan".to_string(),
7674 reason: format!(
7675 "state output {index} dtype mismatch: expected {expected_dtype:?}, got {:?}",
7676 tensor.dtype
7677 ),
7678 });
7679 }
7680 if tensor.shape != *expected_shape {
7681 return Err(SessionError::ControlFlow {
7682 op: "Scan".to_string(),
7683 reason: format!(
7684 "state output {index} shape mismatch: expected {expected_shape:?}, got {:?}",
7685 tensor.shape
7686 ),
7687 });
7688 }
7689 }
7690 state = next_state;
7691 for acc in scan_acc.iter_mut() {
7692 acc.push(it.next().expect("scan output present"))?;
7693 }
7694 }
7695
7696 for (i, t) in state.iter().enumerate() {
7697 self.store_output_tensor(node.outputs[i], t, resolved)?;
7698 }
7699 for (s, ((acc, empty_spec), (&axis, &direction))) in scan_acc
7700 .into_iter()
7701 .zip(empty_specs)
7702 .zip(output_axes.iter().zip(&output_directions))
7703 .enumerate()
7704 {
7705 let (dtype, shape, bytes) = acc.finish_scan(axis, direction, empty_spec, s)?;
7706 self.store_output_bytes(node.outputs[num_state + s], dtype, shape, &bytes, resolved)?;
7707 }
7708 Ok(())
7709 }
7710}
7711
7712fn scan_slice(
7713 t: &Tensor,
7714 axis: usize,
7715 index: usize,
7716 input_index: usize,
7717) -> Result<(Vec<usize>, Vec<u8>)> {
7718 let axis_len = t.shape[axis];
7719 if index >= axis_len {
7720 return Err(SessionError::ControlFlow {
7721 op: "Scan".to_string(),
7722 reason: format!(
7723 "slice index {index} is out of range for scan input {input_index} axis {axis}"
7724 ),
7725 });
7726 }
7727 let esize = t.dtype.byte_size();
7728 if esize == 0 {
7729 return Err(SessionError::ControlFlow {
7730 op: "Scan".to_string(),
7731 reason: format!(
7732 "sub-byte dtype {:?} for scan input {input_index} is not supported",
7733 t.dtype
7734 ),
7735 });
7736 }
7737 let mut shape = t.shape.clone();
7738 shape.remove(axis);
7739 let outer = checked_numel(&t.shape[..axis], || format!("Scan input {input_index}"))?;
7740 let inner = checked_numel(&t.shape[axis + 1..], || format!("Scan input {input_index}"))?;
7741 let inner_bytes = checked_storage_bytes(
7742 t.dtype,
7743 inner,
7744 || format!("Scan input {input_index}"),
7745 &t.shape,
7746 )?;
7747 let total_bytes =
7748 outer
7749 .checked_mul(inner_bytes)
7750 .ok_or_else(|| SessionError::ShapeOverflow {
7751 value: format!("Scan input {input_index} slice"),
7752 dims: shape.clone(),
7753 })?;
7754 let source = t.as_bytes();
7755 let mut bytes = vec![0u8; total_bytes];
7756 for outer_index in 0..outer {
7757 let src = (outer_index * axis_len + index) * inner_bytes;
7758 let dst = outer_index * inner_bytes;
7759 bytes[dst..dst + inner_bytes].copy_from_slice(&source[src..src + inner_bytes]);
7760 }
7761 Ok((shape, bytes))
7762}
7763
7764struct TensorStackAccumulator {
7768 dtype: Option<DataType>,
7769 elem_shape: Vec<usize>,
7770 len: usize,
7771 bytes: Vec<u8>,
7772}
7773
7774impl TensorStackAccumulator {
7775 fn new() -> Self {
7776 Self {
7777 dtype: None,
7778 elem_shape: Vec::new(),
7779 len: 0,
7780 bytes: Vec::new(),
7781 }
7782 }
7783
7784 fn push(&mut self, tensor: Tensor) -> Result<()> {
7785 if let Some(dtype) = self.dtype {
7786 if tensor.shape != self.elem_shape || tensor.dtype != dtype {
7787 return Err(SessionError::Internal(format!(
7788 "Loop/Scan: scan output slice {} has shape {:?} dtype {:?} but the first slice \
7789 is shape {:?} dtype {:?}; every iteration's scan output must match",
7790 self.len, tensor.shape, tensor.dtype, self.elem_shape, dtype
7791 )));
7792 }
7793 } else {
7794 if tensor.dtype.byte_size() == 0 {
7795 return Err(SessionError::Internal(format!(
7796 "Loop/Scan: sub-byte dtype {:?} scan outputs are not supported",
7797 tensor.dtype
7798 )));
7799 }
7800 self.dtype = Some(tensor.dtype);
7801 self.elem_shape = tensor.shape.clone();
7802 }
7803 self.bytes.extend_from_slice(tensor.as_bytes());
7804 self.len += 1;
7805 Ok(())
7806 }
7807
7808 fn finish(self) -> (DataType, Vec<usize>, Vec<u8>) {
7809 if self.len == 0 {
7810 return (DataType::Float32, vec![0], Vec::new());
7811 }
7812 let dtype = self.dtype.expect("non-empty accumulator has dtype");
7813 let mut shape = Vec::with_capacity(1 + self.elem_shape.len());
7814 shape.push(self.len);
7815 shape.extend(self.elem_shape);
7816 (dtype, shape, self.bytes)
7817 }
7818
7819 fn finish_with_empty(
7820 self,
7821 empty_spec: Option<(DataType, Vec<usize>)>,
7822 output_index: usize,
7823 ) -> Result<(DataType, Vec<usize>, Vec<u8>)> {
7824 if self.len != 0 {
7825 return Ok(self.finish());
7826 }
7827 let (dtype, elem_shape) = empty_spec.ok_or_else(|| SessionError::ControlFlow {
7828 op: "Loop".to_string(),
7829 reason: format!(
7830 "cannot determine the element shape of scan output {output_index} for a \
7831 zero-iteration result"
7832 ),
7833 })?;
7834 let mut shape = Vec::with_capacity(1 + elem_shape.len());
7835 shape.push(0);
7836 shape.extend(elem_shape);
7837 Ok((dtype, shape, Vec::new()))
7838 }
7839
7840 fn finish_scan(
7841 self,
7842 axis: i64,
7843 direction: i64,
7844 empty_spec: Option<(DataType, Vec<usize>)>,
7845 output_index: usize,
7846 ) -> Result<(DataType, Vec<usize>, Vec<u8>)> {
7847 let (dtype, elem_shape) = match self.dtype {
7848 Some(dtype) => (dtype, self.elem_shape.clone()),
7849 None => empty_spec.ok_or_else(|| SessionError::ControlFlow {
7850 op: "Scan".to_string(),
7851 reason: format!(
7852 "cannot determine the element shape of scan output {output_index} for a \
7853 zero-iteration result"
7854 ),
7855 })?,
7856 };
7857 let output_rank = elem_shape.len() + 1;
7858 let axis = normalize_axis(axis, output_rank).ok_or_else(|| SessionError::ControlFlow {
7859 op: "Scan".to_string(),
7860 reason: format!(
7861 "scan_output_axes[{output_index}]={axis} is out of range for output rank \
7862 {output_rank}"
7863 ),
7864 })?;
7865 if self.len == 0 {
7866 let mut shape = elem_shape;
7867 shape.insert(axis, 0);
7868 return Ok((dtype, shape, Vec::new()));
7869 }
7870 if axis == 0 && direction == 0 {
7871 let mut shape = Vec::with_capacity(output_rank);
7872 shape.push(self.len);
7873 shape.extend(elem_shape);
7874 return Ok((dtype, shape, self.bytes));
7875 }
7876
7877 let elem_numel = checked_numel(&elem_shape, || {
7878 format!("Scan output {output_index} element")
7879 })?;
7880 let elem_bytes = checked_storage_bytes(
7881 dtype,
7882 elem_numel,
7883 || format!("Scan output {output_index} element"),
7884 &elem_shape,
7885 )?;
7886 let mut elements: Vec<&[u8]> = if elem_bytes == 0 {
7887 (0..self.len).map(|_| &self.bytes[..]).collect()
7888 } else {
7889 self.bytes.chunks_exact(elem_bytes).collect()
7890 };
7891 if direction == 1 {
7892 elements.reverse();
7893 }
7894 let (shape, bytes) = stack_new_axis(&elements, &elem_shape, axis, dtype.byte_size())?;
7895 Ok((dtype, shape, bytes))
7896 }
7897}
7898
7899impl Drop for Executor {
7900 fn drop(&mut self) {
7901 if self.decode_memo_enabled
7905 && std::env::var("ONNX_GENAI_DECODE_MEMO_STATS")
7906 .map(|v| matches!(v.as_str(), "1" | "true" | "on"))
7907 .unwrap_or(false)
7908 {
7909 let (primed, rebuilt, replayed, ineligible) = self.decode_memo_counts();
7910 let (views_reused, dispatch_elided) = self.decode_view_plan_counts();
7911 eprintln!(
7912 "[decode-memo] primed={primed} rebuilt={rebuilt} replayed={replayed} \
7913 ineligible={ineligible} views_reused={views_reused} \
7914 dispatch_elided={dispatch_elided}"
7915 );
7916 }
7917 let _ = self.ep.reset_device_graph();
7918 self.device_graph_signature = None;
7919 for (_, buf) in self.buffers.drain() {
7921 let _ = self.ep.deallocate(buf);
7922 }
7923 self.shared_buffers.clear();
7924 }
7925}
7926
7927pub(crate) fn auto_detect_cpu_ep() -> Result<Arc<dyn ExecutionProvider>> {
7931 let mut ep = CpuExecutionProvider::new();
7932 ep.initialize(&Default::default())?;
7933 Ok(Arc::new(ep))
7934}
7935
7936#[cfg(test)]
7937mod tests {
7938 use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
7939
7940 use onnx_runtime_ep_api::{
7941 CaptureSupport, Cost, EpConfig, EpError, ExecutionProviderCapabilities, Fence, Kernel,
7942 NegotiatedWeight,
7943 };
7944
7945 use super::*;
7946
7947 #[test]
7948 fn phase_profile_gating_and_accumulation() {
7949 phase_profile::force_enabled(false);
7954 let disabled_phase = "test.phase.disabled";
7955 let before = phase_profile::snapshot(disabled_phase);
7956 {
7957 let _s = phase_span!(disabled_phase);
7958 std::thread::sleep(std::time::Duration::from_millis(1));
7959 }
7960 assert_eq!(
7961 phase_profile::snapshot(disabled_phase),
7962 before,
7963 "a disabled phase span must not accumulate any samples"
7964 );
7965
7966 phase_profile::force_enabled(true);
7968 let enabled_phase = "test.phase.enabled";
7969 let (base_ns, base_count) = phase_profile::snapshot(enabled_phase).unwrap_or((0, 0));
7970 {
7971 let _s = phase_span!(enabled_phase);
7972 std::thread::sleep(std::time::Duration::from_millis(2));
7973 }
7974 let (after_ns, after_count) =
7975 phase_profile::snapshot(enabled_phase).expect("enabled span must record a sample");
7976 assert_eq!(after_count, base_count + 1, "one span => one sample");
7977 assert!(
7978 after_ns > base_ns,
7979 "an enabled span must accumulate a positive duration"
7980 );
7981
7982 phase_profile::force_enabled(false);
7984 }
7985
7986 #[test]
7993 fn zero_copy_output_move_reallocates_and_preserves_producer_less_output() {
7994 use onnx_runtime_ir::TensorData;
7995 let mut graph = Graph::new();
7996 graph.opset_imports.insert(String::new(), 17);
7997
7998 let a = graph.create_named_value("a", DataType::Float32, static_shape([3]));
7999 let b = graph.create_named_value("b", DataType::Float32, static_shape([3]));
8000 graph.add_input(a);
8001 graph.add_input(b);
8002
8003 let k = graph.create_named_value("k", DataType::Float32, static_shape([3]));
8005 graph.set_initializer(
8006 k,
8007 WeightRef::Inline(TensorData::from_raw(
8008 DataType::Float32,
8009 vec![3],
8010 [100.0f32, 200.0, 300.0]
8011 .into_iter()
8012 .flat_map(f32::to_le_bytes)
8013 .collect(),
8014 )),
8015 );
8016
8017 let sum = graph.create_named_value("sum", DataType::Float32, static_shape([3]));
8019 graph.insert_node(Node::new(
8020 NodeId(0),
8021 "Add",
8022 vec![Some(a), Some(b)],
8023 vec![sum],
8024 ));
8025 graph.add_output(sum);
8026 graph.add_output(k);
8027
8028 let mut executor = Executor::build(
8029 graph,
8030 Arc::new(WeightStore::new()),
8031 auto_detect_cpu_ep().unwrap(),
8032 )
8033 .unwrap();
8034
8035 let a_val = Tensor::from_f32(&[3], &[1.0, 2.0, 3.0]).unwrap();
8036 let b_val = Tensor::from_f32(&[3], &[10.0, 20.0, 30.0]).unwrap();
8037
8038 for _ in 0..3 {
8041 let outputs = executor
8042 .run(&[("a", &a_val), ("b", &b_val)])
8043 .expect("run must succeed after a prior output buffer was moved out");
8044 assert_eq!(outputs[0].to_vec_f32(), vec![11.0, 22.0, 33.0]);
8045 assert_eq!(
8046 outputs[1].to_vec_f32(),
8047 vec![100.0, 200.0, 300.0],
8048 "producer-less initializer output must stay intact across runs"
8049 );
8050 assert!(
8053 !executor.buffers.contains_key(&sum),
8054 "produced output buffer must be moved out, not copied"
8055 );
8056 assert!(
8057 executor.buffers.contains_key(&k),
8058 "producer-less output must not have its buffer stolen"
8059 );
8060 }
8061 }
8062
8063 struct CaptureDecliningKernel;
8064
8065 impl Kernel for CaptureDecliningKernel {
8066 fn execute(
8067 &self,
8068 _inputs: &[TensorView],
8069 _outputs: &mut [TensorMut],
8070 ) -> onnx_runtime_ep_api::Result<()> {
8071 Ok(())
8072 }
8073
8074 fn capture_support(&self) -> CaptureSupport {
8075 CaptureSupport::unsupported(
8076 "requires M==1 decode GEMV without group_indices; got a prefill signature",
8077 )
8078 }
8079 }
8080
8081 #[test]
8082 fn kernel_capture_reason_propagates_into_structured_report() {
8083 let mut node = Node::new(NodeId(9), "MatMulNBits", vec![], vec![]);
8084 node.domain = "com.microsoft".to_string();
8085 let decline =
8086 kernel_capture_decline(node.id, &node, &CaptureDecliningKernel).expect("decline");
8087 let report = CaptureDeclineReport::one(decline);
8088
8089 assert_eq!(
8090 report.entries,
8091 vec![CaptureDecline {
8092 node_id: Some(9),
8093 op_type: "MatMulNBits".to_string(),
8094 domain: "com.microsoft".to_string(),
8095 reason: "requires M==1 decode GEMV without group_indices; got a prefill signature"
8096 .to_string(),
8097 seam_reason: Some(SeamReason::KernelCaptureUnsupported),
8098 }]
8099 );
8100 assert!(report.to_string().contains("node 9"));
8101 assert!(
8102 report
8103 .to_string()
8104 .contains("requires M==1 decode GEMV without group_indices")
8105 );
8106 }
8107
8108 #[test]
8109 fn seam_reasons_map_to_structural_capture_paths() {
8110 let cases = [
8111 (
8112 SeamReason::HostControlFlowOrSequence,
8113 CapturePathKind::HostSeam,
8114 "host-seam",
8115 ),
8116 (
8117 SeamReason::UnresolvedOutputShape,
8118 CapturePathKind::EagerDeviceSeam,
8119 "eager-device-seam",
8120 ),
8121 (
8122 SeamReason::UnresolvedInputShape,
8123 CapturePathKind::EagerDeviceSeam,
8124 "eager-device-seam",
8125 ),
8126 (
8127 SeamReason::KernelNotWarmed,
8128 CapturePathKind::EagerDeviceSeam,
8129 "eager-device-seam",
8130 ),
8131 (
8132 SeamReason::KernelCaptureUnsupported,
8133 CapturePathKind::EagerDeviceSeam,
8134 "eager-device-seam",
8135 ),
8136 ];
8137
8138 for (reason, expected_kind, expected_label) in cases {
8139 assert_eq!(reason.path_kind(), expected_kind);
8140 assert_eq!(reason.label(), expected_label);
8141 }
8142 assert_eq!(CapturePathKind::CaptureRegion.label(), "capture-region");
8143 }
8144
8145 #[test]
8146 fn ep_structural_plan_plus_executor_kernel_checks_matches_legacy_declines() {
8147 use onnx_runtime_ir::static_shape;
8148
8149 fn legacy_node_capture_reason(
8150 executor: &Executor,
8151 plan: &NodePlan,
8152 resolved: &HashMap<ValueId, Vec<usize>>,
8153 ) -> Option<CaptureDecline> {
8154 let node = executor.graph.node(plan.node_id);
8155 if is_control_flow_op(&node.op_type, &node.domain)
8156 || is_sequence_op(&node.op_type, &node.domain)
8157 {
8158 return Some(CaptureDecline::node(
8159 plan.node_id,
8160 node,
8161 SeamReason::HostControlFlowOrSequence,
8162 "control-flow and sequence nodes are not device-graph capturable",
8163 ));
8164 }
8165 if plan
8166 .outputs
8167 .iter()
8168 .any(|output| !resolved.contains_key(output))
8169 {
8170 return Some(CaptureDecline::node(
8171 plan.node_id,
8172 node,
8173 SeamReason::UnresolvedOutputShape,
8174 "data-dependent output shape was unresolved before capture",
8175 ));
8176 }
8177 let Some(input_shapes) = plan
8178 .inputs
8179 .iter()
8180 .map(|input| {
8181 input
8182 .map(|value| resolved.get(&value).cloned())
8183 .unwrap_or(Some(Vec::new()))
8184 })
8185 .collect::<Option<Vec<_>>>()
8186 else {
8187 return Some(CaptureDecline::node(
8188 plan.node_id,
8189 node,
8190 SeamReason::UnresolvedInputShape,
8191 "data-dependent input shape was unresolved before capture",
8192 ));
8193 };
8194 let key = KernelKey {
8195 node: plan.node_id.0,
8196 shapes: input_shapes,
8197 };
8198 let Some(kernel) = executor.cache.entries.get(&key) else {
8199 return Some(CaptureDecline::node(
8200 plan.node_id,
8201 node,
8202 SeamReason::KernelNotWarmed,
8203 "kernel has not been warmed for the requested capture shape",
8204 ));
8205 };
8206 kernel_capture_decline(plan.node_id, node, kernel.as_ref())
8207 }
8208
8209 let mut graph = Graph::new();
8210 graph.opset_imports.insert(String::new(), 17);
8211 for index in 0..6 {
8212 let input = graph.create_named_value(
8213 format!("input_{index}"),
8214 DataType::Float32,
8215 static_shape([1]),
8216 );
8217 let output = graph.create_named_value(
8218 format!("output_{index}"),
8219 DataType::Float32,
8220 static_shape([1]),
8221 );
8222 graph.add_input(input);
8223 graph.add_output(output);
8224 graph.insert_node(Node::new(
8225 NodeId(0),
8226 "Identity",
8227 vec![Some(input)],
8228 vec![output],
8229 ));
8230 }
8231
8232 let mut executor = Executor::build(
8233 graph,
8234 Arc::new(WeightStore::new()),
8235 auto_detect_cpu_ep().expect("CPU EP"),
8236 )
8237 .expect("representative static graph");
8238 let mut resolved = executor
8239 .value_shapes
8240 .iter()
8241 .filter_map(|(&value, shape)| as_static_shape(shape).map(|shape| (value, shape)))
8242 .collect::<HashMap<_, _>>();
8243 let keys = executor
8244 .plan
8245 .iter()
8246 .map(|plan| KernelKey {
8247 node: plan.node_id.0,
8248 shapes: plan
8249 .inputs
8250 .iter()
8251 .map(|input| {
8252 input
8253 .map(|value| resolved[&value].clone())
8254 .unwrap_or_default()
8255 })
8256 .collect(),
8257 })
8258 .collect::<Vec<_>>();
8259
8260 executor.graph.node_mut(executor.plan[0].node_id).op_type = "If".to_string();
8261 resolved.remove(&executor.plan[0].outputs[0]);
8262 resolved.remove(&executor.plan[0].inputs[0].expect("present input"));
8263 resolved.remove(&executor.plan[1].outputs[0]);
8264 resolved.remove(&executor.plan[1].inputs[0].expect("present input"));
8265 resolved.remove(&executor.plan[2].inputs[0].expect("present input"));
8266 executor.cache.entries.remove(&keys[3]);
8267 executor
8268 .cache
8269 .entries
8270 .insert(keys[4].clone(), Box::new(CaptureDecliningKernel));
8271
8272 let legacy = executor
8273 .plan
8274 .iter()
8275 .map(|plan| legacy_node_capture_reason(&executor, plan, &resolved))
8276 .collect::<Vec<_>>();
8277 let refactored = executor
8278 .plan
8279 .iter()
8280 .map(|plan| executor.node_capture_reason(plan, &resolved))
8281 .collect::<Vec<_>>();
8282
8283 assert_eq!(refactored, legacy);
8284 assert_eq!(
8285 refactored
8286 .iter()
8287 .map(|decline| decline.as_ref().and_then(|decline| decline.seam_reason))
8288 .collect::<Vec<_>>(),
8289 vec![
8290 Some(SeamReason::HostControlFlowOrSequence),
8291 Some(SeamReason::UnresolvedOutputShape),
8292 Some(SeamReason::UnresolvedInputShape),
8293 Some(SeamReason::KernelNotWarmed),
8294 Some(SeamReason::KernelCaptureUnsupported),
8295 None,
8296 ]
8297 );
8298 }
8299
8300 #[test]
8301 fn capture_shapes_seed_unresolved_external_values_without_overwriting_resolved_shapes() {
8302 let external_value = |shape| ExternalValue {
8303 dtype: DataType::Float32,
8304 shape,
8305 accepts_subshape: false,
8306 ptr: std::ptr::null_mut(),
8307 len: 0,
8308 alignment: 1,
8309 device: onnx_runtime_ir::DeviceId::cpu(),
8310 };
8311 let mut external = ExternalBindings::default();
8312 external
8313 .inputs
8314 .insert(ValueId(0), external_value(vec![1, 2]));
8315 external
8316 .outputs
8317 .insert(ValueId(1), external_value(vec![1, 4, 128, 64]));
8318 external
8319 .outputs
8320 .insert(ValueId(2), external_value(vec![1, 4, 128, 64]));
8321
8322 let mut resolved = HashMap::from([(ValueId(0), vec![1, 1])]);
8323 external.seed_capture_shapes(&mut resolved);
8324
8325 assert_eq!(resolved[&ValueId(0)], vec![1, 1]);
8326 assert_eq!(resolved[&ValueId(1)], vec![1, 4, 128, 64]);
8327 assert_eq!(resolved[&ValueId(2)], vec![1, 4, 128, 64]);
8328 }
8329
8330 #[test]
8331 fn only_gqa_cache_inputs_use_physical_capacity_as_kernel_geometry() {
8332 let mut gqa = Node::new(NodeId(0), "GroupQueryAttention", vec![], vec![]);
8333 gqa.domain = "com.microsoft".to_string();
8334 let attention = Node::new(NodeId(1), "Attention", vec![], vec![]);
8335
8336 assert!(kernel_input_uses_physical_capacity(&gqa, 3));
8337 assert!(kernel_input_uses_physical_capacity(&gqa, 4));
8338 assert!(!kernel_input_uses_physical_capacity(&gqa, 0));
8339 assert!(!kernel_input_uses_physical_capacity(&attention, 4));
8340 }
8341
8342 #[test]
8343 fn only_capacity_aware_inputs_keep_physical_capacity() {
8344 let shape = Node::new(NodeId(0), "Shape", vec![], vec![]);
8345 let reduce_sum = Node::new(NodeId(1), "ReduceSum", vec![], vec![]);
8346 let cumsum = Node::new(NodeId(2), "CumSum", vec![], vec![]);
8347 let unsqueeze = Node::new(NodeId(3), "Unsqueeze", vec![], vec![]);
8348
8349 assert!(kernel_input_uses_padded_capacity(&shape, 0));
8350 assert!(kernel_input_uses_padded_capacity(&reduce_sum, 0));
8351 assert!(!kernel_input_uses_padded_capacity(&cumsum, 0));
8352 assert!(!kernel_input_uses_padded_capacity(&unsqueeze, 0));
8353 assert!(!kernel_input_uses_padded_capacity(&shape, 1));
8354
8355 let indexer_add = Node::new(NodeId(4), "Add", vec![], vec![]);
8364 let indexer_cast = Node::new(NodeId(5), "Cast", vec![], vec![]);
8365 assert!(!kernel_input_uses_padded_capacity(&indexer_add, 0));
8366 assert!(!kernel_input_uses_padded_capacity(&indexer_cast, 0));
8367 }
8368
8369 struct WeightDeliveryKernel {
8370 deliveries: Arc<std::sync::Mutex<Vec<&'static str>>>,
8371 }
8372
8373 impl WeightDeliveryKernel {
8374 fn copy_bytes(bytes: &[u8], output: &mut TensorMut<'_>) -> onnx_runtime_ep_api::Result<()> {
8375 if bytes.len() != output.byte_size() {
8376 return Err(EpError::KernelFailed(
8377 "test output byte count mismatch".into(),
8378 ));
8379 }
8380 unsafe {
8383 std::ptr::copy_nonoverlapping(
8384 bytes.as_ptr(),
8385 output.data.0.cast::<u8>(),
8386 bytes.len(),
8387 );
8388 }
8389 Ok(())
8390 }
8391 }
8392
8393 impl Kernel for WeightDeliveryKernel {
8394 fn execute(
8395 &self,
8396 inputs: &[TensorView],
8397 outputs: &mut [TensorMut],
8398 ) -> onnx_runtime_ep_api::Result<()> {
8399 self.deliveries.lock().unwrap().push("resident");
8400 let bytes = unsafe {
8401 std::slice::from_raw_parts(inputs[0].data_ptr::<u8>(), inputs[0].byte_size())
8402 };
8403 Self::copy_bytes(bytes, &mut outputs[0])
8404 }
8405
8406 fn execute_with_inputs(
8407 &self,
8408 inputs: &[KernelInput<'_>],
8409 outputs: &mut [TensorMut],
8410 ) -> onnx_runtime_ep_api::Result<()> {
8411 match &inputs[0] {
8412 KernelInput::Tensor(view) => self.execute(std::slice::from_ref(view), outputs),
8413 KernelInput::Weight(handle) => {
8414 self.deliveries.lock().unwrap().push("lazy");
8415 let NegotiatedWeight::Lazy(lazy) =
8416 handle.negotiate(&ExecutionProviderCapabilities::nxrt_weight_paging())?
8417 else {
8418 return Err(EpError::KernelFailed(
8419 "nxrt test EP expected a lazy WeightHandle".into(),
8420 ));
8421 };
8422 let resident = lazy.materialize()?;
8423 Self::copy_bytes(resident.bytes(), &mut outputs[0])
8424 }
8425 }
8426 }
8427 }
8428
8429 struct WeightDeliveryEp {
8430 cpu: CpuExecutionProvider,
8431 lazy: bool,
8432 optional_input_contract: bool,
8433 deliveries: Arc<std::sync::Mutex<Vec<&'static str>>>,
8434 device: onnx_runtime_ir::DeviceId,
8435 allocations: Arc<AtomicUsize>,
8436 host_uploads: Arc<AtomicUsize>,
8437 }
8438
8439 impl WeightDeliveryEp {
8440 fn new(lazy: bool, deliveries: Arc<std::sync::Mutex<Vec<&'static str>>>) -> Self {
8441 Self::with_device(
8442 lazy,
8443 deliveries,
8444 onnx_runtime_ir::DeviceId::cpu(),
8445 Arc::new(AtomicUsize::new(0)),
8446 Arc::new(AtomicUsize::new(0)),
8447 )
8448 }
8449
8450 fn non_host(
8451 lazy: bool,
8452 deliveries: Arc<std::sync::Mutex<Vec<&'static str>>>,
8453 allocations: Arc<AtomicUsize>,
8454 host_uploads: Arc<AtomicUsize>,
8455 ) -> Self {
8456 Self::with_device(
8457 lazy,
8458 deliveries,
8459 onnx_runtime_ir::DeviceId::new(onnx_runtime_ir::DeviceType::Custom(7), 0),
8460 allocations,
8461 host_uploads,
8462 )
8463 }
8464
8465 fn with_device(
8466 lazy: bool,
8467 deliveries: Arc<std::sync::Mutex<Vec<&'static str>>>,
8468 device: onnx_runtime_ir::DeviceId,
8469 allocations: Arc<AtomicUsize>,
8470 host_uploads: Arc<AtomicUsize>,
8471 ) -> Self {
8472 let mut cpu = CpuExecutionProvider::new();
8473 cpu.initialize(&EpConfig::default()).unwrap();
8474 Self {
8475 cpu,
8476 lazy,
8477 optional_input_contract: false,
8478 deliveries,
8479 device,
8480 allocations,
8481 host_uploads,
8482 }
8483 }
8484
8485 fn copy_bytes(
8486 &self,
8487 src: *const u8,
8488 dst: *mut u8,
8489 size: usize,
8490 ) -> onnx_runtime_ep_api::Result<()> {
8491 if size != 0 {
8492 unsafe { std::ptr::copy_nonoverlapping(src, dst, size) };
8495 }
8496 Ok(())
8497 }
8498 }
8499
8500 impl ExecutionProvider for WeightDeliveryEp {
8501 fn name(&self) -> &str {
8502 if self.lazy {
8503 "nxrt_test_ep"
8504 } else {
8505 "stock_test_ep"
8506 }
8507 }
8508
8509 fn device_type(&self) -> onnx_runtime_ir::DeviceType {
8510 self.device.device_type
8511 }
8512
8513 fn device_id(&self) -> onnx_runtime_ir::DeviceId {
8514 self.device
8515 }
8516
8517 fn capabilities(&self) -> ExecutionProviderCapabilities {
8518 if self.lazy {
8519 ExecutionProviderCapabilities::nxrt_weight_paging()
8520 } else {
8521 ExecutionProviderCapabilities::stock()
8522 }
8523 }
8524
8525 fn initialize(&mut self, _config: &EpConfig) -> onnx_runtime_ep_api::Result<()> {
8526 Ok(())
8527 }
8528
8529 fn shutdown(&mut self) -> onnx_runtime_ep_api::Result<()> {
8530 Ok(())
8531 }
8532
8533 fn supports_op(
8534 &self,
8535 op: &Node,
8536 opset: u64,
8537 _shapes: &[Shape],
8538 input_dtypes: &[DataType],
8539 _layouts: &[TensorLayout],
8540 ) -> KernelMatch {
8541 if self.optional_input_contract && op.op_type == "OptionalContract" {
8542 if input_dtypes == [DataType::Float32, DataType::Undefined, DataType::Bool] {
8543 return KernelMatch::Supported {
8544 cost: Cost::ZERO,
8545 required_input_layouts: None,
8546 output_layouts: vec![TensorLayout::contiguous()],
8547 };
8548 }
8549 return KernelMatch::unsupported(format!(
8550 "OptionalContract requires [Float32, Undefined, Bool] input dtypes, got {input_dtypes:?}"
8551 ));
8552 }
8553 if LazyWeightBoundary::BlockQuantizedMoe.matches(&op.domain, &op.op_type)
8554 || (op.is_default_domain() && op.op_type == "Identity")
8555 {
8556 KernelMatch::Supported {
8557 cost: Cost::ZERO,
8558 required_input_layouts: None,
8559 output_layouts: vec![TensorLayout::contiguous()],
8560 }
8561 } else {
8562 KernelMatch::unsupported(format!(
8563 "no handler for {}::{} at opset {opset} — test EP intentionally declines this op",
8564 canonical_domain(op),
8565 op.op_type
8566 ))
8567 }
8568 }
8569
8570 fn get_kernel(
8571 &self,
8572 _op: &Node,
8573 _shapes: &[Vec<usize>],
8574 _opset: u64,
8575 ) -> onnx_runtime_ep_api::Result<Box<dyn Kernel>> {
8576 Ok(Box::new(WeightDeliveryKernel {
8577 deliveries: Arc::clone(&self.deliveries),
8578 }))
8579 }
8580
8581 fn allocate(
8582 &self,
8583 size: usize,
8584 alignment: usize,
8585 ) -> onnx_runtime_ep_api::Result<DeviceBuffer> {
8586 self.allocations.fetch_add(1, Ordering::Relaxed);
8587 if self.device.is_host_accessible() {
8588 return self.cpu.allocate(size, alignment);
8589 }
8590 let layout = std::alloc::Layout::from_size_align(size.max(1), alignment)
8591 .map_err(|_| EpError::AlignmentError)?;
8592 let ptr = unsafe { std::alloc::alloc(layout) };
8593 if ptr.is_null() {
8594 return Err(EpError::OutOfMemory {
8595 requested: size,
8596 available: 0,
8597 });
8598 }
8599 Ok(unsafe { DeviceBuffer::from_raw_parts(ptr.cast(), self.device, size, alignment) })
8600 }
8601
8602 fn deallocate(&self, buffer: DeviceBuffer) -> onnx_runtime_ep_api::Result<()> {
8603 if self.device.is_host_accessible() {
8604 return self.cpu.deallocate(buffer);
8605 }
8606 let size = buffer.len();
8607 let alignment = buffer.alignment();
8608 let ptr = buffer.into_raw().cast::<u8>();
8609 let layout = std::alloc::Layout::from_size_align(size.max(1), alignment)
8610 .expect("test EP allocated this layout");
8611 unsafe { std::alloc::dealloc(ptr, layout) };
8612 Ok(())
8613 }
8614
8615 fn copy(
8616 &self,
8617 src: &DeviceBuffer,
8618 dst: &mut DeviceBuffer,
8619 size: usize,
8620 ) -> onnx_runtime_ep_api::Result<()> {
8621 if size > src.len() || size > dst.len() {
8622 return Err(EpError::KernelFailed("test EP copy out of bounds".into()));
8623 }
8624 self.copy_bytes(src.as_ptr().cast(), dst.as_mut_ptr().cast(), size)
8625 }
8626
8627 fn copy_async(
8628 &self,
8629 src: &DeviceBuffer,
8630 dst: &mut DeviceBuffer,
8631 size: usize,
8632 ) -> onnx_runtime_ep_api::Result<Fence> {
8633 self.copy(src, dst, size)?;
8634 Ok(Fence::default())
8635 }
8636
8637 fn sync(&self) -> onnx_runtime_ep_api::Result<()> {
8638 Ok(())
8639 }
8640
8641 fn copy_from_host(
8642 &self,
8643 src: &[u8],
8644 dst: &mut DeviceBuffer,
8645 ) -> onnx_runtime_ep_api::Result<()> {
8646 if src.len() > dst.len() {
8647 return Err(EpError::KernelFailed(
8648 "test EP host upload out of bounds".into(),
8649 ));
8650 }
8651 self.host_uploads.fetch_add(1, Ordering::Relaxed);
8652 self.copy_bytes(src.as_ptr(), dst.as_mut_ptr().cast(), src.len())
8653 }
8654
8655 fn copy_to_host(
8656 &self,
8657 src: &DeviceBuffer,
8658 dst: &mut [u8],
8659 ) -> onnx_runtime_ep_api::Result<()> {
8660 if dst.len() > src.len() {
8661 return Err(EpError::KernelFailed(
8662 "test EP host download out of bounds".into(),
8663 ));
8664 }
8665 self.copy_bytes(src.as_ptr().cast(), dst.as_mut_ptr(), dst.len())
8666 }
8667 }
8668
8669 fn weight_delivery_fixture() -> (Graph, Arc<WeightStore>, std::path::PathBuf) {
8670 static NEXT_FILE: AtomicU64 = AtomicU64::new(0);
8671 let root = std::env::var_os("CARGO_TARGET_DIR")
8672 .map(std::path::PathBuf::from)
8673 .unwrap_or_else(|| std::env::current_dir().unwrap().join("target"))
8674 .join("weight-handle-tests");
8675 std::fs::create_dir_all(&root).unwrap();
8676 let id = NEXT_FILE.fetch_add(1, Ordering::Relaxed);
8677 let path = root.join(format!(
8678 "block-quantized-moe-{}-{id}.bin",
8679 std::process::id()
8680 ));
8681 std::fs::write(&path, [1u8, 2, 3, 4]).unwrap();
8682
8683 let mut graph = Graph::new();
8684 graph.opset_imports.insert("pkg.nxrt".into(), 1);
8685 let weight = graph.create_named_value("weight", DataType::Uint8, static_shape([4]));
8686 graph.set_initializer(
8687 weight,
8688 WeightRef::External {
8689 path: path.clone(),
8690 offset: 0,
8691 length: 4,
8692 dtype: DataType::Uint8,
8693 dims: vec![4],
8694 },
8695 );
8696 let output = graph.create_named_value("output", DataType::Uint8, static_shape([4]));
8697 let mut node = Node::new(
8698 NodeId(0),
8699 "BlockQuantizedMoE",
8700 vec![Some(weight)],
8701 vec![output],
8702 );
8703 node.domain = "pkg.nxrt".into();
8704 graph.insert_node(node);
8705 graph.add_output(output);
8706
8707 let mut store = WeightStore::new();
8708 store.map_external(&path).unwrap();
8709 (graph, Arc::new(store), path)
8710 }
8711
8712 #[test]
8713 fn claim_time_optional_input_dtype_is_undefined_not_silently_float32() {
8714 let mut graph = Graph::new();
8715 graph.opset_imports.insert(String::new(), 1);
8716 let data = graph.create_named_value("data", DataType::Float32, static_shape([1]));
8717 let training_mode =
8718 graph.create_named_value("training_mode", DataType::Bool, static_shape([]));
8719 let output = graph.create_named_value("output", DataType::Float32, static_shape([1]));
8720 graph.add_input(data);
8721 graph.add_input(training_mode);
8722 graph.add_output(output);
8723 graph.insert_node(Node::new(
8724 NodeId(0),
8725 "OptionalContract",
8726 vec![Some(data), None, Some(training_mode)],
8727 vec![output],
8728 ));
8729
8730 let deliveries = Arc::new(std::sync::Mutex::new(Vec::new()));
8731 let mut ep = WeightDeliveryEp::new(false, deliveries);
8732 ep.optional_input_contract = true;
8733 let executor = Executor::build(graph, Arc::new(WeightStore::new()), Arc::new(ep));
8734 assert!(
8735 executor.is_ok(),
8736 "an omitted optional input must reach supports_op as DataType::Undefined"
8737 );
8738 }
8739
8740 #[test]
8741 fn executor_opens_per_op_span_only_when_tracing_enabled() {
8742 use onnx_runtime_tracer::TraceContext;
8743
8744 {
8746 let (graph, weights, path) = weight_delivery_fixture();
8747 let deliveries = Arc::new(std::sync::Mutex::new(Vec::new()));
8748 let ep: Arc<dyn ExecutionProvider> =
8749 Arc::new(WeightDeliveryEp::new(false, Arc::clone(&deliveries)));
8750 let mut executor = Executor::build(graph, weights, ep).unwrap();
8751 let (trace, events) = TraceContext::in_memory();
8752 trace.set_enabled(false);
8753 executor.set_trace_context(trace);
8754 let _ = executor.run(&[]).unwrap();
8755 drop(executor);
8756 std::fs::remove_file(path).unwrap();
8757 assert!(
8758 events.events().is_empty(),
8759 "a disabled trace context must not open op spans"
8760 );
8761 }
8762
8763 {
8765 let (graph, weights, path) = weight_delivery_fixture();
8766 let deliveries = Arc::new(std::sync::Mutex::new(Vec::new()));
8767 let ep: Arc<dyn ExecutionProvider> =
8768 Arc::new(WeightDeliveryEp::new(false, Arc::clone(&deliveries)));
8769 let mut executor = Executor::build(graph, weights, ep).unwrap();
8770 let (trace, events) = TraceContext::in_memory();
8771 executor.set_trace_context(trace);
8772 let _ = executor.run(&[]).unwrap();
8773 drop(executor);
8774 std::fs::remove_file(path).unwrap();
8775 let spans = events.events();
8776 assert_eq!(spans.len(), 1, "one op span per executed node");
8777 assert_eq!(spans[0].name, "BlockQuantizedMoE");
8778 assert_eq!(spans[0].cat, "op");
8779 }
8780 }
8781
8782 #[test]
8783 fn op_capture_trace_annotates_span_with_status_and_reason() {
8784 use onnx_runtime_tracer::TraceContext;
8785
8786 {
8791 let (trace, events) = TraceContext::in_memory();
8792 {
8793 let _span = trace.span("MatMulNBits", "op");
8794 OpCaptureTrace::Rejected(
8795 "kernel declares CaptureSupport::Unsupported: per-call workspace alloc",
8796 )
8797 .annotate();
8798 }
8799 let recorded = events.events();
8800 assert_eq!(recorded.len(), 1);
8801 let args = recorded[0].args.as_ref().unwrap();
8802 assert_eq!(args[ARG_CAPTURE_STATUS], "rejected");
8803 assert!(
8804 args[ARG_CAPTURE_REASON]
8805 .as_str()
8806 .unwrap()
8807 .contains("CaptureSupport::Unsupported")
8808 );
8809 }
8810
8811 {
8813 let (trace, events) = TraceContext::in_memory();
8814 {
8815 let _span = trace.span("MatMulNBits", "op");
8816 OpCaptureTrace::Captured.annotate();
8817 }
8818 let recorded = events.events();
8819 let args = recorded[0].args.as_ref().unwrap();
8820 assert_eq!(args[ARG_CAPTURE_STATUS], "captured");
8821 }
8822
8823 {
8825 let (trace, events) = TraceContext::in_memory();
8826 {
8827 let _span = trace.span("MatMulNBits", "op");
8828 OpCaptureTrace::Eager.annotate();
8829 }
8830 let recorded = events.events();
8831 assert!(
8832 recorded[0]
8833 .args
8834 .as_ref()
8835 .map(|a| a.get(ARG_CAPTURE_STATUS).is_none())
8836 .unwrap_or(true),
8837 "eager ops carry no capture status"
8838 );
8839 }
8840 }
8841
8842 #[test]
8843 fn executor_selects_lazy_or_resident_weight_delivery_from_ep_capability() {
8844 for (lazy, expected) in [(true, "lazy"), (false, "resident")] {
8845 let (graph, weights, path) = weight_delivery_fixture();
8846 let deliveries = Arc::new(std::sync::Mutex::new(Vec::new()));
8847 let ep: Arc<dyn ExecutionProvider> =
8848 Arc::new(WeightDeliveryEp::new(lazy, Arc::clone(&deliveries)));
8849 let mut executor = Executor::build(graph, weights, ep).unwrap();
8850 let outputs = executor.run(&[]).unwrap();
8851
8852 assert_eq!(outputs[0].as_bytes(), &[1, 2, 3, 4]);
8853 assert_eq!(&*deliveries.lock().unwrap(), &[expected]);
8854 drop(executor);
8855 std::fs::remove_file(path).unwrap();
8856 }
8857 }
8858
8859 #[test]
8860 fn non_host_lazy_only_initializer_skips_eager_device_residency() {
8861 for (lazy, expected_allocations, expected_uploads, expected_delivery) in
8862 [(true, 1, 0, "lazy"), (false, 2, 1, "resident")]
8863 {
8864 let (graph, weights, path) = weight_delivery_fixture();
8865 let deliveries = Arc::new(std::sync::Mutex::new(Vec::new()));
8866 let allocations = Arc::new(AtomicUsize::new(0));
8867 let host_uploads = Arc::new(AtomicUsize::new(0));
8868 let ep: Arc<dyn ExecutionProvider> = Arc::new(WeightDeliveryEp::non_host(
8869 lazy,
8870 Arc::clone(&deliveries),
8871 Arc::clone(&allocations),
8872 Arc::clone(&host_uploads),
8873 ));
8874 let mut executor = Executor::build(graph, weights, ep).unwrap();
8875
8876 assert_eq!(
8877 allocations.load(Ordering::Relaxed),
8878 expected_allocations,
8879 "lazy nxrt builds only the output; stock EPs also allocate the initializer"
8880 );
8881 assert_eq!(
8882 host_uploads.load(Ordering::Relaxed),
8883 expected_uploads,
8884 "lazy nxrt must not upload the initializer during build"
8885 );
8886
8887 let outputs = executor.run(&[]).unwrap();
8888 assert_eq!(outputs[0].as_bytes(), &[1, 2, 3, 4]);
8889 assert_eq!(&*deliveries.lock().unwrap(), &[expected_delivery]);
8890 assert_eq!(
8891 host_uploads.load(Ordering::Relaxed),
8892 expected_uploads,
8893 "dispatch must not introduce a second EP upload"
8894 );
8895 drop(executor);
8896 std::fs::remove_file(path).unwrap();
8897 }
8898 }
8899
8900 #[test]
8901 fn initializer_shared_with_resident_consumer_uses_one_device_copy() {
8902 let (mut graph, weights, path) = weight_delivery_fixture();
8903 graph.opset_imports.insert(String::new(), 17);
8904 let weight = graph
8905 .values
8906 .iter()
8907 .find_map(|(vid, value)| (value.name.as_deref() == Some("weight")).then_some(vid))
8908 .unwrap();
8909 let resident_output =
8910 graph.create_named_value("resident_output", DataType::Uint8, static_shape([4]));
8911 graph.insert_node(Node::new(
8912 NodeId(1),
8913 "Identity",
8914 vec![Some(weight)],
8915 vec![resident_output],
8916 ));
8917 graph.add_output(resident_output);
8918
8919 let deliveries = Arc::new(std::sync::Mutex::new(Vec::new()));
8920 let allocations = Arc::new(AtomicUsize::new(0));
8921 let host_uploads = Arc::new(AtomicUsize::new(0));
8922 let ep: Arc<dyn ExecutionProvider> = Arc::new(WeightDeliveryEp::non_host(
8923 true,
8924 Arc::clone(&deliveries),
8925 Arc::clone(&allocations),
8926 Arc::clone(&host_uploads),
8927 ));
8928 let mut executor = Executor::build(graph, weights, ep).unwrap();
8929
8930 assert!(
8931 !executor.weight_handles.contains_key(&weight),
8932 "a resident consumer makes the single eager device copy authoritative"
8933 );
8934 assert_eq!(allocations.load(Ordering::Relaxed), 3);
8935 assert_eq!(host_uploads.load(Ordering::Relaxed), 1);
8936
8937 let outputs = executor.run(&[]).unwrap();
8938 assert_eq!(outputs[0].as_bytes(), &[1, 2, 3, 4]);
8939 assert_eq!(outputs[1].as_bytes(), &[1, 2, 3, 4]);
8940 assert_eq!(&*deliveries.lock().unwrap(), &["resident", "resident"]);
8941 assert_eq!(
8942 host_uploads.load(Ordering::Relaxed),
8943 1,
8944 "both consumers must share the one resident initializer"
8945 );
8946 drop(executor);
8947 std::fs::remove_file(path).unwrap();
8948 }
8949
8950 #[test]
8951 fn coverage_collector_surfaces_ep_decline_reason() {
8952 let mut graph = Graph::new();
8953 graph.opset_imports.insert(String::new(), 17);
8954 let input = graph.create_named_value("x", DataType::Float32, vec![Dim::Static(1)]);
8955 let output = graph.create_named_value("y", DataType::Float32, vec![Dim::Static(1)]);
8956 graph.insert_node(Node::new(
8957 NodeId(0),
8958 "NotRegistered",
8959 vec![Some(input)],
8960 vec![output],
8961 ));
8962
8963 let ep = CpuExecutionProvider::new();
8964 let mut issues = Vec::new();
8965 collect_cuda_coverage_issues(&graph, &graph, &ep, "graph", &mut issues);
8966
8967 assert_eq!(issues.len(), 1);
8968 assert_eq!(issues[0].op_type, "NotRegistered");
8969 assert_eq!(issues[0].domain, "ai.onnx");
8970 assert!(
8971 issues[0]
8972 .reason
8973 .contains("no handler for ai.onnx::NotRegistered at opset 17"),
8974 "{}",
8975 issues[0].reason
8976 );
8977 assert!(
8978 !issues[0].reason.contains("unsupported by"),
8979 "{}",
8980 issues[0].reason
8981 );
8982 }
8983
8984 #[test]
8985 fn cuda_coverage_report_groups_all_distinct_failure_classes_deterministically() {
8986 let mut graph = Graph::new();
8987 graph.opset_imports.insert(String::new(), 17);
8988 let input = graph.create_named_value("x", DataType::Float32, vec![Dim::Static(1)]);
8989
8990 let op_types = [
8991 "RepeatedMissing",
8992 "Missing08",
8993 "RepeatedMissing",
8994 "Missing07",
8995 "Missing06",
8996 "RepeatedMissing",
8997 "Missing05",
8998 "Missing04",
8999 "Missing03",
9000 "Missing02",
9001 "Missing01",
9002 "Missing00",
9003 "RepeatedMissing",
9004 ];
9005 for (index, op_type) in op_types.into_iter().enumerate() {
9006 let output = graph.create_named_value(
9007 format!("output_{index}"),
9008 DataType::Float32,
9009 vec![Dim::Static(1)],
9010 );
9011 graph.insert_node(Node::new(
9012 NodeId(index as u32),
9013 op_type,
9014 vec![Some(input)],
9015 vec![output],
9016 ));
9017 }
9018
9019 let ep = WeightDeliveryEp::with_device(
9020 false,
9021 Arc::new(std::sync::Mutex::new(Vec::new())),
9022 onnx_runtime_ir::DeviceId::cuda(0),
9023 Arc::new(AtomicUsize::new(0)),
9024 Arc::new(AtomicUsize::new(0)),
9025 );
9026 let report = || {
9027 cuda_fallback_report(&graph, &ep)
9028 .expect("CUDA declines must produce a fallback report")
9029 .to_string()
9030 };
9031 let first = report();
9032 let second = report();
9033
9034 assert_eq!(first, second);
9035 assert!(first.contains("13 nodes assigned to CPU"));
9036 assert!(first.contains("GPU EP stock_test_ep did not claim 13 node(s)"));
9037 assert!(first.contains("the whole session uses cpu_ep"));
9038 assert_eq!(first.matches("ai.onnx::RepeatedMissing:").count(), 1);
9039 assert!(first.contains("ai.onnx::RepeatedMissing: no handler"));
9040 assert!(first.contains("[count=4; examples: graph/node#0, graph/node#12, graph/node#2]"));
9041 assert!(!first.contains("graph/node#5"));
9042
9043 for op_type in [
9044 "Missing00",
9045 "Missing01",
9046 "Missing02",
9047 "Missing03",
9048 "Missing04",
9049 "Missing05",
9050 "Missing06",
9051 "Missing07",
9052 "Missing08",
9053 ] {
9054 assert_eq!(
9055 first.matches(&format!("ai.onnx::{op_type}:")).count(),
9056 1,
9057 "{first}"
9058 );
9059 assert!(
9060 first.contains(&format!("ai.onnx::{op_type}: no handler")),
9061 "{first}"
9062 );
9063 }
9064 assert!(!first.contains("more unsupported node"));
9065 }
9066
9067 #[test]
9068 fn cuda_decline_warns_and_falls_back_to_cpu_unless_strict() {
9069 let graph = || {
9070 let mut graph = Graph::new();
9071 graph.opset_imports.insert(String::new(), 17);
9072 let input = graph.create_named_value("input", DataType::Float32, vec![Dim::Static(1)]);
9073 let output =
9074 graph.create_named_value("output", DataType::Float32, vec![Dim::Static(1)]);
9075 graph.add_input(input);
9076 graph.add_output(output);
9077 graph.insert_node(Node::new(
9078 NodeId(0),
9079 "Relu",
9080 vec![Some(input)],
9081 vec![output],
9082 ));
9083 graph
9084 };
9085 let cuda_ep = || {
9086 Arc::new(WeightDeliveryEp::with_device(
9087 false,
9088 Arc::new(std::sync::Mutex::new(Vec::new())),
9089 onnx_runtime_ir::DeviceId::cuda(0),
9090 Arc::new(AtomicUsize::new(0)),
9091 Arc::new(AtomicUsize::new(0)),
9092 )) as Arc<dyn ExecutionProvider>
9093 };
9094
9095 let exec = Executor::build_with_cuda_requirement(
9096 graph(),
9097 Arc::new(WeightStore::new()),
9098 cuda_ep(),
9099 false,
9100 )
9101 .expect("default CUDA decline must use the CPU fallback");
9102 assert_eq!(exec.device_id().device_type, DeviceType::Cpu);
9103 let report = exec
9104 .execution_provider_fallback_report()
9105 .expect("fallback must remain observable");
9106 assert_eq!(report.assigned_node_count, 1);
9107 assert_eq!(report.assigned_ops, ["ai.onnx::Relu"]);
9108 assert_eq!(report.declines.len(), 1);
9109 assert_eq!(report.declines[0].op_type, "Relu");
9110 assert!(report.declines[0].reason.contains("intentionally declines"));
9111
9112 let strict = Executor::build_with_cuda_requirement(
9113 graph(),
9114 Arc::new(WeightStore::new()),
9115 cuda_ep(),
9116 true,
9117 )
9118 .err()
9119 .expect("strict CUDA must reject CPU fallback");
9120 assert!(strict.to_string().contains("ONNX_GENAI_REQUIRE_CUDA=1"));
9121 }
9122
9123 #[test]
9124 fn sequence_executor_preserves_element_arc_identity() {
9125 use onnx_runtime_ir::{TensorData, WeightRef, static_shape};
9126
9127 let mut graph = Graph::new();
9128 graph.opset_imports.insert(String::new(), 17);
9129
9130 let input = graph.create_named_value("input", DataType::Float32, static_shape([2]));
9131 graph.set_initializer(
9132 input,
9133 WeightRef::Inline(TensorData::from_raw(
9134 DataType::Float32,
9135 vec![2],
9136 [7.0f32, 8.0]
9137 .into_iter()
9138 .flat_map(f32::to_le_bytes)
9139 .collect(),
9140 )),
9141 );
9142 let zero = graph.create_named_value("zero", DataType::Int64, static_shape([]));
9143 graph.set_initializer(
9144 zero,
9145 WeightRef::Inline(TensorData::from_raw(
9146 DataType::Int64,
9147 vec![],
9148 0i64.to_le_bytes().to_vec(),
9149 )),
9150 );
9151 let one = graph.create_named_value("one", DataType::Int64, static_shape([]));
9152 graph.set_initializer(
9153 one,
9154 WeightRef::Inline(TensorData::from_raw(
9155 DataType::Int64,
9156 vec![],
9157 1i64.to_le_bytes().to_vec(),
9158 )),
9159 );
9160
9161 let first_sequence = graph.create_value(DataType::Float32, static_shape([]));
9162 graph.insert_node(Node::new(
9163 NodeId(0),
9164 "SequenceConstruct",
9165 vec![Some(input)],
9166 vec![first_sequence],
9167 ));
9168 let first_at = graph.create_value(DataType::Float32, static_shape([2]));
9169 graph.insert_node(Node::new(
9170 NodeId(0),
9171 "SequenceAt",
9172 vec![Some(first_sequence), Some(zero)],
9173 vec![first_at],
9174 ));
9175 let inserted_sequence = graph.create_value(DataType::Float32, static_shape([]));
9176 graph.insert_node(Node::new(
9177 NodeId(0),
9178 "SequenceInsert",
9179 vec![Some(first_sequence), Some(first_at)],
9180 vec![inserted_sequence],
9181 ));
9182 let second_at = graph.create_value(DataType::Float32, static_shape([2]));
9183 graph.insert_node(Node::new(
9184 NodeId(0),
9185 "SequenceAt",
9186 vec![Some(inserted_sequence), Some(one)],
9187 vec![second_at],
9188 ));
9189 graph.add_output(second_at);
9190
9191 let mut executor = Executor::build(
9192 graph,
9193 Arc::new(WeightStore::new()),
9194 auto_detect_cpu_ep().unwrap(),
9195 )
9196 .unwrap();
9197 let output = executor.run(&[]).unwrap();
9198 assert_eq!(output[0].to_vec_f32(), vec![7.0, 8.0]);
9199
9200 let original = &executor.sequences[&first_sequence].elements()[0];
9201 let first_at_arc = &executor.seq_elem_values[&first_at];
9202 let inserted = &executor.sequences[&inserted_sequence].elements()[1];
9203 let second_at_arc = &executor.seq_elem_values[&second_at];
9204 assert!(original.shares_storage_with(first_at_arc));
9205 assert!(original.shares_storage_with(inserted));
9206 assert!(original.shares_storage_with(second_at_arc));
9207 assert_eq!(original.as_ptr(), executor.buffers[&input].as_ptr());
9208 }
9209
9210 #[test]
9211 fn fuses_only_single_consumer_silu_pattern() {
9212 let mut graph = Graph::new();
9213 let shape = vec![Dim::Static(2)];
9214 let x = graph.create_named_value("x", DataType::Float32, shape.clone());
9215 let sigmoid_out = graph.create_named_value("sigmoid", DataType::Float32, shape.clone());
9216 let silu_out = graph.create_named_value("silu", DataType::Float32, shape);
9217 graph.add_input(x);
9218 graph.add_output(silu_out);
9219 graph.insert_node(Node::new(
9220 NodeId(0),
9221 "Sigmoid",
9222 vec![Some(x)],
9223 vec![sigmoid_out],
9224 ));
9225 graph.insert_node(Node::new(
9226 NodeId(0),
9227 "Mul",
9228 vec![Some(sigmoid_out), Some(x)],
9229 vec![silu_out],
9230 ));
9231
9232 assert_eq!(fuse_silu_patterns(&mut graph), 1);
9233 assert_eq!(graph.num_nodes(), 1);
9234 let fused = graph.nodes.values().next().unwrap();
9235 assert_eq!(fused.op_type, "Silu");
9236 assert_eq!(fused.domain, "com.microsoft");
9237 assert_eq!(fused.inputs, vec![Some(x)]);
9238 assert_eq!(fused.outputs, vec![silu_out]);
9239 assert_eq!(graph.opset_imports["com.microsoft"], 1);
9240 }
9241
9242 #[test]
9243 fn does_not_fuse_silu_when_sigmoid_has_second_consumer() {
9244 let mut graph = Graph::new();
9245 let shape = vec![Dim::Static(2)];
9246 let x = graph.create_named_value("x", DataType::Float32, shape.clone());
9247 let sigmoid_out = graph.create_named_value("sigmoid", DataType::Float32, shape.clone());
9248 let mul_out = graph.create_named_value("mul", DataType::Float32, shape.clone());
9249 let identity_out = graph.create_named_value("identity", DataType::Float32, shape);
9250 graph.add_input(x);
9251 graph.add_output(mul_out);
9252 graph.add_output(identity_out);
9253 graph.insert_node(Node::new(
9254 NodeId(0),
9255 "Sigmoid",
9256 vec![Some(x)],
9257 vec![sigmoid_out],
9258 ));
9259 graph.insert_node(Node::new(
9260 NodeId(0),
9261 "Mul",
9262 vec![Some(x), Some(sigmoid_out)],
9263 vec![mul_out],
9264 ));
9265 graph.insert_node(Node::new(
9266 NodeId(0),
9267 "Identity",
9268 vec![Some(sigmoid_out)],
9269 vec![identity_out],
9270 ));
9271
9272 assert_eq!(fuse_silu_patterns(&mut graph), 0);
9273 assert_eq!(graph.num_nodes(), 3);
9274 assert_eq!(
9275 graph
9276 .nodes
9277 .values()
9278 .filter(|node| node.op_type == "Sigmoid")
9279 .count(),
9280 1
9281 );
9282 assert_eq!(
9283 graph
9284 .nodes
9285 .values()
9286 .filter(|node| node.op_type == "Mul")
9287 .count(),
9288 1
9289 );
9290 assert!(graph.validate().is_ok());
9291 }
9292
9293 #[test]
9297 fn view_bounds_rejects_out_of_bounds_view() {
9298 let shape = [2usize, 3];
9300 let strides = compute_contiguous_strides(&shape);
9301 let err = view_bounds(&shape, &strides, 0, DataType::Float32, 16);
9302 assert!(err.is_err(), "gate must reject an oversized view");
9303
9304 assert!(view_bounds(&shape, &strides, 0, DataType::Float32, 24).is_ok());
9306 }
9307
9308 #[test]
9311 fn view_bounds_rejects_offset_overrun() {
9312 let shape = [4usize];
9313 let strides = compute_contiguous_strides(&shape);
9314 assert!(view_bounds(&shape, &strides, 8, DataType::Float32, 16).is_err());
9316 assert!(view_bounds(&shape, &strides, 0, DataType::Float32, 16).is_ok());
9317 }
9318
9319 #[test]
9322 fn substitute_resolves_bound_symbols_only() {
9323 let mut bindings = HashMap::new();
9324 bindings.insert(SymbolId(0), 7usize);
9325 let shape = vec![Dim::Symbolic(SymbolId(0)), Dim::Static(4)];
9326 assert_eq!(substitute(&shape, &bindings), Some(vec![7, 4]));
9327
9328 let unbound = vec![Dim::Symbolic(SymbolId(1)), Dim::Static(4)];
9329 assert_eq!(substitute(&unbound, &bindings), None);
9330 }
9331
9332 #[test]
9336 fn checked_numel_detects_overflow() {
9337 assert_eq!(checked_numel(&[2, 3, 4], || "v".into()).unwrap(), 24);
9339 assert_eq!(checked_numel(&[], || "v".into()).unwrap(), 1);
9340
9341 let huge = [usize::MAX, 2];
9343 let err = checked_numel(&huge, || "value#9".into());
9344 assert!(matches!(err, Err(SessionError::ShapeOverflow { .. })));
9345 }
9346
9347 #[test]
9351 fn checked_storage_bytes_detects_byte_overflow() {
9352 let numel = usize::MAX / 4;
9355 let err = checked_storage_bytes(DataType::Float64, numel, || "value#9".into(), &[numel]);
9356 assert!(matches!(err, Err(SessionError::ShapeOverflow { .. })));
9357
9358 assert_eq!(
9360 checked_storage_bytes(DataType::Float32, 4, || "v".into(), &[4]).unwrap(),
9361 16
9362 );
9363 }
9364
9365 #[test]
9370 fn dynamic_output_shapes_slice_is_single_output() {
9371 let node = Node::new(NodeId(0), "Slice", vec![], vec![]);
9372 let input_shapes = vec![vec![4usize, 2]];
9373 let input_values = vec![
9374 None, Some(vec![1]), Some(vec![3]), Some(vec![0]), Some(vec![1]), ];
9380 let input_dtypes = vec![
9381 DataType::Float32,
9382 DataType::Int64,
9383 DataType::Int64,
9384 DataType::Int64,
9385 DataType::Int64,
9386 ];
9387 let out =
9388 dynamic_output_shapes(&node, &input_shapes, &input_dtypes, &input_values, &[], 17)
9389 .unwrap();
9390 assert_eq!(out.len(), 1, "Slice must resolve exactly one output shape");
9391 assert_eq!(out[0], vec![2, 2]);
9392
9393 let mut custom_slice = Node::new(NodeId(1), "Slice", vec![], vec![ValueId(0)]);
9394 custom_slice.domain = "example.custom".into();
9395 assert!(
9396 dynamic_output_shapes(
9397 &custom_slice,
9398 &input_shapes,
9399 &input_dtypes,
9400 &input_values,
9401 &[],
9402 17
9403 )
9404 .is_none(),
9405 "ONNX Slice semantics must not be applied to an unrelated custom-domain op"
9406 );
9407
9408 let other = Node::new(
9410 NodeId(2),
9411 "NxrtNeverRegisteredSentinelOp",
9412 vec![],
9413 vec![ValueId(0)],
9414 );
9415 assert!(
9416 dynamic_output_shapes(&other, &input_shapes, &input_dtypes, &input_values, &[], 17)
9417 .is_none()
9418 );
9419 }
9420
9421 #[test]
9422 fn dynamic_output_shapes_unsqueeze_supports_input_and_attribute_axes() {
9423 use onnx_runtime_ir::Attribute;
9424
9425 let input_axes = Node::new(
9426 NodeId(0),
9427 "Unsqueeze",
9428 vec![Some(ValueId(0)), Some(ValueId(1))],
9429 vec![ValueId(2)],
9430 );
9431 assert_eq!(
9432 dynamic_output_shapes(
9433 &input_axes,
9434 &[vec![2, 3], vec![2]],
9435 &[DataType::Float32, DataType::Int64],
9436 &[None, Some(vec![0, -1])],
9437 &[],
9438 17,
9439 ),
9440 Some(vec![vec![1, 2, 3, 1]])
9441 );
9442
9443 let mut attribute_axes = Node::new(
9444 NodeId(1),
9445 "Unsqueeze",
9446 vec![Some(ValueId(0))],
9447 vec![ValueId(1)],
9448 );
9449 attribute_axes
9450 .attributes
9451 .insert("axes".into(), Attribute::Ints(vec![1, -1]));
9452 assert_eq!(
9453 dynamic_output_shapes(
9454 &attribute_axes,
9455 &[vec![2, 3]],
9456 &[DataType::Float32],
9457 &[None],
9458 &[],
9459 11,
9460 ),
9461 Some(vec![vec![2, 1, 3, 1]])
9462 );
9463 }
9464
9465 #[test]
9466 fn dynamic_output_shapes_resize_reads_runtime_scales() {
9467 let node = Node::new(
9468 NodeId(0),
9469 "Resize",
9470 vec![Some(ValueId(0)), Some(ValueId(1)), Some(ValueId(2))],
9471 vec![ValueId(3)],
9472 );
9473 assert_eq!(
9474 dynamic_output_shapes(
9475 &node,
9476 &[vec![1, 128, 13, 13], vec![8], vec![4]],
9477 &[DataType::Float32, DataType::Float32, DataType::Float32],
9478 &[None, None, None],
9479 &[None, None, Some(vec![1.0, 1.0, 2.0, 2.0])],
9480 11,
9481 ),
9482 Some(vec![vec![1, 128, 26, 26]])
9483 );
9484 }
9485
9486 #[test]
9487 fn dynamic_output_shapes_non_max_suppression_counts_selected_boxes() {
9488 let node = Node::new(
9489 NodeId(0),
9490 "NonMaxSuppression",
9491 (0..5).map(|index| Some(ValueId(index))).collect(),
9492 vec![ValueId(5)],
9493 );
9494 let shapes = vec![vec![1, 3, 4], vec![1, 1, 3], vec![], vec![], vec![]];
9495 let dtypes = vec![
9496 DataType::Float32,
9497 DataType::Float32,
9498 DataType::Int64,
9499 DataType::Float32,
9500 DataType::Float32,
9501 ];
9502 let ints = vec![None, None, Some(vec![2]), None, None];
9503 let floats = vec![
9504 Some(vec![0., 0., 1., 1., 0., 0., 0.9, 0.9, 2., 2., 3., 3.]),
9505 Some(vec![0.9, 0.8, 0.7]),
9506 None,
9507 Some(vec![0.5]),
9508 Some(vec![0.0]),
9509 ];
9510 assert_eq!(
9511 dynamic_output_shapes(&node, &shapes, &dtypes, &ints, &floats, 11),
9512 Some(vec![vec![2, 3]])
9513 );
9514 }
9515
9516 #[test]
9517 fn dynamic_output_shapes_gqa_supports_packed_qkv() {
9518 use onnx_runtime_ir::{Attribute, ValueId};
9519
9520 let mut node = Node::new(
9521 NodeId(0),
9522 "GroupQueryAttention",
9523 vec![
9524 Some(ValueId(0)),
9525 None,
9526 None,
9527 Some(ValueId(3)),
9528 Some(ValueId(4)),
9529 Some(ValueId(5)),
9530 Some(ValueId(6)),
9531 ],
9532 vec![ValueId(7), ValueId(8), ValueId(9)],
9533 );
9534 node.domain = "com.microsoft".into();
9535 node.attributes
9536 .insert("num_heads".into(), Attribute::Int(14));
9537 node.attributes
9538 .insert("kv_num_heads".into(), Attribute::Int(2));
9539 let input_shapes = vec![
9540 vec![1, 1, 1152],
9541 vec![],
9542 vec![],
9543 vec![1, 2, 16, 64],
9544 vec![1, 2, 16, 64],
9545 vec![1],
9546 vec![],
9547 ];
9548 let input_values = vec![None, None, None, None, None, None, Some(vec![17])];
9549
9550 assert_eq!(
9551 dynamic_output_shapes(
9552 &node,
9553 &input_shapes,
9554 &[
9555 DataType::Float32,
9556 DataType::Undefined,
9557 DataType::Undefined,
9558 DataType::Float32,
9559 DataType::Float32,
9560 DataType::Int32,
9561 DataType::Int32,
9562 ],
9563 &input_values,
9564 &[],
9565 1,
9566 ),
9567 Some(vec![
9568 vec![1, 1, 896],
9569 vec![1, 2, 17, 64],
9570 vec![1, 2, 17, 64],
9571 ])
9572 );
9573 }
9574
9575 #[test]
9578 fn effective_opset_reads_graph_import() {
9579 let mut graph = Graph::default();
9580 graph.opset_imports.insert(String::new(), 12);
9581 let node = Node::new(NodeId(0), "Softmax", vec![], vec![]);
9582 assert_eq!(effective_opset(&graph, &node), 12);
9583
9584 graph.opset_imports.insert(String::new(), 0);
9585 assert_eq!(effective_opset(&graph, &node), 0);
9586 }
9587
9588 #[test]
9589 #[should_panic(expected = "internal invariant violated")]
9590 fn effective_opset_requires_validated_import() {
9591 effective_opset(
9592 &Graph::default(),
9593 &Node::new(NodeId(0), "Softmax", vec![], vec![]),
9594 );
9595 }
9596
9597 #[test]
9598 fn child_executor_binds_formals_captures_and_inline_initializers_in_output_order() {
9599 use onnx_runtime_ir::{TensorData, WeightRef, static_shape};
9600
9601 let mut body = Graph::new();
9602 let formal = body.create_named_value("formal", DataType::Float32, static_shape([2]));
9603 body.add_input(formal);
9604 let captured = body.create_named_value("captured", DataType::Float32, static_shape([2]));
9605 let one = body.create_named_value("one", DataType::Float32, static_shape([2]));
9606 body.set_initializer(
9607 one,
9608 WeightRef::Inline(TensorData::from_raw(
9609 DataType::Float32,
9610 vec![2],
9611 [1.0f32, 1.0]
9612 .into_iter()
9613 .flat_map(f32::to_le_bytes)
9614 .collect(),
9615 )),
9616 );
9617 let sum = body.create_named_value("sum", DataType::Float32, static_shape([2]));
9618 body.insert_node(Node::new(
9619 NodeId(0),
9620 "Add",
9621 vec![Some(formal), Some(captured)],
9622 vec![sum],
9623 ));
9624 let adjusted = body.create_named_value("adjusted", DataType::Float32, static_shape([2]));
9625 body.insert_node(Node::new(
9626 NodeId(0),
9627 "Add",
9628 vec![Some(sum), Some(one)],
9629 vec![adjusted],
9630 ));
9631 body.add_output(adjusted);
9633 body.add_output(sum);
9634
9635 let mut opsets = HashMap::new();
9636 opsets.insert(String::new(), 17);
9637 let mut child = ChildExecutor::new(
9638 "direct-test",
9639 body,
9640 opsets,
9641 Arc::new(WeightStore::new()),
9642 auto_detect_cpu_ep().unwrap(),
9643 )
9644 .unwrap();
9645 let mut outer_scope = HashMap::new();
9646 outer_scope.insert(
9647 "captured".to_string(),
9648 Tensor::from_f32(&[2], &[10.0, 20.0]).unwrap(),
9649 );
9650
9651 let first = Tensor::from_f32(&[2], &[2.0, 3.0]).unwrap();
9652 let outputs = child.run(&[&first], &outer_scope).unwrap();
9653 assert_eq!(outputs.len(), 2);
9654 assert_eq!(outputs[0].to_vec_f32(), vec![13.0, 24.0]);
9655 assert_eq!(outputs[1].to_vec_f32(), vec![12.0, 23.0]);
9656 assert_eq!(child.stats(), ChildExecutorStats { builds: 1, runs: 1 });
9657
9658 let second = Tensor::from_f32(&[2], &[-1.0, 4.0]).unwrap();
9659 let outputs = child.run(&[&second], &outer_scope).unwrap();
9660 assert_eq!(outputs[0].to_vec_f32(), vec![10.0, 25.0]);
9661 assert_eq!(outputs[1].to_vec_f32(), vec![9.0, 24.0]);
9662 assert_eq!(
9663 child.stats(),
9664 ChildExecutorStats { builds: 1, runs: 2 },
9665 "matching input signatures must reuse the compiled child plan"
9666 );
9667 }
9668
9669 fn unary_child(name: &str) -> ChildExecutor {
9670 let mut body = Graph::new();
9671 let input = body.create_named_value("input", DataType::Float32, Vec::new());
9672 body.add_input(input);
9673 let output = body.create_named_value("output", DataType::Float32, Vec::new());
9674 body.insert_node(Node::new(
9675 NodeId(0),
9676 "Relu",
9677 vec![Some(input)],
9678 vec![output],
9679 ));
9680 body.add_output(output);
9681
9682 let mut opsets = HashMap::new();
9683 opsets.insert(String::new(), 17);
9684 ChildExecutor::new(
9685 name,
9686 body,
9687 opsets,
9688 Arc::new(WeightStore::new()),
9689 auto_detect_cpu_ep().unwrap(),
9690 )
9691 .unwrap()
9692 }
9693
9694 #[test]
9695 fn child_executor_reuses_a_signature_after_an_intervening_signature() {
9696 let mut child = unary_child("a-b-a");
9697 let outer_scope = HashMap::new();
9698 let a = Tensor::from_f32(&[1], &[-1.0]).unwrap();
9699 let b = Tensor::from_f32(&[2], &[-2.0, 3.0]).unwrap();
9700
9701 assert_eq!(
9702 child.run(&[&a], &outer_scope).unwrap()[0].to_vec_f32(),
9703 vec![0.0]
9704 );
9705 assert_eq!(
9706 child.run(&[&b], &outer_scope).unwrap()[0].to_vec_f32(),
9707 vec![0.0, 3.0]
9708 );
9709 assert_eq!(
9710 child.run(&[&a], &outer_scope).unwrap()[0].to_vec_f32(),
9711 vec![0.0]
9712 );
9713 assert_eq!(child.stats(), ChildExecutorStats { builds: 2, runs: 3 });
9714 }
9715
9716 #[test]
9717 fn child_executor_lru_evicts_oldest_signature_only() {
9718 let mut child = unary_child("lru-eviction");
9719 let outer_scope = HashMap::new();
9720 let inputs = (1..=CHILD_EXECUTOR_CACHE_CAPACITY + 1)
9721 .map(|len| Tensor::from_f32(&[len], &vec![len as f32; len]).unwrap())
9722 .collect::<Vec<_>>();
9723
9724 for input in &inputs {
9725 child.run(&[input], &outer_scope).unwrap();
9726 }
9727 assert_eq!(
9728 child.stats(),
9729 ChildExecutorStats {
9730 builds: (CHILD_EXECUTOR_CACHE_CAPACITY + 1) as u64,
9731 runs: (CHILD_EXECUTOR_CACHE_CAPACITY + 1) as u64,
9732 }
9733 );
9734
9735 child.run(&[&inputs[0]], &outer_scope).unwrap();
9736 child.run(&[inputs.last().unwrap()], &outer_scope).unwrap();
9737 assert_eq!(
9738 child.stats(),
9739 ChildExecutorStats {
9740 builds: (CHILD_EXECUTOR_CACHE_CAPACITY + 2) as u64,
9741 runs: (CHILD_EXECUTOR_CACHE_CAPACITY + 3) as u64,
9742 },
9743 "the evicted oldest signature must rebuild while a recent entry remains cached"
9744 );
9745 }
9746
9747 fn captured_add_child(name: &str) -> ChildExecutor {
9748 let mut body = Graph::new();
9749 let input = body.create_named_value("input", DataType::Float32, Vec::new());
9750 body.add_input(input);
9751 let captured = body.create_named_value("captured", DataType::Float32, Vec::new());
9752 let output = body.create_named_value("output", DataType::Float32, Vec::new());
9753 body.insert_node(Node::new(
9754 NodeId(0),
9755 "Add",
9756 vec![Some(input), Some(captured)],
9757 vec![output],
9758 ));
9759 body.add_output(output);
9760
9761 let mut opsets = HashMap::new();
9762 opsets.insert(String::new(), 17);
9763 ChildExecutor::new(
9764 name,
9765 body,
9766 opsets,
9767 Arc::new(WeightStore::new()),
9768 auto_detect_cpu_ep().unwrap(),
9769 )
9770 .unwrap()
9771 }
9772
9773 #[test]
9774 fn child_executor_cached_plan_rebinds_captures_without_stale_state() {
9775 let mut child = captured_add_child("capture-shadowing");
9776 let a_input = Tensor::from_f32(&[1], &[1.0]).unwrap();
9777 let b_input = Tensor::from_f32(&[2], &[2.0, 3.0]).unwrap();
9778
9779 let mut scope = HashMap::new();
9780 scope.insert(
9781 "captured".to_string(),
9782 Tensor::from_f32(&[1], &[10.0]).unwrap(),
9783 );
9784 assert_eq!(
9785 child.run(&[&a_input], &scope).unwrap()[0].to_vec_f32(),
9786 vec![11.0]
9787 );
9788
9789 scope.insert(
9790 "captured".to_string(),
9791 Tensor::from_f32(&[2], &[20.0, 30.0]).unwrap(),
9792 );
9793 assert_eq!(
9794 child.run(&[&b_input], &scope).unwrap()[0].to_vec_f32(),
9795 vec![22.0, 33.0]
9796 );
9797
9798 scope.insert(
9799 "captured".to_string(),
9800 Tensor::from_f32(&[1], &[40.0]).unwrap(),
9801 );
9802 let cached = child.run(&[&a_input], &scope).unwrap()[0].to_vec_f32();
9803 let mut fresh = captured_add_child("capture-shadowing-fresh");
9804 let freshly_compiled = fresh.run(&[&a_input], &scope).unwrap()[0].to_vec_f32();
9805
9806 assert_eq!(cached, vec![41.0]);
9807 assert_eq!(cached, freshly_compiled);
9808 assert_eq!(child.stats(), ChildExecutorStats { builds: 2, runs: 3 });
9809 }
9810
9811 use onnx_runtime_ir::{WeightRef, static_shape};
9814 use std::path::PathBuf;
9815
9816 fn weightstream_tmp_dir() -> PathBuf {
9818 let dir = PathBuf::from(concat!(
9819 env!("CARGO_MANIFEST_DIR"),
9820 "/../../target/weightstream_test"
9821 ));
9822 std::fs::create_dir_all(&dir).expect("create weight-streaming test dir");
9823 dir
9824 }
9825
9826 fn f32_le(data: &[f32]) -> Vec<u8> {
9827 data.iter().flat_map(|v| v.to_le_bytes()).collect()
9828 }
9829
9830 #[test]
9834 fn aligned_external_initializer_is_borrowed_zero_copy() {
9835 let align = TensorLayout::contiguous().alignment;
9836 let path = weightstream_tmp_dir().join("aligned_init.bin");
9837 let w_data = [1.0f32, 2.0, 3.0, 4.0];
9838 std::fs::write(&path, f32_le(&w_data)).unwrap();
9839
9840 let mut store = WeightStore::new();
9841 store.map_external(&path).unwrap();
9842
9843 let mut g = Graph::new();
9844 g.opset_imports.insert(String::new(), 17);
9845 let w = g.create_named_value("W", DataType::Float32, static_shape([4]));
9846 g.set_initializer(
9847 w,
9848 WeightRef::External {
9849 path: path.clone(),
9850 offset: 0, length: 16,
9852 dtype: DataType::Float32,
9853 dims: vec![4],
9854 },
9855 );
9856 let y = g.create_value(DataType::Float32, static_shape([4]));
9857 g.insert_node(Node::new(NodeId(0), "Relu", vec![Some(w)], vec![y]));
9858 g.add_output(y);
9859
9860 let ep = auto_detect_cpu_ep().unwrap();
9861 let exec = Executor::build(g, Arc::new(store), ep).unwrap();
9862
9863 let weight = &exec.graph.initializers[&w];
9864 let src = exec.weights().bytes(weight).unwrap();
9865 assert!(
9866 (src.as_ptr() as usize).is_multiple_of(align),
9867 "mmap window must be aligned for this test to exercise the zero-copy path"
9868 );
9869 let buf = &exec.buffers[&w];
9870 assert!(
9871 buf.is_borrowed(),
9872 "aligned initializer must be borrowed, not copied"
9873 );
9874 assert_eq!(
9875 buf.as_ptr() as *const u8,
9876 src.as_ptr(),
9877 "zero-copy: the buffer must alias the mmap bytes (no copy)"
9878 );
9879
9880 let _ = std::fs::remove_file(&path);
9881 }
9882
9883 #[test]
9886 fn device_unaligned_external_initializer_is_borrowed_at_dtype_alignment() {
9887 let align = TensorLayout::contiguous().alignment;
9888 let path = weightstream_tmp_dir().join("unaligned_init.bin");
9889 let offset = 8usize;
9892 let w_data = [5.0f32, 6.0, 7.0, 8.0];
9893 let mut file = vec![0u8; offset];
9894 file.extend_from_slice(&f32_le(&w_data));
9895 std::fs::write(&path, &file).unwrap();
9896
9897 let mut store = WeightStore::new();
9898 store.map_external(&path).unwrap();
9899
9900 let mut g = Graph::new();
9901 g.opset_imports.insert(String::new(), 17);
9902 let w = g.create_named_value("W", DataType::Float32, static_shape([4]));
9903 g.set_initializer(
9904 w,
9905 WeightRef::External {
9906 path: path.clone(),
9907 offset,
9908 length: 16,
9909 dtype: DataType::Float32,
9910 dims: vec![4],
9911 },
9912 );
9913 let x = g.create_named_value("X", DataType::Float32, static_shape([4]));
9914 g.add_input(x);
9915 let y = g.create_value(DataType::Float32, static_shape([4]));
9916 g.insert_node(Node::new(NodeId(0), "Add", vec![Some(x), Some(w)], vec![y]));
9917 g.add_output(y);
9918
9919 let ep = auto_detect_cpu_ep().unwrap();
9920 let mut exec = Executor::build(g, Arc::new(store), ep).unwrap();
9921
9922 let weight = &exec.graph.initializers[&w];
9923 let src = exec.weights().bytes(weight).unwrap();
9924 assert!(
9925 !(src.as_ptr() as usize).is_multiple_of(align),
9926 "window must be unaligned for this test to exercise the fallback"
9927 );
9928 let buf = &exec.buffers[&w];
9929 assert!(
9930 buf.is_borrowed(),
9931 "dtype-aligned mmap initializer must remain borrowed"
9932 );
9933 assert_eq!(
9934 buf.as_ptr() as *const u8,
9935 src.as_ptr(),
9936 "zero-copy buffer must alias the mmap window"
9937 );
9938 assert_eq!(buf.alignment(), std::mem::align_of::<f32>());
9939
9940 let x_tensor = Tensor::from_f32(&[4], &[10.0, 20.0, 30.0, 40.0]).unwrap();
9942 let out = exec.run(&[("X", &x_tensor)]).unwrap();
9943 assert_eq!(out.len(), 1);
9944 let got = out[0].to_vec_f32();
9945 let want = [15.0f32, 26.0, 37.0, 48.0];
9946 assert_eq!(got.len(), want.len());
9947 for (g, w) in got.iter().zip(want.iter()) {
9948 assert!((g - w).abs() < 1e-5, "got {g}, want {w}");
9949 }
9950
9951 let _ = std::fs::remove_file(&path);
9952 }
9953
9954 #[test]
9955 fn unaligned_external_qmoe_keeps_route_first_enabled_and_matches_legacy() {
9956 use std::ffi::OsString;
9957 use std::sync::{Mutex, OnceLock};
9958
9959 static ENV_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
9960 let _env_guard = ENV_LOCK
9961 .get_or_init(|| Mutex::new(()))
9962 .lock()
9963 .expect("weight-offload env lock");
9964
9965 struct RestoreEnv(Option<OsString>);
9966 impl Drop for RestoreEnv {
9967 fn drop(&mut self) {
9968 if let Some(value) = self.0.take() {
9969 unsafe { std::env::set_var(onnx_runtime_ep_cpu::WEIGHT_OFFLOAD_ENV, value) };
9971 } else {
9972 unsafe { std::env::remove_var(onnx_runtime_ep_cpu::WEIGHT_OFFLOAD_ENV) };
9974 }
9975 }
9976 }
9977
9978 let _restore = RestoreEnv(std::env::var_os(onnx_runtime_ep_cpu::WEIGHT_OFFLOAD_ENV));
9979 let fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
9980 .join("../onnx-runtime-ep-cpu/tests/fixtures/qmoe_weight_offload/model.onnx");
9981 let input_values: Vec<f32> = (0..64).map(|index| index as f32 * 0.03125 - 1.0).collect();
9982 let router_values = vec![
9983 9.0, 0.0, 0.0, 0.0, 0.0, 9.0, 0.0, 0.0, 0.0, 0.0, 9.0, 0.0, 0.0, 0.0, 0.0, 9.0,
9984 ];
9985 let input = Tensor::from_f32(&[4, 16], &input_values).unwrap();
9986 let router = Tensor::from_f32(&[4, 4], &router_values).unwrap();
9987
9988 unsafe { std::env::set_var(onnx_runtime_ep_cpu::WEIGHT_OFFLOAD_ENV, "0") };
9990 let (legacy_graph, legacy_weights) =
9991 onnx_runtime_loader::load_model_with_weights(&fixture).unwrap();
9992 let mut legacy =
9993 Executor::build(legacy_graph, legacy_weights, auto_detect_cpu_ep().unwrap()).unwrap();
9994 let legacy_output = legacy.run(&[("X", &input), ("router", &router)]).unwrap();
9995
9996 unsafe { std::env::set_var(onnx_runtime_ep_cpu::WEIGHT_OFFLOAD_ENV, "1") };
9998 let before = onnx_runtime_ep_cpu::weight_offload_stats();
9999 let (offload_graph, offload_weights) =
10000 onnx_runtime_loader::load_model_with_weights(&fixture).unwrap();
10001 let mut offload = Executor::build(
10002 offload_graph,
10003 offload_weights,
10004 auto_detect_cpu_ep().unwrap(),
10005 )
10006 .unwrap();
10007 for (&value, weight) in &offload.graph.initializers {
10008 let WeightRef::External { .. } = weight else {
10009 continue;
10010 };
10011 let source = offload.weights.bytes(weight).unwrap();
10012 assert!(
10013 !(source.as_ptr() as usize).is_multiple_of(TensorLayout::contiguous().alignment)
10014 );
10015 let buffer = &offload.buffers[&value];
10016 assert!(buffer.is_borrowed());
10017 assert_eq!(buffer.as_ptr() as *const u8, source.as_ptr());
10018 }
10019 let offload_output = offload.run(&[("X", &input), ("router", &router)]).unwrap();
10020 let after = onnx_runtime_ep_cpu::weight_offload_stats();
10021
10022 assert_eq!(
10023 offload_output[0].to_vec_f32(),
10024 legacy_output[0].to_vec_f32()
10025 );
10026 assert!(
10027 after.layer_executions
10028 >= before
10029 .layer_executions
10030 .checked_add(1)
10031 .expect("layer execution counter overflow")
10032 );
10033 assert!(after.bytes_read_from_mmap > before.bytes_read_from_mmap);
10034 }
10035
10036 #[test]
10044 fn producer_backed_initializer_is_not_borrowed() {
10045 let align = TensorLayout::contiguous().alignment;
10046 let path = weightstream_tmp_dir().join("producer_backed_init.bin");
10047 let w_data = [1.0f32, 2.0, 3.0, 4.0];
10048 std::fs::write(&path, f32_le(&w_data)).unwrap();
10049
10050 let mut store = WeightStore::new();
10051 store.map_external(&path).unwrap();
10052
10053 let mut g = Graph::new();
10054 g.opset_imports.insert(String::new(), 17);
10055 let x = g.create_named_value("X", DataType::Float32, static_shape([4]));
10056 g.add_input(x);
10057 let w = g.create_named_value("W", DataType::Float32, static_shape([4]));
10058 g.set_initializer(
10059 w,
10060 WeightRef::External {
10061 path: path.clone(),
10062 offset: 0, length: 16,
10064 dtype: DataType::Float32,
10065 dims: vec![4],
10066 },
10067 );
10068 g.insert_node(Node::new(NodeId(0), "Identity", vec![Some(x)], vec![w]));
10071 let y = g.create_value(DataType::Float32, static_shape([4]));
10072 g.insert_node(Node::new(NodeId(1), "Add", vec![Some(x), Some(w)], vec![y]));
10073 g.add_output(y);
10074
10075 assert!(
10076 g.value(w).producer.is_some(),
10077 "test setup: initializer value must have a producer",
10078 );
10079
10080 let ep = auto_detect_cpu_ep().unwrap();
10081 let exec = Executor::build(g, Arc::new(store), ep).unwrap();
10082
10083 let weight = &exec.graph.initializers[&w];
10084 let src = exec.weights().bytes(weight).unwrap();
10085 assert!(
10086 (src.as_ptr() as usize).is_multiple_of(align),
10087 "mmap window must be aligned so only the producer guard prevents borrowing",
10088 );
10089 let buf = &exec.buffers[&w];
10090 assert!(
10091 !buf.is_borrowed(),
10092 "producer-backed initializer must fall back to an owned writable copy",
10093 );
10094 assert_ne!(
10095 buf.as_ptr() as *const u8,
10096 src.as_ptr(),
10097 "producer-backed initializer must not alias read-only mmap bytes",
10098 );
10099
10100 let _ = std::fs::remove_file(&path);
10101 }
10102
10103 #[test]
10113 fn warm_decode_seeding_admits_previously_unresolved_capture_safe_node() {
10114 use onnx_runtime_ir::{Attribute, static_shape};
10115
10116 let mut graph = Graph::new();
10117 graph.opset_imports.insert(String::new(), 13);
10118 let start = graph.create_named_value("start", DataType::Int64, static_shape([]));
10122 let limit = graph.create_named_value("limit", DataType::Int64, static_shape([]));
10123 let delta = graph.create_named_value("delta", DataType::Int64, static_shape([]));
10124 graph.add_input(start);
10125 graph.add_input(limit);
10126 graph.add_input(delta);
10127 let len_sym = graph.intern_symbol("range_len");
10128 let r = graph.create_named_value("r", DataType::Int64, vec![len_sym.into()]);
10129 graph.insert_node(Node::new(
10130 NodeId(0),
10131 "Range",
10132 vec![Some(start), Some(limit), Some(delta)],
10133 vec![r],
10134 ));
10135 let y = graph.create_named_value("y", DataType::Float32, vec![len_sym.into()]);
10136 let mut cast = Node::new(NodeId(0), "Cast", vec![Some(r)], vec![y]);
10137 cast.attributes
10138 .insert("to".into(), Attribute::Int(DataType::Float32 as i64));
10139 graph.insert_node(cast);
10140 graph.add_output(y);
10141
10142 let mut exec = Executor::build(
10143 graph,
10144 Arc::new(WeightStore::new()),
10145 auto_detect_cpu_ep().unwrap(),
10146 )
10147 .unwrap();
10148 exec.set_decode_memo_enabled(false);
10155
10156 let zero = Tensor::from_raw(DataType::Int64, vec![], &0i64.to_le_bytes()).unwrap();
10157 let four = Tensor::from_raw(DataType::Int64, vec![], &4i64.to_le_bytes()).unwrap();
10158 let one = Tensor::from_raw(DataType::Int64, vec![], &1i64.to_le_bytes()).unwrap();
10159 let inputs = [("start", &zero), ("limit", &four), ("delta", &one)];
10160
10161 let cast_pi = exec
10162 .plan
10163 .iter()
10164 .position(|p| exec.graph.node(p.node_id).op_type == "Cast")
10165 .expect("plan contains the Cast node");
10166
10167 let bindings = exec
10169 .bind_symbols(&inputs, &ExternalBindings::default())
10170 .unwrap();
10171 let pre = exec.resolve_soft(&bindings);
10172 assert!(
10173 !pre.contains_key(&r) && !pre.contains_key(&y),
10174 "Range's runtime-length output (and its Cast) must be data-dependent (unresolved)"
10175 );
10176 let pre_seam = exec
10177 .node_capture_reason(&exec.plan[cast_pi], &pre)
10178 .and_then(|decline| decline.seam_reason);
10179 assert!(
10180 matches!(
10181 pre_seam,
10182 Some(SeamReason::UnresolvedInputShape) | Some(SeamReason::UnresolvedOutputShape)
10183 ),
10184 "without seeding the Cast must be an unresolved-shape seam; got {pre_seam:?}"
10185 );
10186
10187 let out = exec.run(&inputs).unwrap();
10189 assert_eq!(out[0].to_vec_f32(), vec![0.0, 1.0, 2.0, 3.0]);
10190
10191 let bindings2 = exec
10193 .bind_symbols(&inputs, &ExternalBindings::default())
10194 .unwrap();
10195 let mut post = exec.resolve_soft(&bindings2);
10196 assert!(
10197 !post.contains_key(&r),
10198 "resolve_soft alone still omits the data-dependent value"
10199 );
10200 exec.seed_warm_decode_capture_shapes(&mut post, &ExternalBindings::default());
10201 assert_eq!(
10202 post.get(&r),
10203 Some(&vec![4usize]),
10204 "warm seeding must restore Range's exact eager-resolved output shape"
10205 );
10206 assert_eq!(post.get(&y), Some(&vec![4usize]));
10207
10208 let post_seam = exec
10212 .node_capture_reason(&exec.plan[cast_pi], &post)
10213 .and_then(|decline| decline.seam_reason);
10214 assert!(
10215 !matches!(
10216 post_seam,
10217 Some(SeamReason::UnresolvedInputShape) | Some(SeamReason::UnresolvedOutputShape)
10218 ),
10219 "warm-seeded decode shapes must clear the unresolved-shape seam; got {post_seam:?}"
10220 );
10221
10222 let mut mismatched = exec.resolve_soft(&bindings2);
10227 let mut other = ExternalBindings::default();
10228 other.inputs.insert(
10229 start,
10230 ExternalValue {
10231 dtype: DataType::Int64,
10232 shape: vec![],
10233 accepts_subshape: false,
10234 ptr: 0x1000 as *mut std::ffi::c_void,
10235 len: 8,
10236 alignment: 8,
10237 device: onnx_runtime_ir::DeviceId::cpu(),
10238 },
10239 );
10240 exec.seed_warm_decode_capture_shapes(&mut mismatched, &other);
10241 assert!(
10242 !mismatched.contains_key(&r),
10243 "a changed persistent-binding signature must withhold the warm seed"
10244 );
10245 }
10246
10247 #[test]
10257 fn quarantined_op_type_is_forced_to_a_capture_recording_failed_seam() {
10258 use onnx_runtime_ir::{Attribute, static_shape};
10259
10260 let mut graph = Graph::new();
10261 graph.opset_imports.insert(String::new(), 13);
10262 let x = graph.create_named_value("x", DataType::Int64, static_shape([4]));
10263 graph.add_input(x);
10264 let y = graph.create_named_value("y", DataType::Float32, static_shape([4]));
10265 let mut cast = Node::new(NodeId(0), "Cast", vec![Some(x)], vec![y]);
10266 cast.attributes
10267 .insert("to".into(), Attribute::Int(DataType::Float32 as i64));
10268 graph.insert_node(cast);
10269 graph.add_output(y);
10270
10271 let mut exec = Executor::build(
10272 graph,
10273 Arc::new(WeightStore::new()),
10274 auto_detect_cpu_ep().unwrap(),
10275 )
10276 .unwrap();
10277
10278 let cast_pi = exec
10279 .plan
10280 .iter()
10281 .position(|p| exec.graph.node(p.node_id).op_type == "Cast")
10282 .expect("plan contains the Cast node");
10283
10284 let xt = Tensor::from_raw(
10287 DataType::Int64,
10288 vec![4],
10289 &[0i64, 1, 2, 3]
10290 .iter()
10291 .flat_map(|v| v.to_le_bytes())
10292 .collect::<Vec<u8>>(),
10293 )
10294 .unwrap();
10295 let bindings = exec
10296 .bind_symbols(&[("x", &xt)], &ExternalBindings::default())
10297 .unwrap();
10298 let resolved = exec.resolve_soft(&bindings);
10299 let pre_seam = exec
10300 .node_capture_reason(&exec.plan[cast_pi], &resolved)
10301 .and_then(|decline| decline.seam_reason);
10302 assert!(
10303 !matches!(pre_seam, Some(SeamReason::CaptureRecordingFailed)),
10304 "a non-quarantined statically-shaped node must not be a recording-failed seam; \
10305 got {pre_seam:?}"
10306 );
10307
10308 exec.capture_quarantine_ops
10312 .insert(("ai.onnx".to_string(), "Cast".to_string()));
10313 let post = exec.node_capture_reason(&exec.plan[cast_pi], &resolved);
10314 assert_eq!(
10315 post.and_then(|decline| decline.seam_reason),
10316 Some(SeamReason::CaptureRecordingFailed),
10317 "a quarantined op-type must be forced to a CaptureRecordingFailed eager seam"
10318 );
10319 }
10320
10321 #[cfg(test)]
10330 struct DecodeMemoIds {
10331 batch: SymbolId,
10332 seq: SymbolId,
10333 x2: ValueId,
10334 ymul: ValueId,
10335 }
10336
10337 #[cfg(test)]
10338 fn decode_memo_test_graph() -> (Graph, DecodeMemoIds) {
10339 use onnx_runtime_ir::TensorData;
10340 let mut graph = Graph::new();
10341 graph.opset_imports.insert(String::new(), 17);
10342 let batch = graph.intern_symbol("batch");
10343 let seq = graph.intern_symbol("seq");
10344
10345 let x = graph.create_named_value("x", DataType::Float32, vec![batch.into(), seq.into()]);
10347 graph.add_input(x);
10348 let x2 =
10349 graph.create_named_value("x2", DataType::Float32, vec![batch.into(), seq.into()]);
10350 graph.insert_node(Node::new(NodeId(0), "Add", vec![Some(x), Some(x)], vec![x2]));
10351 graph.add_output(x2);
10352
10353 let y = graph.create_named_value("y", DataType::Float32, static_shape([4]));
10355 graph.add_input(y);
10356 let w = graph.create_named_value("w", DataType::Float32, static_shape([4]));
10357 graph.set_initializer(
10358 w,
10359 WeightRef::Inline(TensorData::from_raw(
10360 DataType::Float32,
10361 vec![4],
10362 [1.0f32, 2.0, 3.0, 4.0]
10363 .into_iter()
10364 .flat_map(f32::to_le_bytes)
10365 .collect(),
10366 )),
10367 );
10368 let ymul = graph.create_named_value("ymul", DataType::Float32, static_shape([4]));
10369 graph.insert_node(Node::new(NodeId(0), "Mul", vec![Some(y), Some(w)], vec![ymul]));
10370 graph.add_output(ymul);
10371 (graph, DecodeMemoIds { batch, seq, x2, ymul })
10372 }
10373
10374 #[cfg(test)]
10375 fn decode_memo_run(exec: &mut Executor, batch: usize, seq: usize) -> Vec<Vec<f32>> {
10376 let x = Tensor::from_f32(
10377 &[batch, seq],
10378 &(0..batch * seq).map(|i| i as f32 + 1.0).collect::<Vec<_>>(),
10379 )
10380 .unwrap();
10381 let y = Tensor::from_f32(&[4], &[10.0, 20.0, 30.0, 40.0]).unwrap();
10382 exec.run(&[("x", &x), ("y", &y)])
10383 .unwrap()
10384 .into_iter()
10385 .map(|t| t.to_vec_f32())
10386 .collect()
10387 }
10388
10389 #[test]
10394 fn decode_memo_env_default_on_unless_explicitly_disabled() {
10395 use std::ffi::OsString;
10396 use std::sync::{Mutex, OnceLock};
10397
10398 static ENV_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
10399 let _env_guard = ENV_LOCK
10400 .get_or_init(|| Mutex::new(()))
10401 .lock()
10402 .expect("decode-memo env lock");
10403
10404 struct RestoreEnv(Option<OsString>);
10405 impl Drop for RestoreEnv {
10406 fn drop(&mut self) {
10407 match self.0.take() {
10408 Some(value) => unsafe {
10410 std::env::set_var("ONNX_GENAI_DECODE_MEMO", value)
10411 },
10412 None => unsafe { std::env::remove_var("ONNX_GENAI_DECODE_MEMO") },
10413 }
10414 }
10415 }
10416 let _restore = RestoreEnv(std::env::var_os("ONNX_GENAI_DECODE_MEMO"));
10417
10418 unsafe { std::env::remove_var("ONNX_GENAI_DECODE_MEMO") };
10421 assert!(decode_memo_env_enabled(), "unset must default ON");
10422
10423 for off in ["0", "false", "off", "FALSE", "Off", " 0 ", "\tOFF\n"] {
10425 unsafe { std::env::set_var("ONNX_GENAI_DECODE_MEMO", off) };
10427 assert!(!decode_memo_env_enabled(), "{off:?} must disable the memo");
10428 }
10429
10430 for on in ["1", "true", "on", "ON", "True", " on ", "", " ", "yes", "2", "banana"] {
10432 unsafe { std::env::set_var("ONNX_GENAI_DECODE_MEMO", on) };
10434 assert!(decode_memo_env_enabled(), "{on:?} must keep the memo ON");
10435 }
10436 }
10437
10438 #[test]
10442 fn decode_plan_memo_rebuilds_and_replays() {
10443 let (graph, ids) = decode_memo_test_graph();
10444 let mut exec =
10445 Executor::build(graph, Arc::new(WeightStore::new()), auto_detect_cpu_ep().unwrap())
10446 .unwrap();
10447 exec.set_decode_memo_enabled(true);
10448
10449 decode_memo_run(&mut exec, 1, 4);
10451 assert_eq!(exec.decode_memo_action(), DecodeMemoAction::Primed);
10452 assert!(exec.decode_memo.is_none());
10453
10454 decode_memo_run(&mut exec, 1, 5);
10457 assert_eq!(exec.decode_memo_action(), DecodeMemoAction::Rebuilt);
10458 let memo = exec.decode_memo.as_ref().expect("memo built");
10459 assert!(memo.decode_varying.contains(&ids.seq));
10461 assert!(!memo.decode_varying.contains(&ids.batch));
10462 assert!(memo.invariant_shapes.contains_key(&ids.ymul));
10464 assert!(memo.variant_values.contains(&ids.x2));
10465
10466 let out6 = decode_memo_run(&mut exec, 1, 6);
10469 assert_eq!(exec.decode_memo_action(), DecodeMemoAction::Replayed);
10470 let out7 = decode_memo_run(&mut exec, 1, 7);
10471 assert_eq!(exec.decode_memo_action(), DecodeMemoAction::Replayed);
10472 assert_eq!(out6[0].len(), 6);
10474 assert_eq!(out7[0].len(), 7);
10475
10476 decode_memo_run(&mut exec, 2, 7);
10479 assert_eq!(exec.decode_memo_action(), DecodeMemoAction::Rebuilt);
10480 let memo = exec.decode_memo.as_ref().expect("memo rebuilt");
10481 assert_eq!(memo.reference_bindings.get(&ids.batch), Some(&2));
10482 }
10483
10484 #[test]
10497 fn decode_plan_memo_is_token_exact_over_128_steps() {
10498 const STEPS: usize = 130;
10499
10500 let (off_graph, _) = decode_memo_test_graph();
10501 let mut off = Executor::build(
10502 off_graph,
10503 Arc::new(WeightStore::new()),
10504 auto_detect_cpu_ep().unwrap(),
10505 )
10506 .unwrap();
10507 off.set_decode_memo_enabled(false);
10509 assert!(!off.decode_memo_enabled);
10510
10511 let (on_graph, _) = decode_memo_test_graph();
10512 let mut on = Executor::build(
10513 on_graph,
10514 Arc::new(WeightStore::new()),
10515 auto_detect_cpu_ep().unwrap(),
10516 )
10517 .unwrap();
10518 on.set_decode_memo_enabled(true);
10519
10520 let mut replays = 0usize;
10521 for step in 0..STEPS {
10522 let seq = 3 + step; let ref_out = decode_memo_run(&mut off, 1, seq);
10524 let memo_out = decode_memo_run(&mut on, 1, seq);
10525 assert_eq!(
10526 ref_out, memo_out,
10527 "decode-plan memo diverged from the reference at step {step} (seq={seq})"
10528 );
10529 if on.decode_memo_action() == DecodeMemoAction::Replayed {
10530 replays += 1;
10531 }
10532 }
10533 assert!(
10536 replays >= STEPS - 2,
10537 "expected the memo to replay in steady state; only {replays}/{STEPS} replays"
10538 );
10539 }
10540
10541 #[test]
10551 fn decode_plan_memo_fires_on_persistent_kv_bindings() {
10552 use onnx_runtime_ir::{TensorData, static_shape};
10553
10554 let mut graph = Graph::new();
10557 graph.opset_imports.insert(String::new(), 17);
10558 let l = graph.intern_symbol("L");
10559 let kv = graph.create_named_value("kv", DataType::Float32, vec![l.into()]);
10560 graph.add_input(kv);
10561 let kvout = graph.create_named_value("kvout", DataType::Float32, vec![l.into()]);
10562 graph.insert_node(Node::new(NodeId(0), "Relu", vec![Some(kv)], vec![kvout]));
10563 graph.add_output(kvout);
10564
10565 let y = graph.create_named_value("y", DataType::Float32, static_shape([4]));
10566 graph.add_input(y);
10567 let w = graph.create_named_value("w", DataType::Float32, static_shape([4]));
10568 graph.set_initializer(
10569 w,
10570 WeightRef::Inline(TensorData::from_raw(
10571 DataType::Float32,
10572 vec![4],
10573 [1.0f32, 2.0, 3.0, 4.0]
10574 .into_iter()
10575 .flat_map(f32::to_le_bytes)
10576 .collect(),
10577 )),
10578 );
10579 let ymul = graph.create_named_value("ymul", DataType::Float32, static_shape([4]));
10580 graph.insert_node(Node::new(NodeId(0), "Mul", vec![Some(y), Some(w)], vec![ymul]));
10581 graph.add_output(ymul);
10582
10583 let mut exec = Executor::build(
10584 graph,
10585 Arc::new(WeightStore::new()),
10586 auto_detect_cpu_ep().unwrap(),
10587 )
10588 .unwrap();
10589 exec.set_decode_memo_enabled(true);
10590
10591 const CAP: usize = 128;
10594 let mut kv_binding = exec
10595 .allocate_device_binding(
10596 "kv".into(),
10597 Some("kvout".into()),
10598 DataType::Float32,
10599 vec![CAP],
10600 vec![1],
10601 )
10602 .unwrap();
10603 let ptr0 = kv_binding.device_ptr();
10604 let y_tensor = Tensor::from_f32(&[4], &[10.0, 20.0, 30.0, 40.0]).unwrap();
10605
10606 let mut replays = 0usize;
10607 for step in 0..8usize {
10608 let len = 4 + step; kv_binding.set_logical_shape(vec![len]).unwrap();
10610 let bytes: Vec<u8> = (0..len).flat_map(|i| (i as f32).to_le_bytes()).collect();
10611 kv_binding.write_bytes(0, &bytes).unwrap();
10612 exec.run_with_device_bindings(
10613 &[("y", &y_tensor)],
10614 std::slice::from_mut(&mut kv_binding),
10615 )
10616 .unwrap();
10617 if exec.decode_memo_action() == DecodeMemoAction::Replayed {
10618 replays += 1;
10619 }
10620 }
10621
10622 assert_eq!(kv_binding.device_ptr(), ptr0);
10625
10626 let (primed, rebuilt, replayed, ineligible) = exec.decode_memo_counts();
10627 assert_eq!(
10628 ineligible, 0,
10629 "persistent-KV decode must be memo-eligible, not excluded (the F5 regression)"
10630 );
10631 assert!(primed >= 1, "the first decode step must prime the memo");
10632 assert!(
10633 replayed >= 1,
10634 "steady persistent-KV decode must replay the memo \
10635 (primed={primed} rebuilt={rebuilt} replayed={replayed})"
10636 );
10637 assert_eq!(replays as u64, replayed);
10638 }
10639
10640 #[cfg(test)]
10646 fn stage2_view_graph() -> (Graph, ValueId) {
10647 use onnx_runtime_ir::{TensorData, static_shape};
10648 let mut graph = Graph::new();
10649 graph.opset_imports.insert(String::new(), 17);
10650 let l = graph.intern_symbol("L");
10651 let kv = graph.create_named_value("kv", DataType::Float32, vec![l.into()]);
10652 graph.add_input(kv);
10653 let kvout = graph.create_named_value("kvout", DataType::Float32, vec![l.into()]);
10654 graph.insert_node(Node::new(NodeId(0), "Relu", vec![Some(kv)], vec![kvout]));
10655 graph.add_output(kvout);
10656
10657 let y = graph.create_named_value("y", DataType::Float32, static_shape([4]));
10658 graph.add_input(y);
10659 let w = graph.create_named_value("w", DataType::Float32, static_shape([4]));
10660 graph.set_initializer(
10661 w,
10662 WeightRef::Inline(TensorData::from_raw(
10663 DataType::Float32,
10664 vec![4],
10665 [1.0f32, 2.0, 3.0, 4.0]
10666 .into_iter()
10667 .flat_map(f32::to_le_bytes)
10668 .collect(),
10669 )),
10670 );
10671 let ymul = graph.create_named_value("ymul", DataType::Float32, static_shape([4]));
10672 graph.insert_node(Node::new(NodeId(0), "Mul", vec![Some(y), Some(w)], vec![ymul]));
10673
10674 let yshape = graph.create_named_value("yshape", DataType::Int64, static_shape([2]));
10677 graph.set_initializer(
10678 yshape,
10679 WeightRef::Inline(TensorData::from_raw(
10680 DataType::Int64,
10681 vec![2],
10682 [2i64, 2].into_iter().flat_map(i64::to_le_bytes).collect(),
10683 )),
10684 );
10685 let yview = graph.create_named_value("yview", DataType::Float32, static_shape([2, 2]));
10686 graph.insert_node(Node::new(
10687 NodeId(0),
10688 "Reshape",
10689 vec![Some(ymul), Some(yshape)],
10690 vec![yview],
10691 ));
10692 graph.add_output(yview);
10693 (graph, ymul)
10694 }
10695
10696 #[cfg(test)]
10701 fn stage2_run(exec: &mut Executor, kv_binding: &mut DeviceIoBinding, len: usize, y_bias: f32) -> Vec<f32> {
10702 kv_binding.set_logical_shape(vec![len]).unwrap();
10703 let bytes: Vec<u8> = (0..len).flat_map(|i| (i as f32).to_le_bytes()).collect();
10704 kv_binding.write_bytes(0, &bytes).unwrap();
10705 let y = Tensor::from_f32(
10706 &[4],
10707 &[y_bias + 1.0, y_bias + 2.0, y_bias + 3.0, y_bias + 4.0],
10708 )
10709 .unwrap();
10710 let outs = exec
10711 .run_with_device_bindings(&[("y", &y)], std::slice::from_mut(kv_binding))
10712 .unwrap();
10713 outs.into_iter()
10715 .flatten()
10716 .next()
10717 .expect("yview output")
10718 .to_vec_f32()
10719 }
10720
10721 #[cfg(test)]
10722 fn stage2_kv_binding(exec: &Executor) -> DeviceIoBinding {
10723 exec.allocate_device_binding(
10724 "kv".into(),
10725 Some("kvout".into()),
10726 DataType::Float32,
10727 vec![256],
10728 vec![1],
10729 )
10730 .unwrap()
10731 }
10732
10733 #[test]
10742 fn decode_view_plan_fires_and_is_token_exact_over_128_steps() {
10743 const STEPS: usize = 130;
10744
10745 let (off_graph, _) = stage2_view_graph();
10746 let mut off = Executor::build(
10747 off_graph,
10748 Arc::new(WeightStore::new()),
10749 auto_detect_cpu_ep().unwrap(),
10750 )
10751 .unwrap();
10752 off.set_decode_memo_enabled(false);
10754 assert!(!off.decode_memo_enabled, "reference must run memo-OFF");
10755 let mut off_kv = stage2_kv_binding(&off);
10756
10757 let (on_graph, _) = stage2_view_graph();
10758 let mut on = Executor::build(
10759 on_graph,
10760 Arc::new(WeightStore::new()),
10761 auto_detect_cpu_ep().unwrap(),
10762 )
10763 .unwrap();
10764 on.set_decode_memo_enabled(true);
10765 let mut on_kv = stage2_kv_binding(&on);
10766
10767 for step in 0..STEPS {
10768 let len = 4 + step; let bias = step as f32; let ref_out = stage2_run(&mut off, &mut off_kv, len, bias);
10771 let memo_out = stage2_run(&mut on, &mut on_kv, len, bias);
10772 assert_eq!(
10773 ref_out, memo_out,
10774 "Stage 2 view reuse diverged from the reference at step {step} (L={len})"
10775 );
10776 }
10777
10778 let (views_reused, dispatch_elided) = on.decode_view_plan_counts();
10779 assert!(
10780 views_reused > 0 && dispatch_elided > 0,
10781 "Stage 2 must fire on steady decode (views_reused={views_reused}, \
10782 dispatch_elided={dispatch_elided})"
10783 );
10784 assert!(
10787 views_reused as usize >= STEPS - 4,
10788 "expected steady Stage 2 reuse; only {views_reused}/{STEPS} views reused"
10789 );
10790 assert!(
10791 on.decode_view_plan.is_some(),
10792 "the cached view plan must survive steady-state replay"
10793 );
10794 }
10795
10796 #[test]
10803 fn decode_view_plan_rebuilds_on_source_buffer_move() {
10804 use onnx_runtime_ir::TensorLayout;
10805
10806 let (off_graph, _) = stage2_view_graph();
10807 let mut off = Executor::build(
10808 off_graph,
10809 Arc::new(WeightStore::new()),
10810 auto_detect_cpu_ep().unwrap(),
10811 )
10812 .unwrap();
10813 let mut off_kv = stage2_kv_binding(&off);
10814
10815 let (on_graph, ymul) = stage2_view_graph();
10816 let mut on = Executor::build(
10817 on_graph,
10818 Arc::new(WeightStore::new()),
10819 auto_detect_cpu_ep().unwrap(),
10820 )
10821 .unwrap();
10822 on.set_decode_memo_enabled(true);
10823 let mut on_kv = stage2_kv_binding(&on);
10824
10825 for step in 0..6usize {
10827 let len = 4 + step;
10828 let bias = step as f32;
10829 let r = stage2_run(&mut off, &mut off_kv, len, bias);
10830 let m = stage2_run(&mut on, &mut on_kv, len, bias);
10831 assert_eq!(r, m, "warmup diverged at step {step}");
10832 }
10833 assert!(
10834 on.decode_view_plan.is_some(),
10835 "view plan must be built before the realloc test"
10836 );
10837 let (reused_before, _) = on.decode_view_plan_counts();
10838
10839 let old = on.buffers.remove(&ymul).expect("ymul buffer");
10842 let cap = old.len();
10843 on.ep.deallocate(old).unwrap();
10844 let fresh = on
10845 .ep
10846 .allocate(cap, TensorLayout::contiguous().alignment)
10847 .unwrap();
10848 let moved_ptr = fresh.as_ptr() as usize;
10849 on.buffers.insert(ymul, fresh);
10850 assert!(
10852 !on.stage2_buffer_sig_matches(on.decode_view_plan.as_ref().unwrap()),
10853 "the forced realloc must break the buffer-identity signature"
10854 );
10855
10856 let len = 4 + 6;
10858 let bias = 6.0f32;
10859 let ref_out = stage2_run(&mut off, &mut off_kv, len, bias);
10860 let memo_out = stage2_run(&mut on, &mut on_kv, len, bias);
10861 assert_eq!(
10862 ref_out, memo_out,
10863 "a moved source buffer must force a rebuild, never serve a stale view"
10864 );
10865 let (reused_after, _) = on.decode_view_plan_counts();
10866 assert_eq!(
10867 reused_after, reused_before,
10868 "the mismatched step must NOT reuse cached views (would be stale)"
10869 );
10870 let healed = on.buffers.get(&ymul).expect("ymul rebound").as_ptr() as usize;
10873 assert!(healed == moved_ptr || healed != 0, "ymul must be backed");
10874 }
10875}