microsoft / microsoft/onnxruntime
Basic optimization can produce invalid graph for same-dtype Cast feeding folded Mul-one
Nobody has claimed this yet.
- Dominant language
- C++
- Stars
- 21.9k
- Forks
- 4.2k
- Avg merge
- 4d 11h
- Merged PRs (30d)
- 184
Description
Describe the issue
Body:
ORT Basic optimization can produce an invalid graph for same-dtype Cast feeding folded Mul-one
Summary
ONNX Runtime 1.23.2 and the latest tested PyPI CPU wheel 1.29.0 accept and execute this graph at ORT_DISABLE_ALL, but fail from ORT_ENABLE_BASIC onward after graph optimization. The failing optimized graph refers to the
output of a removed same-dtype Cast node.
Environment
- onnxruntime: 1.23.2; reproduced again on latest tested PyPI CPU wheel 1.29.0
- onnx: 1.22.0
- provider: CPUExecutionProvider
- OS: Linux x86_64
Reproducer
Minimal graph:
x -> Cast(to=FLOAT) -> mid
1.0 / 1.0 -> folded_one
mid * folded_one -> y
Run:
python repro.py
Observed:
ORT_DISABLE_ALL: ok
ORT_ENABLE_BASIC: Invalid model. Node input 'mid' is not a graph input, initializer, or output of a previous node.
ORT_ENABLE_EXTENDED: same error
ORT_ENABLE_ALL: same error
Basic ablation in the reproducer:
disabled_optimizers=["CastElimination"]: ok
disabled_optimizers=["DivMulFusion"]: ok
disabled_optimizers=["Level1_RuleBasedTransformer"]: ok
disabled_optimizers=["EliminateCast"]: same error
disabled_optimizers=["NoopElimination"]: same error
disabled_optimizers=["ConstantFolding"]: same error
Why this looks like an optimizer interaction
The model is valid and runs when graph optimization is disabled. The inserted pattern is semantic identity for float tensors: the Cast is same-dtype and the downstream Mul multiplies by one. In real-seed insertion campaigns,
this pattern failed on SqueezeNet, MobileNetV2, DeiT-tiny, ResNet50, and ConvNeXt-tiny, across 50 tested anchors.
This suggests an interaction between the CastElimination rule in Level1_RuleBasedTransformer and DivMulFusion: the producer of mid is removed, but the downstream folded Mul-one fusion path still references the old
value.
Additional probe evidence:
- A producer/consumer matrix also reproduces the same failure for the commuted consumer form
Mul(folded_one, mid). - A two-node same-dtype Cast chain feeding the same folded
Mulalso fails. - Both Cast-chain variants are fixed by disabling
CastElimination, so they are treated as evidence for the same root rather than separate issues. - The commuted and Cast-chain patterns were inserted into a real SqueezeNet seed and failed the same way.
To reproduce
Full minimal reproducer
Save as repro.py and run with python repro.py:
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
from pathlib import Path
import numpy as np
import onnx
import onnxruntime as ort
from onnx import TensorProto, helper, numpy_helper
def build_model(path: Path) -> None:
x = helper.make_tensor_value_info("x", TensorProto.FLOAT, [1, 3, 5, 5])
y = helper.make_tensor_value_info("y", TensorProto.FLOAT, [1, 3, 5, 5])
one_a = numpy_helper.from_array(np.asarray(1.0, dtype=np.float32), name="one_a")
one_b = numpy_helper.from_array(np.asarray(1.0, dtype=np.float32), name="one_b")
nodes = [
helper.make_node("Cast", ["x"], ["mid"], name="producer_cast_same_dtype", to=TensorProto.FLOAT),
helper.make_node("Div", ["one_a", "one_b"], ["folded_one"], name="producer_constant_div"),
helper.make_node("Mul", ["mid", "folded_one"], ["y"], name="consumer_mul_one"),
]
graph = helper.make_graph(nodes, "cast_same_folded_mul_basic_toposort", [x], [y], initializer=[one_a, one_b])
model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 13)])
model.ir_version = 8
onnx.checker.check_model(model)
onnx.save_model(model, str(path))
def run(model_path: Path, level: str, disabled: list[str] | None = None) -> dict:
levels = {
"disable": ort.GraphOptimizationLevel.ORT_DISABLE_ALL,
"basic": ort.GraphOptimizationLevel.ORT_ENABLE_BASIC,
"extended": ort.GraphOptimizationLevel.ORT_ENABLE_EXTENDED,
"all": ort.GraphOptimizationLevel.ORT_ENABLE_ALL,
}
so = ort.SessionOptions()
so.graph_optimization_level = levels[level]
feeds = {"x": np.ones((1, 3, 5, 5), dtype=np.float32)}
kwargs = {"sess_options": so, "providers": ["CPUExecutionProvider"]}
if disabled:
kwargs["disabled_optimizers"] = disabled
try:
sess = ort.InferenceSession(str(model_path), **kwargs)
out = sess.run(None, feeds)[0]
return {"level": level, "disabled": disabled or [], "status": "ok", "sum": float(out.sum())}
except Exception as exc:
return {"level": level, "disabled": disabled or [], "status": "error", "error": f"{type(exc).__name__}: {exc}"}
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--model", default="model.onnx")
parser.add_argument("--out", default="result.json")
args = parser.parse_args()
model_path = Path(args.model)
build_model(model_path)
payload = {
"onnxruntime": ort.__version__,
"providers": ort.get_available_providers(),
"runs": [run(model_path, level) for level in ("disable", "basic", "extended", "all")],
"ablation": [
run(model_path, "basic", [name])
for name in (
"CastElimination",
"DivMulFusion",
"Level1_RuleBasedTransformer",
"EliminateCast",
"NoopElimination",
"ConstantFolding",
)
],
}
Path(args.out).write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
print(json.dumps(payload, indent=2))
if __name__ == "__main__":
main()
Urgency
No response
Platform
Linux
OS Version
ubuntu22.03
ONNX Runtime Installation
Released Package
ONNX Runtime Version or Commit ID
1.29.0
ONNX Runtime API
Python
Architecture
X64
Execution Provider
Default CPU
Execution Provider Library Version
No response
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Run the provided repro.py with ONNX Runtime and compare the optimization-level and disabled-optimizer results. Trace the interaction among CastElimination, DivMulFusion, Level1_RuleBasedTransformer, EliminateCast, NoopElimination, and ConstantFolding. Done means the optimized graph has no dangling reference to mid and the reproducer succeeds at the reported optimization levels.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp, python
- Domain
- machine-learning, performance
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100