Skip to content

feat(quantization): declare per-algorithm calibration capabilities [1/4] - #2530

Closed
Fridah-nv wants to merge 1 commit into
mainfrom
stack/A-capabilities
Closed

Fridah-nv wants to merge 1 commit into
mainfrom
stack/A-capabilities

Conversation

@Fridah-nv

Copy link
Copy Markdown
Contributor

What does this PR do?

Type of change: refactor + bug fix

Stack 1 of 4, splitting #2292 (scoped calibration pipelines) into independently reviewable pieces. This one lands nothing user-facing and is useful on its own.

# PR
1 capabilities ← you are here
2 write-mask
3 compile algo_cfg
4 execute the plan

Declare what each calibration algorithm does

Each algorithm now states what it reads, writes and assumes as an AlgoCapabilities on its BaseCalibrateModeDescriptor subclass: writes_whole_module, refines, requires, may_write, invalid_if_present, scopable.

They live on the descriptor rather than in a side table keyed by algorithm name. CalibrateModeRegistry is already the one-object-per-algorithm registry, so a second dict would be a parallel registry that can fall out of sync — and an algorithm registered by a user, the documented extension point, would get None from such a lookup. On the descriptor it inherits a conservative default instead.

capabilities_for(algo, cfg) derives them from the algorithm's own kwargs where they genuinely vary: lsq and nvfp4_act_headroom delegate weight scales to a configurable sub-algorithm, and that algorithm's requires and may_write become theirs.

This replaces _mutates_weights

QuantizeAlgorithmConfig._mutates_weights was a hand-maintained ClassVar overridden on four config classes that said exactly what WEIGHT in may_write says. Two statements of one fact drift, and a new algorithm only has to forget one of them — understating it makes layerwise calibration skip the weight write-back and silently discard the algorithm's results.

layerwise.calib_mutates_weights becomes bool | None (None = derive). persistent_materialization(writeback=...) only controls whether weights are copied back, so True is always safe and merely costs I/O, while False is safe iff the algorithm does not write weights. There is no user preference there, only a right answer per algorithm — so the field remains as an explicit opt-out for amax-only algorithms, and an explicit False on a weight-writing one is now rejected at config time.

Also fixes MseCalibrator.reset()

_initial_amax is set only in __init__ and never repopulated, so deleting it in reset() did not reset the instance — it destroyed it. This reproduces on main today: algorithm=['max','mse','max'] crashes, because the next stage to collect stats re-enters the spent calibrator.

NVFP4MSECalibrator.reset() already kept it and documented why; the base class now matches. The memory argument for dropping it does not apply to the class that was doing it — the base calibrator's amax clone is scalar or [out_features], while the large [num_blocks] clone belongs to the subclass that retains it.

Testing

New tests/unit/torch/quantization/test_algo_capabilities.py, plus a regression test for the calibrator fix (mutation-verified: restoring the del fails it).

pytest tests/unit/torch/quantization/ → 1128 passed, 8 skipped. Two pre-existing failures in plugins/test_huggingface.py::test_quantized_transformers_save_restore (a transformers-version issue, reproduced on a clean main worktree) and one pre-existing collection error in test_diffusers_wan_conv3d.py.

Before your PR is "Ready for review"

  • Is this change backward compatible?: ✅ — calib_mutates_weights keeps its old meaning when set explicitly; unset now derives instead of defaulting to a hand-maintained flag.
  • 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?: ❌ — deferred while the stack is under review.
  • Did you get Claude approval on this PR?: ❌ — draft.

🤖 Generated with Claude Code

Each calibration algorithm now states what it reads, writes and assumes as an
`AlgoCapabilities` on its `BaseCalibrateModeDescriptor` subclass:
`writes_whole_module`, `refines`, `requires`, `may_write`, `invalid_if_present`
and `scopable`.

They live on the descriptor rather than a side table keyed by algorithm name.
`CalibrateModeRegistry` is already the one-object-per-algorithm registry, so a
second dict would be a parallel registry that can fall out of sync -- and an
algorithm registered by a user, the documented extension point, would get
`None` from such a lookup. On the descriptor it inherits a conservative default
instead. `capabilities_for(algo, cfg)` derives them from the algorithm's own
kwargs where they genuinely vary: `lsq` and `nvfp4_act_headroom` delegate weight
scales to a configurable sub-algorithm, and that algorithm's `requires` and
`may_write` become theirs.

This subsumes `QuantizeAlgorithmConfig._mutates_weights`, a hand-maintained
ClassVar overridden on four config classes that said exactly what
`WEIGHT in may_write` says. Two statements of one fact drift, and a new
algorithm only has to forget one of them -- understating it makes layerwise
calibration skip the weight write-back and silently discard the algorithm's
results. `layerwise.calib_mutates_weights` becomes `bool | None` (None =
derive): `True` is always safe and merely costs I/O, `False` is safe only if the
algorithm does not write weights, so there is no user preference here, only a
right answer per algorithm. It remains as an explicit opt-out for amax-only
algorithms, and an explicit `False` on a weight-writing one is now rejected at
config time.

Also fixes `MseCalibrator.reset()`, which deleted `_initial_amax`. That field is
set only in `__init__` and never repopulated, so the "reset" left the instance
permanently unusable: `algorithm=['max','mse','max']` crashes on main today
because the next stage to collect stats re-enters the spent calibrator.
`NVFP4MSECalibrator.reset()` already kept it and documented why; the base class
now matches. The amax clone it retains is scalar or `[out_features]`, so the
memory argument for dropping it did not apply to the class that was doing it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Fridah-nv <201670829+Fridah-nv@users.noreply.github.com>
@copy-pr-bot

copy-pr-bot Bot commented Sep 23, 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 Sep 23, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

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

@github-actions

github-actions Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-09-23 21:37 UTC

@Fridah-nv

Copy link
Copy Markdown
Contributor Author

Superseded by #2534 — same commit, branch renamed to the fridah/ convention. GitHub does not allow changing a PR's head branch, so this was recreated.

@Fridah-nv Fridah-nv closed this Sep 23, 2026
@Fridah-nv
Fridah-nv deleted the stack/A-capabilities branch September 23, 2026 21:37
@codecov

codecov Bot commented Sep 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.70130% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 68.92%. Comparing base (a21411a) to head (b240068).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
modelopt/torch/quantization/mode.py 97.87% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2530      +/-   ##
==========================================
+ Coverage   68.89%   68.92%   +0.03%     
==========================================
  Files         605      606       +1     
  Lines       67063    67129      +66     
==========================================
+ Hits        46204    46270      +66     
  Misses      20859    20859              
Flag Coverage Δ
unit 58.47% <98.70%> (+0.04%) ⬆️

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.

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.

1 participant