Repository navigation
Description
Before submitting an issue, please make sure it hasn't been already addressed by searching through the existing and past issues.
Searched tuple index out of range, per-tensor qdq_to_dq, _convert_weight scale, dq_only per-tensor. The scalar half of this was reported in #581 and closed as completed — see "Relationship to #581" below; the suggested fix there does not actually fix it, and both forms still reproduce on main today.
Describe the bug
qdq_to_dq() cannot convert a per-tensor quantized weight. Both ONNX spellings of a per-tensor scale fail, in different ways:
| scale | result on main (23355ed) |
|---|---|
scalar, shape () |
IndexError: tuple index out of range |
1-D, shape (1,) |
ValueError: Scale shape (1,) does not match weight shape (16, 32) along axis 1 |
_convert_weight derives axis from a per-op map and then indexes the scale as if it were per-axis:
- Line 665 guards the shape check with
if scale_array.shape, so a scalar scale skips validation and reaches line 671, wherescale_array.shape[0]raisesIndexError. - A
(1,)scale does not skip validation, and1 != weight_shape[axis]raisesValueError— even though a per-tensor scale is legitimately broadcast across every channel.
Per-tensor is the ONNX default for QuantizeLinear/DequantizeLinear (a scalar y_scale means per-tensor), so any --dq_only / dq_only=True export of a model whose weights were quantized per-tensor fails.
Steps/Code to reproduce bug
import copy
import numpy as np, onnx, onnxruntime as ort
from onnx import helper, numpy_helper, TensorProto
from modelopt.onnx.quantization.qdq_utils import qdq_to_dq
def build(scalar_scale: bool):
w = np.random.RandomState(3).randn(16, 32).astype(np.float32)
amax = np.abs(w).max() / 127.0
s = np.array(amax, np.float32) if scalar_scale else np.array([amax], np.float32)
z = np.array(0, np.int8) if scalar_scale else np.array([0], np.int8)
g = helper.make_graph(
[helper.make_node("QuantizeLinear", ["W", "s", "z"], ["wq"], name="q"),
helper.make_node("DequantizeLinear", ["wq", "s", "z"], ["wdq"], name="dq"),
helper.make_node("MatMul", ["x", "wdq"], ["y"], name="mm")],
"g",
[helper.make_tensor_value_info("x", TensorProto.FLOAT, [4, 16])],
[helper.make_tensor_value_info("y", TensorProto.FLOAT, [4, 32])],
[numpy_helper.from_array(w, "W"), numpy_helper.from_array(s, "s"),
numpy_helper.from_array(z, "z")])
m = helper.make_model(g, opset_imports=[helper.make_opsetid("", 21)])
m.ir_version = 10
onnx.checker.check_model(m) # valid either way
return m
for scalar in (True, False):
try:
qdq_to_dq(copy.deepcopy(build(scalar)))
print(f"scalar={scalar}: converted")
except Exception as e:
print(f"scalar={scalar}: {type(e).__name__}: {e}")Output:
scalar=True: RuntimeError: Failed to convert node q: tuple index out of range
scalar=False: RuntimeError: Failed to convert node q: Scale shape (1,) does not match weight shape (16, 32) along axis 1
Both graphs pass onnx.checker.check_model and both run under onnxruntime before conversion, so the input is valid in each case.
Expected behavior
A per-tensor scale is broadcast across the whole weight, so conversion should succeed and the resulting DQ-only graph should match the QDQ graph numerically — or, if per-tensor weights are deliberately out of scope for qdq_to_dq, fail with a message that says so rather than an IndexError.
Relationship to #581
#581 reported the scalar case and was closed as completed. The fix suggested there was to expand the scalar to shape (1,):
if not scale_array.shape:
scale_array = np.expand_dims(scale_array, axis=0)I applied exactly that on top of main and re-ran the repro: it does not fix the export. It converts the IndexError into the (1,) path's ValueError: Scale shape (1,) does not match weight shape (16, 32) along axis 1, because the shape check at line 665 then rejects the broadcast scale. So the underlying problem — _convert_weight treating every scale as per-axis — is still present, which is why I am filing this rather than commenting on a closed issue.
System information
- Model Optimizer version:
main@ 23355ed - ONNX: 1.21.0
- onnxruntime: 1.30.0
- Python: 3.12, Windows 11 (CPU only; no GPU needed to reproduce)
Activity
kvnloo commentedon Sep 28, 2026
The distinction here seems to be:
_convert_weight()should decide that contract before indexingscale.shape[0]or checking an axis length.I would keep the regression entirely through the real ONNX conversion path:
shape=();shape=(1,);For both per-tensor spellings:
qdq_to_dq()succeeds;onnx.checker;That seems better than special-casing only the scalar
IndexError, because(1,)is the same semantic quantization mode and currently fails differently.If this is unowned, I can take the focused conversion + regression fix.
SID-6921 commentedon Sep 28, 2026
Yes, it is unowned — please take it, and thanks for picking it up. Your framing of the contract matches what I saw: the decision is per-tensor vs per-axis, and it has to happen before anything indexes
scale.shape[0]or compares an axis length. Agreed too that the three-case regression through the real conversion path is the right shape, and that fixing only the scalarIndexErrorwould leave(1,)failing differently for the same semantic mode.Two things from my side that may save you some time.
_convert_weightis being modified right now in #2554, which touches the same axis-selection block. That PR does not fix per-tensor, but you will probably want to base on it or rebase once it lands, or the two will conflict.A one-element scale is a trap worth knowing about before you pick the discriminator. I used
scale_array.ndim == 1for the per-axis branch in #2554 and it regressed graphs that convert onmaintoday, because a per-tensor scale spelled as a one-element 1-D tensor also matches it:mainndim == 1(1, 3, 3, 3)ConvScale shape (1,) does not match weight shape (1, 3, 3, 3) along axis 1(1, 64)Gemm transB=1A single-output-channel Conv is not exotic.
scale_array.size > 1is the discriminator that held up for me — the axis is mathematically irrelevant for a broadcast scale, so it only affects the validation check. Might be worth a fourth case in your regression matrix.Happy to review whenever you open the PR.
juhi10071998 commentedon Oct 1, 2026
hi @ajrasane , could you please help review this in onnx? Thanks! #2613