Repository navigation
[OMNIML-6084] Export an expert-parallel model through the no-gather writer #2753
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
base: shengliangx/hfep-12c-nogather-export
Are you sure you want to change the base?
Changes from all commits
e124449
3d3a9eb
9b9f580
1e2dc89
8e74d3f
848a5ab
8a27ccc
55dc385
e59190b
59077ae
2839200
87311fa
ad1ad14
d820cf3
cfe2c84
0877280
32e4ff9
080c998
b1e789a
95734a5
670002e
d6fd0dd
041c2e9
cd45930
eba585a
57c76b1
a54c18d
c96a024
5f62b52
092c784
423c0a0
6317c6c
4687e26
b0b1f3b
33ba116
ace5805
57f6e20
72a687c
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 |
|---|---|---|
|
|
@@ -62,6 +62,8 @@ | |
| (in-memory-name) behavior rather than emit a silently-wrong checkpoint. | ||
| """ | ||
|
|
||
| import contextlib | ||
| import functools | ||
| import logging | ||
| import re | ||
| from collections.abc import Iterable | ||
|
|
@@ -98,6 +100,18 @@ | |
| ) | ||
| _LEAF_SUFFIXES = (".weight", *_QUANT_STATE_SUFFIXES, ".bias") | ||
|
|
||
| # Checkpoint tensors a load-time rewrite folds into a weight -- compressed-tensors' stored | ||
| # parameters, FP8's activation scale -- and so lists as sources next to it. They name the same | ||
| # module as that weight; here they only matter for reading the module's leaf off a pattern. | ||
| _LOAD_SIBLING_SUFFIXES = ( | ||
| ".weight_packed", | ||
| ".weight_global_scale", | ||
| ".weight_shape", | ||
| ".weight_zero_point", | ||
| ".weight_g_idx", | ||
| ".activation_scale", | ||
| ) | ||
|
|
||
| # Leaves that are per-tensor scalars (0-d) and must be *duplicated*, not split, when | ||
| # a fused module is un-fused. | ||
| _SCALAR_LEAF_SUFFIXES = (".weight_scale_2", ".input_scale") | ||
|
|
@@ -745,6 +759,46 @@ def _drop_shadowed_prefix_renames(model, rules: list[RenameRule]) -> list[Rename | |
| return kept | ||
|
|
||
|
|
||
| @functools.cache | ||
| def _quantization_codec_ops() -> tuple[type, ...]: | ||
| """Transformers' ops that decode or encode a weight's on-disk quantization in place. | ||
|
|
||
| Includes the identity op, which is what older releases (5.5, 5.6) reverse ``Fp8Dequantize`` | ||
| into; later ones reverse it into ``Fp8Quantize``. | ||
| """ | ||
| ops: list[Any] = [] | ||
| # Imported apart: 5.5 imports triton atop its FP8 integration, so without triton that import | ||
| # fails, while the identity op those releases reverse into lives in core_model_loading. | ||
| with contextlib.suppress(Exception): # transformers without the FP8 integration | ||
| from transformers.integrations import finegrained_fp8 | ||
|
|
||
| names = ("Fp8Dequantize", "Fp8Quantize", "Fp8DecodeScale", "Fp8EncodeScale") | ||
| ops += [getattr(finegrained_fp8, n, None) for n in names] | ||
| with contextlib.suppress(Exception): | ||
| from transformers import core_model_loading | ||
|
|
||
| ops.append(getattr(core_model_loading, "_IdentityOp", None)) | ||
| return tuple(op for op in ops if op is not None) | ||
|
Comment on lines
+763
to
+781
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]
@functools.cache
def _quantization_codec_ops() -> tuple[type, ...]:( Also worth noting while here:
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. Done: |
||
|
|
||
|
|
||
| def _decodes_in_place(rev, ops) -> bool: | ||
| """Whether a reversed converter only re-encodes a weight's quantization, under the same name. | ||
|
|
||
| A dequantizing load (FineGrainedFP8 ``dequantize=True``) decodes each weight in place, and | ||
| its reverse would re-encode the weight in the source's format. The export never wants that: | ||
| it writes ModelOpt's own quantized weights and scales. Such a converter renames nothing: the | ||
| weight maps onto itself, and every pattern names a tensor of the same module, the weight or a | ||
| scale the load folded into it. One naming another module would rename it, so it is not skipped. | ||
| """ | ||
| codec_ops = _quantization_codec_ops() | ||
| if not ops or not codec_ops or not all(isinstance(op, codec_ops) for op in ops): | ||
| return False | ||
| sources = [p.strip("^$") for p in _as_list(rev.source_patterns)] | ||
| targets = [p.strip("^$") for p in _as_list(rev.target_patterns)] | ||
| modules = {p.rpartition(".")[0] for p in (*sources, *targets)} | ||
| return bool(sources) and sources[:1] == targets[:1] and len(modules) == 1 | ||
|
|
||
|
|
||
| def _build_reverse_rules( | ||
| model, | ||
| ) -> tuple[list[SplitRule], list[_MergeRule], list[RenameRule], list[str], set[str] | None]: | ||
|
|
@@ -767,6 +821,8 @@ def _build_reverse_rules( | |
| survives in the state dict, so it is un-fused via a :class:`SplitRule`. | ||
| * Dense splitting ``WeightConverter`` (reverse is ``Concatenate`` only): the split | ||
| tensors are re-fused under the transform's original sub-model scope. | ||
| * A dequantizing load's ``WeightConverter`` (quantization codec ops only, same name): no | ||
| rule -- the export writes ModelOpt's own quantization, not the source's. | ||
|
|
||
| Raises :class:`QuantConversionUnsupportedError` for any op shape not covered, so | ||
| the caller falls back to the legacy (in-memory-name) behavior. | ||
|
|
@@ -842,6 +898,8 @@ def _build_reverse_rules( | |
| dim=ops[0].dim, | ||
| ) | ||
| ) | ||
| elif _decodes_in_place(rev, ops): | ||
| continue | ||
| else: | ||
| raise QuantConversionUnsupportedError( | ||
| f"unsupported reverse ops: {[type(o).__name__ for o in ops]}" | ||
|
|
@@ -884,7 +942,7 @@ def _expert_leaf_renames(rev) -> list[RenameRule]: | |
| fused leaf as per-expert parts, mapped back to the hub leaves positionally. | ||
| """ | ||
| src_leaf = _leaf(_as_list(rev.source_patterns)[0]) | ||
| hub_leaves = [_leaf(t) for t in _as_list(rev.target_patterns)] | ||
| hub_leaves = _distinct_leaves(_as_list(rev.target_patterns)) | ||
| part_leaves = _FUSED_EXPERT_PART_NAMES.get(src_leaf, [src_leaf]) | ||
| if len(part_leaves) != len(hub_leaves): | ||
| raise QuantConversionUnsupportedError( | ||
|
|
@@ -903,7 +961,7 @@ def _expert_leaf_renames(rev) -> list[RenameRule]: | |
| def _dense_split_rule(rev, ops) -> SplitRule: | ||
| """Un-fuse a dense (non-expert) fused linear that survives in the state dict.""" | ||
| fused = _leaf_suffix(_as_list(rev.source_patterns)[0]) | ||
| parts = tuple(_leaf_suffix(t) for t in _as_list(rev.target_patterns)) | ||
| parts = tuple("." + leaf for leaf in _distinct_leaves(_as_list(rev.target_patterns))) | ||
| dim = next((op.dim for op in ops if hasattr(op, "dim")), 0) | ||
| return SplitRule(fused_suffix=fused, part_suffixes=parts, dim=dim) | ||
|
|
||
|
|
@@ -912,10 +970,21 @@ def _as_list(x) -> list: | |
| return list(x) if isinstance(x, (list, tuple)) else [x] | ||
|
|
||
|
|
||
| def _distinct_leaves(patterns: list[str]) -> list[str]: | ||
| """The module leaves ``patterns`` name, each once, in order. | ||
|
|
||
| A converter that loading rewrote lists every checkpoint tensor it consumes per module: a | ||
| dequantizing quantizer adds each weight's ``weight_scale_inv``, an on-the-fly decompressor the | ||
| stored ``weight_packed`` / ``weight_scale`` / ``weight_global_scale``. Those all name the same | ||
| module, so repeated leaves collapse to it. | ||
| """ | ||
| return list(dict.fromkeys(_leaf(p) for p in patterns)) | ||
|
|
||
|
|
||
| def _leaf(pattern: str) -> str: | ||
| """Bare leaf name from a conversion pattern, e.g. ``.experts.*.w1.weight`` -> ``w1``.""" | ||
| p = pattern | ||
| for suffix in _LEAF_SUFFIXES: | ||
| p = pattern.removesuffix("$") | ||
| for suffix in (*_LEAF_SUFFIXES, *_LOAD_SIBLING_SUFFIXES): | ||
| if p.endswith(suffix): | ||
| p = p[: -len(suffix)] | ||
| break | ||
|
|
||
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 new EP-aware
offsetis used for the key reindexing below, but_loc_scale(just under this, line 856) still slices a plain scale tensor bydp_offset:In the
add_ep_baselayout (group_size < total_experts, plain weight →dp_offset == 0,n_local == group_size), a scale handed in as a plain tensor spanning all experts satisfiesv.shape[0] > n_localand gets sliced tov[0:group_size]— EP group 0's scales — on every EP rank. That is a silently wrong checkpoint rather than a failure.I believe it is unreachable today: the keep-fused fold derives the fused
*_weight_scalefrom this rank's own (already EP-sliced) weight, soshape[0] == n_localand the branch doesn't fire;_loc_scale's own comment says "a PLAIN tensor over every expert of the group". But the two offsets now disagree, and nothing enforces the assumption.Suggested fix: either slice by
offset(correct in both layouts, sinceoffset == dp_offsetwheneveradd_ep_baseis false), or make the assumption explicit, e.g.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.
Leaving this as it is.
The two offsets belong to two different layouts of a plain scale:
dp_offset. That is the case the comment above describes.offset.Slicing by
offsetalone would therefore mis-slice the group-wide case on every rank withep_rank > 0.Neither case arises today. The keep-fused fold stacks the scales of this rank's own experts, so they arrive as DTensors or with
shape[0] == n_local, and the slice does not run. Choosing the offset by the scale's length would guard a layout no caller produces, so I left the function as #2750 wrote it.