Skip to main content

onnx_runtime_memory/
lib.rs

1//! # `onnx-runtime-memory`
2//!
3//! Liveness-based **activation memory planning** for the ORT 2.0 runtime.
4//!
5//! ## Why this crate exists
6//!
7//! The user's north-star goal is to *run any-size model even when VRAM/RAM is
8//! insufficient, as long as the whole system has enough storage*. Zero-copy
9//! weight streaming already removes weight copies from the budget. The next big
10//! lever is **activation memory**.
11//!
12//! Today the executor allocates **one buffer per graph value for its whole
13//! lifetime**, so peak activation memory is `SUM(every intermediate tensor)`
14//! rather than the *concurrent* peak. Most intermediates are dead long before
15//! the run ends. A liveness-based planner shares one physical buffer among
16//! values whose lifetimes do not overlap, cutting peak activation memory from
17//! `O(N nodes)` to `O(max concurrent live set)` — often a multiple-× reduction
18//! and the key to fitting big models.
19//!
20//! This crate is the **pure, deterministic planning algorithm**, deliberately
21//! decoupled from the risky executor surgery (which lands later). It depends
22//! only on [`onnx_runtime_ir`], contains no `unsafe`, and is free of PyO3 / EP /
23//! session dependencies so it is trivially testable in isolation.
24//!
25//! `onnx-runtime-session` consumes this crate, behind the native executor phase
26//! profiler, to measure the activation peak implied by concrete run shapes and
27//! zero-copy views. The executor still owns its existing buffers today; the
28//! planner measurement is the production call site that de-risks the later
29//! allocator rework.
30//!
31//! ## What it computes
32//!
33//! Given a [`Graph`](onnx_runtime_ir::Graph), a [`ViewMap`] of zero-copy view
34//! aliases, and a **size oracle** (`Fn(ValueId) -> Option<usize>`), the planner
35//! produces an [`ActivationPlan`]:
36//!
37//! * `assignments: ValueId -> SlotId` — which reusable slot backs each value.
38//! * `slots: Vec<SlotInfo>` — each slot's byte capacity.
39//! * `peak_bytes` — the arena size the executor must allocate (sum of slot
40//!   capacities) — the shared, concurrent-peak footprint.
41//! * `naive_bytes` + `savings_ratio` — the one-buffer-per-value baseline and the
42//!   proven reduction.
43//!
44//! The three-step algorithm is: **(1)** compute each buffer owner's live
45//! interval `[def, use_end]` in topological order, folding view consumers and
46//! graph-output liveness into the root owner; **(2)** size every owner via the
47//! oracle, returning [`PlanStatus::Deferred`] if any size is symbolic; **(3)**
48//! greedily walk nodes, allocating each output's slot (best-fit reuse of a
49//! retired slot, else a new one) and retiring inputs *after* the node so a
50//! node's own inputs and any graph output are never clobbered.
51//!
52//! ## Static vs. dynamic shapes
53//!
54//! The same algorithm serves **build-time** and **run-time** planning through
55//! the size oracle. [`plan_activations_static`] plans from fully-static shapes;
56//! any symbolic-shaped activation makes the whole plan [`PlanStatus::Deferred`]
57//! so the executor can re-plan once shapes resolve for a run. A run-time caller
58//! passes its own oracle backed by resolved shapes.
59//!
60//! ## Zero-copy view aliasing
61//!
62//! The executor treats layout/movement-op outputs (`Slice`, `Reshape`,
63//! `Transpose`, …) as zero-copy views that own no buffer and alias a *source*
64//! buffer, pinning the source so it outlives every alias. The planner mirrors
65//! this: a view gets **no slot**; instead it extends its source's live interval
66//! (transitively — a view of a view folds to the root buffer owner). Correct
67//! view-liveness folding is exactly what stops a reused buffer from clobbering a
68//! still-live alias. The caller supplies the `view -> source` edges via
69//! [`ViewMap`]; op names are never hardcoded here.
70//!
71//! ## Executor integration
72//!
73//! `onnx-runtime-session` wires the planner into the executor in increments:
74//!
75//! 1. **Build the [`ViewMap`]** from the executor's existing view plan (the
76//!    `views`/`pinned` machinery in `executor.rs`), mapping each view value to
77//!    its source (root) buffer owner.
78//! 2. **Call the planner** with a size oracle backed by the run's *resolved*
79//!    shapes (build-time static shapes where available; a per-run oracle
80//!    otherwise). This is implemented and exposed as peak-vs-naive stats.
81//! 3. **Allocate `peak_bytes`** as one arena (or `num_slots` `DeviceBuffer`s),
82//!    then map each [`SlotId`] to an offset/allocation.
83//! 4. **Hand each value a `TensorMut` window** into its assigned slot instead of
84//!    the current per-value `buffers: HashMap<ValueId, DeviceBuffer>`.
85//!
86//! Two concerns are explicitly **out of scope** for both this crate and the
87//! current planning contract, and must be handled by the integration PR:
88//!
89//! * **In-place ops** — an op that may safely overwrite an input (e.g. an
90//!   elementwise unary) can share its input's slot for its output. Detecting
91//!   this requires per-op semantics the planner does not model; until then the
92//!   planner conservatively gives each output its own (reused) slot.
93//! * **Fragmentation** — `peak_bytes` is the sum of slot capacities. Packing
94//!   slots into a single arena with alignment/offset assignment (and any
95//!   resulting internal fragmentation) is the executor's responsibility.
96//!
97//! ## Correctness invariants (enforced by [`validate`])
98//!
99//! * No two values with overlapping live intervals share a slot.
100//! * Every graph output has a slot that is never reused after its def.
101//! * A pinned source outlives every view aliasing it (fold correctness).
102
103#![forbid(unsafe_code)]
104
105mod error;
106mod liveness;
107mod options;
108mod oracle;
109mod plan;
110mod validate;
111mod view_map;
112
113pub use error::{PlanError, ValidateError};
114pub use liveness::{Interval, Liveness, compute_liveness};
115pub use options::PlanOptions;
116pub use oracle::{bounded_size, bounded_size_oracle, static_size, static_size_oracle};
117pub use plan::{
118    ActivationPlan, PlanStatus, SlotId, SlotInfo, peak_activation_bytes_at_bounds,
119    plan_activations, plan_activations_static,
120};
121pub use validate::{validate, validate_static};
122pub use view_map::ViewMap;