Skip to main content

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(
59        mut self,
60        resolver: Arc<dyn InitializerResolver>,
61    ) -> Self {
62        self.initializer_resolver = Some(resolver);
63        self
64    }
65
66    /// Resolve an initializer's raw bytes. Inline initializers are always
67    /// available; external references require an attached resolver.
68    pub fn initializer_bytes<'b>(&'b self, weight: &'b WeightRef) -> Option<&'b [u8]> {
69        match weight {
70            WeightRef::Inline(tensor) => Some(&tensor.data),
71            WeightRef::External { .. } => self.initializer_resolver.as_deref()?.bytes(weight),
72        }
73    }
74}
75
76/// A single graph→graph rewrite (see `docs/ORT2.md` §18.1).
77///
78/// Passes mutate the [`Graph`] in place and must preserve its structural
79/// invariants; [`postconditions`](OptimizationPass::postconditions) is checked
80/// after each pass in debug builds by [`run_passes`].
81pub trait OptimizationPass: Send + Sync {
82    /// A short, stable name for logging and error messages.
83    fn name(&self) -> &str;
84
85    /// Apply the rewrite in place.
86    fn run(&self, graph: &mut Graph, ctx: &PassContext) -> Result<()>;
87
88    /// Invariants that must hold after this pass. The default requires the
89    /// graph to pass full structural validation (`Graph::validate`).
90    fn postconditions(&self, graph: &Graph) -> Result<()> {
91        graph
92            .validate()
93            .map_err(|errors| OptimizerError::PostconditionFailed {
94                pass: self.name().to_string(),
95                errors,
96            })
97    }
98}
99
100/// Run `passes` over `graph` in order.
101///
102/// Each pass runs to completion, then — in debug builds only — its
103/// [`postconditions`](OptimizationPass::postconditions) are checked. In release
104/// builds the postcondition check is compiled out for speed, matching the
105/// "checked in debug builds" contract of `docs/ORT2.md` §18.1.
106pub fn run_passes(
107    graph: &mut Graph,
108    passes: &[Box<dyn OptimizationPass>],
109    ctx: &PassContext,
110) -> Result<()> {
111    for pass in passes {
112        pass.run(graph, ctx)?;
113        #[cfg(debug_assertions)]
114        pass.postconditions(graph)?;
115    }
116    Ok(())
117}