Skip to main content

onnx_runtime_optimizer/
constant_folding.rs

1//! Constant folding: replace a node whose inputs are *all* constant
2//! (initializers) with a precomputed initializer (see `docs/architecture/ORT2.md` §18.1).
3//!
4//! ## Boundary (deliberately conservative)
5//!
6//! Fully general constant folding needs a kernel executor — the optimizer has
7//! none — so this pass folds only what the IR can compute *directly and
8//! exactly*, reusing the lessons of the loader's "const-fold-lite"
9//! (`crates/onnx-runtime-loader/src/shape_inference.rs`):
10//!
11//! * **`Constant`** nodes are materialized into initializers (always safe).
12//! * **`Shape`** on a fully-static input becomes an `int64` initializer.
13//! * **Elementwise integer `Add`/`Sub`/`Mul`** on two *same-shape* constant
14//!   `int32`/`int64` tensors are evaluated with **checked** arithmetic; any
15//!   overflow aborts the fold rather than emit a wrong constant.
16//! * **`Concat`** of constant tensors along a static axis (used to assemble a
17//!   `Reshape` shape from a literal prefix plus a folded `Shape` suffix).
18//! * **`Reshape`/`Transpose`** of a constant tensor are pure data relayouts —
19//!   no arithmetic, no precision loss, output size never exceeds input size —
20//!   so they are folded **regardless of tensor size** (see
21//!   [`MAX_WEIGHT_FOLD_ELEMS`]). Model builders emit these around quantized
22//!   MoE expert weights to reorder HF's `gate_up_proj` layout into the
23//!   interleaved layout `QMoE`'s CPU/CUDA kernels require (e.g. mobius's
24//!   `_interleave_gate_up_rows`), relying on the runtime to fold them into a
25//!   literal initializer at load time — exactly what stock ORT's own
26//!   constant-folding does. Without this, downstream weight-placement
27//!   analysis (which requires `QMoE`'s expert-weight inputs to be literal
28//!   initializers) fails on a semantically-valid graph.
29//!
30//! Everything else is left untouched. `Constant`/`Shape`/`Add`/`Sub`/`Mul`/
31//! `Concat` folding is bounded to [`MAX_FOLD_ELEMS`] elements — they exist
32//! for shape computation, so a larger operand indicates something other than
33//! a shape value. `Reshape`/`Transpose` use the much larger
34//! [`MAX_WEIGHT_FOLD_ELEMS`] instead, since folding them is just a bounded
35//! memcpy/permute with no combinatorial cost. Dispatch is purely on op type —
36//! no model-specific names. The invariant is: **never produce a wrong
37//! constant.** When in doubt, do not fold.
38
39use std::cmp::Reverse;
40use std::collections::{BinaryHeap, HashMap};
41
42use onnx_runtime_ir::{
43    Attribute, DataType, Graph, NodeId, TensorData, ValueId, WeightRef, as_static_shape,
44    checked_numel, is_fully_static, read_vec_le, static_shape,
45};
46
47use crate::error::Result;
48use crate::pass::{OptimizationPass, PassContext};
49
50/// Upper bound on the number of elements this pass will materialize for
51/// `Shape`/`Add`/`Sub`/`Mul`/`Concat`. Keeps folding limited to
52/// shape-computation-sized tensors.
53const MAX_FOLD_ELEMS: usize = 1024;
54
55/// Upper bound on the number of elements `Reshape`/`Transpose` will
56/// materialize. These ops never grow data (output size == input size) and
57/// perform no arithmetic, so they are safe to fold at weight scale; this is
58/// only a sanity ceiling against a corrupt/adversarial shape, not a
59/// performance-motivated limit like [`MAX_FOLD_ELEMS`].
60///
61/// Correctness-wise this bound is deliberately generous. It has no *load-time
62/// cost* budget attached: `"basic"` optimization (this pass plus dead-node
63/// elimination) now runs unconditionally on the production native-decode load
64/// path (`onnx-genai-engine::native_decode::{load,proposer}`), so a real
65/// (non-tiny) model with a long `Reshape`/`Transpose` weight-relayout chain
66/// over large expert tensors folds every time that model loads, not just
67/// once. That is a legitimate follow-up (a byte/time budget, or scoping the
68/// unconditional `"basic"` opt-in more narrowly) tracked separately — it does
69/// not change the folds' correctness, which this pass still guarantees.
70const MAX_WEIGHT_FOLD_ELEMS: usize = 1 << 30;
71
72/// Folds constant-input nodes into initializers (bounded, integer/shape only).
73#[derive(Clone, Copy, Debug, Default)]
74pub struct ConstantFolding;
75
76impl OptimizationPass for ConstantFolding {
77    fn name(&self) -> &str {
78        "ConstantFolding"
79    }
80
81    fn run(&self, graph: &mut Graph, _ctx: &PassContext) -> Result<()> {
82        let candidates: Vec<NodeId> = graph
83            .nodes
84            .iter()
85            .filter_map(|(nid, node)| is_candidate(node).then_some(nid))
86            .collect();
87        let mut unresolved = HashMap::with_capacity(candidates.len());
88        let mut dependents: HashMap<ValueId, Vec<NodeId>> =
89            HashMap::with_capacity(candidates.len());
90        // Match ascending fixpoint passes: higher IDs made ready during a wave
91        // join it, while lower/equal IDs wait for the next wave.
92        let mut current_wave = BinaryHeap::new();
93        let mut next_wave = BinaryHeap::new();
94
95        for nid in candidates {
96            let node = graph.node(nid);
97            let Some(inputs) = unresolved_inputs(graph, node) else {
98                continue;
99            };
100            let count = inputs.len();
101            unresolved.insert(nid, count);
102            if count == 0 {
103                current_wave.push(Reverse(nid.0));
104            } else {
105                for input in inputs {
106                    dependents.entry(input).or_default().push(nid);
107                }
108            }
109        }
110
111        while !current_wave.is_empty() {
112            while let Some(Reverse(raw_nid)) = current_wave.pop() {
113                let nid = NodeId(raw_nid);
114                if unresolved.remove(&nid).is_none() || !graph.nodes.contains(nid) {
115                    continue;
116                }
117                let (out, folded) = {
118                    let node = graph.node(nid);
119                    let folded = match node.op_type.as_str() {
120                        "Constant" => eval_constant(node),
121                        "Shape" => fold_shape(graph, node),
122                        "Add" | "Sub" | "Mul" => fold_binary_int(graph, node),
123                        "Concat" => fold_concat(graph, node),
124                        "Reshape" => fold_reshape(graph, node),
125                        "Transpose" => fold_transpose(graph, node),
126                        _ => None,
127                    };
128                    (node.outputs[0], folded)
129                };
130                let Some(tensor) = folded else { continue };
131
132                // Only fold outputs that are still needed (have a consumer or
133                // are graph outputs); dead outputs are DCE's job and folding
134                // them would leave a stale initializer referencing a GC'd id.
135                let needed = graph.outputs.contains(&out)
136                    || graph.try_value(out).is_some_and(|_| graph.has_uses(out));
137                if !needed {
138                    continue;
139                }
140
141                graph.remove_node(nid);
142                // The output survives because it is needed; retype it to the
143                // folded tensor and back it with an inline initializer.
144                if graph.try_value(out).is_none() {
145                    continue;
146                }
147                let dims = tensor.dims.clone();
148                let dtype = tensor.dtype;
149                let v = graph.value_mut(out);
150                v.dtype = dtype;
151                v.shape = static_shape(dims);
152                graph.set_initializer(out, WeightRef::Inline(tensor));
153
154                for consumer in dependents.remove(&out).unwrap_or_default() {
155                    let Some(count) = unresolved.get_mut(&consumer) else {
156                        continue;
157                    };
158                    *count -= 1;
159                    if *count == 0 {
160                        let wave = if consumer.0 > nid.0 {
161                            &mut current_wave
162                        } else {
163                            &mut next_wave
164                        };
165                        wave.push(Reverse(consumer.0));
166                    }
167                }
168            }
169            std::mem::swap(&mut current_wave, &mut next_wave);
170        }
171        Ok(())
172    }
173}
174
175fn is_candidate(node: &onnx_runtime_ir::Node) -> bool {
176    matches!(node.domain.as_str(), "" | "ai.onnx")
177        && node.outputs.len() == 1
178        && matches!(
179            node.op_type.as_str(),
180            "Constant" | "Shape" | "Add" | "Sub" | "Mul" | "Concat" | "Reshape" | "Transpose"
181        )
182}
183
184fn unresolved_inputs(graph: &Graph, node: &onnx_runtime_ir::Node) -> Option<Vec<ValueId>> {
185    match node.op_type.as_str() {
186        "Constant" => Some(Vec::new()),
187        "Shape" => {
188            let input = node.inputs.first().copied().flatten()?;
189            let shape = &graph.try_value(input)?.shape;
190            if shape.len() <= MAX_FOLD_ELEMS && is_fully_static(shape) {
191                Some(Vec::new())
192            } else {
193                Some(vec![input])
194            }
195        }
196        "Add" | "Sub" | "Mul" => {
197            if node.inputs.len() != 2 {
198                return None;
199            }
200            let inputs = [node.inputs[0]?, node.inputs[1]?];
201            Some(
202                inputs
203                    .into_iter()
204                    .filter(|&input| inline_const(graph, input).is_none())
205                    .collect(),
206            )
207        }
208        "Concat" => {
209            if node.inputs.is_empty() {
210                return None;
211            }
212            let inputs: Vec<ValueId> = node.inputs.iter().copied().collect::<Option<_>>()?;
213            Some(
214                inputs
215                    .into_iter()
216                    .filter(|&input| inline_const(graph, input).is_none())
217                    .collect(),
218            )
219        }
220        "Reshape" => {
221            if node.attr("allowzero").and_then(Attribute::as_int) == Some(1) {
222                return None; // rare `allowzero=1` semantics: bail conservatively
223            }
224            let data = node.inputs.first().copied().flatten()?;
225            let shape = node.inputs.get(1).copied().flatten()?;
226            Some(
227                [data, shape]
228                    .into_iter()
229                    .filter(|&input| inline_const(graph, input).is_none())
230                    .collect(),
231            )
232        }
233        "Transpose" => {
234            let data = node.inputs.first().copied().flatten()?;
235            Some(
236                [data]
237                    .into_iter()
238                    .filter(|&input| inline_const(graph, input).is_none())
239                    .collect(),
240            )
241        }
242        _ => None,
243    }
244}
245
246/// The inline constant tensor backing `value`, if any (external weights, which
247/// are large, are never folded).
248fn inline_const(graph: &Graph, value: ValueId) -> Option<&TensorData> {
249    match graph.initializers.get(&value)? {
250        WeightRef::Inline(t) => Some(t),
251        WeightRef::External { .. } => None,
252    }
253}
254
255/// Materialize a `Constant` node's value into a concrete [`TensorData`].
256fn eval_constant(node: &onnx_runtime_ir::Node) -> Option<TensorData> {
257    if let Some(Attribute::Tensor(t)) = node.attr("value") {
258        return Some(t.clone());
259    }
260    if let Some(ints) = node.attr("value_ints").and_then(Attribute::as_ints) {
261        let mut data = Vec::with_capacity(ints.len() * 8);
262        for &i in ints {
263            data.extend_from_slice(&i.to_le_bytes());
264        }
265        return Some(TensorData::from_raw(
266            DataType::Int64,
267            vec![ints.len()],
268            data,
269        ));
270    }
271    if let Some(i) = node.attr("value_int").and_then(Attribute::as_int) {
272        return Some(TensorData::from_raw(
273            DataType::Int64,
274            Vec::new(),
275            i.to_le_bytes().to_vec(),
276        ));
277    }
278    None
279}
280
281/// Fold `Shape(x)` when `x` has a fully-static shape into an `int64` vector,
282/// honoring the optional `start`/`end` slice attributes (Python-style
283/// slicing semantics: negative indices count from the end, out-of-range
284/// values clamp).
285fn fold_shape(graph: &Graph, node: &onnx_runtime_ir::Node) -> Option<TensorData> {
286    let input = node.inputs.first().copied().flatten()?;
287    let shape = &graph.try_value(input)?.shape;
288    let dims = as_static_shape(shape)?;
289    if dims.len() > MAX_FOLD_ELEMS {
290        return None;
291    }
292    let rank = dims.len() as i64;
293    let clamp = |v: i64| -> usize { v.clamp(0, rank) as usize };
294    let start = node
295        .attr("start")
296        .and_then(Attribute::as_int)
297        .map_or(0, |v| clamp(if v < 0 { v + rank } else { v }));
298    let end = node
299        .attr("end")
300        .and_then(Attribute::as_int)
301        .map_or(dims.len(), |v| clamp(if v < 0 { v + rank } else { v }));
302    let sliced = if start < end { &dims[start..end] } else { &[] };
303    let mut data = Vec::with_capacity(sliced.len() * 8);
304    for &d in sliced {
305        data.extend_from_slice(&(d as i64).to_le_bytes());
306    }
307    Some(TensorData::from_raw(
308        DataType::Int64,
309        vec![sliced.len()],
310        data,
311    ))
312}
313
314/// Fold elementwise integer `Add`/`Sub`/`Mul` on two same-shape constant
315/// tensors. Uses checked arithmetic; overflow aborts (returns `None`).
316fn fold_binary_int(graph: &Graph, node: &onnx_runtime_ir::Node) -> Option<TensorData> {
317    if node.inputs.len() != 2 {
318        return None;
319    }
320    let a = inline_const(graph, node.inputs[0]?)?;
321    let b = inline_const(graph, node.inputs[1]?)?;
322    if a.dtype != b.dtype || a.dims != b.dims {
323        return None; // no broadcasting / mixed dtype in v1
324    }
325    if !matches!(a.dtype, DataType::Int32 | DataType::Int64) {
326        return None;
327    }
328    let numel = a.numel();
329    if numel > MAX_FOLD_ELEMS {
330        return None;
331    }
332    let op = node.op_type.as_str();
333    let apply = |x: i64, y: i64| -> Option<i64> {
334        match op {
335            "Add" => x.checked_add(y),
336            "Sub" => x.checked_sub(y),
337            "Mul" => x.checked_mul(y),
338            _ => None,
339        }
340    };
341
342    match a.dtype {
343        DataType::Int64 => {
344            let (xs, ys) = (read_i64(a)?, read_i64(b)?);
345            let mut data = Vec::with_capacity(numel * 8);
346            for (x, y) in xs.into_iter().zip(ys) {
347                data.extend_from_slice(&apply(x, y)?.to_le_bytes());
348            }
349            Some(TensorData::from_raw(DataType::Int64, a.dims.clone(), data))
350        }
351        DataType::Int32 => {
352            let (xs, ys) = (read_i32(a)?, read_i32(b)?);
353            let mut data = Vec::with_capacity(numel * 4);
354            for (x, y) in xs.into_iter().zip(ys) {
355                let r = apply(x as i64, y as i64)?;
356                let r32: i32 = r.try_into().ok()?; // must fit back into i32
357                data.extend_from_slice(&r32.to_le_bytes());
358            }
359            Some(TensorData::from_raw(DataType::Int32, a.dims.clone(), data))
360        }
361        _ => None,
362    }
363}
364
365/// Fold `Concat` of same-dtype constant tensors along a static axis.
366///
367/// Bounded to [`MAX_FOLD_ELEMS`] like the other shape-value folds above —
368/// `Concat` here exists only to assemble a `Reshape` shape from a literal
369/// prefix plus a folded `Shape` suffix, never to concatenate model weights.
370fn fold_concat(graph: &Graph, node: &onnx_runtime_ir::Node) -> Option<TensorData> {
371    let axis_attr = node.attr("axis").and_then(Attribute::as_int)?;
372    let inputs: Vec<&TensorData> = node
373        .inputs
374        .iter()
375        .map(|slot| inline_const(graph, (*slot)?))
376        .collect::<Option<_>>()?;
377    let first = *inputs.first()?;
378    if first.dtype == DataType::String || !first.strings.is_empty() {
379        return None;
380    }
381    let elem_size = first.dtype.byte_size();
382    if elem_size == 0 {
383        return None; // sub-byte packed types unsupported here
384    }
385    let rank = first.dims.len();
386    if rank == 0 {
387        return None;
388    }
389    let axis = normalize_axis(axis_attr, rank)?;
390    let mut axis_sum = 0usize;
391    for t in &inputs {
392        if t.dtype != first.dtype || t.dims.len() != rank {
393            return None;
394        }
395        for (i, (&a, &b)) in t.dims.iter().zip(first.dims.iter()).enumerate() {
396            if i != axis && a != b {
397                return None;
398            }
399        }
400        axis_sum = axis_sum.checked_add(t.dims[axis])?;
401    }
402    let mut out_dims = first.dims.clone();
403    out_dims[axis] = axis_sum;
404    let numel = checked_numel(&out_dims)?;
405    if numel > MAX_FOLD_ELEMS {
406        return None;
407    }
408    let outer: usize = out_dims[..axis].iter().product();
409    let inner: usize = out_dims[axis + 1..].iter().product();
410    let mut out_bytes = vec![0u8; numel.checked_mul(elem_size)?];
411    let mut dst = 0usize;
412    for o in 0..outer {
413        for t in &inputs {
414            let slab_len = t.dims[axis].checked_mul(inner)?.checked_mul(elem_size)?;
415            let src_off = o.checked_mul(slab_len)?;
416            let src = t.data.get(src_off..src_off + slab_len)?;
417            out_bytes[dst..dst + slab_len].copy_from_slice(src);
418            dst += slab_len;
419        }
420    }
421    Some(TensorData::from_raw(first.dtype, out_dims, out_bytes))
422}
423
424fn normalize_axis(axis: i64, rank: usize) -> Option<usize> {
425    let r = rank as i64;
426    let a = if axis < 0 { axis + r } else { axis };
427    (0..r).contains(&a).then_some(a as usize)
428}
429
430/// Fold `Reshape` of a constant tensor. A pure metadata change: the raw
431/// bytes are copied byte-for-byte (no element reordering), so this is safe
432/// for any dtype (including sub-byte packed ones) and any size up to
433/// [`MAX_WEIGHT_FOLD_ELEMS`].
434fn fold_reshape(graph: &Graph, node: &onnx_runtime_ir::Node) -> Option<TensorData> {
435    let data_id = node.inputs.first().copied().flatten()?;
436    let shape_id = node.inputs.get(1).copied().flatten()?;
437    let data = inline_const(graph, data_id)?;
438    let shape_tensor = inline_const(graph, shape_id)?;
439    if shape_tensor.dtype != DataType::Int64 {
440        return None;
441    }
442    if data.dtype == DataType::String || !data.strings.is_empty() {
443        return None;
444    }
445    let numel = data.checked_numel()?;
446    if numel > MAX_WEIGHT_FOLD_ELEMS {
447        return None;
448    }
449    let shape_vals = read_i64(shape_tensor)?;
450    let resolved = resolve_reshape_dims(&data.dims, &shape_vals)?;
451    if checked_numel(&resolved)? != numel {
452        return None;
453    }
454    Some(TensorData::from_raw(
455        data.dtype,
456        resolved,
457        data.data.clone(),
458    ))
459}
460
461/// Resolve ONNX `Reshape` target dims: `0` copies the input dim at that
462/// position, at most one `-1` is inferred from the remaining element count,
463/// anything else is taken literally.
464fn resolve_reshape_dims(input_dims: &[usize], shape_vals: &[i64]) -> Option<Vec<usize>> {
465    let mut resolved = Vec::with_capacity(shape_vals.len());
466    let mut infer_at: Option<usize> = None;
467    for (i, &v) in shape_vals.iter().enumerate() {
468        if v == -1 {
469            if infer_at.is_some() {
470                return None; // at most one -1
471            }
472            infer_at = Some(i);
473            resolved.push(0); // placeholder, filled in below
474        } else if v == 0 {
475            resolved.push(*input_dims.get(i)?);
476        } else {
477            resolved.push(usize::try_from(v).ok()?);
478        }
479    }
480    if let Some(i) = infer_at {
481        let known = resolved
482            .iter()
483            .enumerate()
484            .filter(|&(idx, _)| idx != i)
485            .try_fold(1usize, |acc, (_, &d)| acc.checked_mul(d))?;
486        if known == 0 {
487            return None; // ambiguous / would divide by zero
488        }
489        let total = checked_numel(input_dims)?;
490        if total % known != 0 {
491            return None;
492        }
493        resolved[i] = total / known;
494    }
495    Some(resolved)
496}
497
498/// Fold `Transpose` of a constant tensor by physically permuting its raw
499/// bytes according to `perm` (or the default reversed-axis order). Bounded
500/// to [`MAX_WEIGHT_FOLD_ELEMS`]; sub-byte packed dtypes are rejected since
501/// permuting axes could split a packed byte across output elements.
502fn fold_transpose(graph: &Graph, node: &onnx_runtime_ir::Node) -> Option<TensorData> {
503    let input = node.inputs.first().copied().flatten()?;
504    let data = inline_const(graph, input)?;
505    if data.dtype == DataType::String || !data.strings.is_empty() {
506        return None;
507    }
508    let elem_size = data.dtype.byte_size();
509    if elem_size == 0 {
510        return None;
511    }
512    let rank = data.dims.len();
513    let perm: Vec<usize> = match node.attr("perm").and_then(Attribute::as_ints) {
514        Some(ints) => {
515            if ints.len() != rank {
516                return None;
517            }
518            let mut p = Vec::with_capacity(rank);
519            for &v in ints {
520                let v = usize::try_from(v).ok()?;
521                if v >= rank {
522                    return None;
523                }
524                p.push(v);
525            }
526            p
527        }
528        None => (0..rank).rev().collect(),
529    };
530    let mut seen = vec![false; rank];
531    for &p in &perm {
532        if std::mem::replace(&mut seen[p], true) {
533            return None; // not a permutation
534        }
535    }
536    let numel = data.checked_numel()?;
537    if numel > MAX_WEIGHT_FOLD_ELEMS {
538        return None;
539    }
540    if data.data.len() != numel.checked_mul(elem_size)? {
541        return None; // malformed tensor; be conservative
542    }
543    let out_dims: Vec<usize> = perm.iter().map(|&p| data.dims[p]).collect();
544    let mut in_strides = vec![1usize; rank];
545    for i in (0..rank.saturating_sub(1)).rev() {
546        in_strides[i] = in_strides[i + 1].checked_mul(data.dims[i + 1])?;
547    }
548    let mut out_strides = vec![1usize; rank];
549    for i in (0..rank.saturating_sub(1)).rev() {
550        out_strides[i] = out_strides[i + 1].checked_mul(out_dims[i + 1])?;
551    }
552    let mut out_bytes = vec![0u8; numel.checked_mul(elem_size)?];
553    let mut idx = vec![0usize; rank];
554    for o in 0..numel {
555        let mut rem = o;
556        for (k, stride) in out_strides.iter().enumerate() {
557            idx[k] = rem / stride;
558            rem %= stride;
559        }
560        let mut in_flat = 0usize;
561        for (k, &p) in perm.iter().enumerate() {
562            in_flat += idx[k] * in_strides[p];
563        }
564        let src = in_flat * elem_size;
565        let dst = o * elem_size;
566        out_bytes[dst..dst + elem_size].copy_from_slice(&data.data[src..src + elem_size]);
567    }
568    Some(TensorData::from_raw(data.dtype, out_dims, out_bytes))
569}
570
571fn read_i64(t: &TensorData) -> Option<Vec<i64>> {
572    if t.data.len() != t.numel() * 8 {
573        return None;
574    }
575    read_vec_le(&t.data).ok()
576}
577
578fn read_i32(t: &TensorData) -> Option<Vec<i32>> {
579    if t.data.len() != t.numel() * 4 {
580        return None;
581    }
582    read_vec_le(&t.data).ok()
583}
584
585#[cfg(test)]
586mod tests {
587    use super::*;
588    use crate::DeadNodeElimination;
589    use onnx_runtime_ir::{Node, NodeId};
590    use onnx_runtime_loader::{Model, encode_model};
591
592    fn int64_tensor(dims: Vec<usize>, vals: &[i64]) -> TensorData {
593        let mut data = Vec::new();
594        for &v in vals {
595            data.extend_from_slice(&v.to_le_bytes());
596        }
597        TensorData::from_raw(DataType::Int64, dims, data)
598    }
599
600    fn const_init(graph: &mut Graph, name: &str, dims: Vec<usize>, vals: &[i64]) -> ValueId {
601        let shape = static_shape(dims.clone());
602        let v = graph.create_named_value(name, DataType::Int64, shape);
603        graph.set_initializer(v, WeightRef::Inline(int64_tensor(dims, vals)));
604        v
605    }
606
607    fn run_reference_ascending_fixpoint(graph: &mut Graph) {
608        loop {
609            let mut changed = false;
610            let node_ids: Vec<NodeId> = graph.nodes.keys().collect();
611            for nid in node_ids {
612                if !graph.nodes.contains(nid) {
613                    continue;
614                }
615                let node = graph.node(nid).clone();
616                if !matches!(node.domain.as_str(), "" | "ai.onnx") || node.outputs.len() != 1 {
617                    continue;
618                }
619                let out = node.outputs[0];
620                let folded = match node.op_type.as_str() {
621                    "Constant" => eval_constant(&node),
622                    "Shape" => fold_shape(graph, &node),
623                    "Add" | "Sub" | "Mul" => fold_binary_int(graph, &node),
624                    "Concat" => fold_concat(graph, &node),
625                    "Reshape" => fold_reshape(graph, &node),
626                    "Transpose" => fold_transpose(graph, &node),
627                    _ => None,
628                };
629                let Some(tensor) = folded else { continue };
630                let needed = graph.outputs.contains(&out)
631                    || graph.try_value(out).is_some_and(|_| graph.has_uses(out));
632                if !needed {
633                    continue;
634                }
635
636                graph.remove_node(nid);
637                if graph.try_value(out).is_some() {
638                    let dims = tensor.dims.clone();
639                    let dtype = tensor.dtype;
640                    let v = graph.value_mut(out);
641                    v.dtype = dtype;
642                    v.shape = static_shape(dims);
643                    graph.set_initializer(out, WeightRef::Inline(tensor));
644                    changed = true;
645                }
646            }
647            if !changed {
648                break;
649            }
650        }
651    }
652
653    fn serialized(graph: &Graph) -> Vec<u8> {
654        encode_model(&Model::new(graph)).expect("serialize graph")
655    }
656
657    fn schedule_sensitive_chain() -> (Graph, ValueId) {
658        let mut g = Graph::new();
659        g.opset_imports.insert(String::new(), 17);
660        let init = const_init(&mut g, "init", vec![1], &[2]);
661        let a = g.create_named_value("a", DataType::Int64, static_shape([1]));
662        let b = g.create_named_value("b", DataType::Int64, static_shape([1]));
663        let out = g.create_named_value("out", DataType::Int64, static_shape([1]));
664
665        let mut constant = Node::new(NodeId(0), "Constant", vec![], vec![a]);
666        constant.attributes.insert(
667            "value".into(),
668            Attribute::Tensor(int64_tensor(vec![1], &[3])),
669        );
670        g.insert_node(constant);
671        g.insert_node(Node::new(
672            NodeId(0),
673            "Add",
674            vec![Some(a), Some(init)],
675            vec![b],
676        ));
677        g.insert_node(Node::new(NodeId(0), "Shape", vec![Some(b)], vec![out]));
678        g.add_output(out);
679        (g, out)
680    }
681
682    #[test]
683    fn ascending_wave_folds_constant_add_before_shape_consumer() {
684        let (base, out) = schedule_sensitive_chain();
685        let mut reference = base.clone();
686        let mut worklist = base;
687        run_reference_ascending_fixpoint(&mut reference);
688        ConstantFolding
689            .run(&mut worklist, &PassContext::new())
690            .unwrap();
691
692        assert_eq!(worklist.num_nodes(), 0, "Constant, Add, and Shape fold");
693        assert!(!worklist.nodes.values().any(|node| node.op_type == "Add"));
694        assert_eq!(
695            read_i64(inline_const(&worklist, out).unwrap()),
696            Some(vec![1])
697        );
698        assert_eq!(serialized(&worklist), serialized(&reference));
699        assert!(worklist.validate().is_ok());
700    }
701
702    #[test]
703    fn ascending_wave_leaves_lower_dead_producer_unfolded() {
704        let mut base = Graph::new();
705        base.opset_imports.insert(String::new(), 17);
706        let init = const_init(&mut base, "init", vec![1], &[2]);
707        let a = base.create_named_value("a", DataType::Int64, static_shape([1]));
708        let b = base.create_named_value("b", DataType::Int64, static_shape([1]));
709        let out = base.create_named_value("out", DataType::Int64, static_shape([1]));
710
711        base.insert_node(Node::new(
712            NodeId(0),
713            "Add",
714            vec![Some(a), Some(init)],
715            vec![b],
716        ));
717        base.insert_node(Node::new(NodeId(0), "Shape", vec![Some(b)], vec![out]));
718        let mut constant = Node::new(NodeId(0), "Constant", vec![], vec![a]);
719        constant.attributes.insert(
720            "value".into(),
721            Attribute::Tensor(int64_tensor(vec![1], &[3])),
722        );
723        base.insert_node(constant);
724        base.add_output(out);
725
726        let mut reference = base.clone();
727        let mut worklist = base;
728        run_reference_ascending_fixpoint(&mut reference);
729        ConstantFolding
730            .run(&mut worklist, &PassContext::new())
731            .unwrap();
732
733        assert_eq!(worklist.num_nodes(), 1);
734        assert_eq!(worklist.nodes.values().next().unwrap().op_type, "Add");
735        assert!(inline_const(&worklist, b).is_none());
736        assert_eq!(serialized(&worklist), serialized(&reference));
737        assert!(worklist.validate().is_ok());
738    }
739
740    fn seeded_dag(mut seed: u64, nodes: usize) -> Graph {
741        fn next(seed: &mut u64) -> u64 {
742            *seed = seed
743                .wrapping_mul(6_364_136_223_846_793_005)
744                .wrapping_add(1_442_695_040_888_963_407);
745            *seed
746        }
747
748        let mut g = Graph::new();
749        g.opset_imports.insert(String::new(), 17);
750        let init = const_init(&mut g, "init", vec![1], &[1]);
751        let values: Vec<ValueId> = (0..nodes)
752            .map(|i| g.create_named_value(format!("v{i}"), DataType::Int64, static_shape([1])))
753            .collect();
754        let mut definitions = Vec::with_capacity(nodes);
755        for i in 0..nodes {
756            let mut node = if i == 0 || next(&mut seed).is_multiple_of(4) {
757                let mut constant = Node::new(NodeId(0), "Constant", vec![], vec![values[i]]);
758                constant.attributes.insert(
759                    "value".into(),
760                    Attribute::Tensor(int64_tensor(vec![1], &[(next(&mut seed) % 8) as i64])),
761                );
762                constant
763            } else if next(&mut seed).is_multiple_of(3) {
764                let input = values[(next(&mut seed) as usize) % i];
765                Node::new(NodeId(0), "Shape", vec![Some(input)], vec![values[i]])
766            } else {
767                let pick_input = |seed: &mut u64| {
768                    if next(seed).is_multiple_of(4) {
769                        init
770                    } else {
771                        values[(next(seed) as usize) % i]
772                    }
773                };
774                Node::new(
775                    NodeId(0),
776                    "Add",
777                    vec![Some(pick_input(&mut seed)), Some(pick_input(&mut seed))],
778                    vec![values[i]],
779                )
780            };
781            node.name = format!("node_{i}");
782            definitions.push(node);
783        }
784
785        let mut order: Vec<usize> = (0..nodes).collect();
786        for i in (1..nodes).rev() {
787            order.swap(i, (next(&mut seed) as usize) % (i + 1));
788        }
789        for index in order {
790            g.insert_node(definitions[index].clone());
791        }
792        for (i, &value) in values.iter().enumerate() {
793            if i + 1 == nodes || (i > nodes / 2 && next(&mut seed).is_multiple_of(11)) {
794                g.add_output(value);
795            }
796        }
797        g
798    }
799
800    #[test]
801    fn seeded_dags_are_byte_identical_to_ascending_fixpoint() {
802        for seed in 0..32 {
803            let base = seeded_dag(seed, 96);
804            assert!(base.validate().is_ok(), "seed {seed}");
805            let mut reference = base.clone();
806            let mut worklist = base;
807            run_reference_ascending_fixpoint(&mut reference);
808            ConstantFolding
809                .run(&mut worklist, &PassContext::new())
810                .unwrap();
811            assert_eq!(serialized(&worklist), serialized(&reference), "seed {seed}");
812        }
813    }
814
815    #[test]
816    fn folds_add_of_two_const_inputs() {
817        let mut g = Graph::new();
818        g.opset_imports.insert(String::new(), 17);
819        let a = const_init(&mut g, "a", vec![3], &[1, 2, 3]);
820        let b = const_init(&mut g, "b", vec![3], &[10, 20, 30]);
821        let out = g.create_named_value("out", DataType::Int64, static_shape([3]));
822        g.insert_node(Node::new(
823            NodeId(0),
824            "Add",
825            vec![Some(a), Some(b)],
826            vec![out],
827        ));
828        g.add_output(out);
829
830        ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
831
832        assert_eq!(g.num_nodes(), 0, "Add should be folded away");
833        let t = inline_const(&g, out).expect("out is now an initializer");
834        assert_eq!(read_i64(t).unwrap(), vec![11, 22, 33]);
835        assert!(g.validate().is_ok());
836    }
837
838    #[test]
839    fn folds_sub_and_mul() {
840        for (op, expect) in [("Sub", vec![9, 18, 27]), ("Mul", vec![10, 40, 90])] {
841            let mut g = Graph::new();
842            g.opset_imports.insert(String::new(), 17);
843            let a = const_init(&mut g, "a", vec![3], &[10, 20, 30]);
844            let b = const_init(&mut g, "b", vec![3], &[1, 2, 3]);
845            let out = g.create_named_value("out", DataType::Int64, static_shape([3]));
846            g.insert_node(Node::new(NodeId(0), op, vec![Some(a), Some(b)], vec![out]));
847            g.add_output(out);
848
849            ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
850            let t = inline_const(&g, out).unwrap();
851            assert_eq!(read_i64(t).unwrap(), expect, "op {op}");
852        }
853    }
854
855    #[test]
856    fn does_not_fold_when_one_input_is_non_const() {
857        let mut g = Graph::new();
858        g.opset_imports.insert(String::new(), 17);
859        let a = const_init(&mut g, "a", vec![3], &[1, 2, 3]);
860        // `b` is a graph input, not a constant.
861        let b = g.create_named_value("b", DataType::Int64, static_shape([3]));
862        g.add_input(b);
863        let out = g.create_named_value("out", DataType::Int64, static_shape([3]));
864        g.insert_node(Node::new(
865            NodeId(0),
866            "Add",
867            vec![Some(a), Some(b)],
868            vec![out],
869        ));
870        g.add_output(out);
871
872        ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
873        assert_eq!(g.num_nodes(), 1, "must not fold with a non-const input");
874        assert!(inline_const(&g, out).is_none());
875        assert!(g.validate().is_ok());
876    }
877
878    #[test]
879    fn does_not_fold_mismatched_shapes() {
880        let mut g = Graph::new();
881        g.opset_imports.insert(String::new(), 17);
882        let a = const_init(&mut g, "a", vec![3], &[1, 2, 3]);
883        let b = const_init(&mut g, "b", vec![2], &[10, 20]);
884        let out = g.create_named_value("out", DataType::Int64, static_shape([3]));
885        g.insert_node(Node::new(
886            NodeId(0),
887            "Add",
888            vec![Some(a), Some(b)],
889            vec![out],
890        ));
891        g.add_output(out);
892
893        ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
894        assert_eq!(g.num_nodes(), 1, "no broadcasting in v1");
895    }
896
897    #[test]
898    fn does_not_fold_overflow() {
899        let mut g = Graph::new();
900        g.opset_imports.insert(String::new(), 17);
901        let a = const_init(&mut g, "a", vec![1], &[i64::MAX]);
902        let b = const_init(&mut g, "b", vec![1], &[1]);
903        let out = g.create_named_value("out", DataType::Int64, static_shape([1]));
904        g.insert_node(Node::new(
905            NodeId(0),
906            "Add",
907            vec![Some(a), Some(b)],
908            vec![out],
909        ));
910        g.add_output(out);
911
912        ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
913        assert_eq!(g.num_nodes(), 1, "overflow must abort the fold");
914    }
915
916    #[test]
917    fn folds_constant_node_to_initializer() {
918        let mut g = Graph::new();
919        g.opset_imports.insert(String::new(), 17);
920        let out = g.create_named_value("c", DataType::Int64, static_shape([2]));
921        let mut node = Node::new(NodeId(0), "Constant", vec![], vec![out]);
922        node.attributes.insert(
923            "value".into(),
924            Attribute::Tensor(int64_tensor(vec![2], &[7, 8])),
925        );
926        g.insert_node(node);
927        // Keep `out` alive with a consumer.
928        let sink = g.create_named_value("sink", DataType::Int64, static_shape([2]));
929        g.insert_node(Node::new(
930            NodeId(0),
931            "Identity",
932            vec![Some(out)],
933            vec![sink],
934        ));
935        g.add_output(sink);
936
937        ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
938        assert!(g.try_node(NodeId(0)).is_none(), "Constant folded away");
939        let t = inline_const(&g, out).unwrap();
940        assert_eq!(read_i64(t).unwrap(), vec![7, 8]);
941        assert!(g.validate().is_ok());
942    }
943
944    #[test]
945    fn folds_shape_of_static_input() {
946        let mut g = Graph::new();
947        g.opset_imports.insert(String::new(), 17);
948        let x = g.create_named_value("x", DataType::Float32, static_shape([2, 3, 4]));
949        g.add_input(x);
950        let out = g.create_named_value("s", DataType::Int64, static_shape([3]));
951        g.insert_node(Node::new(NodeId(0), "Shape", vec![Some(x)], vec![out]));
952        g.add_output(out);
953
954        ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
955        let t = inline_const(&g, out).expect("Shape folded to initializer");
956        assert_eq!(read_i64(t).unwrap(), vec![2, 3, 4]);
957        assert!(g.validate().is_ok());
958    }
959
960    #[test]
961    fn folds_transitively_to_fixpoint() {
962        // Constant c1, Constant c2, then Add(c1, c2) -> out. All should fold.
963        let mut g = Graph::new();
964        g.opset_imports.insert(String::new(), 17);
965
966        let c1 = g.create_named_value("c1", DataType::Int64, static_shape([2]));
967        let mut n1 = Node::new(NodeId(0), "Constant", vec![], vec![c1]);
968        n1.attributes.insert(
969            "value".into(),
970            Attribute::Tensor(int64_tensor(vec![2], &[1, 2])),
971        );
972        g.insert_node(n1);
973
974        let c2 = g.create_named_value("c2", DataType::Int64, static_shape([2]));
975        let mut n2 = Node::new(NodeId(0), "Constant", vec![], vec![c2]);
976        n2.attributes.insert(
977            "value".into(),
978            Attribute::Tensor(int64_tensor(vec![2], &[3, 4])),
979        );
980        g.insert_node(n2);
981
982        let out = g.create_named_value("out", DataType::Int64, static_shape([2]));
983        g.insert_node(Node::new(
984            NodeId(0),
985            "Add",
986            vec![Some(c1), Some(c2)],
987            vec![out],
988        ));
989        g.add_output(out);
990
991        ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
992        assert_eq!(g.num_nodes(), 0, "both constants and the Add fold away");
993        let t = inline_const(&g, out).unwrap();
994        assert_eq!(read_i64(t).unwrap(), vec![4, 6]);
995        assert!(g.validate().is_ok());
996    }
997
998    fn constant_chain(nodes: usize, reverse_node_ids: bool) -> (Graph, ValueId) {
999        let mut g = Graph::new();
1000        g.opset_imports.insert(String::new(), 17);
1001        let zero = const_init(&mut g, "zero", vec![1], &[0]);
1002        let one = const_init(&mut g, "one", vec![1], &[1]);
1003        let mut values = Vec::with_capacity(nodes + 1);
1004        values.push(zero);
1005        for _ in 0..nodes {
1006            values.push(g.create_value(DataType::Int64, static_shape([1])));
1007        }
1008
1009        if reverse_node_ids {
1010            for i in (1..=nodes).rev() {
1011                g.insert_node(Node::new(
1012                    NodeId(0),
1013                    "Add",
1014                    vec![Some(values[i - 1]), Some(one)],
1015                    vec![values[i]],
1016                ));
1017            }
1018        } else {
1019            for i in 1..=nodes {
1020                g.insert_node(Node::new(
1021                    NodeId(0),
1022                    "Add",
1023                    vec![Some(values[i - 1]), Some(one)],
1024                    vec![values[i]],
1025                ));
1026            }
1027        }
1028
1029        let out = values[nodes];
1030        g.add_output(out);
1031        (g, out)
1032    }
1033
1034    #[test]
1035    fn reverse_node_id_chain_matches_forward_order() {
1036        let (mut forward, forward_out) = constant_chain(64, false);
1037        let (mut reverse, reverse_out) = constant_chain(64, true);
1038
1039        let reverse_ids = reverse.topological_order().unwrap();
1040        assert!(
1041            reverse_ids.windows(2).all(|ids| ids[0].0 > ids[1].0),
1042            "test graph must have reverse dependency NodeIds"
1043        );
1044
1045        ConstantFolding
1046            .run(&mut forward, &PassContext::new())
1047            .unwrap();
1048        ConstantFolding
1049            .run(&mut reverse, &PassContext::new())
1050            .unwrap();
1051
1052        assert_eq!(forward.num_nodes(), 0);
1053        assert_eq!(reverse.num_nodes(), 0);
1054        assert_eq!(
1055            inline_const(&forward, forward_out),
1056            inline_const(&reverse, reverse_out)
1057        );
1058        assert_eq!(
1059            read_i64(inline_const(&reverse, reverse_out).unwrap()),
1060            Some(vec![64])
1061        );
1062        assert!(forward.validate().is_ok());
1063        assert!(reverse.validate().is_ok());
1064    }
1065
1066    #[test]
1067    fn does_not_fold_float_binary() {
1068        let mut g = Graph::new();
1069        g.opset_imports.insert(String::new(), 17);
1070        let mk = |g: &mut Graph, name: &str| {
1071            let v = g.create_named_value(name, DataType::Float32, static_shape([2]));
1072            g.set_initializer(
1073                v,
1074                WeightRef::Inline(TensorData::from_raw(
1075                    DataType::Float32,
1076                    vec![2],
1077                    vec![0u8; 8],
1078                )),
1079            );
1080            v
1081        };
1082        let a = mk(&mut g, "a");
1083        let b = mk(&mut g, "b");
1084        let out = g.create_named_value("out", DataType::Float32, static_shape([2]));
1085        g.insert_node(Node::new(
1086            NodeId(0),
1087            "Mul",
1088            vec![Some(a), Some(b)],
1089            vec![out],
1090        ));
1091        g.add_output(out);
1092
1093        ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
1094        assert_eq!(g.num_nodes(), 1, "float folding is out of scope in v1");
1095    }
1096
1097    fn raw_const_init(
1098        graph: &mut Graph,
1099        name: &str,
1100        dtype: DataType,
1101        dims: Vec<usize>,
1102        data: Vec<u8>,
1103    ) -> ValueId {
1104        let shape = static_shape(dims.clone());
1105        let v = graph.create_named_value(name, dtype, shape);
1106        graph.set_initializer(
1107            v,
1108            WeightRef::Inline(TensorData::from_raw(dtype, dims, data)),
1109        );
1110        v
1111    }
1112
1113    fn ints_attr_node(op_type: &str, inputs: Vec<Option<ValueId>>, outputs: Vec<ValueId>) -> Node {
1114        Node::new(NodeId(0), op_type, inputs, outputs)
1115    }
1116
1117    #[test]
1118    fn folds_reshape_with_literal_shape() {
1119        let mut g = Graph::new();
1120        g.opset_imports.insert(String::new(), 17);
1121        let data = raw_const_init(&mut g, "x", DataType::Uint8, vec![2, 3], (0u8..6).collect());
1122        let shape = const_init(&mut g, "shape", vec![1], &[6]);
1123        let out = g.create_named_value("out", DataType::Uint8, static_shape([6]));
1124        g.insert_node(ints_attr_node(
1125            "Reshape",
1126            vec![Some(data), Some(shape)],
1127            vec![out],
1128        ));
1129        g.add_output(out);
1130
1131        ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
1132        let t = inline_const(&g, out).expect("Reshape folded to initializer");
1133        assert_eq!(t.dims, vec![6]);
1134        assert_eq!(t.data, (0u8..6).collect::<Vec<_>>());
1135        assert!(g.validate().is_ok());
1136    }
1137
1138    #[test]
1139    fn folds_reshape_with_inferred_dim() {
1140        let mut g = Graph::new();
1141        g.opset_imports.insert(String::new(), 17);
1142        let data = raw_const_init(&mut g, "x", DataType::Uint8, vec![2, 3], (0u8..6).collect());
1143        let shape = const_init(&mut g, "shape", vec![2], &[3, -1]);
1144        let out = g.create_named_value("out", DataType::Uint8, static_shape([3, 2]));
1145        g.insert_node(ints_attr_node(
1146            "Reshape",
1147            vec![Some(data), Some(shape)],
1148            vec![out],
1149        ));
1150        g.add_output(out);
1151
1152        ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
1153        let t = inline_const(&g, out).expect("Reshape folded to initializer");
1154        assert_eq!(t.dims, vec![3, 2]);
1155        assert_eq!(t.data, (0u8..6).collect::<Vec<_>>());
1156    }
1157
1158    #[test]
1159    fn folds_reshape_beyond_shape_fold_bound() {
1160        // Reshape/Transpose must fold at weight scale, well past MAX_FOLD_ELEMS
1161        // (the bound reserved for shape-computation-sized Add/Sub/Mul/Concat).
1162        let numel = MAX_FOLD_ELEMS * 4;
1163        let mut g = Graph::new();
1164        g.opset_imports.insert(String::new(), 17);
1165        let data = raw_const_init(&mut g, "x", DataType::Uint8, vec![numel], vec![0u8; numel]);
1166        let shape = const_init(&mut g, "shape", vec![2], &[2, (numel / 2) as i64]);
1167        let out = g.create_named_value("out", DataType::Uint8, static_shape([2, numel / 2]));
1168        g.insert_node(ints_attr_node(
1169            "Reshape",
1170            vec![Some(data), Some(shape)],
1171            vec![out],
1172        ));
1173        g.add_output(out);
1174
1175        ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
1176        let t = inline_const(&g, out).expect("large Reshape must still fold");
1177        assert_eq!(t.dims, vec![2, numel / 2]);
1178    }
1179
1180    #[test]
1181    fn folds_transpose_permutes_bytes() {
1182        let mut g = Graph::new();
1183        g.opset_imports.insert(String::new(), 17);
1184        // 2x3 tensor: rows [0,1,2] and [3,4,5]; perm=[1,0] transposes to
1185        // 3x2: [0,3, 1,4, 2,5].
1186        let data = raw_const_init(
1187            &mut g,
1188            "x",
1189            DataType::Uint8,
1190            vec![2, 3],
1191            vec![0, 1, 2, 3, 4, 5],
1192        );
1193        let out = g.create_named_value("out", DataType::Uint8, static_shape([3, 2]));
1194        let mut node = ints_attr_node("Transpose", vec![Some(data)], vec![out]);
1195        node.attributes
1196            .insert("perm".into(), Attribute::Ints(vec![1, 0]));
1197        g.insert_node(node);
1198        g.add_output(out);
1199
1200        ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
1201        let t = inline_const(&g, out).expect("Transpose folded to initializer");
1202        assert_eq!(t.dims, vec![3, 2]);
1203        assert_eq!(t.data, vec![0, 3, 1, 4, 2, 5]);
1204    }
1205
1206    #[test]
1207    fn folds_concat_along_axis_zero() {
1208        let mut g = Graph::new();
1209        g.opset_imports.insert(String::new(), 17);
1210        let a = const_init(&mut g, "a", vec![2], &[1, 2]);
1211        let b = const_init(&mut g, "b", vec![3], &[3, 4, 5]);
1212        let out = g.create_named_value("out", DataType::Int64, static_shape([5]));
1213        let mut node = ints_attr_node("Concat", vec![Some(a), Some(b)], vec![out]);
1214        node.attributes.insert("axis".into(), Attribute::Int(0));
1215        g.insert_node(node);
1216        g.add_output(out);
1217
1218        ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
1219        let t = inline_const(&g, out).expect("Concat folded to initializer");
1220        assert_eq!(read_i64(t).unwrap(), vec![1, 2, 3, 4, 5]);
1221    }
1222
1223    #[test]
1224    fn folds_gate_up_interleave_chain_to_a_literal_initializer() {
1225        // Mirrors mobius's `_interleave_gate_up_rows`: reorder fc1 gate/up
1226        // rows from HF-concatenated `[E, 2*inter, ...]` layout to QMoE's
1227        // interleaved `[g_0, u_0, g_1, u_1, ...]` layout, entirely at
1228        // constant-fold time (relying on Shape(start=2)+Concat+Reshape+
1229        // Transpose+Concat+Reshape all folding into one literal initializer).
1230        let num_experts = 2usize;
1231        let half = 2usize;
1232        let fc1_out = 2 * half;
1233        let trailing = 3usize;
1234        let numel = num_experts * fc1_out * trailing;
1235
1236        let mut g = Graph::new();
1237        g.opset_imports.insert(String::new(), 17);
1238        let tensor = raw_const_init(
1239            &mut g,
1240            "fc1_experts_weights",
1241            DataType::Uint8,
1242            vec![num_experts, fc1_out, trailing],
1243            (0u8..numel as u8).collect(),
1244        );
1245
1246        let trailing_shape = g.create_named_value("trailing", DataType::Int64, static_shape([1]));
1247        let mut shape_node = ints_attr_node("Shape", vec![Some(tensor)], vec![trailing_shape]);
1248        shape_node
1249            .attributes
1250            .insert("start".into(), Attribute::Int(2));
1251        g.insert_node(shape_node);
1252
1253        let split_const = g.create_named_value("split_const", DataType::Int64, static_shape([3]));
1254        let mut split_const_node = ints_attr_node("Constant", vec![], vec![split_const]);
1255        split_const_node.attributes.insert(
1256            "value_ints".into(),
1257            Attribute::Ints(vec![num_experts as i64, 2, half as i64]),
1258        );
1259        g.insert_node(split_const_node);
1260
1261        let split_shape = g.create_named_value("split_shape", DataType::Int64, static_shape([4]));
1262        let mut split_concat = ints_attr_node(
1263            "Concat",
1264            vec![Some(split_const), Some(trailing_shape)],
1265            vec![split_shape],
1266        );
1267        split_concat
1268            .attributes
1269            .insert("axis".into(), Attribute::Int(0));
1270        g.insert_node(split_concat);
1271
1272        let reshaped = g.create_named_value(
1273            "reshaped",
1274            DataType::Uint8,
1275            static_shape([num_experts, 2, half, trailing]),
1276        );
1277        g.insert_node(ints_attr_node(
1278            "Reshape",
1279            vec![Some(tensor), Some(split_shape)],
1280            vec![reshaped],
1281        ));
1282
1283        let transposed = g.create_named_value(
1284            "transposed",
1285            DataType::Uint8,
1286            static_shape([num_experts, half, 2, trailing]),
1287        );
1288        let mut transpose_node =
1289            ints_attr_node("Transpose", vec![Some(reshaped)], vec![transposed]);
1290        transpose_node
1291            .attributes
1292            .insert("perm".into(), Attribute::Ints(vec![0, 2, 1, 3]));
1293        g.insert_node(transpose_node);
1294
1295        let merge_const = g.create_named_value("merge_const", DataType::Int64, static_shape([2]));
1296        let mut merge_const_node = ints_attr_node("Constant", vec![], vec![merge_const]);
1297        merge_const_node.attributes.insert(
1298            "value_ints".into(),
1299            Attribute::Ints(vec![num_experts as i64, fc1_out as i64]),
1300        );
1301        g.insert_node(merge_const_node);
1302
1303        let merge_shape = g.create_named_value("merge_shape", DataType::Int64, static_shape([3]));
1304        let mut merge_concat = ints_attr_node(
1305            "Concat",
1306            vec![Some(merge_const), Some(trailing_shape)],
1307            vec![merge_shape],
1308        );
1309        merge_concat
1310            .attributes
1311            .insert("axis".into(), Attribute::Int(0));
1312        g.insert_node(merge_concat);
1313
1314        let result = g.create_named_value(
1315            "result",
1316            DataType::Uint8,
1317            static_shape([num_experts, fc1_out, trailing]),
1318        );
1319        g.insert_node(ints_attr_node(
1320            "Reshape",
1321            vec![Some(transposed), Some(merge_shape)],
1322            vec![result],
1323        ));
1324        g.add_output(result);
1325
1326        ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
1327        DeadNodeElimination
1328            .run(&mut g, &PassContext::new())
1329            .unwrap();
1330
1331        assert_eq!(
1332            g.num_nodes(),
1333            0,
1334            "the entire interleave chain must fold to a literal initializer"
1335        );
1336        let t = inline_const(&g, result).expect("result must be a literal initializer");
1337        assert_eq!(t.dims, vec![num_experts, fc1_out, trailing]);
1338        // Expert 0 rows: g0=[0,1,2] g1=[3,4,5] u0=[6,7,8] u1=[9,10,11].
1339        // Expert 1 rows: g0=[12,13,14] g1=[15,16,17] u0=[18,19,20] u1=[21,22,23].
1340        // Interleaved as [g0,u0,g1,u1] per expert.
1341        assert_eq!(
1342            t.data,
1343            vec![
1344                0, 1, 2, 6, 7, 8, 3, 4, 5, 9, 10, 11, //
1345                12, 13, 14, 18, 19, 20, 15, 16, 17, 21, 22, 23,
1346            ]
1347        );
1348        assert!(g.validate().is_ok());
1349    }
1350}