Skip to content

Commit ff85b06

Browse files
committed
utilize load-time shape inference during runtime
* Reshape uses inferred shape if present without re-parsing the node * metal plan can skip its own CPU inference if all nodes' shapes are known
1 parent 6bc39b9 commit ff85b06

8 files changed

Lines changed: 192 additions & 46 deletions

File tree

benchmarks/perf-runners/onnx-pr-bench/Cargo.lock

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

benchmarks/perf-runners/onnx-pr-bench/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ publish = false
99
[dependencies]
1010
image = { version = "0.25", default-features = false, features = ["jpeg", "png"] }
1111
mimalloc = { version = "0.1", default-features = false }
12+
rustc-hash = "2.1.3"
1213
serde_json = "1"
1314
yscv-detect = { path = "../../../crates/yscv-detect" }
1415
yscv-kernels = { path = "../../../crates/yscv-kernels", default-features = false }

benchmarks/perf-runners/onnx-pr-bench/src/main.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ use yscv_onnx::{
1414
};
1515
use yscv_tensor::Tensor;
1616

17+
use rustc_hash::FxHashMap;
18+
1719
#[derive(Clone, Copy)]
1820
enum FillMode {
1921
Zero,
@@ -686,7 +688,7 @@ fn run_case(
686688
})
687689
.collect::<Result<_, _>>()?;
688690
let feed: Vec<(&str, &Tensor)> = inputs.iter().map(|(n, t)| (n.as_str(), t)).collect();
689-
let input_map: std::collections::HashMap<String, Tensor> = inputs.iter().cloned().collect();
691+
let input_map: FxHashMap<String, Tensor> = inputs.iter().cloned().collect();
690692
let shape_inference = infer_shapes_from_tensors(&model, &input_map);
691693
let graph_cost = graph_cost(&model, &shape_inference);
692694
let graph_cost_text = graph_cost_report(&graph_cost);

crates/yscv-onnx/src/runner/execute.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ use super::*;
1010
pub(crate) fn run_onnx_model_jit(
1111
model: &OnnxModel,
1212
mut env: TensorEnv<'_, '_>,
13+
specialization: Option<&ShapeSpecialization>,
1314
) -> Result<FxHashMap<String, Tensor>, OnnxError> {
15+
let reshape_shapes = specialization.map(|plan| &plan.reshape_shapes);
1416
let branches = &model.runtime_index.node_branches;
1517
let use_counts_by_id = &model.runtime_index.use_counts_by_id;
1618
let output_id_mask = build_output_id_mask(model, &env, use_counts_by_id.len());
@@ -69,6 +71,7 @@ pub(crate) fn run_onnx_model_jit(
6971
&mut env0,
7072
&mut remaining0,
7173
&output_id_mask,
74+
reshape_shapes,
7275
|nidx| branches_ref.get(nidx).copied() == Some(0),
7376
&mut c_ns,
7477
&mut o_ns,
@@ -87,6 +90,7 @@ pub(crate) fn run_onnx_model_jit(
8790
&mut env1,
8891
&mut remaining1,
8992
&output_id_mask,
93+
reshape_shapes,
9094
|nidx| branches_ref.get(nidx).copied() == Some(1),
9195
&mut c_ns,
9296
&mut o_ns,
@@ -115,6 +119,7 @@ pub(crate) fn run_onnx_model_jit(
115119
&mut env,
116120
&mut remaining,
117121
&output_id_mask,
122+
reshape_shapes,
118123
|nidx| {
119124
branches_ref.get(nidx).copied() != Some(0)
120125
&& branches_ref.get(nidx).copied() != Some(1)
@@ -132,6 +137,7 @@ pub(crate) fn run_onnx_model_jit(
132137
&mut env,
133138
&mut remaining_uses,
134139
&output_id_mask,
140+
reshape_shapes,
135141
|_| true,
136142
&mut conv_ns,
137143
&mut other_ns,

crates/yscv-onnx/src/runner/metal/compile.rs

Lines changed: 64 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,35 @@ use yscv_kernels::metal_backend::metal_conv::MetalInference;
1010

1111
use crate::error::OnnxError;
1212
use crate::loader::{OnnxModel, OnnxNode};
13+
use crate::shape_infer::{ShapeMap, TensorShape, infer_shapes};
14+
15+
fn can_skip_cpu_shape_discovery(model: &OnnxModel, shapes: &ShapeMap) -> bool {
16+
model.nodes.iter().all(|node| {
17+
matches!(
18+
node.op_type.as_str(),
19+
"Conv"
20+
| "Add"
21+
| "Sub"
22+
| "Mul"
23+
| "Div"
24+
| "Sigmoid"
25+
| "Relu"
26+
| "Concat"
27+
| "Transpose"
28+
| "Reshape"
29+
| "MatMul"
30+
) && node
31+
.outputs
32+
.iter()
33+
.filter(|name| !name.is_empty())
34+
.all(|name| {
35+
shapes
36+
.get(name)
37+
.and_then(TensorShape::as_known_dims)
38+
.is_some()
39+
})
40+
})
41+
}
1342

1443
/// Compile a Metal execution plan for the given ONNX model.
1544
/// Runs a shape-inference pass on CPU, then pre-allocates Metal buffers
@@ -31,35 +60,43 @@ pub fn compile_metal_plan(
3160
let debug_metal = false;
3261
let mut env = TensorEnv::from_model(model);
3362
env.insert(input_name.to_string(), input_tensor.clone());
34-
// We need tensor shapes AND data for fallback ops. Some ops (Split) consume
35-
// their inputs, so we snapshot shapes + data for fallback-eligible outputs
36-
// immediately after each node executes.
37-
let mut cpu_shapes: FxHashMap<String, Vec<usize>> = FxHashMap::default();
63+
let input_shapes: ShapeMap = FxHashMap::from_iter([(
64+
input_name.to_string(),
65+
TensorShape::known(input_tensor.shape().to_vec()),
66+
)]);
67+
let inferred = infer_shapes(model, &input_shapes);
68+
// A supported, fully-known graph needs shapes but not a CPU execution. Keep
69+
// the existing CPU walk for every other graph because fallback nodes may
70+
// need their concrete values, not merely their output dimensions.
71+
let skip_cpu_prepass = std::env::var("METAL_COMPARE").is_err()
72+
&& inferred.diagnostics.is_empty()
73+
&& can_skip_cpu_shape_discovery(model, &inferred.shapes);
74+
let mut cpu_shapes: FxHashMap<String, Vec<usize>> = inferred
75+
.shapes
76+
.iter()
77+
.filter_map(|(name, shape)| shape.as_known_dims().map(|dims| (name.clone(), dims)))
78+
.collect();
3879
let mut cpu_data: FxHashMap<String, Vec<f32>> = FxHashMap::default();
39-
for (ni, node) in model.nodes.iter().enumerate() {
40-
if let Err(e) = execute_node_cpu_for_metal_compile(node, &mut env)
41-
&& debug_metal
42-
{
43-
eprintln!(
44-
" [metal] CPU pass node {} {} '{}' FAILED: {}",
45-
ni, node.op_type, node.name, e
46-
);
47-
}
48-
// Snapshot outputs that Metal will need for cpu_fallback
49-
for out_name in &node.outputs {
50-
if out_name.is_empty() {
51-
continue;
80+
if !skip_cpu_prepass {
81+
for (ni, node) in model.nodes.iter().enumerate() {
82+
if let Err(e) = execute_node_cpu_for_metal_compile(node, &mut env)
83+
&& debug_metal
84+
{
85+
eprintln!(
86+
" [metal] CPU pass node {} {} '{}' FAILED: {}",
87+
ni, node.op_type, node.name, e
88+
);
5289
}
53-
if let Some(t) = env.get(out_name) {
54-
cpu_shapes.insert(out_name.clone(), t.shape().to_vec());
55-
// Only save data for cpu_fallback-eligible ops (shape ops, etc.)
56-
// to avoid excessive memory usage.
57-
// Save data for any op that might need cpu_fallback
58-
// (shape ops, unknown ops, etc.) — limit to small tensors to save memory
59-
let n_elem = t.len();
60-
if n_elem <= 1_000_000 {
61-
// ~4MB limit per tensor
62-
cpu_data.insert(out_name.clone(), t.data().to_vec());
90+
// Snapshot outputs that Metal will need for cpu_fallback.
91+
for out_name in &node.outputs {
92+
if out_name.is_empty() {
93+
continue;
94+
}
95+
if let Some(t) = env.get(out_name) {
96+
cpu_shapes.insert(out_name.clone(), t.shape().to_vec());
97+
if t.len() <= 1_000_000 {
98+
cpu_data.insert(out_name.clone(), t.data().to_vec());
99+
}
63100
}
64101
}
65102
}

0 commit comments

Comments
 (0)