microsoft / microsoft/onnxruntime

Basic optimization can produce invalid graph for same-dtype Cast feeding folded Mul-one

Open
#32,416 0 comments 0 reactions 0 assignees View on GitHub

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 Mul also 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.