Skip to main content

onnx_runtime_loader/
lib.rs

1//! # `onnx-runtime-loader`
2//!
3//! Loads ONNX models from disk into the [`onnx_runtime_ir::Graph`] IR
4//! (see `docs/ORT2.md` §19).
5//!
6//! Pipeline ([`load_model`] / [`load_model_with_weights`]):
7//! 1. [`proto`] — decode the ONNX protobuf (`prost` types generated from the
8//!    vendored `onnx.proto3`) into a `ModelProto`.
9//! 2. [`graph_builder`] — build an [`onnx_runtime_ir::Graph`] (nodes, values,
10//!    symbolic dim interning, opset imports), upholding the §3.5 invariants.
11//! 3. [`weights`] — resolve inline and external initializer data (external
12//!    files are memory-mapped into a [`WeightStore`]).
13//! 4. Static/symbolic shape inference via
14//!    [`onnx-runtime-shape-inference`](onnx_runtime_shape_inference): the loader
15//!    owns the "loader = shape-inference" seam, so after the [`Graph`] is built
16//!    (with initializers applied) it runs the extensible per-op registry to
17//!    populate every value's shape and dtype. Values that cannot be resolved
18//!    statically (genuinely data-dependent extents) are left symbolic for the
19//!    session to resolve just-in-time.
20//!
21//! ## Obtaining weight bytes at session time
22//!
23//! Use [`load_model_with_weights`] (or [`load_model_bytes_with_weights`]) to
24//! receive both the [`Graph`] and an [`Arc<WeightStore>`]. Then, given any
25//! [`onnx_runtime_ir::WeightRef`] stored in `graph.initializers`, call
26//! [`WeightStore::bytes`] to get the raw little-endian byte slice:
27//!
28//! ```ignore
29//! let (graph, store) = load_model_with_weights("model.onnx")?;
30//! for (vid, weight_ref) in &graph.initializers {
31//!     let bytes: &[u8] = store.bytes(weight_ref).expect("weight bytes live");
32//!     // ... hand bytes to a kernel
33//! }
34//! ```
35//!
36//! The `Arc` keeps all memory maps alive as long as any clone of it exists, so
37//! kernel dispatch can store `Arc<WeightStore>` alongside the `Graph` without
38//! lifetime coupling.
39
40use std::path::Path;
41use std::sync::Arc;
42
43use onnx_runtime_ir::Graph;
44use onnx_runtime_shape_inference::{InferenceRegistry, MergePolicy};
45
46use crate::graph_builder::BuiltGraph;
47
48pub mod encoder;
49pub mod epcontext;
50pub mod graph_builder;
51pub mod proto;
52pub mod weights;
53pub mod writer;
54
55mod pathsafe;
56
57pub use encoder::{
58    encode_model, encode_model_proto, write_model, Model, ModelMetadata, DEFAULT_IR_VERSION,
59};
60pub use epcontext::{
61    ep_context_node_ids, ep_context_nodes, is_ep_context_op, resolve_ep_context, EmbedMode,
62    EpContextBlob, EpContextNode,
63};
64pub use error::LoaderError;
65pub use weights::WeightStore;
66pub use writer::{dump_ep_context, EpContextDumpConfig, EpContextPartition};
67
68mod error {
69    use std::path::PathBuf;
70
71    /// Errors produced while loading an ONNX model.
72    #[derive(Debug, thiserror::Error)]
73    pub enum LoaderError {
74        #[error("failed to read model file {path}: {source}")]
75        Io {
76            path: PathBuf,
77            #[source]
78            source: std::io::Error,
79        },
80
81        #[error("failed to parse ONNX protobuf: {0}")]
82        ProtobufParse(String),
83
84        #[error("unsupported opset: domain={domain}, version={version}")]
85        UnsupportedOpset { domain: String, version: u64 },
86
87        #[error("external data file not found: {path}")]
88        ExternalDataNotFound { path: PathBuf },
89
90        #[error("external data path rejected ({reason}): {path}")]
91        ExternalDataPath { path: String, reason: &'static str },
92
93        #[error("weight mmap failed: {0}")]
94        Mmap(String),
95
96        #[error("EPContext node error: {0}")]
97        EpContext(String),
98
99        #[error("EPContext external path rejected ({reason}): {path}")]
100        EpContextPath { path: String, reason: &'static str },
101
102        #[error("graph construction failed: {0}")]
103        GraphBuild(String),
104
105        #[error("unsupported ONNX data_type {raw} at {context}")]
106        UnsupportedDataType { raw: i32, context: String },
107
108        #[error("shape inference failed: {0}")]
109        ShapeInference(#[from] onnx_runtime_shape_inference::ShapeInferError),
110
111        #[error(transparent)]
112        Ir(#[from] onnx_runtime_ir::IrError),
113    }
114}
115
116/// Load a model from a filesystem path, producing a fully-built [`Graph`].
117///
118/// Runs the full pipeline: parse → build → load weights → shape inference.
119/// External initializer data is resolved relative to the model file's
120/// directory.
121///
122/// # Note on external weights
123///
124/// The returned `Graph` stores [`onnx_runtime_ir::WeightRef::External`]
125/// descriptors (path / offset / length) for weights held in external data
126/// files, but the memory maps that back those bytes are **dropped** when this
127/// function returns. Callers that need to read external weight bytes must
128/// either re-map the files themselves or use [`load_model_with_weights`] which
129/// keeps the maps alive via the returned [`Arc<WeightStore>`].
130pub fn load_model(path: impl AsRef<Path>) -> Result<Graph, LoaderError> {
131    Ok(load_model_with_weights(path)?.0)
132}
133
134/// Load a model from an in-memory protobuf buffer, producing a [`Graph`].
135///
136/// External initializer data (if any) is resolved relative to the current
137/// working directory.
138///
139/// # Note on external weights
140///
141/// Same caveat as [`load_model`]: external weight bytes are not accessible
142/// from the returned `Graph` alone. Use [`load_model_bytes_with_weights`] to
143/// keep them live.
144pub fn load_model_bytes(bytes: &[u8]) -> Result<Graph, LoaderError> {
145    Ok(load_model_bytes_with_weights(bytes, Path::new("."))?.0)
146}
147
148/// Load a model from a filesystem path, returning both the [`Graph`] and the
149/// live [`WeightStore`] that backs all initializer data.
150///
151/// The [`Arc<WeightStore>`] keeps every external-data memory map alive for as
152/// long as any clone of the `Arc` exists. At session time, given a
153/// [`onnx_runtime_ir::WeightRef`] from `graph.initializers`, call
154/// [`WeightStore::bytes`] to obtain the raw little-endian byte slice — this
155/// works for both [`WeightRef::Inline`] and [`WeightRef::External`] weights.
156///
157/// External initializer data is resolved relative to the model file's
158/// directory.
159pub fn load_model_with_weights(
160    path: impl AsRef<Path>,
161) -> Result<(Graph, Arc<WeightStore>), LoaderError> {
162    let path = path.as_ref();
163    let bytes = std::fs::read(path).map_err(|source| LoaderError::Io {
164        path: path.to_path_buf(),
165        source,
166    })?;
167    let model_dir = path.parent().unwrap_or_else(|| Path::new("."));
168    build_from_bytes_with_weights(&bytes, model_dir)
169}
170
171/// Load a model from an in-memory protobuf buffer, returning both the
172/// [`Graph`] and the live [`WeightStore`] that backs all initializer data.
173///
174/// External initializer data (if any) is resolved relative to `base_dir`.
175/// The [`Arc<WeightStore>`] keeps every memory map alive for as long as any
176/// clone of the `Arc` exists.
177pub fn load_model_bytes_with_weights(
178    bytes: &[u8],
179    base_dir: impl AsRef<Path>,
180) -> Result<(Graph, Arc<WeightStore>), LoaderError> {
181    build_from_bytes_with_weights(bytes, base_dir.as_ref())
182}
183
184fn build_from_bytes_with_weights(
185    bytes: &[u8],
186    model_dir: &Path,
187) -> Result<(Graph, Arc<WeightStore>), LoaderError> {
188    let model = proto::decode_model(bytes)?;
189    let BuiltGraph {
190        mut graph,
191        name_map,
192    } = graph_builder::build_graph(&model)?;
193
194    let store = weights::load_weights(&model, model_dir, &name_map)?;
195    // Copy descriptors into the graph; the store's mmaps stay alive via Arc.
196    for (&vid, weight) in &store.weights {
197        graph.set_initializer(vid, weight.clone());
198    }
199
200    // Static/symbolic shape inference (the loader owns this seam). Run the
201    // extensible per-op registry over the fully-built graph — inputs,
202    // initializers, and node outputs — to populate every value's shape and
203    // dtype. `Permissive`: prefer the more specific dim on a benign
204    // disagreement and keep going, and reconcile graph outputs with their
205    // declared shapes rather than clobbering them. Values that stay symbolic
206    // (genuinely data-dependent extents) are left for the session's JIT
207    // fallback to resolve at run time.
208    let registry = InferenceRegistry::default_registry();
209    let opset_imports = graph.opset_imports.clone();
210    registry.infer_graph(&mut graph, &opset_imports, MergePolicy::Permissive)?;
211
212    Ok((graph, Arc::new(store)))
213}