Add GPTQ quantization support for K-last MoE architectures - #2610
Conversation
…ent) This commit checkpoints the in-progress MoE quantization work before a larger refactor that deletes QuantLinear/QuantEmbedding in favour of storing every quantized weight (2D linear, 2D embedding, 3D MoE experts) as a QuantTensor nn.Parameter on the original host module. Included so far: - New olive/common/quant/patterns.py for re: prefix matching in modules_to_not_convert / overrides. - New olive/common/quant/tensor.py with QuantTensor wrapper subclass (_make_wrapper_subclass + __torch_function__ + __torch_dispatch__), supporting 2D and 3D layouts. - LayerWrapper.get_experts() / get_router() accessors. - 3D quantize helpers in olive/common/quant/utils.py. - moe field on OliveHfQuantizationConfig. - _process_model_before_weight_loading skips ModuleList(Expert) subtrees when moe=False, fixing a latent silent-quantization bug for Mixtral / PhiMoE / Qwen2/3-MoE. - Fused-3D MoE support in prepare_model / finalize via QuantTensor parameters; current save layout uses _qweight buffer suffixes — to be replaced in the upcoming refactor with the canonical <param>.qweight/.scales/.qzeros layout. - ModelBuilder raises NotImplementedError for Olive-quantized MoE checkpoints (Mobius is the intended consumer). - Test additions: test/common/quant/test_patterns.py, test/common/quant/test_tensor.py, TestOliveHfQuantizerMoE / TestRegexOverrides in test_hf_utils.py, test/passes/pytorch/test_quant_utils.py for flatten helper, test_olive_quantized_model_raises_for_moe in test_model_builder.py. - 294 tests pass; lintrunner clean (--skip PYLINT). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Switch Olive's native quantization representation to a single design: every quantized weight is an nn.Parameter(QuantTensor) on the original host nn.Linear / nn.Embedding / fused-3D experts module, with sibling <pname>_qweight / _scales / _qzeros buffers aliasing the QuantTensor's inner tensors. Save: a state-dict hook drops the QuantTensor parameter entry; the buffers already carry the data (plain Tensors, safetensors-friendly). Load: HF's loader fills the buffers natively via dotted paths; a post-load helper re-binds the QuantTensor inner refs to the freshly loaded buffer storage. QuantLinear / QuantEmbedding (olive/common/quant/nn.py) are kept only as ONNX-exportable wrappers used by make_export_compatible_quant; they are no longer the runtime representation. * New olive/common/quant/state_dict.py with install_quant_tensor_param and refresh_quant_tensor_refs helpers. * OliveHfQuantizer rewritten for the new layout (placeholder install before weight load + ref refresh after). * finalize() in passes/pytorch/quant_utils.py installs QuantTensor params via install_quant_tensor_param (replaces the old flatten_quant_tensor_params helper). * prepare_model skips modules whose weight is already a QuantTensor, so composing multiple Rtn passes on top of a partially quantized model works. * make_export_compatible_quant detects nn.Linear / nn.Embedding whose weight is a QuantTensor and swaps them with QuantLinear / QuantEmbedding wrappers before any model dtype casting, preserving the existing com.microsoft::MatMulNBits / com.microsoft::GatherBlockQuantized symbolic export path. * OliveQuantizedModel (model_builder.py) normalizes the new <dotted>.weight_qweight key layout back to the legacy <dotted>.qweight layout for the existing genai loader, and raises NotImplementedError for moe=True checkpoints. * Tests updated to assert against QuantTensor weight instead of isinstance(module, QuantLinear); legacy tie_quant_modules tests removed; new install_quant_tensor_param test suite added. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…er to N-D * Remove olive/common/quant/nn.py (QuantModule, QuantLinear, QuantEmbedding) entirely. The only purpose of those modules was ONNX export, which is now handled by reusing the existing QuantLinearNbit from olive/common/hf/quant.py and a new parallel QuantEmbeddingNbit (com.microsoft::GatherBlockQuantized symbolic) in the same file. * Add QuantLinearNbit.from_quant_tensor / QuantEmbeddingNbit.from_quant_tensor factories so make_export_compatible_quant can swap any nn.Linear / nn.Embedding whose weight is a QuantTensor into the export wrappers. * Generalize WeightQuantizer (get_num_groups, get_qparam_shape, find_qparams, quantize, dequantize, _reshape_tensor) and pack_to_uint8 / unpack_from_uint8 to operate on any N-D tensor; quantization is always along the last dim, leading dims are preserved. * Drop quantize_along_leading_dim / pack_to_uint8_along_last / unpack_from_uint8_along_last and the explicit 3D leading-dim loops in QuantTensor.from_float and _dequantize. * Delete test/common/quant/test_nn.py; add N-D tests for the generalized quantizer + pack helpers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Instead of pre-walking the safetensors dict to rewrite ``<dotted>.weight_qweight`` -> ``<dotted>.qweight``, derive the destination attribute name inside ``set_tensor`` once we already know ``submodule`` is a ``QuantizedTensorModule``. Strip any of the known Olive buffer suffixes (``QWEIGHT_SUFFIX``, ``SCALES_SUFFIX``, ``QZEROS_SUFFIX`` from ``olive.common.quant.state_dict``) from the last path component to produce the bare ``qweight`` / ``scales`` / ``qzeros`` attribute that the genai ``QuantizedTensorModule`` expects. Also drops internal dev-iteration version labels from comments and docstrings in olive/common/quant and olive/passes/onnx. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Both QuantTensor's 2D layout and QuantLinearNbit's MatMulNBits
buffer layout pack the quantization axis as uint8 with the same
in-byte order (low nibble = elem[2j], high nibble = elem[2j+1] for
4-bit, etc.). They differ only in qweight rank: QuantTensor uses
(out, in / pack_factor), QuantLinearNbit uses
(out, n_blocks, blob_size) where n_blocks * blob_size ==
in / pack_factor. So the conversion is a pure reshape; the previous
unpack -> .t() -> from_tensors round-trip is unnecessary.
scales and qzeros buffer shapes also match exactly between the two
layouts, so they are copied as-is. For symmetric weights
(QuantTensor.qzeros is None) we fill the QuantLinearNbit.qzeros
buffer with the packed midq pattern that the contrib op expects.
Verified numerically: F.linear via QuantTensor and the
dequantize-from-buffers path through QuantLinearNbit produce
bit-identical outputs across {4,8} bits, {symmetric, asymmetric},
{groupwise, per-channel}.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The ORT contrib MatMulNBits / GatherBlockQuantized ops treat a missing zero-points input as midq for unsigned quantization, matching Olive's symmetric-quantization convention. Drop the synthetic packed-midq buffer that was previously emitted for symmetric weights and instead omit the input entirely: * QuantLinearNbit gains a has_qzeros flag (default True for back-compat); pack/from_tensors/from_quant_tensor pass through None as needed. * QuantLinearTorchFunction (TorchScript + dynamo) skips the qzeros input when None, inserting an empty placeholder only when g_idx must be positionally aligned. * QuantEmbeddingTorchFunction.symbolic gains the missing dynamo arg exposed by the new symmetric-embedding export path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
g_idx alongside a missing qzeros is not a real combination in Olive (GPTQ always produces qzeros), so skip the empty-tensor placeholder and just omit qzeros from the input list entirely. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Replace duplicated model walks in hf_utils._process_model_before_weight_loading and quant_utils.prepare_model with a shared iter_quant_targets helper that returns a list of (module, dotted_name, param_name, shape, dtype, device, kind) entries. Selection rules (lm_head/embeds/moe category flags, skip patterns, extra_skip_modules, already-quantized) live in one place. - QuantLinearNbit/QuantEmbeddingNbit: raise instead of synthesising a placeholder when g_idx is supplied alongside symmetric quantization. - tie_quant_word_embeddings: require both input and output embeddings to already be QuantTensor-backed with matching shape/dtype before tying. - Fix CodeQL mismatched-assignment false positives in QuantTensor dispatch (index args directly), fix ruff D205/D401/PLW0108/A002 warnings. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Neither attribute is read anywhere — the post-walk loop that produced the literal skip-name list was a leftover from before the refactor. The configured patterns already live on quantization_config. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Stop tagging modules with quant_info / quant_info_3d and stop branching on 2D vs 3D in the quantization passes. The quantizer already operates along the last dim regardless of rank, so a single iteration over parameters that carry a quant_info attribute is enough. - QuantTarget slims to (module, module_name, pname, full_name) with a .param property; the caller reads shape/dtype/device from the parameter directly. No more 'kind' field. - prepare_model writes target.param.quant_info in one pass — both 2D linear/embedding weights and fused experts parameters use the same code path. The quant_info_3d dict-stash on experts modules is gone. - finalize iterates every parameter that has quant_info, calls QuantTensor.from_float (already rank-generic), and installs in place. - GPTQ and AutoClip read module.weight.quant_info; module discovery uses hasattr(module.weight, 'quant_info') instead of a module-level attribute. - HF placeholder install pulls shape/dtype/device off target.param and the placeholder builder is now rank-generic. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The dataclass had four fields and a one-line .param property, used by two callers. A plain tuple is shorter, matches how the layerwise quantization loop already iterates over (module, pname, param, info) tuples, and removes the unused module_name field and dead for_each_target helper. QuantTarget remains as a type alias for the public signature. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Filter fused-MoE params to 2D/3D ranks in iter_quant_targets so a 1D bias-like parameter fails at selection time instead of much later in finalize. * refresh_quant_tensor_refs: also check isinstance(param.data, QuantTensor) for forward-compat with future torch versions that may not return the underlying subclass from nn.Parameter(). * OliveHfQuantizationConfig: replace bare '# pylint: disable' with the specific super-init-not-called rule; use output.get(k) in to_dict. * finalize: log a warning when moe=True that the resulting checkpoint isn't directly ONNX-exportable via the Olive conversion pass — it must be consumed by an MoE-aware model builder. * Add regression test that _module_weight_has_quant_info ignores nn.LayerNorm / nn.Conv2d / unmarked nn.Linear (defends GPTQ/AutoClip discovery against future drift). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- __torch_dispatch__ clone/contiguous now forwards extra args/kwargs. - iter_quant_targets skips ALL nn.Embedding when embeds=False (positional / token-type embeddings like GPT-2 wpe are no longer silently quantized). - WeightQuantizer assertion message: 2/4/8-bit (was 4/8-bit). - tie_quant_word_embeddings: mark dst aliased buffers non-persistent so safetensors save emits one copy of qweight/scales/qzeros. - finalize: group selected params by host module so each module's to(device)/to(cpu) cycle runs once for MoE experts modules carrying multiple 3D weight params. - state_dict: add ensure_state_dict_hooks(model) defensive walk that installs the save hook on every host module that owns a QuantTensor parameter (idempotent). - Add test_forward_parity.py: bit-exact eager parity for full models (embedding + linears) and fused 3D MoE forwards, plus end-to-end ONNX export -> onnxruntime numerical parity for Olive-quantized nn.Linear via make_export_compatible_quant. - Enable pylint by adding file-level protected-access disables on the files that intentionally touch nn.Module._parameters. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…main Resolve conflicts in quant_utils.py by combining the param-level iter_quant_targets walk with main's QKV-aware override renormalization and QuantTensor-based already-quantized detection. Adapt kquant.py and its tests to the storage-only (param-level quant_info) API.
…x validator + tests
… fixes Addresses adversarial-review findings on the MoE weight-level quantization work (QuantTensor storage-only design): Round-1 (8 items, per PR #2584 review): - Various correctness/robustness fixes to selection, patterns, and QuantTensor construction found in the first review pass. Round-2 (4 items, per remediation_plan_v2.md): - R2-1: patterns.py — reject nested-group alternation (`(a|b)` inside a repeated group) at any nesting depth, not just top-level. Regex safety check docstrings demoted from "prevents ReDoS" to "best-effort UX check, not a security boundary" per explicit decision not to add the `regex` third-party dependency (Option A only). - R2-2: tensor.py/hf_utils.py/state_dict.py — thread an explicit `is_placeholder` flag through QuantTensor's lifecycle so init-style ops (`zero_`, `normal_`, etc.) only no-op on real placeholders, and raise on any other QuantTensor (previously any QuantTensor silently no-op'd, masking real bugs). - R2-3: tensor.py — reject rank>1 boolean-mask indexing instead of misclassifying it as a safe leading-dim integer index. - R2-4: selection.py/defaults.yaml — rewrite `_config_indicates_moe` to reuse the existing `resolve_alias()` nested-config mechanism plus a bounded sub-config sweep, fixing DBRX-style nested MoE config detection. Known unresolved issues (found in round-3 review, NOT fixed in this commit — see PR description for details and rationale): - uint8-dtype tensor indices are still misclassified as safe integer indices (same bug class as R2-3, different dtype). - `copy_()` does not propagate/clear `is_placeholder`. - `refresh_quant_tensor_refs` clears `is_placeholder` unconditionally, not gated on an actual data load completing. - Arbitrary-rank integer indexing (added for top-k MoE routing) can produce a QuantTensor that can't be dequantized or re-indexed. - A third ReDoS bypass via `(?#...)` inline-comment regex syntax. All changes verified: 314 tests passing in test/common/quant and test/passes/pytorch/test_rtn, lintrunner clean.
transformers>=5.x defaults save_pretrained(save_original_format=True),
which for Mixtral-family MoE architectures round-trips the on-disk
state dict through a legacy per-expert nn.Linear-shaped layout
(splitting the fused-3D experts.gate_up_proj/down_proj into
experts.{i}.w1/w2/w3.weight and back). That reshape/(un)fuse machinery
assumes plain float weight tensors and silently drops the trailing
group-size dimension of our quantized _scales/_qzeros buffers, which
crashes real forward() calls on the reloaded model.
Request the new non-legacy on-disk format (save_original_format=False)
when supported, so the fused-3D quantized buffers round-trip byte-for-
byte as-is. Falls back to the default for older transformers versions
that don't accept this kwarg (they also predate the legacy-format
conversion machinery, so there's nothing to opt out of).
Add test_rtn_moe_real_forward_after_reload, a regression test that
quantizes a real MoE model, saves via the actual pass output, reloads
from disk, and calls the model's real forward() -- asserting no crash,
no NaN/Inf, and that the fused-3D scales buffer keeps its group-size
dimension. Verified this test fails at the exact corrupted-shape
assertion without the fix, and passes with it.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 85549b60-0fb9-4d65-a4e8-7a8995939d68
Two related silent-data-integrity bugs in the placeholder lifecycle: 1. copy_() did not propagate/clear is_placeholder. Copying real data into a placeholder QuantTensor left it flagged as a placeholder, so a later in-place initializer could still silently no-op and discard the just-copied real data. Fix: mirror the source's is_placeholder state after copy_. 2. refresh_quant_tensor_refs() unconditionally cleared is_placeholder for every QuantTensor parameter in the model, even though it is invoked once for the whole model with no per-parameter signal about whether that parameter's checkpoint key was actually present. A parameter with a missing key keeps its original placeholder buffer objects untouched, so we can detect an actual load per-parameter by comparing buffer object identity: only clear is_placeholder if at least one buffer object was actually swapped by the loader. Added regression tests for both fixes plus the missing-key case in test_tensor.py.
Item 1: treat torch.uint8 tensor indices as legacy boolean masks in _is_bool_index, matching PyTorch's own semantics for that dtype (same bug class as the earlier boolean-mask fix, different dtype variant). Item 3 (strengthened): refresh_quant_tensor_refs now accepts an optional checkpoint_keys set and, when provided, uses exact full-dotted-key membership as the authoritative is_placeholder determination instead of the weaker buffer-identity heuristic (which in-place .copy_() loaders can fool). OliveHfQuantizer now captures checkpoint_files (a kwarg HF's preprocess_model already passes to _process_model_before_weight_loading) via a new _read_checkpoint_keys() helper that reads .safetensors headers through safe_open().keys(), and forwards it to _process_model_after_weight_loading. Item 4: QuantTensor.__getitem__ now rejects rank>=2 integer-tensor indices (e.g. an un-flattened (tokens, k) top-k routing tensor) instead of silently producing a >3D QuantTensor that can never be dequantized or re-indexed. Callers needing a multi-dim batch of expert ids should flatten to 1-D first and reshape the dense output afterward; this restriction (Option A) is chosen over generalizing arbitrary-rank support (Option B) because there is no validated caller or design for relaxing _maybe_dense's rank-based OOM guard, and no real consumer uses rank>=2 indexing today (confirmed via repo-wide search; even GPTQModel's reference MoE calibration uses per-expert scalar indexing, not batched rank>=2 gather). Item 5: documented as a known, deferred issue rather than fixed. Found a concrete working ReDoS-scanner bypass via `(?#...)` inline-comment regex syntax (the 3rd consecutive bypass of this blacklist-enumeration check). Per discussion, this is not treated as a security vulnerability under Olive's current trust model -- re: patterns are trusted, user-authored config running in the user's own process, not adversarial input crossing a trust boundary. Added a NOTE/TODO in patterns.py recording the bypass mechanism, the decision not to patch it now, and a sketched runtime-timeout-based alternative to revisit if this config path is ever exposed to untrusted input. Tests: 317 tests in test/common/quant/ pass (up from 306), plus 44 tests across test/passes/pytorch/test_rtn.py, test_gptq.py, test_kquant.py. lintrunner clean.
- Fix Critical: tied lm_head/embed_tokens embeddings corrupted after checkpoint reload (refresh_quant_tensor_refs rewritten to dedupe shared QuantTensor objects, pick one canonical source site, and alias all hosting modules' buffers back to it). - Fix Major: refresh_quant_tensor_refs silently left placeholder (zero) weights when a checkpoint was missing expected keys; now fails closed with a RuntimeError, requiring ALL mandatory buffers (qweight+scales, +qzeros if asymmetric) to show complete load evidence (AND logic, not OR) to avoid false negatives on partial buffer loads. - Fix Major: ModelBuilder ignored regex `overrides` for per-layer bits/group_size, now resolved via match_override. - Fix Major: QuantEmbeddingNbit had no ORT block_size validation (GatherBlockQuantized requires power-of-2, >=16); added _validate_onnx_block_size to both QuantEmbeddingNbit and QuantLinearNbit. - Fix Major: QuantEmbeddingNbit.from_quant_tensor scales/qzeros shape mismatch; now reshaped like QuantLinearNbit. - Add torch.equal override for QuantTensor: transformers 5.4's tie_weights() calls torch.equal on tied meta-device params before Olive's postprocess_model hook runs, which previously crashed. Found via a full-PR review pass (readability, code, critical, deep, integration reviewers + qa-tester) requested to confirm mergeability. Fixed across two rounds: a comprehensive fix for all findings, then a targeted fix for a partial-buffer false-negative in the fail-closed check that the round-1 targeted re-review (code + critical reviewers) caught. 448 tests passing (test/common/quant, test/common/hf/test_quant.py, test/passes/pytorch/test_rtn.py, test/passes/pytorch/test_gptq.py, test/passes/pytorch/test_kquant.py, test/passes/pytorch/test_quant_utils.py, test/passes/pytorch/test_autoclip.py, test/passes/onnx/test_model_builder.py), lintrunner clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 85549b60-0fb9-4d65-a4e8-7a8995939d68
…k-moe-quant-extend-rtn-weight # Conflicts: # test/passes/onnx/test_model_builder.py
Extends the Gptq pass to fused MoE experts weights for the 8 architectures that store them natively as (num_experts, out_features, in_features): qwen2_moe, qwen3_moe, phimoe, mixtral, deepseek_v3, granitemoe, olmoe, jamba. - New olive/passes/pytorch/moe_calib.py: per-expert calibration via transformers' ALL_EXPERTS_FUNCTIONS registry (no monkey-patching), one independent (K, K) Hessian per expert, explicit record on/off switch for the true-sequential second pass, routing coverage report, and fail-closed version/capability/allow-list gating. - gptq.py: enable allow_moe, add moe_fallback_threshold (default 0.5% of the calibration tokens reaching the module, GPTQModel's convention), per-expert RTN fallback for cold experts, and extract the column sweep into a reusable gptq_quantize_weight(). - quant_utils.py: discover quant targets by any quant_info-carrying parameter instead of the hardcoded `weight` name, and route fused MoE modules through the calibration session instead of forward hooks. - wrapper.py: add missing MLP (granitemoe, jamba) and ROUTER (phimoe, granitemoe, jamba) mappings; tolerate non-QKV attention (DeepSeek-V3 MLA). - selection.py: always exclude MoE routers from quantization, by resolved module identity so bare nn.Linear routers (Jamba) cannot slip through.
Critical * Make MoE calibration lifecycle exception-safe: run_layerwise_quantization now wraps the whole calibration loop in try/finally so moe_session.finish(), the use_cache restore, hook removal and the progress bar close unconditionally. MoeCalibrationSession.start() restores the model's experts implementation if its own stale-swap validation fails. Major * Refuse nested/repeated MoeCalibrationSession.start() instead of clobbering the saved experts implementation. * Verify the "olive_moe_calib" registry entry is Olive's recording forward by identity before calibrating. * Cross-check the resolved experts class against the model_type (SUPPORTED_MOE_EXPERTS_CLASSES) so a spoofed model_type cannot bypass the allow-list. * get_attention_inputs() keeps its strict all-or-nothing default; partial resolution is now opt-in via partial_ok=True, used only by the QKV-group call sites in quant_utils. rotate.py keeps its positional contract. * get_mlp_inputs()/get_mlp_outputs() drop unresolved projections (returning [] for MoE blocks) instead of raising AttributeError, mirroring the existing attention accessors. * Warn up front when the estimated per-layer Hessian working set exceeds 4 GiB instead of letting calibration OOM with no explanation. Minor * RTN-fallback experts are fake-quantized in _process_moe_param so the true-sequential re-run sees on-grid weights for every expert. * Coverage token_counts are derived from the recorded Hessian sample counts instead of a counter hardcoded to "gate_up_proj". Tests: 80 tests in test/passes/pytorch/test_gptq_moe.py (new coverage for lifecycle restoration, re-entrancy, registry collision, model_type spoofing, memory preflight, token-count derivation and the projection accessors). lintrunner --all-files is clean.
Always remove the first-layer capture hook and return pre-layer modules to CPU when calibration input collection exits, including when model forward raises a non-sentinel exception.
Describe the implemented OR-to-fallback behavior accurately: both routing skew and sample sufficiency checks must pass before GPTQ is used for an expert.
…n catch - quant_utils.py: add explanatory comment for the intentional ValueError sentinel used to short-circuit calibration forward passes early. - test_gptq_moe.py: catch Exception instead of BaseException in the test thread target, since the thread only needs to observe assertion/runtime failures raised by MoeCalibrationSession.start(), not signals like SystemExit/KeyboardInterrupt. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e10674c1-6909-4b09-9f5a-d41b28c89d2d
## Problem PR #2584 merged RTN MoE quantization (`moe=True`) support without a layout check on fused-expert weights. RTN's `WeightQuantizer` groups unconditionally along a tensor's last dimension, which is only correct when the fused expert weight is stored `(num_experts, out_features, in_features)` -- K last. Architectures such as gpt-oss store the transposed `(num_experts, in, out)` layout instead, and are silently mis-quantized (wrong axis grouped, no error) today on `main` when run through `moe=True`. ## Fix Adds `olive/passes/pytorch/moe_support.py` with `check_moe_layout_support`, gated into RTN's `_run_for_config` after `prepare_model()` and before `finalize()`. The check trusts `transformers`'s own `is_transposed` attribute (set by the `use_experts_implementation` decorator, not derived from config/checkpoint data) directly: - Accept only experts modules that report `is_transposed is False`. - Reject anything where `is_transposed` is missing or not a `bool` (covers older transformers releases, undecorated architectures such as llama4/aria, and unrecognized implementations). - Reject `is_transposed=True` (e.g. gpt-oss). - Exempt classic per-expert `nn.ModuleList` experts (e.g. Mixtral/PhiMoE on older transformers) that carry no direct 3D parameter. A `trust_remote_code` custom experts implementation that misreports its own `is_transposed` is out of scope: that is treated as user-introduced misuse of an explicitly opted-in trust boundary, not a layout Olive can independently verify. No architecture allow-list is used. Only affects the `moe=True` path -- gated behind `if qcfg.moe:` and only runs when experts modules are actually detected, so non-MoE quantization is unaffected. ## Testing 28/28 relevant tests pass (`test/passes/pytorch/test_moe_support.py`, `test/passes/pytorch/test_rtn.py`); lintrunner clean. Note: PR #2610 (GPTQ MoE) will stack on top of this branch to reuse `check_moe_layout_support` and avoid duplicating the layout logic. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e10674c1-6909-4b09-9f5a-d41b28c89d2d
# Conflicts: # olive/passes/pytorch/rtn.py # test/passes/pytorch/test_rtn.py
CI failure diagnosis:
|
## Describe your changes Extends the PyTorch `KQuant` pass to support quantizing fused MoE expert weights, mirroring the layout-safety approach already applied to RTN in #2616: - Generalizes `kquant_find_qparams` to N-D tensors so fused expert weights of shape `(E, OUT, K)` can be quantized directly. - Adds an `allow_moe`/`moe` config flag, gated behind the shared `check_moe_layout_support` guard from `moe_support.py` so quantization fails closed on transposed or unverifiable expert layouts instead of silently producing wrong results. - Fixes the discovery loop to use `_iter_quant_info_params` (was silently skipping non-`weight`-named MoE params before). - Fixes the MoE gate to key off this invocation's own `config.moe` request rather than the merged `qcfg.moe` (same bug independently found by the Copilot automated reviewer on #2616 and fixed there; KQuant had copied the same buggy pattern). Based on `moe-layout-guard-fix2` (#2616) since this only depends on `moe_support.py`, not on any GPTQ-specific work in #2610/#2612. Real-model perplexity numbers for this pass (granite-3.0-1b-a400m-base, OLMoE-1B-7B-0924, Qwen1.5-MoE-A2.7B) are in the "KQuant PPL (Δ, time)" column of the three-model benchmark table in #2612's PR description, alongside the existing RTN/GPTQ results for the same models. ## Checklist before requesting a review - [x] Add unit tests for this change. - [x] Make sure all tests can pass. - [ ] Update documents if necessary. - [x] Lint and apply fixes to your code by running `lintrunner -a` - [ ] Is this a user-facing change? If yes, give a description of this change to be included in the release notes. ## (Optional) Issue link --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e10674c1-6909-4b09-9f5a-d41b28c89d2d
Jamba's Mamba/SSM block (in_proj/x_proj/dt_proj/out_proj) was being swept into the generic nn.Linear quantization walk in iter_quant_targets(), same as the pre-existing router exclusion gap. These weights feed a state-space recursion rather than a plain matmul, so quantizing them generically was never intentional. On transformers <=5.14.1 this silently mis-quantized dt_proj (its forward hook happened to fire). On 5.15.0, a Mamba forward refactor stopped calling dt_proj directly, so quant_info.data was never collected and test_gptq_moe_end_to_end[jamba] started hard-failing with 'does not have quant_info.data initialized!' after rebasing onto the now-unpinned transformers. Adds LayerWrapper.get_mamba()/MAMBA mapping and _collect_mamba_modules() in selection.py, mirroring the existing router-exclusion pattern, plus a regression test.
## Describe your changes `OnnxConversion` with `use_dynamo_exporter=True` monkeypatches `transformers.cache_utils.DynamicLayer.lazy_initialization` at the **class level** (via `_patch_dynamic_layer_for_export()`) in order to make ONNX export work, but never restored the original method afterward. Because the patch is class-level (not instance-level), it silently affected every subsequent `DynamicLayer`/`DynamicCache` instance created later in the same process, for any model class. The patched implementation also had its own bug: it always initialized `self.values` from `key_states` (ignoring `value_states`). This is harmless when key/value head dimensions match, but corrupts the KV cache shape for architectures where they differ (e.g. DeepSeek-V3's MLA, where `qk_rope_head_dim + qk_nope_head_dim` != `v_head_dim`), producing errors such as: ``` RuntimeError: Sizes of tensors must match except in dimension 2. Expected size 16 but got size 8 ``` This was discovered as a cross-test pollution bug: running `test/passes/onnx/test_common.py::test_resave_model` (any model, dynamo export) before a DeepSeek-V3 test in the same pytest process (as CI does, since it runs the whole `test/` directory in one process) leaves the patched `lazy_initialization` in place and corrupts the later test's cache handling — even though the two tests use entirely unrelated models. ### Fix - Converted `_patch_dynamic_layer_for_export()` into a `contextlib.contextmanager` that saves the original `DynamicLayer.lazy_initialization`, applies the patch, yields, and restores the original in a `finally` block, so the patch is undone even if export raises. - Fixed the cache initialization bug to use `value_states` (falling back to `key_states` only if `value_states is None`) instead of always reusing `key_states` for both keys and values. - Wrapped the `torch.onnx.export(...)` call site in `with patch_context:` (using `contextlib.nullcontext()` for the transformers `<5.0` branch that doesn't need this patch). ### Verification - Added regression tests: one asserting `DynamicLayer.lazy_initialization` is restored after a normal patched export path and correctly preserves distinct key/value shapes, and one asserting it is restored even when the patched block raises. - Reproduced the original cross-test pollution locally by running `test/passes/onnx/test_common.py` together with a DeepSeek-V3 GPTQ MoE test in the same pytest process; confirmed the failure occurs before this fix and is resolved after it. - `lintrunner` clean. ## Checklist before requesting a review - [x] Add unit tests for this change. - [x] Make sure all tests can pass. - [ ] Update documents if necessary. - [x] Lint and apply fixes to your code by running `lintrunner -a` - [ ] Is this a user-facing change? If yes, give a description of this change to be included in the release notes. ## (Optional) Issue link Discovered while investigating a CI-only `deepseek_v3` failure reported on #2610 (unrelated to that PR's changes; caused by this pre-existing global-state leak, only reproducible when the whole `test/` suite runs in one process, as CI does). --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e10674c1-6909-4b09-9f5a-d41b28c89d2d
#2610's full-team review raised the default sufficiency multiplier from k=1 (N=K) to k=2 (N=2K) after measuring the real RTN-vs-GPTQ crossover sits closer to 1.5x-2x K. Reran all three benchmark models (granite-3.0-1b-a400m-base, OLMoE-1B-7B-0924, Qwen1.5-MoE-A2.7B) with identical methodology under the new default and updated: - moe-gptq.md: default value, OLMoE empirical example recomputed at k=1 (historical) and cross-referenced at k=2, fallback-rate table, wall-time section. - profiling-benchmark-example.md: main results table now reflects k=2, plus a new k=1 vs k=2 side-by-side comparison table and analysis of why the higher fallback rate did not measurably hurt perplexity or wall-time on these three models. Numbers labeled explicitly as k=1/k=2 (not "old/new") throughout, per review convention, since both configurations remain independently reproducible via --pass_config.
cc31e9d to
ed15186
Compare
|
cc Jambay Kinley (@jambayk) CI passes |
#2610's full-team review raised the default sufficiency multiplier from k=1 (N=K) to k=2 (N=2K) after measuring the real RTN-vs-GPTQ crossover sits closer to 1.5x-2x K. Reran all three benchmark models (granite-3.0-1b-a400m-base, OLMoE-1B-7B-0924, Qwen1.5-MoE-A2.7B) with identical methodology under the new default and updated: - moe-gptq.md: default value, OLMoE empirical example recomputed at k=1 (historical) and cross-referenced at k=2, fallback-rate table, wall-time section. - profiling-benchmark-example.md: main results table now reflects k=2, plus a new k=1 vs k=2 side-by-side comparison table and analysis of why the higher fallback rate did not measurably hurt perplexity or wall-time on these three models. Numbers labeled explicitly as k=1/k=2 (not "old/new") throughout, per review convention, since both configurations remain independently reproducible via --pass_config.
## Describe your changes Stacked on top of #2610 (this PR targets `b1-gptq-moe`, not `main`). Adds a manual validation script for comparing perplexity/size before and after quantizing a real (downloaded) HF checkpoint, plus three onboarding docs under `skills/olive/references/` for quantization work in this repo: - `scripts/quantize_and_compare_perplexity.py` — generic, pass-agnostic script (works for any registered Olive PyTorch quantization pass, not just GPTQ/MoE) that loads a real model, quantizes it, and reports weights-size and WikiText-2 perplexity deltas. This is the same style of real-model validation tool that surfaced real RTN bugs in #2584 after synthetic-model unit tests had already passed. - `skills/olive/references/quantization-onboarding.md` — general RTN/GPTQ pass onboarding: shared config surface, when to use RTN vs. GPTQ, calibration split hygiene. - `skills/olive/references/moe-gptq.md` — MoE-GPTQ-specific onboarding: why MoE needs its own calibration path, the K-last layout allow-list, the dual fallback-threshold design (#2610), and what real-model benchmarking showed about fallback rates and quantization wall-time. - `skills/olive/references/profiling-benchmark-example.md` — worked example of running the benchmark script and interpreting its output. ### Three-model benchmark (bits=4, group_size=128, sym=true, full WikiText-2 `train` calibration, full `test` eval) | Model | Baseline PPL | RTN PPL (Δ, time) | GPTQ PPL (Δ, time) | KQuant PPL (Δ, time) | Fallback experts | | --- | --- | --- | --- | --- | --- | | granite-3.0-1b-a400m-base | 6.2877 | 7.5861 (+1.2984, 8.0s) | 6.9560 (+0.6683, 658.7s) | 7.5162 (+1.2286, 12.1s) | 2/768 (0.3%) | | OLMoE-1B-7B-0924 | 6.6182 | 7.1091 (+0.4909, 52.6s) | 6.8966 (+0.2784, 1499.3s) | 7.0507 (+0.4325, 71.8s) | 10/1024 (1.0%) | | Qwen1.5-MoE-A2.7B | 6.4246 | 6.9251 (+0.5005, 85.8s) | 6.6117 (+0.1872, 2475.6s) | 6.9318 (+0.5072, 148.2s) | 0/1440 (0.0%) | GPTQ consistently beats RTN on perplexity delta across all three models, at a real (but model-size/expert-count-correlated, not cleanly separable) wall-time cost. See `moe-gptq.md` for the full discussion, including the OLMoE layer-2/expert-5 case that empirically validates the dual fallback-threshold design from #2610. KQuant (#2618) numbers added for comparison: KQuant is data-free (no calibration set, no per-expert fallback concept — the "Fallback experts" column doesn't apply to it) and its quantization time is close to RTN's (both are cheap, uncalibrated passes), but its perplexity delta tracks RTN's rather than GPTQ's on all three models. All three KQuant runs used `moe=true` and forced `experts_implementation="eager"` at inference (`grouped_mm` cannot run against `QuantTensor`-wrapped experts; see #2619). ### Notes - This PR depends on `b1-gptq-moe` (#2610): `capture_moe_fallback_counts()` in the script unconditionally imports `olive.passes.pytorch.moe_calib`, which only exists on that branch. Please review/merge #2610 first. - Went through a full internal review pass (readability/correctness/adversarial/spec-adherence/ cross-module) before opening; findings incorporated include: fixing pass-name resolution to use the actual pass registry (`OlivePackageConfig.import_pass_module`) instead of guessing module paths, several docstring/arithmetic corrections in the reference docs, and hedging a couple of causal claims that the 3-data-point benchmark can't fully support. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e10674c1-6909-4b09-9f5a-d41b28c89d2d
Describe your changes
Adds GPTQ support for Mixture-of-Experts (MoE) models whose fused expert weights are stored
natively as
(num_experts, out_features, in_features)— i.e. K (the reduction dim) is alreadythe last dimension, matching GPTQ's existing
(OUT, K)assumption with no layout transposerequired ("K-last" architectures). This builds on #2584 (RTN MoE quantization /
QuantTensor/ModelWrapperinfrastructure, not yet merged) and targets this exact PR's branch as its base.Layout gating (updated): rather than a fixed
model_typeallow-list, GPTQ now delegatesfused-expert layout validation to the same shared, strict
is_transposed-based guard used byRTN (
olive/passes/pytorch/moe_support.py::check_moe_layout_support) — trusting the metadatatransformers' own
@use_experts_implementationdecorator attaches, rather than a staticper-architecture list. Any experts module reporting (or defaulting to) a transposed
(E, K, OUT)layout, or missing/unverifiableis_transposedmetadata (e.g.gpt_oss,llama4,aria, or any architecture whose experts haven't adopted the fused-expertsdecorator), fails closed with an actionable error message — those require a
layout-normalization step before GPTQ's Hessian accumulation and are intentionally out of
scope here (planned as a follow-up PR). GPTQ additionally requires the experts module to
be forward-interceptable (
.configpresent, not a barenn.ModuleList), since — unlikeRTN — it must record per-expert Hessians via a forward-hook swap.
Key design points
ALL_EXPERTS_FUNCTIONSregistry(
transformers >= 5.0), not monkey-patching: a single generic calibration function isregistered once and swapped in per-model via
set_experts_implementation, so everydecorated Experts module routes through it uniformly — no per-architecture branching. Each
expert gets its own independent
(K, K)Hessian (no cross-expert pooling), and an explicitrecord on/off switch prevents double-recording on GPTQ's true-sequential second pass.
calibration-set threshold (
moe_fallback_threshold, default 0.5%), following GPTQModel'sconvention. Guarantees GPTQ+fallback is never worse than plain RTN for a given expert,
including the zero-sample case (no Hessian exists at all).
forward call's own routing-index argument (never re-implements top-k routing, which would be
actively wrong for architectures like DeepSeek-V3 that apply grouped, bias-corrected scoring
before top-k).
nn.Linearrouters such as Jamba's), matching GPTQ Model/AWQ/vLLM convention. This is ashared-infrastructure change in
iter_quant_targetsand affects the existing RTN MoE pathtoo, not just GPTQ.
LayerWrapper(olive/common/hf/wrapper.py) gained MLP/router mapping entries forgranitemoe(block_sparse_moe) andjamba(feed_forward), fixing a pre-existing crashfor these two architectures that affected RTN as well as GPTQ.
transformersversion + sharedis_transposed-based layout guard(see above) + a forward-interceptability check (
.configpresence, rejecting barenn.ModuleListexperts) — verified before any weight mutation.tensor shapes and warns above a threshold (DeepSeek-V3-scale configs can require tens of GB
per layer); this is a known v1 limitation, not solved here.
This PR went through a 5-reviewer fan-out (readability, correctness, adversarial/critical, deep
spec-adherence, cross-module integration) plus a QA pass that executed concrete repros for the
highest-risk claims (exception safety, layout-gate bypass, RTN-fallback boundary). All Critical/
Major findings from that round were fixed in a follow-up commit, including:
exception-safe calibration lifecycle (state restoration on error), a re-entrancy guard and
registry-identity check on the experts-implementation swap, scoping the
get_attention_inputs()partial-resolution change back to only the call sites that need it (toavoid silently breaking
rotate.py's positional QKV assumption), the Hessian-memory preflightwarning, on-grid RTN-fallback weights before the true-sequential re-run, and coverage counts no
longer hardcoded to a specific parameter name. The allow-list + class-identity cross-check
originally used for layout gating was later replaced with the shared
is_transposed-basedguard described above, to align with RTN's approach.
A subsequent full-PR review pass (same 5-reviewer + QA fan-out, but against the whole diff
rather than incrementally) found a few additional pre-existing issues predating this round —
most notably that
get_mlp_inputs/get_mlp_outputsneed the samepartial_okopt-in treatmentget_attention_inputsalready received, to avoidrotate.pysilently skipping MoE MLP rotation.Fixes for these are being applied as a follow-up commit.
Checklist before requesting a review
lintrunner -aThis is user-facing (new
Gptqpass capability for MoE models viaallow_moe/moe=True+moe_fallback_threshold) — release note: "Added GPTQ quantization support for Mixture-of-Experts models with native (K-last) expert weight layouts: Qwen2-MoE, Qwen3-MoE, Phi-MoE,
Mixtral, DeepSeek-V3, Granite-MoE, OLMoE, and Jamba."
(Optional) Issue link
Builds on #2584. Related to #2599.