Repository navigation
[OMNIML-6063] [3/3] Load a checkpoint its model spec decompresses through the expert-parallel loader #2747
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-11a1-decompress-experts
Are you sure you want to change the base?
[OMNIML-6063] [3/3] Load a checkpoint its model spec decompresses through the expert-parallel loader #2747
Changes from all commits
52841d5
affd24a
7c4b2b1
68b37a0
5c973fc
8da2cf9
07ce8da
73401c9
4afe98e
a40e009
897d5fb
08ea171
abab5eb
dc7457e
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 | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -71,6 +71,7 @@ | |||||||||||||||||||||||||||||
| _num_experts_from_config, | ||||||||||||||||||||||||||||||
| _routed_expert_keys, | ||||||||||||||||||||||||||||||
| _routed_expert_modules, | ||||||||||||||||||||||||||||||
| config_levels, | ||||||||||||||||||||||||||||||
| register_torch_a2a, | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
| from modelopt.torch.utils.plugins.hf_checkpoint_utils import ( | ||||||||||||||||||||||||||||||
|
|
@@ -1624,14 +1625,22 @@ def _seed_parallel_states(model: nn.Module, expert_names, dense_state, expert_st | |||||||||||||||||||||||||||||
| _EP_SOURCE_FORMATS: frozenset[str | None] = frozenset({None}) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| def _decompressed_as_loaded(cfg) -> bool: | ||||||||||||||||||||||||||||||
| """Whether a model spec installed the on-the-fly decompressor for this config's checkpoint.""" | ||||||||||||||||||||||||||||||
| # Importing modelopt.torch.models registers every model spec; only expert-parallel loads need them. | ||||||||||||||||||||||||||||||
| from modelopt.torch.models import _onfly_decompress | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| return _onfly_decompress.is_marked(cfg) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| def _ckpt_quant_method(cfg): | ||||||||||||||||||||||||||||||
| """Return the checkpoint's native quantization method, or ``None``. | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Return the checkpoint's native quantization method (e.g. ``"fp8"`` for block-fp8 DeepSeek-V3), | ||||||||||||||||||||||||||||||
| or ``None`` if the checkpoint isn't natively quantized. Checks the top config, then a VLM's | ||||||||||||||||||||||||||||||
| ``text_config`` or ``llm_config``. | ||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||
| for c in (cfg, getattr(cfg, "text_config", None), getattr(cfg, "llm_config", None)): | ||||||||||||||||||||||||||||||
| for c in config_levels(cfg): | ||||||||||||||||||||||||||||||
| qc = getattr(c, "quantization_config", None) | ||||||||||||||||||||||||||||||
| m = qc.get("quant_method") if isinstance(qc, dict) else getattr(qc, "quant_method", None) | ||||||||||||||||||||||||||||||
| if m: | ||||||||||||||||||||||||||||||
|
|
@@ -1740,7 +1749,7 @@ def load_and_prepare_ep( | |||||||||||||||||||||||||||||
| config_kwargs["attn_implementation"] = attn_implementation | ||||||||||||||||||||||||||||||
| hf_config = AutoConfig.from_pretrained(ckpt_path, **config_kwargs) | ||||||||||||||||||||||||||||||
| quant_method = _ckpt_quant_method(hf_config) | ||||||||||||||||||||||||||||||
| if quant_method not in _EP_SOURCE_FORMATS: | ||||||||||||||||||||||||||||||
| if quant_method not in _EP_SOURCE_FORMATS and not _decompressed_as_loaded(hf_config): | ||||||||||||||||||||||||||||||
| # transformers' own quantizer would load it as stored, for PTQ to treat as plain weights. | ||||||||||||||||||||||||||||||
| dequantized = ", ".join(sorted(f for f in _EP_SOURCE_FORMATS if f)) | ||||||||||||||||||||||||||||||
| loads = "unquantized checkpoints" + ( | ||||||||||||||||||||||||||||||
|
|
@@ -1752,6 +1761,24 @@ def load_and_prepare_ep( | |||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
| # The swapped plan is this load's own: the caller's config keeps the plan its checkpoint declares. | ||||||||||||||||||||||||||||||
| hf_config = copy.deepcopy(hf_config) | ||||||||||||||||||||||||||||||
| if _decompressed_as_loaded(hf_config): | ||||||||||||||||||||||||||||||
| # The decompressor hands the experts over dequantized. Left on the config (Kimi-K2.5's | ||||||||||||||||||||||||||||||
| # config class copies its text config's to the top level), the checkpoint's | ||||||||||||||||||||||||||||||
| # quantization_config would build transformers' compressed-tensors quantizer, which from | ||||||||||||||||||||||||||||||
| # 5.13 claims the same expert converters first. Deleted rather than set to None: before | ||||||||||||||||||||||||||||||
| # 5.11 transformers takes any quantization_config attribute, None too, for a quantized | ||||||||||||||||||||||||||||||
| # checkpoint. Every level _ckpt_quant_method reads is cleared. | ||||||||||||||||||||||||||||||
| levels = config_levels(hf_config) | ||||||||||||||||||||||||||||||
| for cfg in levels: | ||||||||||||||||||||||||||||||
| vars(cfg).pop("quantization_config", None) | ||||||||||||||||||||||||||||||
| # Checked once every level is cleared: a property forwarding to a nested level's is gone | ||||||||||||||||||||||||||||||
| # then, while a class attribute, or a property of its own, is not. | ||||||||||||||||||||||||||||||
| if kept := [cfg for cfg in levels if hasattr(cfg, "quantization_config")]: | ||||||||||||||||||||||||||||||
| raise NotImplementedError( | ||||||||||||||||||||||||||||||
| f"{type(kept[0]).__name__} keeps quantization_config off its instance, so this " | ||||||||||||||||||||||||||||||
| "load cannot clear it, and transformers' own quantizer would compete with the " | ||||||||||||||||||||||||||||||
| "decompressor. Decompress the checkpoint first." | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
| ep_cfg = _ep_config(hf_config) # for VLMs the plan lives on .text_config | ||||||||||||||||||||||||||||||
| ep_plan = dict(getattr(ep_cfg, "base_model_ep_plan", None) or {}) | ||||||||||||||||||||||||||||||
| if not ep_plan: | ||||||||||||||||||||||||||||||
|
|
@@ -1843,8 +1870,43 @@ def gpu() -> tuple[bool, bool, str | None]: | |||||||||||||||||||||||||||||
| dtype = getattr(hf_config, "dtype", None) or torch.bfloat16 | ||||||||||||||||||||||||||||||
| # The load and the export both read the local snapshot, made only once the checks pass, so a | ||||||||||||||||||||||||||||||
| # refused load downloads nothing. | ||||||||||||||||||||||||||||||
| compressed_names: set[str] = set() | ||||||||||||||||||||||||||||||
| with _refused_together("the local checkpoint", alone=False): | ||||||||||||||||||||||||||||||
| _, ckpt_path = ensure_local_checkpoint(ckpt_path) | ||||||||||||||||||||||||||||||
| if _decompressed_as_loaded(hf_config): | ||||||||||||||||||||||||||||||
| # Only the experts decompress as they load, and the config cannot say what else the | ||||||||||||||||||||||||||||||
| # checkpoint compressed, or in which dtypes: what it stores is checked before anything | ||||||||||||||||||||||||||||||
| # loads. Imported here for the reason _decompressed_as_loaded gives. | ||||||||||||||||||||||||||||||
| from modelopt.torch.models import _onfly_decompress | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| tensor_files = _onfly_decompress.check_compressed_keys(hf_config, ckpt_path) | ||||||||||||||||||||||||||||||
| # Minus "weight": a float format's anchor is .weight itself, and an unplaced plain weight | ||||||||||||||||||||||||||||||
| # is what the export has always carried as stored. | ||||||||||||||||||||||||||||||
| compressed_names = set(_onfly_decompress.stored_names(hf_config)) - {"weight"} | ||||||||||||||||||||||||||||||
| # An MTP layer is stored past the decoder layers the model builds, so its compressed | ||||||||||||||||||||||||||||||
| # experts would be refused only after the whole load, by the check that follows it. | ||||||||||||||||||||||||||||||
| # Refused here instead, before anything loads, from the names just read; that check | ||||||||||||||||||||||||||||||
| # stays for any other shape. transformers 5.18 builds no decoder layer past | ||||||||||||||||||||||||||||||
| # num_hidden_layers, so this refuses nothing that check would let through there. | ||||||||||||||||||||||||||||||
| built = getattr(ep_cfg, "num_hidden_layers", None) | ||||||||||||||||||||||||||||||
| past = ( | ||||||||||||||||||||||||||||||
| [] | ||||||||||||||||||||||||||||||
| if built is None | ||||||||||||||||||||||||||||||
| else sorted( | ||||||||||||||||||||||||||||||
| name | ||||||||||||||||||||||||||||||
| for name in tensor_files | ||||||||||||||||||||||||||||||
| if name.rpartition(".")[2] in compressed_names | ||||||||||||||||||||||||||||||
| and (layer := re.search(r"(?:^|\.)layers\.(\d+)\.", name)) | ||||||||||||||||||||||||||||||
| and int(layer[1]) >= built | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
|
Comment on lines
+1891
to
+1902
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 Performance] This scans every shard header a second time. Two smaller points in the same comprehension:
Concretely, the cheapest version that keeps the check where it is: tensor_names = _onfly_decompress.check_compressed_keys(hf_config, ckpt_path)
compressed_names = set(_onfly_decompress.stored_names(hf_config)) - {"weight"}
built = getattr(ep_cfg, "num_hidden_layers", None)
past = (
sorted(
name
for name in tensor_names
if name.rpartition(".")[2] in compressed_names
and (layer := re.search(r"(?:^|\.)layers\.(\d+)\.", name))
and int(layer[1]) >= built
)
if built is not None
else []
)(with
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. |
||||||||||||||||||||||||||||||
| if past: | ||||||||||||||||||||||||||||||
| raise NotImplementedError( | ||||||||||||||||||||||||||||||
| f"The checkpoint stores {len(past)} compressed tensors past the model's {built} " | ||||||||||||||||||||||||||||||
| f"decoder layers, such as {past[0]} (an MTP layer's, typically): the model builds " | ||||||||||||||||||||||||||||||
| "no parameter for them, and the export would copy them as stored, with no " | ||||||||||||||||||||||||||||||
| "quantization_config to read them. Decompress the checkpoint first." | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
|
Comment on lines
+1903
to
+1909
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] This early refusal is strictly broader than the post-load one it front-runs: the Not worth restructuring for; what would help is making the message say what was actually observed rather than asserting a cause, since
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 23eb10d. The message now reports what the check saw: the tensors lie past the model's decoder layers, "an MTP layer's, typically", and the model builds no parameter for them. The comment above it states the assumption: on transformers 5.18, which builds no decoder layer past |
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| # Experts are sharded over ``ep`` and replicated over ``edp``; the dense part is replicated over | ||||||||||||||||||||||||||||||
| # the whole world (``dp``). | ||||||||||||||||||||||||||||||
|
|
@@ -1918,7 +1980,16 @@ def gpu() -> tuple[bool, bool, str | None]: | |||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| # The checkpoint tensors the model has no parameter for (an MTP head), worked out once, at load, | ||||||||||||||||||||||||||||||
| # for the export to carry. | ||||||||||||||||||||||||||||||
| record_unplaced_source_keys(model, ckpt_path) | ||||||||||||||||||||||||||||||
| unplaced = record_unplaced_source_keys(model, ckpt_path) | ||||||||||||||||||||||||||||||
| # The export would copy them as stored, and this load dropped the quantization_config that | ||||||||||||||||||||||||||||||
| # reads a compressed one, such as an MTP layer's experts. A suffix is enough to tell them: | ||||||||||||||||||||||||||||||
| # check_compressed_keys refused any compressed tensor outside the experts. | ||||||||||||||||||||||||||||||
| if carried := [key for key in unplaced if key.rpartition(".")[2] in compressed_names]: | ||||||||||||||||||||||||||||||
| raise NotImplementedError( | ||||||||||||||||||||||||||||||
| f"The checkpoint stores {len(carried)} compressed tensors the model has no parameter " | ||||||||||||||||||||||||||||||
| f"for, such as {carried[0]}: the export would copy them as stored, with no " | ||||||||||||||||||||||||||||||
| "quantization_config to read them. Decompress the checkpoint first." | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
Comment on lines
+1984
to
1993
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] This refusal is sound only as long as On a release that does not record it, every built expert's stored tensors land in Cheap fix: skip the refusal when the record is missing, so the behaviour degrades to the pre-PR carry rather than to a false block — unplaced = record_unplaced_source_keys(model, ckpt_path)
# Only the record from_pretrained kept of the converters it ran resolves a compressed
# expert's stored names: the stock mapping has .weight sources. Without it every expert's
# would look unplaced, so there is nothing to tell an MTP layer's from them.
if getattr(model, "_weight_conversions", None) is None:
compressed_names = set()or, if you would rather keep the block loud, raise a message that names the real cause (
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. I checked this against every release the decompression supports, and it does not happen there.
A guard for a missing record would be dead code on every supported release, so I'd rather not add it.
Comment on lines
+1983
to
1993
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] This refusal is correct, but it lands after The information for the common shape is already in hand before anything loads: Not blocking, and I see the argument for keeping (Checked and fine:
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 9bc105a. Before anything loads, the loader now refuses compressed tensors stored at a layer index at or past the EP config's It lives in the expert-parallel loader, which has the EP config, so The loader test's new |
||||||||||||||||||||||||||||||
| # The model carries the ep submesh, for calibration to re-install the all-to-all forwards | ||||||||||||||||||||||||||||||
| # over (expert_parallel_mesh); transformers sets it differently by release. | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
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
- {"weight"}subtraction is the load-bearing part of the refusal 120 lines down, and nothing says why it is there. A reader has to know that forformat: float-quantizedthe scheme'scompression_param_namesincludes"weight"itself (the anchor), and that an unplaced plain.weightis precisely the thing the export can carry as stored — drop the subtraction and every uncompressed MTP tensor gets refused; keep it without a comment and the next person is likely to "clean it up".Worth one clause:
Same for the matcher at line 2017:
key.rpartition(".")[2] in compressed_namesis a suffix-only test, which is sound only becausecheck_compressed_keyshas already refused any compressed stem without.experts.in it. That cross-module coupling is the reason a non-expert...weight_scalecannot reach this list, and it is worth naming in the comment at 2015 so a later relaxation ofcheck_compressed_keysdoesn't quietly widen what gets carried.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.
Taken in e2e7486. Both places now say why they hold:
compressed_namesdrops"weight": a float format's anchor is.weightitself, and a plain unplaced weight is what the export has always carried as stored;check_compressed_keysalready refused any compressed tensor outside the experts.