microsoft / microsoft/onnxruntime

Basic optimization can produce invalid graph for Identity feeding folded Mul-one

Open
#32,413 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

ORT Basic optimization can produce an invalid graph for Identity feeding folded Mul-one

Summary

ONNX Runtime 1.23.2 and the latest tested PyPI CPU wheel 1.29.0 accept and execute this model at ORT_DISABLE_ALL, but fail from ORT_ENABLE_BASIC onward after graph optimization. The failing optimized graph refers to the
output of an eliminated Identity 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 -> Identity -> mid
1.0 / 1.0 -> folded_one
mid * folded_one -> wrapped
wrapped + 0.0 -> y

Run:

python repro.py

Observed result:

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=["EliminateIdentity"]: ok
disabled_optimizers=["DivMulFusion"]: ok
disabled_optimizers=["Level1_RuleBasedTransformer"]: ok
disabled_optimizers=["EliminateDropout"]: same error
disabled_optimizers=["NoopElimination"]: same error
disabled_optimizers=["ConstantFolding"]: same error

Why this looks like an optimizer interaction

The graph is valid and runs with optimization disabled. The pattern is semantic identity for finite float inputs. In real-seed insertion campaigns, the same pattern failed on SqueezeNet, MobileNetV2, DeiT-tiny, ResNet50, and
ConvNeXt-tiny. Disabling either EliminateIdentity or DivMulFusion makes the failure disappear, while disabling ConstantFolding or NoopElimination does not.

This suggests an interaction between the producer-deletion rule and DivMulFusion: the Identity producer of mid is removed, but a downstream folded Mul-one fusion path still references the old mid value.

Additional probe evidence:

  • A producer/consumer matrix also reproduces the same failure for the commuted consumer form Mul(folded_one, mid).
  • The commuted pattern was inserted into a real SqueezeNet seed and failed the same way.
  • These variants still collapse to the same root: EliminateIdentity removes the producer value, and DivMulFusion leaves a stale reference.
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])
    zero = numpy_helper.from_array(np.asarray(0.0, dtype=np.float32), name="zero")
    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("Identity", ["x"], ["mid"], name="producer_identity"),
        helper.make_node("Div", ["one_a", "one_b"], ["folded_one"], name="producer_constant_div"),
        helper.make_node("Mul", ["mid", "folded_one"], ["wrapped"], name="consumer_mul_one"),
        helper.make_node("Add", ["wrapped", "zero"], ["y"], name="tail_add_zero"),
    ]
    graph = helper.make_graph(nodes, "identity_folded_mul_basic_toposort", [x], [y], initializer=[zero, 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 (
                "EliminateIdentity",
                "DivMulFusion",
                "Level1_RuleBasedTransformer",
                "EliminateDropout",
                "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

x86_64

ONNX Runtime Installation

Built from Source

ONNX Runtime Version or Commit ID

1.23.2

ONNX Runtime API

Python

Architecture

X86

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 supplied repro.py with the listed optimization levels and disabled optimizers to confirm the invalid graph. Then trace the EliminateIdentity and DivMulFusion optimizer paths and their handling of the mid value. Done means the optimized graph remains valid for the reproducer, including the commuted Mul form, with regression coverage for the failure.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp, python
Domain
machine-learning
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.