Skip to content

[OMNIML-5613] Quantize ResNet residual adds in torch ONNX example - #2024

Open
ajrasane wants to merge 4 commits into
mainfrom
ajrasane/quantize-resnet-residual-adds
Open

[OMNIML-5613] Quantize ResNet residual adds in torch ONNX example#2024
ajrasane wants to merge 4 commits into
mainfrom
ajrasane/quantize-resnet-residual-adds

Conversation

@ajrasane

@ajrasane ajrasane commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Type of change: Bug fix

Quantizes shortcut inputs in timm ResNet BasicBlock and Bottleneck modules immediately before residual addition. The new residual quantizers are calibrated with the example's calibration data and support INT8, FP8, MXFP8, NVFP4, and Auto activation-quantized modes.

The ONNX example test verifies that all 16 ResNet-50 residual Add nodes have a Q/DQ input and feed directly into Relu. The README and changelog document the new behavior.

Usage

python examples/torch_onnx/torch_quant_to_onnx.py \
    --timm_model_name=resnet50 \
    --quantize_mode=int8 \
    --onnx_save_path=resnet50.int8.onnx

Testing

  • Ran all configured pre-commit hooks on the modified files.
  • Exported a randomly initialized INT8 ResNet-50 with one calibration sample.
  • Validated the exported model with ONNX checker.
  • Verified all 16 residual Add nodes have Q/DQ immediately before the addition.
  • Built and executed the model with TensorRT 10.15 using strongly typed mode.

Before your PR is "Ready for review"

Make sure you read and follow Contributor guidelines and your commits are signed (git commit -s -S).

Make sure you read and follow the Security Best Practices (e.g. avoiding hardcoded trust_remote_code=True, torch.load(..., weights_only=False), pickle, etc.).

  • Is this change backward compatible?: ✅
  • If you copied code from any other sources or added a new PIP dependency, did you follow guidance in CONTRIBUTING.md: N/A
  • Did you write any new necessary tests?: ✅
  • Did you update Changelog?: ✅

🤖 Generated by Codex (AI agent).

Summary by CodeRabbit

  • Bug Fixes

    • Improved torch ONNX ResNet quantization by quantizing shortcut/residual-path inputs before residual addition across supported quantization modes (including automatic formats when applicable).
  • Tests

    • Added ONNX graph checks to verify that residual additions are correctly quantized after export (validated for ResNet50).
  • Documentation

    • Updated the torch ONNX quantization example documentation and the changelog to reflect the residual-path quantization behavior.

ajrasane added 2 commits July 28, 2026 17:24
Signed-off-by: ajrasane <131806219+ajrasane@users.noreply.github.com>
Signed-off-by: ajrasane <131806219+ajrasane@users.noreply.github.com>
@copy-pr-bot

copy-pr-bot Bot commented Jul 28, 2026

Copy link
Copy Markdown

Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message here.

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The Torch ONNX ResNet example now quantizes residual shortcut inputs before addition, calibrates those quantizers, validates exported ONNX graphs, and documents the behavior.

Changes

ResNet residual quantization

Layer / File(s) Summary
Residual quantizer installation and calibration
examples/torch_onnx/torch_quant_to_onnx.py
ResNet residual quantizers are configured by quantization mode, calibrated with the data loader, and enabled after calibration.
Exported ONNX residual validation
tests/examples/torch_onnx/test_torch_quant_to_onnx.py
The ResNet test exports to a temporary path and verifies that all residual Add inputs originate from quantized paths.
Example documentation and changelog
examples/torch_onnx/README.md, CHANGELOG.rst
Documentation and release notes describe residual shortcut input quantization before addition.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Main
  participant ResNetBlocks
  participant DataLoader
  participant TensorQuantizer
  participant ONNXTest
  Main->>ResNetBlocks: Install residual quantizers
  Main->>DataLoader: Run calibration batches
  DataLoader->>TensorQuantizer: Collect activation ranges
  Main->>TensorQuantizer: Load amax and enable quantization
  Main->>ONNXTest: Export quantized model
  ONNXTest->>ONNXTest: Verify Add inputs originate from quantized paths
Loading

Suggested reviewers: vishalpandya1990, kevalmorabia97

🚥 Pre-merge checks | ✅ 6
✅ Passed checks (6 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Security Anti-Patterns ✅ Passed Changed Python files add no forbidden security patterns; no torch.load/allow_pickle/trust_remote_code/eval/exec/nosec, and no dependency files changed.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly states the main change: quantizing ResNet residual adds in the torch ONNX example.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ajrasane/quantize-resnet-residual-adds

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Jul 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 66.65%. Comparing base (33d05b0) to head (d2a78de).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2024      +/-   ##
==========================================
- Coverage   66.83%   66.65%   -0.18%     
==========================================
  Files         519      519              
  Lines       58916    58916              
==========================================
- Hits        39376    39272     -104     
- Misses      19540    19644     +104     
Flag Coverage Δ
unit 54.89% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

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

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ajrasane
ajrasane marked this pull request as ready for review July 28, 2026 18:03
@ajrasane
ajrasane requested review from a team as code owners July 28, 2026 18:03
@ajrasane ajrasane self-assigned this Jul 28, 2026
Signed-off-by: ajrasane <131806219+ajrasane@users.noreply.github.com>

@cjluo-nv cjluo-nv left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Bot review (bedrock-claude-opus-5) — DM the bot to share feedback.

Small, focused change (+90/-2) that adds per-block residual-add quantizers to the timm ResNet path of the torch→ONNX example, with CHANGELOG + README updates and an ONNX-level assertion in the existing example test. No licensing concerns; no prompt-injection content in the PR metadata. A few things worth addressing before merge:

  1. Duplicated calibration logic / extra data pass. _add_resnet_residual_quantizers re-implements the enable_calib → forward-loop → load_calib_amax dance that _calibrate_uncalibrated_quantizers (same file) already performs, and adds a second full pass over the calibration set for ResNet (the first happens inside quantize_model for the FP8 Conv overrides). If the residual quantizer were attached as input_quantizer on the activation module and created before quantize_model, both the calibration and the dead-quantizer guard would come for free from the existing helpers. Even more idiomatic: modelopt already supports this via QuantModuleRegistry.register({nn.ReLU: "nn.ReLU"})(QuantInputBase) (exactly what modelopt/torch/quantization/nn/modules/quant_activations.py does for nn.LeakyReLU) plus a {"parent_class": "nn.ReLU", "quantizer_name": "*input_quantizer"} config entry, which gets calibrated by mtq.quantize's forward loop and is visible to mtq.print_quant_summary / modelopt state. Please either reuse one of these paths or note in the PR/comment why the manual hook is needed.

  2. Residual quantizers bypass the file's own amax<=0/NaN guard, and load_calib_amax() is strict. _disable_dead_quantizers only inspects input_quantizer/output_quantizer/weight_quantizer, and it runs inside quantize_model — i.e. before these quantizers exist. A residual quantizer that calibrates to amax == 0 (or NaN) will therefore reach the FP8 exporter, which is precisely the scale = 448 / amax division the guard exists to prevent. Also, unlike _calibrate_uncalibrated_quantizers, this code calls load_calib_amax() without strict=False, so any block that didn't see data raises.

  3. auto mode picks the residual format from the search space, not the search result. After mtq.auto_quantize, each block's actual format is known (e.g. block.conv3.input_quantizer._num_bits); deriving num_bits from the union of requested formats can give an FP8 residual Q/DQ next to an INT8-quantized block (or the reverse), which is what the rest of this file goes to some length to avoid for TRT.

Minor: the two new functions are the only helpers in this file without docstrings; and assert len(residual_adds) == 16 asserts that every Add in the exported graph is a residual add, which will break confusingly if the exporter ever emits an unrelated Add — consider filtering to the 16 residual adds (e.g. by producer/consumer pattern) before the count assertion.

if not residual_quantizers:
return

for quantizer in residual_quantizers:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Bot comment.

This block duplicates _calibrate_uncalibrated_quantizers (a few dozen lines below) and costs a second full pass over the calibration set for ResNet, since quantize_model already ran one for the FP8 Conv overrides.

Two cheaper options:

  • Attach the quantizer as input_quantizer on the activation module and create it before quantize_model — then _calibrate_uncalibrated_quantizers and _disable_dead_quantizers both pick it up automatically, and this whole calibration block goes away.
  • Or use the framework path: QuantModuleRegistry.register({nn.ReLU: "nn.ReLU"})(QuantInputBase) (see modelopt/torch/quantization/nn/modules/quant_activations.py, which does exactly this for nn.LeakyReLU) plus a {"parent_class": "nn.ReLU", "quantizer_name": "*input_quantizer", "cfg": {...}} entry restricted to *act3*/*act2*, so mtq.quantize's forward loop calibrates it and print_quant_summary reports it.

model.train(was_training)

for quantizer in residual_quantizers:
quantizer.load_calib_amax()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Bot comment.

Two robustness gaps relative to the sibling helper in this file:

  1. load_calib_amax() is strict here, whereas _calibrate_uncalibrated_quantizers uses load_calib_amax(strict=False); a block that saw no data will raise instead of being skipped.
  2. These quantizers are created after _disable_dead_quantizers runs (inside quantize_model), and that helper only looks at input_quantizer/output_quantizer/weight_quantizer — so a residual quantizer that calibrates to amax == 0 or NaN reaches the FP8 exporter and hits the scale = 448 / amax blow-up the guard was added for.

Suggest either naming the attribute so the existing helpers cover it, or applying the same amax <= 0 / isnan disable check to residual_quantizers after load_calib_amax(strict=False).

# Dynamic block quantizers do not support the residual path's 4D tensors.
num_bits = (4, 3)
elif quantize_mode == "auto":
activation_formats = set(auto_quantization_formats) - {"INT4_AWQ_CFG"}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Bot comment.

In auto mode this infers the residual format from the requested search space rather than what auto_quantize actually selected per block, so a block that ended up INT8 can get an FP8 residual Q/DQ (or vice versa) — the kind of precision mismatch the rest of this file works hard to avoid for TRT. Since this runs after the search, the per-block answer is available, e.g. getattr(block.conv3, "input_quantizer", None)._num_bits (fall back to the current heuristic if the quantizer is absent/disabled).

consumers[input_name].append(node)

residual_adds = [node for node in model.graph.node if node.op_type == "Add"]
assert len(residual_adds) == 16

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Bot comment.

residual_adds is every Add node in the graph, so this asserts "ResNet-50 exports exactly 16 Adds and all of them are residual". That's incidentally true today, but it will fail confusingly if the exporter ever emits an unrelated Add (bias/fused pattern, autocast helper, etc.). Prefer identifying the residual adds structurally (e.g. Add whose single consumer is the Relu that follows the block) and then asserting the count of those is 16.

return (module.residual_quantizer(inputs[0]),)


def _add_resnet_residual_quantizers(model, quantize_mode, auto_quantization_formats, data_loader):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Bot comment.

Nit: every other helper in this module has a docstring explaining the TRT/architecture reason it exists (see _disable_low_channel_conv_input_quantizers). Please add one here covering why only act2/act3 are hooked, why FP8 per-tensor is used instead of MXFP8/NVFP4 blocks for the 4D residual tensor, and why int4_awq is skipped.

@coderabbitai coderabbitai Bot 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.

Warning

CodeRabbit couldn't request changes on this pull request because it doesn't have sufficient GitHub permissions.

Please grant CodeRabbit Pull requests: Read and write permission and re-run the review.

👉 Steps to fix this

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@examples/torch_onnx/torch_quant_to_onnx.py`:
- Around line 223-239: Update _add_resnet_residual_quantizers and the
surrounding auto-quantization flow so residual quantizers are installed and
configured before mtq.auto_quantize() runs. For auto mode, include these
residual quantizer modules in every candidate format configuration used by the
search, ensuring their forced INT8/FP8 precision is scored and counted toward
the effective-bits constraint while preserving the existing non-auto behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 9cd948d5-f05e-4d18-9714-517039e16f2d

📥 Commits

Reviewing files that changed from the base of the PR and between 87c9f8c and c15755c.

📒 Files selected for processing (4)
  • CHANGELOG.rst
  • examples/torch_onnx/README.md
  • examples/torch_onnx/torch_quant_to_onnx.py
  • tests/examples/torch_onnx/test_torch_quant_to_onnx.py

Comment thread examples/torch_onnx/torch_quant_to_onnx.py Outdated
Signed-off-by: ajrasane <131806219+ajrasane@users.noreply.github.com>

@coderabbitai coderabbitai Bot 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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
examples/torch_onnx/torch_quant_to_onnx.py (1)

636-642: ⚠️ Potential issue | 🟠 Major

Install residual quantizers before the quantization/search pass.

At Line 636, residual quantizers are added only after quantized_model has been created. In auto mode, they are therefore absent from candidate scoring and effective-bits constraints; the later heuristic can also choose a format different from the per-block format selected by AutoQuantize. The standard path additionally requires a second calibration pass and runs dead-quantizer cleanup before these modules exist.

Move installation/configuration before quantization, or explicitly integrate these quantizers into AutoQuantize and rerun cleanup after calibration.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/torch_onnx/torch_quant_to_onnx.py` around lines 636 - 642, Move the
`_add_resnet_residual_quantizers` installation and configuration before the
quantization/search pass creates `quantized_model`, so residual quantizers
participate in AutoQuantize candidate scoring and effective-bits constraints.
Ensure calibration and dead-quantizer cleanup operate on these modules, and
remove the current post-quantization-only installation path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@examples/torch_onnx/torch_quant_to_onnx.py`:
- Around line 636-642: Move the `_add_resnet_residual_quantizers` installation
and configuration before the quantization/search pass creates `quantized_model`,
so residual quantizers participate in AutoQuantize candidate scoring and
effective-bits constraints. Ensure calibration and dead-quantizer cleanup
operate on these modules, and remove the current post-quantization-only
installation path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 0ba0d581-0d68-4e4d-993c-47c5819b72dd

📥 Commits

Reviewing files that changed from the base of the PR and between c15755c and d2a78de.

📒 Files selected for processing (4)
  • CHANGELOG.rst
  • examples/torch_onnx/README.md
  • examples/torch_onnx/torch_quant_to_onnx.py
  • tests/examples/torch_onnx/test_torch_quant_to_onnx.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • examples/torch_onnx/README.md
  • CHANGELOG.rst
  • tests/examples/torch_onnx/test_torch_quant_to_onnx.py

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants