onnx_runtime_loader/epcontext.rs
1//! `com.microsoft::EPContext` node support — the load path (§55.3).
2//!
3//! An `EPContext` node is the **on-disk / interchange form** of a compiled-EP
4//! context (§55.1): an ordinary ONNX contrib node (`op_type = "EPContext"`,
5//! `domain = "com.microsoft"`) that either embeds a pre-compiled vendor blob
6//! inline or references it in an external file. This module provides:
7//!
8//! * [`EpContextNode`] — a typed *view* over an ordinary IR [`Node`], parsing
9//! the §55.2 attributes. No new IR node kind is introduced.
10//! * [`ep_context_nodes`] — enumerate the `EPContext` nodes in a [`Graph`].
11//! * [`resolve_ep_context`] — turn the `ep_cache_context` attribute into an
12//! [`EpContextBlob`] (inline bytes, or a read-only `mmap` of an external file
13//! resolved relative to the model directory, §55.3).
14//!
15//! The loader **never interprets** the blob bytes — they are opaque vendor
16//! payload handed to whichever EP claims the node's `source` key (§55.6),
17//! which is out of scope here (owned by the session + ep-api).
18
19use std::fs::File;
20use std::path::{Path, PathBuf};
21
22use memmap2::Mmap;
23use onnx_runtime_ir::{Attribute, Graph, Node, NodeId, ValueId};
24
25use crate::{pathsafe::guarded_join, LoaderError};
26
27/// The op type of an EPContext node. Shared with the §55.4 writer so the dump
28/// path never re-invents the literal.
29pub(crate) const EP_CONTEXT_OP: &str = "EPContext";
30/// The (single, non-aliased) domain EPContext nodes live in. The shape-inference
31/// registry only normalises the default ONNX domain (`ai.onnx` ⇄ `""`); there is
32/// no `com.microsoft` alias to normalise, so an exact match is correct (§55.3).
33/// Shared with the §55.4 writer (opset import + node domain).
34pub(crate) const MS_DOMAIN: &str = "com.microsoft";
35
36/// Attribute names (§55.2). Shared between the load path (this module) and the
37/// §55.4 dump path (`crate::writer`) so both sides key on identical strings.
38pub(crate) mod attr {
39 pub const MAIN_CONTEXT: &str = "main_context";
40 pub const EP_CACHE_CONTEXT: &str = "ep_cache_context";
41 pub const EMBED_MODE: &str = "embed_mode";
42 pub const EP_SDK_VERSION: &str = "ep_sdk_version";
43 pub const SOURCE: &str = "source";
44 pub const PARTITION_NAME: &str = "partition_name";
45}
46
47/// Whether the payload lives inline in the node or in an external file (§55.2).
48///
49/// Mirrors the `embed_mode` attribute: `1` (or absent, the §55.2 default) means
50/// [`Embedded`](EmbedMode::Embedded); `0` means [`ExternalFile`](EmbedMode::ExternalFile).
51#[derive(Clone, Copy, Debug, PartialEq, Eq)]
52pub enum EmbedMode {
53 /// `embed_mode = 1`: `ep_cache_context` holds the compiled blob inline.
54 Embedded,
55 /// `embed_mode = 0`: `ep_cache_context` holds an external file path
56 /// (relative to the ONNX model file).
57 ExternalFile,
58}
59
60impl EmbedMode {
61 /// Decode the `embed_mode` attribute integer. Only `0` selects the external
62 /// file; every other value (including the §55.2 default `1`) is treated as
63 /// [`Embedded`] — the fail-closed choice, since it never touches the
64 /// filesystem.
65 fn from_attr_int(v: i64) -> Self {
66 match v {
67 0 => EmbedMode::ExternalFile,
68 _ => EmbedMode::Embedded,
69 }
70 }
71}
72
73/// Where the compiled blob physically lives after load-time resolution (§55.3).
74///
75/// The loader treats the bytes as opaque; it never parses them.
76#[derive(Debug)]
77pub enum EpContextBlob {
78 /// `embed_mode = 1`: bytes owned inline (copied out of `ep_cache_context`).
79 Embedded(Vec<u8>),
80 /// `embed_mode = 0`: a read-only `mmap` of an external file resolved
81 /// *relative to the model directory*. Never eagerly copied into the graph.
82 External {
83 /// The resolved absolute path to the external context file.
84 path: PathBuf,
85 /// Read-only memory map of the file's bytes.
86 map: Mmap,
87 },
88}
89
90impl EpContextBlob {
91 /// Borrow the raw blob bytes regardless of where they live.
92 pub fn bytes(&self) -> &[u8] {
93 match self {
94 EpContextBlob::Embedded(b) => b,
95 EpContextBlob::External { map, .. } => &map[..],
96 }
97 }
98}
99
100/// A typed view over a `com.microsoft::EPContext` node in the [`Graph`] IR
101/// (§55.3). Backed by an ordinary [`Node`] plus its attributes — no separate IR
102/// node kind. Constructed via [`EpContextNode::from_node`] or enumerated with
103/// [`ep_context_nodes`].
104///
105/// Variadic boundary tensors are read straight from the underlying node via
106/// [`inputs`](EpContextNode::inputs) / [`outputs`](EpContextNode::outputs); no
107/// fixed arity is assumed.
108#[derive(Debug)]
109pub struct EpContextNode<'g> {
110 /// The id of the backing IR node.
111 pub node: NodeId,
112 /// `source` attribute — the EP dispatch key (§55.6). `None` if absent.
113 pub source: Option<&'g str>,
114 /// `main_context != 0` (§55.2 default `true` when the attribute is absent).
115 pub main_context: bool,
116 /// Embedded vs. external payload (§55.2 default [`EmbedMode::Embedded`]).
117 pub embed_mode: EmbedMode,
118 /// `ep_sdk_version` attribute — SDK/toolchain version (diagnostics).
119 pub sdk_version: Option<&'g str>,
120 /// `partition_name` attribute — the ORT-partitioned graph name.
121 pub partition_name: Option<&'g str>,
122 /// The backing node, used to reach the variadic i/o and the raw
123 /// `ep_cache_context` payload.
124 inner: &'g Node,
125}
126
127impl<'g> EpContextNode<'g> {
128 /// Build the typed view if `node` is a `com.microsoft::EPContext` node,
129 /// otherwise `None`. Attribute parsing is defensive: missing or wrong-typed
130 /// attributes fall back to the §55.2 defaults rather than erroring.
131 pub fn from_node(node: &'g Node) -> Option<Self> {
132 if !is_ep_context_op(&node.op_type, &node.domain) {
133 return None;
134 }
135 let a = &node.attributes;
136 let main_context = a
137 .get(attr::MAIN_CONTEXT)
138 .and_then(Attribute::as_int)
139 .map(|v| v != 0)
140 .unwrap_or(true);
141 let embed_mode = a
142 .get(attr::EMBED_MODE)
143 .and_then(Attribute::as_int)
144 .map(EmbedMode::from_attr_int)
145 .unwrap_or(EmbedMode::Embedded);
146 Some(EpContextNode {
147 node: node.id,
148 source: str_attr(node, attr::SOURCE),
149 main_context,
150 embed_mode,
151 sdk_version: str_attr(node, attr::EP_SDK_VERSION),
152 partition_name: str_attr(node, attr::PARTITION_NAME),
153 inner: node,
154 })
155 }
156
157 /// The node's variadic input value slots (partition boundary inputs, in
158 /// order). `None` slots preserve positional arity for skipped optionals.
159 pub fn inputs(&self) -> &'g [Option<ValueId>] {
160 &self.inner.inputs
161 }
162
163 /// The node's variadic output values (partition boundary outputs, in order).
164 pub fn outputs(&self) -> &'g [ValueId] {
165 &self.inner.outputs
166 }
167
168 /// The backing [`Node`].
169 pub fn inner(&self) -> &'g Node {
170 self.inner
171 }
172
173 /// Raw `ep_cache_context` bytes, if present. STRING attributes are stored in
174 /// the IR as raw bytes (see `Attribute::String`), so a binary payload is
175 /// never mangled by UTF-8 decoding. A `UINT8`/opaque tensor is accepted as a
176 /// fallback (e.g. a hand-built graph) (§55.3).
177 fn ep_cache_context_bytes(&self) -> Option<&'g [u8]> {
178 match self.inner.attributes.get(attr::EP_CACHE_CONTEXT)? {
179 Attribute::String(s) => Some(s),
180 Attribute::Tensor(t) => Some(&t.data),
181 _ => None,
182 }
183 }
184}
185
186/// Whether `(op_type, domain)` identifies a `com.microsoft::EPContext` node.
187pub fn is_ep_context_op(op_type: &str, domain: &str) -> bool {
188 op_type == EP_CONTEXT_OP && domain == MS_DOMAIN
189}
190
191/// Read a non-empty string attribute, returning `None` when absent, empty, or
192/// not a string.
193fn str_attr<'g>(node: &'g Node, name: &str) -> Option<&'g str> {
194 node.attributes
195 .get(name)
196 .and_then(Attribute::as_str)
197 .filter(|s| !s.is_empty())
198}
199
200/// Enumerate the `com.microsoft::EPContext` nodes in `graph` as typed views
201/// (§55.3, recognition helper). The session dispatches each one on its `source`
202/// key via the `EpContextRegistry` (§55.6, owned by ep-api/session).
203pub fn ep_context_nodes(graph: &Graph) -> impl Iterator<Item = EpContextNode<'_>> {
204 graph.nodes.values().filter_map(EpContextNode::from_node)
205}
206
207/// The [`NodeId`]s of the `EPContext` nodes in `graph`, in node-arena order.
208pub fn ep_context_node_ids(graph: &Graph) -> Vec<NodeId> {
209 ep_context_nodes(graph).map(|n| n.node).collect()
210}
211
212/// Resolve the payload for one `EPContext` node (§55.3).
213///
214/// * `embed_mode = 1` → copy the inline `ep_cache_context` bytes into
215/// [`EpContextBlob::Embedded`].
216/// * `embed_mode = 0` → treat `ep_cache_context` as a path **relative to the
217/// model directory** (identical policy to external-weight resolution, §19.2),
218/// guard it against traversal/absolute escapes, then open + `mmap` it
219/// read-only into [`EpContextBlob::External`]. The bytes are never eagerly
220/// copied into the graph.
221pub fn resolve_ep_context(
222 model_dir: &Path,
223 n: &EpContextNode,
224) -> Result<EpContextBlob, LoaderError> {
225 let raw = n.ep_cache_context_bytes().ok_or_else(|| {
226 LoaderError::EpContext(format!(
227 "EPContext node {:?} is missing the 'ep_cache_context' attribute",
228 n.node
229 ))
230 })?;
231
232 match n.embed_mode {
233 EmbedMode::Embedded => Ok(EpContextBlob::Embedded(raw.to_vec())),
234 EmbedMode::ExternalFile => {
235 let rel = std::str::from_utf8(raw).map_err(|_| {
236 LoaderError::EpContext(format!(
237 "EPContext node {:?}: external 'ep_cache_context' path is not valid UTF-8",
238 n.node
239 ))
240 })?;
241 let path = resolve_external_path(model_dir, rel)?;
242 let file = File::open(&path).map_err(|_| LoaderError::ExternalDataNotFound {
243 path: path.clone(),
244 })?;
245 // SAFETY: identical idiom to `weights.rs`'s external-data mmap — the
246 // `File` is held open for the duration of the map and the bytes are
247 // only ever read immutably (opaque vendor blob). This is the same,
248 // and only, `unsafe` pattern the loader already relies on.
249 let map = unsafe { Mmap::map(&file) }.map_err(|e| LoaderError::Mmap(e.to_string()))?;
250 Ok(EpContextBlob::External { path, map })
251 }
252 }
253}
254
255/// Join a relative external-context path onto `model_dir`, rejecting anything
256/// that escapes the model directory (§55.3 path-safety).
257///
258/// Like external-weight resolution (`weights.rs` §19.2), this rejects absolute
259/// paths, filesystem roots/prefixes, and any `..` component before joining the
260/// stored location to the model directory.
261fn resolve_external_path(model_dir: &Path, rel: &str) -> Result<PathBuf, LoaderError> {
262 guarded_join(model_dir, rel).map_err(|reason| LoaderError::EpContextPath {
263 path: rel.to_string(),
264 reason,
265 })
266}