[Bug][Relax][ONNX] A model-local Add function executes the built-in Add instead of its Sub body

未关闭
#20,871 0 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

维护者通常 1 天内回复

还没有人认领这个 Issue。

评估

难度
4/5
预计耗时
3-5 天
新手友好度
25/100
Issue 类型
缺陷
描述清晰度
基本清楚
活跃度
活跃
技术栈
python
领域
compilers

调研方向

Start in python/tvm/relax/frontend/onnx/onnx_frontend.py, where the node loop reads node.op_type and _convert_operator picks the built-in conversion map by name alone, ignoring node.domain and any model-local function. Run the repro script's local_function case, which should return 21 instead of 15, and compare with the inlined_control case. Done means local functions are either expanded from their body or explicitly rejected, with a regression test added.

由索引模型根据 Issue 内容生成。

描述

The smallest example I found is a one-node call to a local function.

I reduced a wrong-result case to a model-local function named Add in the domain example.local. Its body contains Sub, so inputs 18 and 3 must produce 15. Relax returns 21. Inlining that exact function before importing restores 15.

A runnable case

Save the code below as repro.py. With an LLVM-enabled TVM installation and the dependencies listed at the end, run OMP_NUM_THREADS=1 OPENBLAS_NUM_THREADS=1 MKL_NUM_THREADS=1 python repro.py.

from pathlib import Path
import hashlib
import numpy as np
import onnx
import onnxruntime as ort
import tvm
from onnx import TensorProto as T, helper as h, numpy_helper as nh
from onnx.reference import ReferenceEvaluator
from tvm import relax
from tvm.relax.frontend.onnx import from_onnx


def make(nodes, inputs, outputs, initializers=(), opset=18):
    model = h.make_model(h.make_graph(nodes, "repro", inputs, outputs, list(initializers)),
                         opset_imports=[h.make_opsetid("", opset)], ir_version=10)
    onnx.checker.check_model(model, full_check=True)
    return model


def tvm_run(model, feeds):
    mod = from_onnx(model, keep_params_in_input=False)
    names = [value.name for value in model.graph.input]
    executable = relax.build(mod, target="llvm", relax_pipeline="default")
    result = relax.VirtualMachine(executable, tvm.cpu())["main"](
        *[tvm.runtime.tensor(feeds[name]) for name in names])
    return [result.numpy()] if hasattr(result, "numpy") else [v.numpy() for v in result]


def save(label, model, feeds):
    folder = Path("artifacts") / label
    folder.mkdir(parents=True, exist_ok=True)
    onnx.checker.check_model(model, full_check=True)
    onnx.save(model, folder / "model.onnx")
    np.savez(folder / "inputs.npz", **feeds)
    print(label, "model_sha256", hashlib.sha256((folder / "model.onnx").read_bytes()).hexdigest())
    print(label, "inputs_sha256", hashlib.sha256((folder / "inputs.npz").read_bytes()).hexdigest())


def reference(model, feeds, expected, check_ort=True):
    outputs = ReferenceEvaluator(model).run(None, feeds)
    for a, b in zip(outputs, expected, strict=True):
        assert a.shape == b.shape and a.dtype == b.dtype, (a.shape, a.dtype, b.shape, b.dtype)
        np.testing.assert_allclose(a, b, rtol=1e-5, atol=1e-6)
    if check_ort:
        opts = ort.SessionOptions()
        opts.intra_op_num_threads = opts.inter_op_num_threads = 1
        opts.graph_optimization_level = ort.GraphOptimizationLevel.ORT_DISABLE_ALL
        outputs = ort.InferenceSession(model.SerializeToString(), opts,
            providers=["CPUExecutionProvider"]).run(None, feeds)
        for a, b in zip(outputs, expected, strict=True):
            assert a.shape == b.shape and a.dtype == b.dtype
            np.testing.assert_allclose(a, b, rtol=1e-5, atol=1e-6)


def error_line(error):
    return type(error).__name__ + ": " + str(error).splitlines()[-1]


def same(actual, expected):
    assert actual.shape == expected.shape and actual.dtype == expected.dtype
    np.testing.assert_allclose(actual, expected, rtol=1e-5, atol=1e-6)


from onnx.inliner import inline_local_functions
function = h.make_function("example.local", "Add", ["a", "b"], ["out"],
    [h.make_node("Sub", ["a", "b"], ["out"])], [h.make_opsetid("", 18)])
model = h.make_model(h.make_graph(
    [h.make_node("Add", ["x", "z"], ["y"], domain="example.local")], "local_function", 
    [h.make_tensor_value_info(n, T.FLOAT, [1]) for n in ("x", "z")],
    [h.make_tensor_value_info("y", T.FLOAT, [1])]),
    opset_imports=[h.make_opsetid("", 18), h.make_opsetid("example.local", 1)],
    functions=[function], ir_version=10)
feeds = {"x": np.array([18], dtype="float32"), "z": np.array([3], dtype="float32")}
expected = feeds["x"] - feeds["z"]
save("local_function", model, feeds)
reference(model, feeds, [expected])
actual = tvm_run(model, feeds)[0]
print("model-local Add body is Sub; expected", expected.tolist(), "TVM", actual.tolist())
assert actual.tolist() == [21.0]
inlined = inline_local_functions(model)
save("inlined_control", inlined, feeds)
same(tvm_run(inlined, feeds)[0], expected)
print("function-inlining control passed")
What I see
model-local Add body is Sub; expected [15.0] TVM [21.0]
function-inlining control passed

The expected/actual distinction is: Local function: 15; TVM: 21. Identical model after ONNX function inlining: TVM returns 15.

Why I think this is the relevant boundary

An ONNX function is resolved using its domain and name, with the model-local function body defining its computation. The witness passes full ONNX checking; ReferenceEvaluator and unoptimized ORT CPU both execute Sub and return 15.

The node construction loop obtains node.op_type, and _convert_operator selects the built-in conversion map using only that name. This silently substitutes a different function instead of respecting domain/body or explicitly declining unsupported functions.

Scope of this example

The preserved source and controls identify domain/body dispatch, rather than Relax closure lifetime or function-reference rewriting. Targeted searches did not identify an earlier report for this exact model-local function collision; no global novelty claim is made.

This self-contained script is a newly constructed minimal diagnostic from the same graph-testing investigation, not a byte-identical replay of an archived model. It needs only TVM, ONNX, NumPy and ONNX Runtime, downloads no files, and writes its generated models and inputs to artifacts/. The assertions check both the observed failure/wrong result and the normal controls; exit code zero means that diagnostic reproduced. No global novelty or current-main execution claim is made.

Reproduction setup

Linux x86_64 CPU/LLVM; Python 3.12.3; TVM 0.28.dev0; LLVM 18.1.3; ONNX 1.22.0; NumPy 2.5.4; ONNX Runtime 1.29.0 (CPUExecutionProvider).

The installed TVM binary's exact build source revision is UNKNOWN. Source links are a separate static inspection of main at 70ec7d5bf20326ef301ee3f06eb6aed51760331d; that revision was not built or executed for this report.

Loaded binary SHA256 identities:

  • libtvm_ffi.so: 12072d0ccd9d5cb2c6dac80d42f4217ab50dbf795db03197000614a7a770a816
  • libtvm_compiler.so: 574a573fe994e5eda7352ddd8d685e64e45c7934bdeeaab57a5a272a77792ab1
  • libtvm_runtime.so: 4175916fc0ce6266c912629d2255094b94087425c6ab53ffd5e004d4c292c828
  • libtvm_runtime_extra.so: 79c93351fca12dd7851964890cea7da65dd2b2e0e64e5c28bfed56c878b70817

The code and inputs are small enough to recreate without an attachment. For an exact file check, repro.py has SHA256 47a5735855a75cc25bc03c41b0029f1dd24d179ee4a937af645143624375832e. Generated model/input checksums from this run:

  • artifacts/inlined_control/inputs.npz: 25ae56865463aa811bec82cec1025975105523acf9ee32da8b290dfff420f868
  • artifacts/inlined_control/model.onnx: f8eb546276aea5630f35253dba5c98d9465b46317f63d4351a96fedbe69b0a4a
  • artifacts/local_function/inputs.npz: 25ae56865463aa811bec82cec1025975105523acf9ee32da8b290dfff420f868
  • artifacts/local_function/model.onnx: ebbf49c2d2e8a35742da88cbcb8dfc547c80d004d5315eedb6f4b093041573b4

I can follow up with a focused regression test and adjust the example if another supported lowering path is preferred.

主要语言
Python
星标
13.9k
派生
4k
平均合并
7 小时 59 分钟
30 天内合并 PR
159

环境准备

这个项目没有提供开发容器、Dockerfile 或贡献指南,环境需要你自己搭建:先看它的 README,通用步骤见我们的新手贡献指南。

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

apache/tvm 的其他 Issue

查看 apache/tvm 的全部 Issue

相似的 Issue

更多 Python Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。