use super::*;
use std::sync::atomic::{AtomicU64, Ordering};
#[cfg(test)]
pub(crate) static PREBIND_FAST_PATH_TEST_HITS: std::sync::atomic::AtomicUsize =
std::sync::atomic::AtomicUsize::new(0);
#[cfg(test)]
pub(crate) static PREBIND_FALLBACK_TEST_HITS: std::sync::atomic::AtomicUsize =
std::sync::atomic::AtomicUsize::new(0);
pub(super) fn resolve_kernel_constant_inputs<'a>(
graph: &'a Graph,
weights: &'a onnx_runtime_loader::WeightStore,
inputs: &[Option<ValueId>],
input_shapes: &'a [Vec<usize>],
) -> Result<Vec<Option<KernelConstantInput<'a>>>> {
inputs
.iter()
.enumerate()
.map(|(index, input)| {
let Some(value) = input else {
return Ok(None);
};
let Some(weight) = graph.initializers.get(value) else {
return Ok(None);
};
let bytes = weights.bytes(weight).ok_or_else(|| {
SessionError::Internal(format!(
"initializer value {} could not be resolved for kernel preparation",
value.0
))
})?;
let shape = input_shapes.get(index).ok_or_else(|| {
SessionError::Internal(format!(
"kernel preparation has no shape for initializer input {index}"
))
})?;
Ok(Some(KernelConstantInput {
dtype: graph.value(*value).dtype,
shape,
bytes,
}))
})
.collect()
}
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
pub(super) struct KernelKey {
pub(super) node: u32,
pub(super) shapes: Vec<Vec<usize>>,
}
impl KernelKey {
#[inline]
pub(super) fn matches_shapes(&self, input_shapes: &[Vec<usize>]) -> bool {
self.shapes.len() == input_shapes.len()
&& self
.shapes
.iter()
.zip(input_shapes.iter())
.all(|(a, b)| a.as_slice() == b.as_slice())
}
}
#[derive(Default)]
struct KvCacheSlots {
past_inputs: &'static [usize],
present_outputs: &'static [usize],
last_axis_outputs: &'static [usize],
}
fn attention_kv_cache_slots(node: &Node) -> Option<KvCacheSlots> {
if node.domain == "com.microsoft" && node.op_type == "GroupQueryAttention" {
return Some(KvCacheSlots {
past_inputs: &[3, 4],
present_outputs: &[1, 2],
last_axis_outputs: &[],
});
}
if node.is_default_domain() && node.op_type == "Attention" {
return Some(KvCacheSlots {
past_inputs: &[4, 5],
present_outputs: &[1, 2],
last_axis_outputs: &[],
});
}
if node.domain == "pkg.nxrt" && node.op_type == "IndexShare" {
return Some(KvCacheSlots {
past_inputs: &[3, 4],
present_outputs: &[1, 2],
last_axis_outputs: &[],
});
}
if (node.domain == "pkg.nxrt" || node.domain == "com.microsoft")
&& node.op_type == "CompressedSparseAttention"
{
return Some(KvCacheSlots {
past_inputs: &[],
present_outputs: &[1, 3],
last_axis_outputs: &[5],
});
}
None
}
fn kv_growing_symbol(shape: &Shape) -> Option<SymbolId> {
if shape.len() < 2 {
return None;
}
match shape[shape.len() - 2] {
Dim::Symbolic(sym) => Some(sym),
Dim::Static(_) => None,
}
}
fn last_axis_growing_symbol(shape: &Shape) -> Option<SymbolId> {
match shape.last()? {
Dim::Symbolic(sym) => Some(*sym),
Dim::Static(_) => None,
}
}
pub(super) fn compute_capture_growing_symbols(graph: &Graph) -> HashSet<SymbolId> {
compute_capture_growing_symbols_excluding(graph, &HashSet::new())
}
pub(super) fn compute_capture_growing_symbols_excluding(
graph: &Graph,
pinned: &HashSet<SymbolId>,
) -> HashSet<SymbolId> {
let mut growing = collect_structural_growing_symbols_excluding(graph, pinned);
growing.extend(graph.symbol_opaque.iter().copied());
close_disqualifying_set(graph, &mut growing);
if !pinned.is_empty() {
growing.retain(|sym| !pinned.contains(sym));
}
growing
}
fn collect_structural_growing_symbols_excluding(
graph: &Graph,
pinned: &HashSet<SymbolId>,
) -> HashSet<SymbolId> {
let mut growing = HashSet::new();
for node in graph.nodes.values() {
let Some(slots) = attention_kv_cache_slots(node) else {
continue;
};
for &idx in slots.past_inputs {
if let Some(Some(vid)) = node.inputs.get(idx).copied()
&& let Some(value) = graph.try_value(vid)
&& let Some(sym) = kv_growing_symbol(&value.shape)
{
growing.insert(sym);
}
}
for &idx in slots.present_outputs {
if let Some(&vid) = node.outputs.get(idx)
&& let Some(value) = graph.try_value(vid)
&& let Some(sym) = kv_growing_symbol(&value.shape)
{
growing.insert(sym);
}
}
for &idx in slots.last_axis_outputs {
if let Some(&vid) = node.outputs.get(idx)
&& let Some(value) = graph.try_value(vid)
&& let Some(sym) = last_axis_growing_symbol(&value.shape)
{
growing.insert(sym);
}
}
}
let boundary_is_growing_kv = |vid: ValueId| -> Option<SymbolId> {
let value = graph.try_value(vid)?;
let name = value.name.as_deref()?;
let is_kv_boundary = name.starts_with("past") || name.starts_with("present");
if is_kv_boundary && value.shape.len() == 4 {
kv_growing_symbol(&value.shape)
} else {
None
}
};
for &vid in graph.inputs.iter().chain(graph.outputs.iter()) {
if let Some(sym) = boundary_is_growing_kv(vid) {
growing.insert(sym);
}
}
if !pinned.is_empty() {
growing.retain(|sym| !pinned.contains(sym));
}
growing
}
pub(super) fn collect_freeze_safe_mask_symbols(graph: &Graph) -> HashSet<SymbolId> {
use super::geometry::{
is_additive_mask_builder_op, is_capacity_form_attention_mask_input,
mask_binding_feeds_additive_causal_builder,
};
use std::collections::HashMap;
let mut consumers: HashMap<ValueId, Vec<(NodeId, usize)>> = HashMap::new();
for (node_id, node) in graph.nodes.iter() {
for (slot, value) in node.inputs.iter().enumerate() {
if let Some(vid) = value {
consumers.entry(*vid).or_default().push((node_id, slot));
}
}
}
let mut symbols = HashSet::new();
let collect_shape = |symbols: &mut HashSet<SymbolId>, vid: ValueId| {
if let Some(value) = graph.try_value(vid) {
for axis in 0..value.shape.len() {
if let Dim::Symbolic(sym) = value.shape[axis] {
symbols.insert(sym);
}
}
}
};
for &mask in &graph.inputs {
if !mask_binding_feeds_additive_causal_builder(graph, mask) {
continue;
}
let mut visited: HashSet<ValueId> = HashSet::new();
let mut frontier = vec![mask];
while let Some(value) = frontier.pop() {
if !visited.insert(value) {
continue;
}
collect_shape(&mut symbols, value);
for &(node_id, slot) in consumers.get(&value).map_or(&[][..], Vec::as_slice) {
let node = graph.node(node_id);
if is_capacity_form_attention_mask_input(node, slot) {
continue;
}
if is_additive_mask_builder_op(node) {
for out in &node.outputs {
frontier.push(*out);
}
}
}
}
}
symbols
}
pub(super) fn collect_capacity_pinned_kv_symbols(graph: &Graph) -> HashSet<SymbolId> {
let mut pinned = HashSet::new();
for node in graph.nodes.values() {
let Some(slots) = attention_kv_cache_slots(node) else {
continue;
};
let capacity_form = !slots.past_inputs.is_empty()
&& slots
.past_inputs
.iter()
.all(|&idx| super::geometry::kernel_input_uses_physical_capacity(node, idx));
if !capacity_form {
continue;
}
for &idx in slots.past_inputs {
if let Some(Some(vid)) = node.inputs.get(idx).copied()
&& let Some(value) = graph.try_value(vid)
&& let Some(sym) = kv_growing_symbol(&value.shape)
{
pinned.insert(sym);
}
}
for &idx in slots.present_outputs {
if let Some(&vid) = node.outputs.get(idx)
&& let Some(value) = graph.try_value(vid)
&& let Some(sym) = kv_growing_symbol(&value.shape)
{
pinned.insert(sym);
}
}
}
pinned
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum CaptureClassifier {
FailSafe,
Denylist,
}
impl CaptureClassifier {
fn from_env() -> Self {
match std::env::var("ONNX_GENAI_CAPTURE_CLASSIFIER") {
Ok(v) => match v.trim().to_ascii_lowercase().as_str() {
"denylist" | "deny" | "0" => Self::Denylist,
_ => Self::FailSafe,
},
Err(_) => Self::FailSafe,
}
}
}
pub(super) fn compute_capture_disqualifying_symbols(graph: &Graph) -> HashSet<SymbolId> {
let mode = CaptureClassifier::from_env();
let set = match mode {
CaptureClassifier::FailSafe => compute_not_pinned_symbols(graph),
CaptureClassifier::Denylist => compute_capture_growing_symbols(graph),
};
if std::env::var("ONNX_GENAI_LOG_GROWING_SYMBOLS").is_ok() {
eprintln!(
"[onnx-genai-capture] classifier={mode:?} build-time disqualifying-symbol set: {} symbol(s): {:?}",
set.len(),
set
);
}
set
}
pub(super) fn compute_capture_disqualifying_symbols_excluding(
graph: &Graph,
pinned: &HashSet<SymbolId>,
) -> HashSet<SymbolId> {
let mode = CaptureClassifier::from_env();
let set = match mode {
CaptureClassifier::FailSafe => compute_not_pinned_symbols_excluding(graph, pinned),
CaptureClassifier::Denylist => compute_capture_growing_symbols_excluding(graph, pinned),
};
if std::env::var("ONNX_GENAI_LOG_GROWING_SYMBOLS").is_ok() {
eprintln!(
"[onnx-genai-capture] classifier={mode:?} build-time disqualifying-symbol set: \
{} symbol(s): {:?} (pinned-capacity KV: {:?})",
set.len(),
set,
pinned,
);
}
set
}
pub(super) fn compute_not_pinned_symbols(graph: &Graph) -> HashSet<SymbolId> {
compute_not_pinned_symbols_excluding(graph, &HashSet::new())
}
pub(super) fn compute_not_pinned_symbols_excluding(
graph: &Graph,
pinned: &HashSet<SymbolId>,
) -> HashSet<SymbolId> {
let mut set = collect_structural_growing_symbols_excluding(graph, pinned);
set.extend(graph.symbol_opaque.iter().copied());
if let Some(floor) = graph.inference_symbol_floor {
let has_provenance: HashSet<SymbolId> =
graph.symbol_derivations.iter().map(|&(d, _)| d).collect();
let candidates = graph
.symbol_constraints
.keys()
.copied()
.chain(graph.symbol_derivations.iter().flat_map(|&(d, s)| [d, s]))
.chain(graph.symbol_unifications.iter().flat_map(|&(a, b)| [a, b]));
for sym in candidates {
if sym.0 >= floor && !has_provenance.contains(&sym) {
set.insert(sym);
}
}
}
close_disqualifying_set(graph, &mut set);
if !pinned.is_empty() {
set.retain(|sym| !pinned.contains(sym));
}
set
}
fn close_disqualifying_set(graph: &Graph, set: &mut HashSet<SymbolId>) {
if graph.symbol_unifications.is_empty() && graph.symbol_derivations.is_empty() {
return;
}
let mut adj: HashMap<SymbolId, Vec<SymbolId>> = HashMap::new();
for &(a, b) in &graph.symbol_unifications {
if a != b {
adj.entry(a).or_default().push(b);
adj.entry(b).or_default().push(a);
}
}
for &(derived, source) in &graph.symbol_derivations {
if derived != source {
adj.entry(source).or_default().push(derived);
}
}
if adj.is_empty() {
return;
}
let mut work: Vec<SymbolId> = set.iter().copied().collect();
while let Some(sym) = work.pop() {
let Some(neighbors) = adj.get(&sym) else {
continue;
};
for &next in neighbors {
if set.insert(next) {
work.push(next);
}
}
}
}
pub(super) fn node_capture_seq_independent(
graph: &Graph,
node: &Node,
growing: &HashSet<SymbolId>,
) -> bool {
if node.outputs.is_empty() {
return false;
}
let edge_free_of_growing = |vid: ValueId| {
graph
.try_value(vid)
.is_none_or(|value| !shape_references_any(&value.shape, growing))
};
node.outputs.iter().all(|&vid| edge_free_of_growing(vid))
&& node
.inputs
.iter()
.all(|input| input.is_none_or(edge_free_of_growing))
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct CacheStats {
pub entries: usize,
pub hits: u64,
pub misses: u64,
pub prebind_hits: u64,
pub evictions: u64,
}
#[derive(Default)]
pub(crate) struct KernelCache {
pub(super) entries: HashMap<KernelKey, Box<dyn onnx_runtime_ep_api::Kernel>>,
pub(super) last_used: HashMap<KernelKey, AtomicU64>,
pub(super) clock: AtomicU64,
pub(super) hits: u64,
pub(super) misses: u64,
pub(super) evictions: u64,
pub(super) prebind_hits: AtomicU64,
block_quantized_moe_traffic_request: Option<u32>,
}
const DEFAULT_VARIANTS_PER_NODE: usize = 4;
fn variants_per_node() -> usize {
static RESOLVED: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
*RESOLVED.get_or_init(|| {
std::env::var("ONNX_RUNTIME_KERNEL_CACHE_VARIANTS_PER_NODE")
.ok()
.and_then(|value| value.trim().parse::<usize>().ok())
.filter(|variants| *variants > 0)
.unwrap_or(DEFAULT_VARIANTS_PER_NODE)
})
}
impl KernelCache {
#[inline]
pub(super) fn contains(&self, key: &KernelKey) -> bool {
self.entries.contains_key(key)
}
pub(super) fn arm_block_quantized_moe_traffic(&mut self, request_id: u32) -> Result<usize> {
let mut visited = HashSet::new();
let mut armed = 0;
let mut failure = None;
for (key, kernel) in &mut self.entries {
if visited.insert(key.node) {
match kernel.arm_block_quantized_moe_traffic(request_id) {
Ok(true) => armed += 1,
Ok(false) => {}
Err(error) => {
failure = Some(error);
break;
}
}
}
}
if let Some(error) = failure {
let mut rollback = HashSet::new();
for (key, kernel) in &mut self.entries {
if rollback.insert(key.node) {
let _ = kernel.disarm_block_quantized_moe_traffic();
}
}
return Err(error.into());
}
self.block_quantized_moe_traffic_request = (armed != 0).then_some(request_id);
Ok(armed)
}
pub(super) fn reset_block_quantized_moe_traffic(&mut self) -> Result<()> {
let mut visited = HashSet::new();
for (key, kernel) in &mut self.entries {
if visited.insert(key.node) {
kernel.reset_block_quantized_moe_traffic()?;
}
}
Ok(())
}
pub(super) fn snapshot_block_quantized_moe_traffic(
&self,
) -> Result<onnx_runtime_ep_api::BlockQuantizedMoeTraffic> {
let mut visited = HashSet::new();
let mut total = onnx_runtime_ep_api::BlockQuantizedMoeTraffic::default();
let mut physical_dram_bytes = 0_u64;
let mut physical_complete = true;
let mut observed = false;
for (key, kernel) in &self.entries {
if !visited.insert(key.node) {
continue;
}
let Some(snapshot) = kernel.snapshot_block_quantized_moe_traffic()? else {
continue;
};
observed = true;
total.uploaded_whole_bank_bytes = total
.uploaded_whole_bank_bytes
.checked_add(snapshot.uploaded_whole_bank_bytes)
.ok_or_else(|| SessionError::Internal("uploaded BQMoE bytes overflow".into()))?;
total.committed_whole_bank_bytes = total
.committed_whole_bank_bytes
.checked_add(snapshot.committed_whole_bank_bytes)
.ok_or_else(|| SessionError::Internal("committed BQMoE bytes overflow".into()))?;
total.logical_route_demand_bytes = total
.logical_route_demand_bytes
.checked_add(snapshot.logical_route_demand_bytes)
.ok_or_else(|| SessionError::Internal("logical BQMoE bytes overflow".into()))?;
total.unique_selected_expert_bytes = total
.unique_selected_expert_bytes
.checked_add(snapshot.unique_selected_expert_bytes)
.ok_or_else(|| SessionError::Internal("unique BQMoE bytes overflow".into()))?;
total.page_ins = total
.page_ins
.checked_add(snapshot.page_ins)
.ok_or_else(|| SessionError::Internal("BQMoE page-ins overflow".into()))?;
match snapshot.physical_dram_bytes {
Some(bytes) => {
physical_dram_bytes =
physical_dram_bytes.checked_add(bytes).ok_or_else(|| {
SessionError::Internal("physical BQMoE bytes overflow".into())
})?;
}
None => physical_complete = false,
}
}
total.physical_dram_bytes = (observed && physical_complete).then_some(physical_dram_bytes);
total.byte_hit_rate = total.physical_dram_bytes.and_then(|physical| {
(total.logical_route_demand_bytes != 0)
.then(|| 1.0 - (physical as f64 / total.logical_route_demand_bytes as f64).min(1.0))
});
Ok(total)
}
#[cfg(feature = "gpu-tests")]
pub(super) fn inject_block_quantized_moe_traffic_fault_for_test(
&self,
fault: onnx_runtime_ep_cuda::kernels::block_quantized_moe::BlockQuantizedMoeTrafficFaultForTest,
) -> Result<()> {
let mut visited = HashSet::new();
let mut injected = 0usize;
for (key, kernel) in &self.entries {
if !visited.insert(key.node) {
continue;
}
let Some(kernel) = kernel
.as_any()
.downcast_ref::<onnx_runtime_ep_cuda::kernels::block_quantized_moe::BlockQuantizedMoEKernel>()
else {
continue;
};
kernel.inject_route_telemetry_fault_for_test(fault)?;
injected += 1;
}
if injected == 0 {
return Err(SessionError::Internal(
"no CUDA BlockQuantizedMoE kernel was available for traffic fault injection".into(),
));
}
Ok(())
}
pub(super) fn disarm_block_quantized_moe_traffic(&mut self) -> Result<()> {
let mut visited = HashSet::new();
for (key, kernel) in &mut self.entries {
if visited.insert(key.node) {
kernel.disarm_block_quantized_moe_traffic()?;
}
}
self.block_quantized_moe_traffic_request = None;
Ok(())
}
fn tick(&self) -> u64 {
self.clock.fetch_add(1, Ordering::Relaxed)
}
fn touch(&self, key: &KernelKey) {
if let Some(slot) = self.last_used.get(key) {
slot.store(self.tick(), Ordering::Relaxed);
}
}
fn evict_surplus_variants(
&mut self,
node: u32,
ep: &dyn ExecutionProvider,
graph_tokens: [Option<DeviceGraphToken>; DeviceGraphSlot::COUNT],
) -> Result<()> {
let bound = variants_per_node();
let mut variants = self
.entries
.keys()
.filter(|key| key.node == node)
.map(|key| {
let used = self
.last_used
.get(key)
.map(|slot| slot.load(Ordering::Relaxed))
.unwrap_or(0);
(used, key.clone())
})
.collect::<Vec<_>>();
if variants.len() <= bound {
return Ok(());
}
variants.sort_by_key(|(used, _)| *used);
let surplus = variants.len() - bound;
for token in graph_tokens.into_iter().flatten() {
ep.reset_owned_device_graph(token)?;
}
for (_, key) in variants.into_iter().take(surplus) {
self.entries.remove(&key);
self.last_used.remove(&key);
self.evictions += 1;
}
Ok(())
}
pub(super) fn stats(&self) -> CacheStats {
CacheStats {
entries: self.entries.len(),
hits: self.hits,
misses: self.misses,
prebind_hits: self.prebind_hits.load(Ordering::Relaxed),
evictions: self.evictions,
}
}
#[inline]
pub(super) fn get_prebound<'a>(
&'a self,
binding: &KernelKey,
input_shapes: &[Vec<usize>],
) -> Option<&'a dyn onnx_runtime_ep_api::Kernel> {
if !binding.matches_shapes(input_shapes) {
return None;
}
let kernel = self.entries.get(binding)?.as_ref();
self.touch(binding);
self.prebind_hits.fetch_add(1, Ordering::Relaxed);
#[cfg(test)]
PREBIND_FAST_PATH_TEST_HITS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
Some(kernel)
}
#[inline]
pub(super) fn has_prebound(&self, binding: &KernelKey, input_shapes: &[Vec<usize>]) -> bool {
binding.matches_shapes(input_shapes) && self.entries.contains_key(binding)
}
#[allow(clippy::too_many_arguments)]
pub(super) fn get_or_create(
&mut self,
node_id: NodeId,
node: &Node,
input_shapes: &[Vec<usize>],
input_dtypes: &[DataType],
constant_inputs: &[bool],
constant_values: &[Option<KernelConstantInput<'_>>],
opset: u64,
capture_seq_independent: bool,
artifact_config: ExecutorArtifactConfig,
artifact_readiness: &mut ProviderArtifactReadiness,
ep: &dyn ExecutionProvider,
graph_tokens: [Option<DeviceGraphToken>; DeviceGraphSlot::COUNT],
) -> Result<(&dyn onnx_runtime_ep_api::Kernel, KernelKey)> {
let key = KernelKey {
node: node_id.0,
shapes: input_shapes.to_vec(),
};
if self.entries.contains_key(&key) {
self.hits += 1;
} else {
let next_misses = self.misses.checked_add(1).ok_or_else(|| {
EpError::KernelFailed(
"kernel cache miss counter exhausted; refusing to wrap provider artifact \
readiness"
.to_string(),
)
})?;
let next_readiness = artifact_readiness.checked_next_epoch()?;
let shared_constant_state = self
.entries
.iter()
.find(|(existing, _)| existing.node == key.node)
.and_then(|(_, kernel)| kernel.shareable_constant_state());
let shape_dims: Vec<Shape> = input_shapes
.iter()
.map(|s| s.iter().map(|&d| Dim::Static(d)).collect())
.collect();
let layouts = vec![TensorLayout::contiguous(); input_shapes.len()];
if let KernelMatch::Unsupported { reason } =
ep.supports_op(node, opset, &shape_dims, input_dtypes, &layouts)
{
return Err(SessionError::unsupported_op(
node,
node_id,
opset,
ep.name(),
reason,
));
}
let mut kernel = match ep.get_kernel_for_executor(
artifact_config.provider(),
artifact_config.executor(),
artifact_config.generation(),
node,
input_shapes,
opset,
) {
Ok(kernel) => kernel,
Err(EpError::NoEpForOp {
domain,
op_type,
opset,
}) => {
return Err(SessionError::unsupported_op(
node,
node_id,
opset,
ep.name(),
format!(
"no handler for {domain}::{op_type} at opset {opset} — add a claim+handler"
),
));
}
Err(error) => return Err(error.into()),
};
kernel.set_constant_inputs(constant_inputs);
let adopted = if let Some(state) = shared_constant_state {
kernel.adopt_shareable_constant_state(state)?
} else {
false
};
if !adopted {
kernel.prepare_constant_inputs(constant_values, ep)?;
}
if !adopted && let Some(request_id) = self.block_quantized_moe_traffic_request {
kernel.arm_block_quantized_moe_traffic(request_id)?;
}
kernel.set_capture_seq_independent(capture_seq_independent);
self.entries.insert(key.clone(), kernel);
self.last_used
.insert(key.clone(), AtomicU64::new(self.tick()));
self.misses = next_misses;
artifact_readiness.advance_to(next_readiness);
self.evict_surplus_variants(key.node, ep, graph_tokens)?;
}
self.touch(&key);
#[cfg(test)]
PREBIND_FALLBACK_TEST_HITS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let kernel_ref = self.entries.get(&key).expect("just inserted").as_ref();
Ok((kernel_ref, key))
}
}