fix(awq): stop variable-length calibration collapsing to the last batch - #3036
Conversation
Variable-length calibration batches cannot be concatenated on dim 0, so
_layer_input_features() silently kept only tensors[-1] per module. Every
earlier batch was discarded from AWQ scale statistics; for fine-grained
MoE models this reduces per-expert calibration to a single batch.
Add an opt-in, model-declared aggregation policy:
- models may define awq_input_feature_aggregation(module_name) returning
{"mode": "token_rows", "max_tokens": N, "capture_root": bool} for
pointwise modules; ragged captures are packed into deterministic,
bounded [1, retained, hidden] token rows so every batch contributes
- moe_lifecycle forwards the shared MoE-root input to the processor once
per calibration batch (deduped) when the policy requests capture_root
- capture order is restored to calibration-batch order before collapse
- per-module aggregation stats (mode/raw/retained/batches) are recorded
and surfaced in the quant log; the previously silent latest-batch drop
is now visible as mode=latest_batch with raw vs retained token counts
Default behavior for models without a policy is unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5598cc7e40
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Address review: uniform sampling over concatenated offsets could skip a short batch entirely (e.g. lengths [1000, 1, 1000] with max_tokens=8), breaking the stated every-batch guarantee. Reserve one row per batch and distribute the remaining budget proportionally to batch length (largest remainder, ties by batch order), sampling evenly inside each batch. When max_tokens is below the batch count, take one leading row from evenly spaced batches. Also document why group fallback compares raw routed tokens (corpus coverage) rather than the policy's retained sampling bound. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rate Downstream exhaustive enumeration (1,136,541 small-scale combinations) found 3,499 cases where the single-pass leftover distribution stranded budget once high-remainder batches saturated: lengths [1, 1, 1, 5] with max_tokens=7 returned only 6 rows. Length-1 batches are the common case for MoE experts hit by a single token, so this is not a corner case. Distribute leftover budget over multiple passes; retained_tokens <= total_tokens guarantees capacity, so the loop always terminates with zero leftover. Add an exhaustive small-scale property test asserting retained == min(total, budget) and per-batch coverage for every combination. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Qubitium
left a comment
There was a problem hiding this comment.
@Leonccaa Codex review:
-
P1 — Fallback behavior regressed for legacy ragged inputs.
_should_fallback_groupalways usesraw_tokens. Forlatest_batch, raw tokens include discarded batches while AWQ uses only the final batch, so the default fallback threshold can be incorrectly bypassed. Use raw counts only fortoken_rows; otherwise use retained rows. -
P1 — The advertised fix is inactive for all in-tree models.
The patch adds an optional hook lookup, but no public model definesawq_input_feature_aggregation. Therefore every current model still followslatest_batch. Add model implementations and integration tests, or narrow the PR scope. -
P1 — Padding tokens can contaminate the new statistics.
record_moe_root_input_featurerecordshidden_stateswithout applying the active keep mask. Padded rows can enter token-row sampling and inflate coverage. Apply masking consistently before root capture. -
P2 —
nsamplesreporting changed for models without a policy.
apply_quantuses retained rows unconditionally, so legacylatest_batchmodules now report only the final batch instead of total calibration samples. -
P2 — Empty captures underfill the token budget.
_pack_token_rowsfails for zero-row tensors;[0, 10]withmax_tokens=1returns zero rows. Filter empty batches or distribute quotas only across non-empty batches.
Validation: six targeted new unit tests passed; compilation and whitespace checks passed. Full integration testing was blocked by an existing CUDA/CPU device mismatch, and GitHub reports no PR checks.
|
@Leonccaa Also I don't see a model or model def that actually enables the new aggregation policy so I cannot run ci tests on this PR with a real (small) model. Can you push a model def change that enables this? |
|
@Leonccaa Currently doing PR cleanup and refractor. Will let you know when I am done and you can re-test on your end. |
|
@Leonccaa I have pushed all the changes.
The rest of small pattern fixes. Please test and verify on your end and push any regression you find. |
Qubitium
left a comment
There was a problem hiding this comment.
Re-reviewed the current head. The earlier requested changes are resolved, and successful AWQ layer finalization now also releases child/root capture tasks, aggregation telemetry, replay kwargs, module tracking, and scale context on both early-completion paths. Focused regression coverage passes with no remaining code findings.
Thanks for pushing these changes and the final cleanup. We reviewed the latest head. The automatic :moe enablement and calibration-derived token-row budget address our remaining concerns, including removal of the fixed 512-token cap. Our focused validation has passed, including a tiny Qwen3-MoE AWQ ExpertsRoutingBypass quantize → save → reload smoke test. We are currently running a full Qwen 3.8-Flash-Next AWQ quantization with this fix. Qwen 3.8-Flash-Next uses ExpertsRoutingBypass, so this run directly exercises the new root-capture path. We’ll report back after the full quantization and post-quantization checks finish, and will push any regression we find. Thanks again. |
|
@Leonccaa e2e testing on real model found more lifecycle bugs in the AWQ processor when this new pr fix/feature is activated. Pending fix incoming. |
|
@Leonccaa Bug fixed and more refractor passed tests. You can run your own tests now. |
|
Validation update for commit
The review findings are covered in the current head: active keep-mask application during root capture, raw-vs-retained fallback accounting, legacy |
|
@Leonccaa Please create new PR if you find any bugs. |
Summary
Fixes #3035.
Variable-length calibration batches cannot be concatenated on dim 0, so
_layer_input_features()silently kept onlytensors[-1]per module — every earlier batch was discarded from AWQ scale statistics. For mixture-of-experts models this collapses per-expert calibration to a single batch.This PR adds a model-declared feature-aggregation policy so de-fused pointwise expert projections pack every ragged batch into deterministic, bounded token rows, plus per-module aggregation accounting. Public model classes with a module tree marked
:moeinherit the policy; dense-only models remain on the existing path.What Changed
awq_input_feature_aggregation(module_name)returning{"mode": "token_rows", "capture_root": bool}orNone, with an optional explicitmax_tokensoverride.BaseQModelenables it for the declared expert root and its de-fused pointwise children;AWQProcessor._feature_aggregation_policy()validates and applies it.AWQProcessor._pack_token_rows(): packs ragged captures into[1, retained_tokens, hidden]under a workload-derived bound. The automatic budget uses one largest observed batch equivalent, raised only when needed to retain at least one row from every contributing batch, and never exceeds raw routed rows. Remaining capacity is distributed proportionally to batch length, with rows sampled evenly inside each batch._record_input_feature()gainsdedupe_batch;record_moe_root_input_feature()records the shared pre-router expert input when the policy requestscapture_root.mode,raw_tokens,retained_tokens,batches) are recorded for both activation and scale features and surfaced in quant log rows.nsamplesreports retained tokens when a policy is active and is unchanged otherwise.raw_tokens(corpus coverage routed to the module) rather than the policy's retained sampling bound. For the unchanged latest-batch fallback, the policy reports the actual retained rows.Review Follow-ups Included
:moeroot per module-tree variant. The audit corrected the missingmlpmarker for GPT-OSS and added Granite MoE Hybrid's Defuser-expandedblock_sparse_moeexpert paths and marker._feature_stats,_scale_feature_by_module, and_feature_task_names); the AWQ-prefixed kwargs key remains namespaced because it crosses into model scaling hooks.Tests
76 passed.