fix(quantization): validate bias scale in QDQ Conv → QLinearConv fusion (#28229)
## Summary
- Add `CheckConvBiasScale` validator inside
`ConvNodeGroupSelector::Check`
- Skip QDQ Conv → QLinearConv fusion when bias DQ scale ≠ input_scale ×
weight_scale (within 1% relative tolerance)
- Adds Python test coverage for both matching and mismatched bias scales
## Motivation
Fixes #24711.
The ONNX QLinearConv spec requires the int32 bias to use scale `x_scale
× w_scale[i]` so the fused kernel can reuse it directly. The current QDQ
selector only verifies the bias dtype is INT32 — it never checks that
the bias DQ's scale satisfies this relationship. When a model is
constructed with an arbitrary bias scale (e.g. user-supplied or from a
non-canonical quantizer), the selector still fuses the subgraph and the
QLinearConv kernel produces silently wrong outputs at
`ORT_ENABLE_EXTENDED` and above on CPU EP. CUDA and
disabled-optimization paths produce correct results, making the bug
particularly hard to diagnose.
## Changes
-
`onnxruntime/core/optimizer/qdq_transformer/selectors_actions/qdq_selectors.cc`:
add `CheckConvBiasScale` static helper. Returns `false` (skip fusion)
when:
- any of x/w/b scales is not a constant initializer
- any scale dtype is not float32
- `x_scale` is not a scalar / 1-element rank-1 tensor
- `b_scale` length is neither 1 nor `num_channels`
- any per-channel bias scale differs from `x_scale × w_scale[i]` by more
than `atol=1e-6 + rtol=1e-2 × |expected|`
- `onnxruntime/test/python/quantization/test_qdq.py`: new
`TestConvBiasScaleValidation` class with two cases — mismatched bias
scale (asserts optimized output matches unoptimized) and matching bias
scale (asserts correctness preserved when fusion is allowed).
## Test Plan
- `python -m pytest
onnxruntime/test/python/quantization/test_qdq.py::TestConvBiasScaleValidation
-v`
- Existing QDQ Conv tests (`verify_quantize_conv` family) should
continue to pass — fusion is unchanged for canonical quantizer-produced
models where bias_scale equals input_scale × weight_scale exactly.
- Reproduce the issue with the model from #24711 and confirm CPU
`ORT_ENABLE_ALL` output now matches `ORT_DISABLE_ALL`.
---------
Co-authored-by: Tianlei Wu <tlwu@microsoft.com>