use super::*;
pub(super) fn log_capture_segmentation(schedule: &CaptureSchedule) {
let captured = schedule.captured_segments();
let seams = schedule.segments.len() - captured;
eprintln!(
"[onnx-genai-capture] segmented CUDA graph: {captured} captured segment(s), \
{seams} eager seam(s)"
);
for boundary in &schedule.boundaries {
match boundary.node_id {
Some(id) => {
let seam_label = boundary
.seam_reason
.map(SeamReason::label)
.unwrap_or("unclassified-seam");
eprintln!(
"[onnx-genai-capture] seam node {id} ({}::{}) [{seam_label}] ran eagerly: {}",
boundary.domain, boundary.op_type, boundary.reason
);
}
None => eprintln!(
"[onnx-genai-capture] seam ({}): {}",
boundary.op_type, boundary.reason
),
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct ControlFlowStats {
pub subgraph_builds: u64,
pub subgraph_runs: u64,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct DeviceAllocationCounts {
pub allocations: u64,
pub frees: u64,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CapturePathKind {
CaptureRegion,
EagerDeviceSeam,
HostSeam,
}
impl CapturePathKind {
pub const fn label(self) -> &'static str {
match self {
Self::CaptureRegion => "capture-region",
Self::EagerDeviceSeam => "eager-device-seam",
Self::HostSeam => "host-seam",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SeamReason {
HostControlFlowOrSequence,
UnresolvedOutputShape,
UnresolvedInputShape,
KernelNotWarmed,
KernelCaptureUnsupported,
ClassifierDisqualified,
CaptureRecordingFailed,
}
impl SeamReason {
pub const fn path_kind(self) -> CapturePathKind {
match self {
Self::HostControlFlowOrSequence => CapturePathKind::HostSeam,
Self::UnresolvedOutputShape
| Self::UnresolvedInputShape
| Self::KernelNotWarmed
| Self::CaptureRecordingFailed
| Self::ClassifierDisqualified
| Self::KernelCaptureUnsupported => CapturePathKind::EagerDeviceSeam,
}
}
pub const fn label(self) -> &'static str {
self.path_kind().label()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CaptureDecline {
pub node_id: Option<u32>,
pub op_type: String,
pub domain: String,
pub reason: String,
pub seam_reason: Option<SeamReason>,
}
impl CaptureDecline {
pub(super) fn node(
node_id: NodeId,
node: &Node,
seam_reason: SeamReason,
reason: impl Into<String>,
) -> Self {
Self {
node_id: Some(node_id.0),
op_type: node.op_type.clone(),
domain: canonical_domain(node),
reason: reason.into(),
seam_reason: Some(seam_reason),
}
}
pub(super) fn graph(reason: impl Into<String>) -> Self {
Self {
node_id: None,
op_type: "<graph>".to_string(),
domain: "nxrt".to_string(),
reason: reason.into(),
seam_reason: None,
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct CaptureDeclineReport {
pub entries: Vec<CaptureDecline>,
}
impl CaptureDeclineReport {
pub(super) fn one(decline: CaptureDecline) -> Self {
Self {
entries: vec![decline],
}
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ExecutionProviderDecline {
pub node: String,
pub domain: String,
pub op_type: String,
pub reason: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ExecutionProviderFallbackReport {
pub requested_provider: String,
pub fallback_provider: String,
pub assigned_node_count: usize,
pub assigned_ops: Vec<String>,
pub declines: Vec<ExecutionProviderDecline>,
}
impl std::fmt::Display for ExecutionProviderFallbackReport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{} nodes assigned to CPU (ops: {}) — GPU EP {} did not claim {} node(s): {}. \
Heterogeneous CUDA+CPU placement is unavailable, so the whole session uses {}",
self.assigned_node_count,
self.assigned_ops.join(", "),
self.requested_provider,
self.declines.len(),
format_cuda_coverage_issues(&self.declines),
self.fallback_provider,
)
}
}
impl std::fmt::Display for CaptureDeclineReport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "CUDA graph capture rejected")?;
for (index, decline) in self.entries.iter().enumerate() {
if index == 0 {
write!(f, ": ")?;
} else {
write!(f, "; ")?;
}
match decline.node_id {
Some(node_id) => write!(
f,
"node {node_id} ({}::{}) — {}",
decline.domain, decline.op_type, decline.reason
)?,
None => write!(f, "{} — {}", decline.op_type, decline.reason)?,
}
}
Ok(())
}
}
pub enum DeviceGraphCaptureResult {
Captured(Vec<Option<Tensor>>),
NotCapturable(CaptureDeclineReport),
}
#[allow(clippy::large_enum_variant)]
pub(super) enum ScopedRunResult {
Executed(ScopedOutputs),
NotCapturable(CaptureDeclineReport),
}
pub(super) fn kernel_capture_decline(
node_id: NodeId,
node: &Node,
kernel: &dyn Kernel,
) -> Option<CaptureDecline> {
kernel.capture_support().reason().map(|reason| {
CaptureDecline::node(node_id, node, SeamReason::KernelCaptureUnsupported, reason)
})
}
pub(super) fn structural_capture_decline(
node_id: NodeId,
node: &Node,
decline: StructuralCaptureDecline,
) -> CaptureDecline {
let seam_reason = match decline {
StructuralCaptureDecline::HostControlFlowOrSequence => {
SeamReason::HostControlFlowOrSequence
}
StructuralCaptureDecline::UnresolvedOutputShape => SeamReason::UnresolvedOutputShape,
StructuralCaptureDecline::UnresolvedInputShape => SeamReason::UnresolvedInputShape,
};
CaptureDecline::node(node_id, node, seam_reason, decline.reason())
}
pub(super) fn capture_segmentation_logging_enabled() -> bool {
static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*ENABLED.get_or_init(|| {
std::env::var("ONNX_GENAI_LOG_CAPTURE_SEGMENTS")
.is_ok_and(|value| value == "1" || value.eq_ignore_ascii_case("true"))
})
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(super) enum RunMode {
Eager,
Capture,
Replay,
}
#[derive(Clone, Copy)]
pub(super) enum OpCaptureTrace<'a> {
Eager,
Captured,
Rejected(&'a str),
}
pub(super) const ARG_CAPTURE_STATUS: &str = "capture_status";
pub(super) const ARG_CAPTURE_REASON: &str = "capture_reason";
impl OpCaptureTrace<'_> {
pub(super) fn annotate(self) {
match self {
OpCaptureTrace::Eager => {}
OpCaptureTrace::Captured => {
annotate_current_span_with(|| {
onnx_runtime_tracer::Args::new().with(ARG_CAPTURE_STATUS, "captured")
});
}
OpCaptureTrace::Rejected(reason) => {
annotate_current_span_with(|| {
onnx_runtime_tracer::Args::new()
.with(ARG_CAPTURE_STATUS, "rejected")
.with(ARG_CAPTURE_REASON, reason)
});
}
}
}
}
pub(super) struct SegmentCaptureGuard<'a> {
pub(super) ep: &'a dyn ExecutionProvider,
pub(super) token: DeviceGraphToken,
pub(super) armed: bool,
}
impl<'a> SegmentCaptureGuard<'a> {
pub(super) fn arm(ep: &'a dyn ExecutionProvider, token: DeviceGraphToken) -> Self {
Self {
ep,
token,
armed: true,
}
}
pub(super) fn disarm(&mut self) {
self.armed = false;
}
}
impl Drop for SegmentCaptureGuard<'_> {
fn drop(&mut self) {
if self.armed {
let _ = self.ep.abort_owned_device_graph_capture(self.token);
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) struct ScheduledSegment {
pub(super) start: usize,
pub(super) end: usize,
pub(super) captured: bool,
pub(super) graph_index: usize,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub(super) struct CaptureSchedule {
pub(super) segments: Vec<ScheduledSegment>,
pub(super) boundaries: Vec<CaptureDecline>,
}
impl CaptureSchedule {
pub(super) fn captured_segments(&self) -> usize {
self.segments.iter().filter(|seg| seg.captured).count()
}
pub(super) fn is_single_graph(&self) -> bool {
self.segments.len() == 1 && self.segments[0].captured
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) struct DeviceBindingSignature {
pub(super) input_name: String,
pub(super) binds_input: bool,
pub(super) output_name: Option<String>,
pub(super) dtype: DataType,
pub(super) physical_shape: Vec<usize>,
pub(super) kernel_input_shape: Vec<usize>,
pub(super) accepts_output_subshape: bool,
pub(super) exposes_logical_input_shape: bool,
pub(super) mask_decode_freeze_safe: bool,
pub(super) fixed_physical_strides: bool,
pub(super) device_ptr: usize,
}
impl Executor {
pub(crate) fn pin_fixed_capacity_kv_capture_symbols(&mut self) -> usize {
let mut pinned = collect_capacity_pinned_kv_symbols(&self.graph);
pinned.extend(collect_freeze_safe_mask_symbols(&self.graph));
if pinned.is_empty() {
return 0;
}
self.capture_growing_symbols =
compute_capture_disqualifying_symbols_excluding(&self.graph, &pinned);
let count = pinned.len();
self.capacity_pinned_kv_symbols = pinned;
if std::env::var("ONNX_GENAI_LOG_GROWING_SYMBOLS").is_ok() {
eprintln!(
"[onnx-genai-capture] pinned {} fixed-capacity KV seq / freeze-safe mask symbol(s): {:?}; \
disqualifying set now {} symbol(s)",
count,
self.capacity_pinned_kv_symbols,
self.capture_growing_symbols.len(),
);
}
count
}
pub(super) fn seed_control_flow_capture_shapes(
&self,
resolved: &mut HashMap<ValueId, Vec<usize>>,
) {
for &vid in &self.control_flow_output_values {
if resolved.contains_key(&vid) {
continue;
}
if let Some(shape) = self.buffer_shapes.get(&vid) {
resolved.insert(vid, shape.clone());
}
}
}
pub(super) fn seed_warm_decode_capture_shapes(
&mut self,
resolved: &mut HashMap<ValueId, Vec<usize>>,
external: &ExternalBindings,
) {
self.cap_mut().capture_warm_seeded.clear();
if self.cap().capture_warm_signature.as_ref() != Some(&external.capture_signature()) {
return;
}
let external_values: HashSet<ValueId> = external
.inputs
.keys()
.chain(external.outputs.keys())
.copied()
.collect();
let warm: Vec<(ValueId, Vec<usize>)> = self
.cap()
.capture_warm_shapes
.iter()
.map(|(&vid, shape)| (vid, shape.clone()))
.collect();
for (vid, shape) in warm {
if resolved.contains_key(&vid)
|| external_values.contains(&vid)
|| self.graph.initializers.contains_key(&vid)
|| self.sequence_values.contains(&vid)
{
continue;
}
self.cap_mut()
.capture_warm_seeded
.insert(vid, shape.clone());
resolved.insert(vid, shape);
}
}
pub(super) fn control_flow_seam_invalidated(
&self,
pi: usize,
resolved: &HashMap<ValueId, Vec<usize>>,
) -> bool {
let node = self.graph.node(self.plan[pi].node_id);
if !is_control_flow_op(&node.op_type, &node.domain) {
return false;
}
self.plan[pi].outputs.iter().any(|out| {
match (self.cap().capture_cf_shapes.get(out), resolved.get(out)) {
(Some(captured), Some(current)) => captured != current,
(Some(_), None) => true,
_ => false,
}
})
}
pub(super) fn node_capture_reason(
&self,
plan: &NodePlan,
resolved: &HashMap<ValueId, Vec<usize>>,
) -> Option<CaptureDecline> {
let node = self.graph.node(plan.node_id);
if self
.cap()
.capture_quarantine_ops
.contains(&(canonical_domain(node), node.op_type.clone()))
{
return Some(CaptureDecline::node(
plan.node_id,
node,
SeamReason::CaptureRecordingFailed,
"kernel aborted device-graph recording on a prior capture pass; \
quarantined to an eager seam",
));
}
let outputs_resolved = plan
.outputs
.iter()
.all(|output| resolved.contains_key(output));
let inputs_resolved = plan.inputs.iter().all(|input| match input {
Some(value) => resolved.contains_key(value),
None => true,
});
if let Some(decline) = self.ep.plan_capture_region(
node,
CaptureRegionShapeStatus {
inputs_resolved,
outputs_resolved,
},
) {
return Some(structural_capture_decline(plan.node_id, node, decline));
}
assert!(
inputs_resolved && outputs_resolved,
"EP capture-region policy admitted a node with unresolved shapes"
);
if !node_capture_seq_independent(&self.graph, node, &self.capture_growing_symbols) {
return Some(CaptureDecline::node(
plan.node_id,
node,
SeamReason::ClassifierDisqualified,
"capture classifier disqualified this node: an input or output \
shape depends on a growing (KV/total-sequence-length) symbol, so \
capturing it would replay a stale launch grid — forced eager seam",
));
}
let input_shapes = plan
.inputs
.iter()
.map(|input| {
input.map_or_else(Vec::new, |value| {
resolved
.get(&value)
.cloned()
.expect("resolved input shape checked above")
})
})
.collect();
let key = KernelKey {
node: plan.node_id.0,
shapes: input_shapes,
};
let Some(kernel) = self.cache.entries.get(&key) else {
return Some(CaptureDecline::node(
plan.node_id,
node,
SeamReason::KernelNotWarmed,
"kernel has not been warmed for the requested capture shape",
));
};
kernel_capture_decline(plan.node_id, node, kernel.as_ref())
}
pub(super) fn plan_capture_segments(
&self,
resolved: &HashMap<ValueId, Vec<usize>>,
external: &ExternalBindings,
) -> std::result::Result<CaptureSchedule, CaptureDeclineReport> {
if self
.graph
.outputs
.iter()
.any(|output| !external.outputs.contains_key(output))
{
return Err(CaptureDeclineReport::one(CaptureDecline::graph(
"every graph output must use a persistent device binding during capture",
)));
}
let declines: Vec<Option<CaptureDecline>> = self
.plan
.iter()
.map(|plan| self.node_capture_reason(plan, resolved))
.collect();
let mut segments: Vec<ScheduledSegment> = Vec::new();
let mut boundaries: Vec<CaptureDecline> = Vec::new();
let mut next_graph_index = 0usize;
let mut pi = 0usize;
while pi < declines.len() {
let captured = declines[pi].is_none();
let start = pi;
while pi < declines.len() && declines[pi].is_none() == captured {
if let Some(decline) = &declines[pi] {
boundaries.push(decline.clone());
}
pi += 1;
}
let graph_index = if captured {
let index = next_graph_index;
next_graph_index += 1;
index
} else {
0
};
segments.push(ScheduledSegment {
start,
end: pi,
captured,
graph_index,
});
}
if next_graph_index == 0 {
return Err(CaptureDeclineReport {
entries: boundaries,
});
}
Ok(CaptureSchedule {
segments,
boundaries,
})
}
pub(super) fn collect_segment_kernels(
&self,
seg: &ScheduledSegment,
resolved: &HashMap<ValueId, Vec<usize>>,
) -> Result<Vec<&dyn onnx_runtime_ep_api::Kernel>> {
let mut kernels = Vec::with_capacity(seg.end - seg.start);
for pi in seg.start..seg.end {
let plan = &self.plan[pi];
let input_shapes = plan
.inputs
.iter()
.map(|input| {
input
.map(|value| resolved.get(&value).cloned())
.unwrap_or(Some(Vec::new()))
})
.collect::<Option<Vec<_>>>()
.ok_or_else(|| {
SessionError::Internal(format!(
"segment kernel node {} lost its resolved input shape before capture",
plan.node_id.0
))
})?;
let key = KernelKey {
node: plan.node_id.0,
shapes: input_shapes,
};
let kernel = self.cache.entries.get(&key).ok_or_else(|| {
SessionError::Internal(format!(
"segment kernel node {} was not warmed before capture",
plan.node_id.0
))
})?;
kernels.push(kernel.as_ref());
}
Ok(kernels)
}
}