Repository navigation
[OMNIML-6060] [3/3] Refuse an expert-parallel load that would calibrate the experts wrong #2741
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
cb00b2f
76d4dfe
7fc9e9f
a38c47c
c917396
16902fa
f857a05
65353fe
e8ea09e
01f034c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2145,6 +2145,47 @@ def _ep_aware(model, accelerator_device_map, hf_quantizer=None, *args, **kwargs) | |||||||||||||||||||||||||||||||||||||||||||||
| hf_modeling_utils.get_total_byte_count = orig | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| def _refuse_unquantizable_experts(expert_modules) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||
| """Refuse routed experts that expert-parallel PTQ would quantize wrong. | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| It quantizes the routed experts as they are, through the grouped GEMM of their ``torch_a2a`` | ||||||||||||||||||||||||||||||||||||||||||||||
| forward (as for DeepSeek, GLM and MiniMax). Experts stored transposed, ``(num_experts, in_dim, | ||||||||||||||||||||||||||||||||||||||||||||||
| out_dim)`` like GPT-OSS's, that would leave unquantized. Experts ModelOpt quantizes through a | ||||||||||||||||||||||||||||||||||||||||||||||
| wrapper of their own, like Llama4's, ``mtq.quantize`` would convert to it, and its forward drops | ||||||||||||||||||||||||||||||||||||||||||||||
| the process group the all-to-all transport passes in. The generic fused-experts wrapper is no | ||||||||||||||||||||||||||||||||||||||||||||||
| bar: it quantizes through the grouped GEMM, and ``mtq.quantize`` registers it, for the whole | ||||||||||||||||||||||||||||||||||||||||||||||
| process, for every fused-experts class it meets, so a second load of the same class sees it. A | ||||||||||||||||||||||||||||||||||||||||||||||
| fused-experts wrapper that quantizes on the eager forward only, like FP8's, is refused: the | ||||||||||||||||||||||||||||||||||||||||||||||
| transport runs the grouped GEMM. | ||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||
| # Imported here: the HF quantization plugin loads under import_plugin, so a release it cannot | ||||||||||||||||||||||||||||||||||||||||||||||
| # load leaves this module's other loaders working; only this refusal needs it. | ||||||||||||||||||||||||||||||||||||||||||||||
| from modelopt.torch.quantization.plugins.huggingface import ( | ||||||||||||||||||||||||||||||||||||||||||||||
| _eager_only_experts, | ||||||||||||||||||||||||||||||||||||||||||||||
| _transposed_experts, | ||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| # Read once per rule: a one-shot iterable would let the second rule pass on nothing. | ||||||||||||||||||||||||||||||||||||||||||||||
| expert_modules = tuple(expert_modules) | ||||||||||||||||||||||||||||||||||||||||||||||
| # Quantization's own rules: it leaves transposed experts unquantized, and its torch_a2a check | ||||||||||||||||||||||||||||||||||||||||||||||
| # refuses eager-only ones. This refuses both at load. | ||||||||||||||||||||||||||||||||||||||||||||||
| transposed = _transposed_experts(expert_modules) | ||||||||||||||||||||||||||||||||||||||||||||||
| if transposed: | ||||||||||||||||||||||||||||||||||||||||||||||
| raise NotImplementedError( | ||||||||||||||||||||||||||||||||||||||||||||||
| f"Expert-parallel PTQ cannot quantize {', '.join(transposed)}: these experts store their " | ||||||||||||||||||||||||||||||||||||||||||||||
| "weights transposed, which the grouped quantization leaves unquantized. Quantize the " | ||||||||||||||||||||||||||||||||||||||||||||||
| "model without expert parallelism." | ||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||
| wrapped = _eager_only_experts(expert_modules) | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+2170
to
+2179
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [SUGGESTION]
Suggested change
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Taken in 9e8c81f: the refusal takes its experts as a tuple before the first rule reads them. |
||||||||||||||||||||||||||||||||||||||||||||||
| if wrapped: | ||||||||||||||||||||||||||||||||||||||||||||||
| raise NotImplementedError( | ||||||||||||||||||||||||||||||||||||||||||||||
| f"Expert-parallel PTQ cannot quantize {', '.join(wrapped)}: ModelOpt quantizes these " | ||||||||||||||||||||||||||||||||||||||||||||||
| "experts through a wrapper of their own, whose forward drops the expert-parallel " | ||||||||||||||||||||||||||||||||||||||||||||||
| "transport, or on their eager forward only, which the transport does not run. Quantize " | ||||||||||||||||||||||||||||||||||||||||||||||
| "the model without expert parallelism." | ||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| def _seed_parallel_states(model: nn.Module, expert_names, dense_state, expert_state) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||
| """Seed every module's ``_parallel_state`` for calibration's amax sync. | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -2822,6 +2863,71 @@ def gpu() -> tuple[bool, bool, str | None]: | |||||||||||||||||||||||||||||||||||||||||||||
| f"{type(model).__name__} matches, so calibration would reduce the experts' amax over " | ||||||||||||||||||||||||||||||||||||||||||||||
| "ranks holding different experts. Quantize the model without expert parallelism." | ||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||
| # Calibration tells routed experts by their names, and reduces any other module's weight amax | ||||||||||||||||||||||||||||||||||||||||||||||
| # over the expert-parallel group, which would merge the scales of different experts. Imported | ||||||||||||||||||||||||||||||||||||||||||||||
| # here, as fsdp2_wrap does: the quantization package sits above this module. | ||||||||||||||||||||||||||||||||||||||||||||||
| from modelopt.torch.quantization.model_calib import _is_routed_expert | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| unnamed = [name for name in expert_modules if not _is_routed_expert(name)] | ||||||||||||||||||||||||||||||||||||||||||||||
| if unnamed: | ||||||||||||||||||||||||||||||||||||||||||||||
| raise NotImplementedError( | ||||||||||||||||||||||||||||||||||||||||||||||
| f"Expert-parallel PTQ cannot calibrate {len(unnamed)} routed-expert container(s), e.g. " | ||||||||||||||||||||||||||||||||||||||||||||||
| f"{unnamed[0]}: calibration tells routed experts by an 'experts' in their names, so it " | ||||||||||||||||||||||||||||||||||||||||||||||
| "would merge their weight scales over ranks holding different experts. Quantize the " | ||||||||||||||||||||||||||||||||||||||||||||||
| "model without expert parallelism." | ||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||
| if not num_experts: | ||||||||||||||||||||||||||||||||||||||||||||||
| # The config names no count for the check before the load: the experts' weights carry it | ||||||||||||||||||||||||||||||||||||||||||||||
| # whole as DTensors (transformers 5.16 on; 5.16.0 splits an uneven count without an error). | ||||||||||||||||||||||||||||||||||||||||||||||
| # A count they do not show either is refused, as the distribution plan's decision refuses | ||||||||||||||||||||||||||||||||||||||||||||||
| # a count it cannot read. | ||||||||||||||||||||||||||||||||||||||||||||||
| counts = { | ||||||||||||||||||||||||||||||||||||||||||||||
| param.shape[0] | ||||||||||||||||||||||||||||||||||||||||||||||
| for module in expert_modules.values() | ||||||||||||||||||||||||||||||||||||||||||||||
| for param in module.parameters(recurse=False) | ||||||||||||||||||||||||||||||||||||||||||||||
| if isinstance(param, DTensor) | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| if not counts or any(count % ep_degree for count in counts): | ||||||||||||||||||||||||||||||||||||||||||||||
| raise ValueError( | ||||||||||||||||||||||||||||||||||||||||||||||
| f"{plan!r}: ep_size={ep_degree} must divide the routed experts, which the all-to-all " | ||||||||||||||||||||||||||||||||||||||||||||||
| "transport shares out equally over the expert-parallel ranks; the config names no " | ||||||||||||||||||||||||||||||||||||||||||||||
| f"count, and the loaded experts' weights show {sorted(counts) or 'none'}." | ||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||
| # Each rank must hold its share of every container's experts, num_experts // ep_size: the share the | ||||||||||||||||||||||||||||||||||||||||||||||
| # all-to-all transport routes tokens by. A load that leaves them whole on every rank (a plan whose | ||||||||||||||||||||||||||||||||||||||||||||||
| # expert weights no style shards) or shards them over a mesh of another size would run without an | ||||||||||||||||||||||||||||||||||||||||||||||
| # error, computing tokens against other experts' weights and merging the scales of different | ||||||||||||||||||||||||||||||||||||||||||||||
| # experts. The check compares counts: it catches a wrong share, not a same-size mesh. A DTensor | ||||||||||||||||||||||||||||||||||||||||||||||
| # (transformers 5.16 on) shows the whole count and this rank's share; a plain tensor shows the | ||||||||||||||||||||||||||||||||||||||||||||||
| # share, checked against the config's count when it names one. A model whose containers show | ||||||||||||||||||||||||||||||||||||||||||||||
| # neither, as containers owning no weights themselves would, is refused too: an unchecked share | ||||||||||||||||||||||||||||||||||||||||||||||
| # passes as a correct one. The check reads the containers' weights together, so it relies on a | ||||||||||||||||||||||||||||||||||||||||||||||
| # model's routed experts sharing one layout, as every transformers MoE's do. | ||||||||||||||||||||||||||||||||||||||||||||||
| # Every rank refuses alike, whichever ranks hold a wrong share: the guard tells every rank. | ||||||||||||||||||||||||||||||||||||||||||||||
| held = [ | ||||||||||||||||||||||||||||||||||||||||||||||
| (param.shape[0], param.to_local().shape[0]) | ||||||||||||||||||||||||||||||||||||||||||||||
| if isinstance(param, DTensor) | ||||||||||||||||||||||||||||||||||||||||||||||
| else (num_experts, param.shape[0]) | ||||||||||||||||||||||||||||||||||||||||||||||
| for module in expert_modules.values() | ||||||||||||||||||||||||||||||||||||||||||||||
| for param in module.parameters(recurse=False) | ||||||||||||||||||||||||||||||||||||||||||||||
| if isinstance(param, DTensor) or num_experts | ||||||||||||||||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+2907
to
+2914
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [SUGGESTION] Same for the Not blocking; the current form is correct for every container in the matrix you tested.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Keeping it as is. Every container this load meets holds only expert-major direct params, so the check reads exactly the expert weights. A future container with a direct param that is not expert-major is a layout this load has never run. Refusing it loudly is the fail-closed choice; skipping the param would let an unchecked share through. If such a container appears, the refusal names it, and we can narrow the check to the |
||||||||||||||||||||||||||||||||||||||||||||||
| if not held: | ||||||||||||||||||||||||||||||||||||||||||||||
| raise RuntimeError( | ||||||||||||||||||||||||||||||||||||||||||||||
| f"{plan!r}: no routed-expert weight shows how its experts are shared out over the " | ||||||||||||||||||||||||||||||||||||||||||||||
| "expert-parallel ranks, so the all-to-all transport's expert-to-rank mapping cannot " | ||||||||||||||||||||||||||||||||||||||||||||||
| "be checked. Quantize the model without expert parallelism." | ||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||
| wrong = sorted({(whole, share) for whole, share in held if share * ep_degree != whole}) | ||||||||||||||||||||||||||||||||||||||||||||||
| if wrong: | ||||||||||||||||||||||||||||||||||||||||||||||
| whole, share = wrong[0] | ||||||||||||||||||||||||||||||||||||||||||||||
| raise RuntimeError( | ||||||||||||||||||||||||||||||||||||||||||||||
| f"{plan!r}: this rank holds {share} of a routed-expert container's {whole} experts, " | ||||||||||||||||||||||||||||||||||||||||||||||
| f"not {whole // ep_degree}: they are not sharded over this rank's expert-parallel " | ||||||||||||||||||||||||||||||||||||||||||||||
| "group, so the all-to-all transport would send tokens to ranks without their " | ||||||||||||||||||||||||||||||||||||||||||||||
| "experts, and calibration would merge the scales of different experts." | ||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||
| _refuse_unquantizable_experts(expert_modules.values()) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| # The dense part shards over the whole world, the experts' ranks: a forward's dense all-gathers | ||||||||||||||||||||||||||||||||||||||||||||||
| # and expert all-to-alls then run in turn on the same ranks, so they cannot deadlock. | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[SUGGESTION] The shape half of this rule only discriminates for gated containers; for an ungated one it is a no-op, so the gate is flag-only there.
For
_QuantNemotronHExperts(_first_proj_attr = "up_proj", no gate fusion):(E, out, in):up_projis(E, inter, hidden),down_projis(E, hidden, inter)→up_proj.shape[-1] == down_proj.shape[1] == hidden→ not transposed ✓(E, in, out):up_projis(E, hidden, inter),down_projis(E, inter, hidden)→up_proj.shape[-1] == down_proj.shape[1] == inter→ also reads as not transposed ✗The dims swap in lockstep because there is no
2 *intermediateasymmetry to break the tie, so an ungated transposed container is only caught if it setsis_transposed. The docstring is technically accurate ("a gated fused-experts one shows it either way"), but since this PR promotes the helper from a registration-time warning into_refuse_unquantizable_experts's load-time refusal gate, a reader of that refusal will reasonably assume transposed experts are always caught. Worth saying the negative explicitly, e.g.:No shipped model has ungated-transposed experts today, and the behaviour is unchanged from the pre-refactor expression, so this is doc-only.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Postponed: agreed, an ungated container's two layouts compare equal, so for it only
is_transposedtells. The docstring scopes the shape test to gated containers without stating that negative; spelling it out is a doc-only change, left for a follow-up rather than this approved slice.