Conversation
|
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. |
Contributor
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: trueComment |
Fridah-nv
added this pull request to stack #2538
September 23, 2026 21:57
Wires the compiled plan up to execution, and adds the one capability that only makes sense once stages run in sequence: which NVFP4 weight grid each needs. Each stage is applied as its own calibration mode through the ordinary `apply_mode`, so it is recorded in the modelopt state under its own name -- `["quantize", "max_calibrate", "mse_calibrate"]` -- and the mode graph sees the sequence rather than one opaque entry. Values derived from the plan, the `should_process` write-mask and the hand-off kwargs, travel through `mode_kwargs`, which reaches the convert entrypoint and is deliberately never saved: they describe how this run was produced, not what the model is. `skip_max_init` returns to `mse`, `local_hessian` and `gptq` as a function parameter the executor supplies from `derive_handoff`. It is not user-facing config: it is a fact about what the previous stage produced, and a user who sets it by hand is asserting something the compiler already knows. This is what lets a range search feed GPTQ -- GPTQ compensates rounding error against a specific grid, so when `mse` searched a better one the compensation must be computed against that grid, not a fresh max one. NVFP4 grid type becomes part of the contract. `awq_lite` folds a smoothing scale into the weight and needs scales the kernel derives at run time; `mse` and `local_hessian` with `fp8_scale_sweep` search stored per-block scales and need a static grid. `requires_weight_scales` declares which, a `prepare` hook upgrades dynamic to static at the start of the stage that needs it, and static never downgrades because that would discard a completed search. The requirement belongs to the consumer, so the upgrade happens at the start of the stage that needs it rather than the end of the one before -- a standalone run must not be dragged into a state it never asked for. The rule that enforces this walks the plan in order rather than checking the model once, because `prepare` changes grid type between stages: what a later stage sees is the initial layout plus every upgrade before it. It also filters to weight-side quantizers, since the requirement is about weight block scales and recipes deliberately pair a static weight grid with dynamic activations. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Fridah-nv <201670829+Fridah-nv@users.noreply.github.com>
Fridah-nv
force-pushed
the
fridah/algo-cfg-4-execute
branch
from
September 23, 2026 22:02
d1ed511 to
bed7410
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Brief
What changes: Different algorithms can now run on different parts of a model, and chain — a range search can feed GPTQ, and an NVFP4 grid mismatch is rejected rather than silently mis-calibrating.
How: Implements plan execution by treating each algorithm as a mode: every stage is applied through the ordinary
apply_mode, so the sequence is recorded in modelopt state rather than hidden inside one opaque entry. Values derived from the plan — the write-mask, theskip_max_inithand-off — travel viamode_kwargs, which reaches the convert entrypoint and is never saved.What does this PR do?
Type of change: new feature
Stack 4 of 4, splitting #2292. Base: #2536. This is where the feature becomes usable.
algo_cfgOne mode per stage
Each stage is applied as its own calibration mode through the ordinary
apply_mode, so it is recorded in the modelopt state under its own name —["quantize", "max_calibrate", "mse_calibrate"]— and the mode graph sees the sequence rather than one opaque entry.Values derived from the plan — the
should_processwrite-mask from #2535, the hand-off kwargs — travel throughmode_kwargs, which reaches the convert entrypoint and is deliberately never saved: they describe how this run was produced, not what the model is.The hand-off
skip_max_initreturns tomse,local_hessianandgptqas a function parameter the executor supplies fromderive_handoff. It is not user-facing config: it is a fact about what the previous stage produced, and a user setting it by hand would be asserting something the compiler already knows.This is what lets a range search feed GPTQ. GPTQ compensates rounding error against a specific quantization grid, so when
msesearched a better one the compensation must be computed against that grid, not a fresh max one — which is the order DeepCompressor's QoQ recipes ship (qoq-gchn.yaml:enable_calib_rangethenenable_kernel_gptq). Previouslygptq()re-derived amax unconditionally and the search in front of it was discarded.NVFP4 grid type becomes part of the contract
The sharpest case for capabilities being per stage rather than per model.
awq_litefolds a smoothing scale into the weight and needs block scales the kernel derives at run time (type: dynamic);mse/local_hessianwithfp8_scale_sweepsearch stored per-block scales and needtype: static. Running either against the wrong layout is not a tuning difference — it fails, or silently searches nothing.requires_weight_scalesdeclares which. Apreparehook upgrades dynamic to static at the start of the stage that needs it; static never downgrades, since that would discard a completed search. The requirement belongs to the consumer, which is why the upgrade happens at the start of the stage that needs it rather than the end of the one before — a standalone run must not be dragged into a state it never asked for.So the intended flow —
awq_liteon a dynamic grid, thenmsewith the sweep on a static one — is one plan, and the reverse is rejected at compile with the reason named.Three defects the compile sweeps found, fixed here
Exhaustive sweeps (13,134 compiles) over this stack found four defects; three belong with the grid contract:
preparemutates grid type between stages, but the rule read the model as it is at compile time.[mse(fp8_scale_sweep), awq_clip]compiled clean and was rejected by the identical config once stage 0's prepare had run — so at runtimeawq_clipgot the static grid the rule exists to keep it away from. It now walks the plan in order. 24 of 112 static-then-dynamic chains were wrongly accepted; now 0.prepareupgraded activations too. No role filter, so on the shape the shipped static presets use —nvfp4_staticfor*weight_quantizer, dynamicnvfp4for*input_quantizer— all input quantizers were silently converted to static and lost their amax.awq_clipon a model whose weight grid was dynamic because its activations were static.Testing
test_algo_cfg.pygrows to 60 tests: per-stage mode recording,mse → gptqpreserving the searched amax bit-identically,awq_lite → mse,awq_fullreproduced exactly as its two-stage pipeline, the grid contract (upgrade, activations left alone, upgrade visible to later stages, static activations not blocking a weight-side algorithm), and legacy-path numerical equivalence.Each defect fix is mutation-verified: the fix is reverted, the specific test confirmed to fail, and the fix restored.
pytest tests/unit/torch/quantization/→ 1195 passed, 8 skipped, plus the same two pre-existingtest_huggingface.pyfailures and one pre-existing collection error described in #2534.Cost of the execution model, and what it leaves open
Each stage is a separate
apply_modehanded the sameforward_loop, so N stages that needactivations cost N forward passes over the calibration set. Invisible on a toy model, not
invisible on a 9B with 512 sequences. Worth stating explicitly since it is a consequence of the
one-mode-per-stage choice, not an accident.
What it does not cost: chained stages could never share a forward anyway —
mse → gptqmeansGPTQ reads the grid MSE produced. The avoidable case is stages on disjoint scopes, which today
still pay for a pass each.
That optimisation stays reachable, and deliberately so. The write-mask predicate is a lowering of
structured information that survives on
AlgoStage— selector, glob, exclusions — so an optimisercan decide two stages are disjoint from the plan itself rather than introspecting an opaque
callable. Batching them would mean enabling collection for both stages' targets, running one pass,
and finishing each separately; that works while their quantizer sets do not overlap, which disjoint
scopes guarantee. It is a contained change in
_apply_calibration_plan, needing nothing from themask.
Reads are whole-model by necessity, not by choice: activations arriving at a scoped module depend on
the entire network upstream, so narrowing reads would make a scoped search optimise against a
distribution the deployed model never sees. The one legitimate narrowing — layerwise calibration —
already exists and is orthogonal, which is why the mask is keyed on module identity and survives
that reparenting.
The asymmetry a future change will have to confront: the mask is per-module boolean, while the
capability model reasons per (token, role). The compiler knows a stage writes
weight_amaxbutnot
input_amax; the runtime mask can only say "this module, yes or no"._reject_partial_module_scopeexists to cover that gap. Two stages writing different roles of the same module concurrently would
require dropping that rule and making the mask per-(module, token).
Known limitations
nvfp4_act_headroomtakes noshould_process;svdquantandlsqswallow it via**kwargsand would run over the whole model.scopablerecords this and compile rejects a scoped stage for them rather than mis-calibrating; whole-model use is unchanged.auto_quantize, andsvdquant(compiles and validates, never executed).algo_cfg[prototype] #2292 predate the grid contract and were not re-measured; see that PR for the caveat.Before your PR is "Ready for review"
algo_cfgthe old path, its numerics and its saved state are unchanged;algorithm="max"and the equivalentalgo_cfgproduce bit-identical amax.CONTRIBUTING.md: N/A🤖 Generated with Claude Code