onnx_runtime_optimizer/pass.rs
1//! Pass infrastructure: the [`OptimizationPass`] trait, a minimal
2//! [`PassContext`], and the [`run_passes`] pipeline runner (see
3//! `docs/ORT2.md` §18.1).
4
5use std::sync::Arc;
6
7use onnx_runtime_ir::{Graph, WeightRef};
8
9use crate::error::{OptimizerError, Result};
10
11/// Resolves graph initializer descriptors to their backing bytes.
12pub trait InitializerResolver: Send + Sync {
13 /// Resolve an initializer descriptor to its raw little-endian bytes.
14 fn bytes<'a>(&'a self, weight: &'a WeightRef) -> Option<&'a [u8]>;
15}
16
17/// Shared, read-only context threaded through every pass.
18///
19/// **Phase-1 minimalism.** The design in `docs/ORT2.md` §18.1 gives this struct
20/// `cost_model`, `ep_registry`, and `target_devices` fields. Those depend on
21/// crates or analyses that do not exist yet, and none of the device-independent
22/// Phase-1 passes
23/// ([`DeadNodeElimination`](crate::DeadNodeElimination),
24/// [`ConstantFolding`](crate::ConstantFolding), [`OpFusion`](crate::OpFusion))
25/// need them. The only current service is an optional initializer resolver for
26/// EP-scoped passes that physically rewrite immutable weights.
27///
28/// It is `#[non_exhaustive]` so the Phase-2b cost-model / EP-registry /
29/// placement fields can be added without breaking downstream construction.
30#[derive(Clone, Default)]
31#[non_exhaustive]
32pub struct PassContext {
33 initializer_resolver: Option<Arc<dyn InitializerResolver>>,
34 // Phase 2b (deferred): pub cost_model: Arc<CostModel>,
35 // pub ep_registry: Arc<EpRegistry>,
36 // pub target_devices: Vec<DeviceId>,
37}
38
39impl std::fmt::Debug for PassContext {
40 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41 f.debug_struct("PassContext")
42 .field(
43 "initializer_resolver",
44 &self.initializer_resolver.as_ref().map(|_| "<resolver>"),
45 )
46 .finish()
47 }
48}
49
50impl PassContext {
51 /// A context with no device/cost information (the Phase-1 default).
52 pub fn new() -> Self {
53 Self::default()
54 }
55
56 /// Attach the resolver that exposes inline or externally mapped initializer
57 /// bytes to passes that rewrite immutable weights.
58 pub fn with_initializer_resolver(mut self, resolver: Arc<dyn InitializerResolver>) -> Self {
59 self.initializer_resolver = Some(resolver);
60 self
61 }
62
63 /// Resolve an initializer's raw bytes. Inline initializers are always
64 /// available; external references require an attached resolver.
65 pub fn initializer_bytes<'b>(&'b self, weight: &'b WeightRef) -> Option<&'b [u8]> {
66 match weight {
67 WeightRef::Inline(tensor) => Some(&tensor.data),
68 WeightRef::External { .. } => self.initializer_resolver.as_deref()?.bytes(weight),
69 }
70 }
71}
72
73/// A single graph→graph rewrite (see `docs/ORT2.md` §18.1).
74///
75/// Passes mutate the [`Graph`] in place and must preserve its structural
76/// invariants; [`postconditions`](OptimizationPass::postconditions) is checked
77/// after each pass in debug builds by [`run_passes`].
78pub trait OptimizationPass: Send + Sync {
79 /// A short, stable name for logging and error messages.
80 fn name(&self) -> &str;
81
82 /// Apply the rewrite in place.
83 fn run(&self, graph: &mut Graph, ctx: &PassContext) -> Result<()>;
84
85 /// Invariants that must hold after this pass. The default requires the
86 /// graph to pass full structural validation (`Graph::validate`).
87 fn postconditions(&self, graph: &Graph) -> Result<()> {
88 graph
89 .validate()
90 .map_err(|errors| OptimizerError::PostconditionFailed {
91 pass: self.name().to_string(),
92 errors,
93 })
94 }
95}
96
97/// Run `passes` over `graph` in order.
98///
99/// Each pass runs to completion, then — in debug builds only — its
100/// [`postconditions`](OptimizationPass::postconditions) are checked. In release
101/// builds the postcondition check is compiled out for speed, matching the
102/// "checked in debug builds" contract of `docs/ORT2.md` §18.1.
103pub fn run_passes(
104 graph: &mut Graph,
105 passes: &[Box<dyn OptimizationPass>],
106 ctx: &PassContext,
107) -> Result<()> {
108 for pass in passes {
109 pass.run(graph, ctx)?;
110 #[cfg(debug_assertions)]
111 pass.postconditions(graph)?;
112 }
113 Ok(())
114}