use std::collections::HashSet;
use std::path::{Path, PathBuf};
use onnx_runtime_ir::{Attribute, Graph, Node, NodeId, ValueId};
use crate::LoaderError;
use crate::encoder::{Model, encode_model};
use crate::epcontext::{EP_CONTEXT_OP, MS_DOMAIN, attr};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EpContextDumpConfig {
pub enable: bool,
pub file_path: Option<PathBuf>,
pub embed_mode: u8,
}
impl Default for EpContextDumpConfig {
fn default() -> Self {
Self {
enable: false,
file_path: None,
embed_mode: 1,
}
}
}
impl EpContextDumpConfig {
fn is_external(&self) -> bool {
self.embed_mode == 0
}
}
#[derive(Clone, Copy, Debug)]
pub struct EpContextPartition<'a> {
pub source: &'a str,
pub ep_sdk_version: &'a str,
pub partition_name: &'a str,
pub main_context: bool,
pub blob: &'a [u8],
pub covered_nodes: &'a [NodeId],
}
pub fn dump_ep_context(
model: &Model,
orig_path: &Path,
partitions: &[EpContextPartition],
config: &EpContextDumpConfig,
) -> Result<PathBuf, LoaderError> {
let out_path = resolve_output_path(orig_path, config);
if !config.enable {
return Ok(out_path);
}
let out_dir = out_path
.parent()
.filter(|p| !p.as_os_str().is_empty())
.map(Path::to_path_buf)
.unwrap_or_else(|| PathBuf::from("."));
let mut graph = model.graph.clone();
graph
.opset_imports
.entry(MS_DOMAIN.to_string())
.or_insert(1);
let graph_outputs: HashSet<ValueId> = graph.outputs.iter().copied().collect();
#[cfg(debug_assertions)]
let original_graph_outputs = graph.outputs.clone();
let mut replacements = Vec::with_capacity(partitions.len());
for (index, part) in partitions.iter().enumerate() {
replacements.push(prepare_partition(
&graph,
&graph_outputs,
&out_dir,
&out_path,
config,
index,
part,
)?);
}
graph.replace_node_groups(replacements, &graph_outputs);
#[cfg(debug_assertions)]
debug_assert_eq!(graph.outputs, original_graph_outputs);
let out_model = Model {
graph: &graph,
metadata: model.metadata.clone(),
weights: model.weights,
};
let bytes = encode_model(&out_model)?;
std::fs::write(&out_path, bytes).map_err(|source| LoaderError::Io {
path: out_path.clone(),
source,
})?;
Ok(out_path)
}
fn prepare_partition(
graph: &Graph,
graph_outputs: &HashSet<ValueId>,
out_dir: &Path,
out_path: &Path,
config: &EpContextDumpConfig,
index: usize,
part: &EpContextPartition,
) -> Result<(Vec<NodeId>, Node), LoaderError> {
let covered: HashSet<NodeId> = part.covered_nodes.iter().copied().collect();
if covered.is_empty() {
return Err(LoaderError::EpContext(
"EPContext dump: partition has no covered nodes".to_string(),
));
}
let (inputs, outputs) = partition_boundary(graph, &covered, graph_outputs);
let ep_cache_context = if config.is_external() {
let rel = sidecar_filename(out_path, index, part.source, part.partition_name);
let sidecar = out_dir.join(&rel);
std::fs::write(&sidecar, part.blob).map_err(|source| LoaderError::Io {
path: sidecar,
source,
})?;
Attribute::String(rel.into_bytes())
} else {
Attribute::String(part.blob.to_vec())
};
let mut node = Node::new(NodeId(0), EP_CONTEXT_OP, inputs, outputs);
node.domain = MS_DOMAIN.to_string();
let attrs = &mut node.attributes;
attrs.insert(
attr::MAIN_CONTEXT.to_string(),
Attribute::Int(part.main_context as i64),
);
attrs.insert(
attr::EMBED_MODE.to_string(),
Attribute::Int(if config.is_external() { 0 } else { 1 }),
);
attrs.insert(
attr::SOURCE.to_string(),
Attribute::String(part.source.as_bytes().to_vec()),
);
if !part.ep_sdk_version.is_empty() {
attrs.insert(
attr::EP_SDK_VERSION.to_string(),
Attribute::String(part.ep_sdk_version.as_bytes().to_vec()),
);
}
if !part.partition_name.is_empty() {
attrs.insert(
attr::PARTITION_NAME.to_string(),
Attribute::String(part.partition_name.as_bytes().to_vec()),
);
}
attrs.insert(attr::EP_CACHE_CONTEXT.to_string(), ep_cache_context);
Ok((part.covered_nodes.to_vec(), node))
}
fn partition_boundary(
graph: &Graph,
covered: &HashSet<NodeId>,
graph_outputs: &HashSet<ValueId>,
) -> (Vec<Option<ValueId>>, Vec<ValueId>) {
let mut ordered: Vec<NodeId> = covered.iter().copied().collect();
ordered.sort_by_key(|n| n.0);
let mut inputs: Vec<Option<ValueId>> = Vec::new();
let mut seen_in: HashSet<ValueId> = HashSet::new();
let mut outputs: Vec<ValueId> = Vec::new();
let mut seen_out: HashSet<ValueId> = HashSet::new();
for nid in &ordered {
let node = graph.node(*nid);
for slot in &node.inputs {
let Some(vid) = *slot else { continue };
let external = match graph.value(vid).producer {
Some(prod) => !covered.contains(&prod),
None => true, };
if external && seen_in.insert(vid) {
inputs.push(Some(vid));
}
}
}
for nid in &ordered {
let node = graph.node(*nid);
for &vid in &node.outputs {
let escapes = graph_outputs.contains(&vid)
|| graph
.consumers(vid)
.into_iter()
.any(|consumer| !covered.contains(&consumer));
if escapes && seen_out.insert(vid) {
outputs.push(vid);
}
}
}
(inputs, outputs)
}
fn resolve_output_path(orig_path: &Path, config: &EpContextDumpConfig) -> PathBuf {
if let Some(p) = &config.file_path {
return p.clone();
}
let dir = orig_path
.parent()
.filter(|p| !p.as_os_str().is_empty())
.map(Path::to_path_buf)
.unwrap_or_else(|| PathBuf::from("."));
let stem = orig_path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("model");
dir.join(format!("{stem}_ctx.onnx"))
}
fn sidecar_filename(out_path: &Path, index: usize, source: &str, partition: &str) -> String {
let stem = out_path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("model");
let src = sanitize_component(source);
let part = sanitize_component(partition);
if part.is_empty() {
format!("{stem}_p{index}_{src}.bin")
} else {
format!("{stem}_p{index}_{src}_{part}.bin")
}
}
fn sanitize_component(s: &str) -> String {
s.chars()
.map(|c| {
if c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.') {
c
} else {
'_'
}
})
.collect()
}