Skip to content

fix(torchlib): clamp quantize_per_tensor to quant_min/quant_max - #3052

Open
Mohammed Alkindi (MohammedAlkindi) wants to merge 2 commits into
microsoft:mainfrom
MohammedAlkindi:fix/clamp-quantize-per-tensor
Open

Mohammed Alkindi (MohammedAlkindi) wants to merge 2 commits into
microsoft:mainfrom
MohammedAlkindi:fix/clamp-quantize-per-tensor

Conversation

@MohammedAlkindi

Copy link
Copy Markdown
Contributor

quantized_decomposed_quantize_per_tensor takes quant_min and quant_max and never uses them. It emits only QuantizeLinear, which saturates to the full range of dtype, so a reduced range is silently ignored.

Its sibling quantized_decomposed_quantize_per_channel in the same file already handles this, and its comment says why: "QuantizeLinear saturates to the full range of dtype. PyTorch clamps to the explicit quant_min/quant_max instead." The per-tensor path never got the same treatment.

Measured in onnxruntime on opset 20, scale 1.0, zero_point 0, quant_min=0, quant_max=20, int8:

input                  [-50, -1,  0,  5, 20, 25, 100,  3]
QuantizeLinear only    [-50, -1,  0,  5, 20, 25, 100,  3]
QuantizeLinear + Clip  [  0,  0,  0,  5, 20, 20,  20,  3]

The second row is what ships today; the third matches PyTorch's reference, clamp(round(input/scale) + zero_point, quant_min, quant_max).

The test mirrors test_quantize_per_channel_clamps_to_quant_min_max directly above it.

ruff check and ruff format --check on ruff 0.15.1 with the repo's pyproject.toml both pass on the two changed files.

Could not verify: I did not run the nox matrix across the torch-nightly, onnx-weekly and ort-nightly variants. The behavioural evidence above comes from onnxruntime directly rather than through a torch export.

quantize_per_tensor accepted quant_min and quant_max but emitted only QuantizeLinear, which saturates to the full range of dtype. quantize_per_channel already clips to the explicit bounds for this reason; this applies the same treatment to the per-tensor path.

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.

Copilot review overview

🟡 Changes recommended

Tensor-valued parameters in the registered .tensor2 overload are incorrectly converted into constants.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)
What changed in this PR

Adds explicit range clamping to per-tensor quantization to match PyTorch semantics.

Changes:

  • Clips quantized values to quant_min/quant_max.
  • Adds an end-to-end reduced-range regression test.
File Description
quantized_decomposed.py Adds per-tensor range clipping.
e2e_ops_tests.py Tests reduced-range quantization.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +41 to +48
quantized = op.QuantizeLinear(input, scale, common.constant(zero_point, dtype=dtype))
# QuantizeLinear saturates to the full range of ``dtype``. PyTorch clamps to the
# explicit ``quant_min``/``quant_max`` instead, so clamp to match its semantics.
return op.Clip(
quantized,
common.constant(quant_min, dtype=dtype),
common.constant(quant_max, dtype=dtype),
)
@gramalingam

Copy link
Copy Markdown
Collaborator

Please address copilot comments

@codecov

codecov Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 72.73%. Comparing base (08898ad) to head (fa0dc9c).

Files with missing lines Patch % Lines
...unction_libs/torch_lib/ops/quantized_decomposed.py 0.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3052      +/-   ##
==========================================
- Coverage   72.73%   72.73%   -0.01%     
==========================================
  Files         265      265              
  Lines       32358    32359       +1     
  Branches     3069     3069              
==========================================
  Hits        23536    23536              
- Misses       7779     7780       +1     
  Partials     1043     1043              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

The .tensor and .tensor2 overloads pass zero_point, quant_min and
quant_max as tensors, so they reach the function as graph values.
common.constant cannot materialize a graph value into an initializer, and
exporting .tensor2 failed with "int() argument must be a string, a
bytes-like object or a real number, not 'SymbolicTensor'". Cast a graph
value in the graph instead, and keep the constant path for the scalar
overload.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

3 participants