Skip to content

Fix TransposeOptimizer type error on zero-point-less DequantizeLinear (#28716)#29192

Merged
tianleiwu merged 3 commits into
microsoft:mainfrom
tairenpiao:fix-transpose-optimizer-nodearg-name
Jun 20, 2026
Merged

Fix TransposeOptimizer type error on zero-point-less DequantizeLinear (#28716)#29192
tianleiwu merged 3 commits into
microsoft:mainfrom
tairenpiao:fix-transpose-optimizer-nodearg-name

Conversation

@tairenpiao

@tairenpiao tairenpiao commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Problem

With graph_optimization_level >= ORT_ENABLE_BASIC, a model with mixed int8/uint8 QDQ inside a local function fails session init:

Type Error: Type (tensor(int8)) of output arg (QuantizeLinear_out0) of node (QuantizeLinear) does not match expected type (tensor(uint8)).

Root cause

The issue's "duplicate NodeArg name" title is a misdiagnosis — QuantizeLinear_out0 is created exactly once. The real cause: when TransposeOptimizer pushes a Transpose through a DequantizeLinear that has no zero-point (MakeQDQNodeUnit), it inserts a new QuantizeLinear reusing the DQ's inputs. With no zero-point and no output_dtype, ONNX type inference defaults the new Q's output to uint8, but the value-info copied from the DQ input is int8Graph::Resolve() rejects it.

Fix

In MakeQDQNodeUnit, when the DQ has no zero-point, set the inserted Q's output_dtype to the DQ input's element type (ONNX domain only — output_dtype exists from opset 21; the zero-point path is unchanged).

Verification

Reporter's repro model:

ORT_DISABLE_ALL  -> OK   (before & after)
ORT_ENABLE_BASIC -> FAIL (before)  ->  OK (after)
  • After the fix, the optimized model passes onnx.checker.check_model(full_check=True); the inserted node now carries output_dtype=int8.
  • Added regression test TransposeOptimizerTests.TestDequantizeLinearNoZeroPoint (Transpose → no-zp int8 DequantizeLinear → Transpose; expects the Transposes to cancel).

Scope

ONNX domain, opset 21+ (where output_dtype exists). Pre-21 and com.microsoft QDQ rely on an explicit zero-point and are unaffected.

Fixes #28716

@tairenpiao

tairenpiao commented Jun 20, 2026

Copy link
Copy Markdown
Contributor Author

Heads up — #28729 also fixes #28716, but from a different angle (the duplicate-name theory).

The real break on the repro is a type mismatch: a zero-point-less int8 DequantizeLinear makes the new QuantizeLinear default to uint8 instead of int8, which fails Graph::Resolve(). This PR sets output_dtype to fix it — repro loads fine under ORT_ENABLE_BASIC.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a TransposeOptimizer session-initialization type error triggered when commuting a Transpose through an ONNX-domain DequantizeLinear that omits the optional zero-point (opset 21+). The optimizer synthesizes a QuantizeLinear; without output_dtype, ONNX type inference defaults that output to uint8, conflicting with copied value-info (e.g., int8) and causing Graph::Resolve() to fail.

Changes:

  • When inserting a QuantizeLinear for a zero-point-less DequantizeLinear, set output_dtype to match the DQ input element type (ONNX domain, opset 21+ only).
  • Add a regression test ensuring the transpose pair cancels in the no-zero-point int8 DQ pattern (and that optimization/resolve succeeds).

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
onnxruntime/core/optimizer/transpose_optimization/onnx_transpose_optimization.cc Pins inserted QuantizeLinear’s output_dtype for zero-point-less ONNX DequantizeLinear to prevent type-inference mismatches during Resolve().
onnxruntime/test/optimizer/transpose_optimizer_test.cc Adds a targeted regression test for the no-zero-point DQ case (opset 21), verifying transposes cancel and the optimized graph resolves.

Comment thread onnxruntime/core/optimizer/transpose_optimization/onnx_transpose_optimization.cc Outdated

@tianleiwu tianleiwu left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the careful re-diagnosis — the root cause (a zero-point-less DQ making the inserted QuantizeLinear default to uint8 while the copied value-info is int8) is correct, and pinning output_dtype is the right minimal fix. A couple of confirmations and one non-blocking suggestion:

  • static_cast<int64_t>(dq_input_dtype) is sound: api::DataType shares the ONNX TensorProto_DataType numeric values, so it is a valid output_dtype.
  • Excluding UINT8/UNDEFINED and gating on IsOnnxDomain are both correct and keep the zero-point and com.microsoft paths unchanged.

Non-blocking suggestion (inline): the corrective output_dtype is only emitted for ONNX opset ≥ 21, but there is no opset gate around MakeQDQNodeUnit, so the original Resolve() mismatch still occurs for an opset 19/20 zero-point-less int8 DQ (and the pinned type would itself be invalid if the DQ input were a type QuantizeLinear can't produce, e.g. int32). Consider skipping the push-through (return false) when the needed output_dtype can't be represented. Pre-existing limitation, so not a blocker.

@tianleiwu tianleiwu left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

The approach is sound — using double accumulators for CPU integer reductions and saturating abs for CUDA no-op paths correctly eliminates UB and overflow. Tests are comprehensive.

One blocking issue: missing CUDA template instantiations will cause linker failures (see inline comment).

@tianleiwu
tianleiwu dismissed their stale review June 20, 2026 23:15

Dismissing: this review was posted in error to the wrong PR (content was unrelated to #29192). Re-reviewing with correct feedback.

@tianleiwu tianleiwu left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed on head ec6e7b4.

The fix is correct, minimal, and conservative. Pinning the inserted QuantizeLinear's output_dtype to the DQ input type only on the no-zero-point path is the right root-cause fix; the zero-point path (where the Q output type is inferred from the zero-point) is left unchanged.

The earlier concern about there being no opset gate around MakeQDQNodeUnit is now addressed: the !IsOnnxDomain(dq_domain) || !domain_opset || *domain_opset < 21 || !IsQuantizeLinearOutputType(...) guard bails out (returns false) whenever output_dtype can't be represented (pre-opset-21, non-ONNX domain, or a non-quantizable type), and the bail-out happens before any node is added, so the graph stays valid. All call sites tolerate a false return and leave a valid DQ -> Transpose -> consumer graph.

Other checks:

  • IsQuantizeLinearOutputType is complete relative to the api::DataType enum (int8/uint8, int16/uint16, four fp8 variants; the enum has no int4/uint4).
  • The regression test exercises both branches: opset 21 (transposes cancel) and opset 19 (push-through skipped, model still initializes).

LGTM.

@tianleiwu
tianleiwu merged commit d374636 into microsoft:main Jun 20, 2026
85 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] TransposeOptimizer creates duplicate NodeArg name "QuantizeLinear_out0" for mixed uint8/int8 QDQ graphs, causing TypeError on session init

3 participants