Repository navigation
[OMNIML-6060] Load an expert-parallel plan through load_pretrained #2780
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-6c-fp8-dequant
Are you sure you want to change the base?
Changes from all commits
deed748
4d620d6
9f29877
0bd710e
8084410
8683554
4f7fd53
9deac71
ae1af9a
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 | ||||
|---|---|---|---|---|---|---|
|
|
@@ -20,6 +20,7 @@ | |||||
| import inspect | ||||||
| import json | ||||||
| import logging | ||||||
| import math | ||||||
| import os | ||||||
| import re | ||||||
| from collections import defaultdict | ||||||
|
|
@@ -413,10 +414,20 @@ def record_unplaced_source_keys( | |||||
| return keys | ||||||
|
|
||||||
|
|
||||||
| _EXPERT_PARALLEL = "expert-parallel" | ||||||
| _FSDP2 = "FSDP2" | ||||||
| # What each sharded load takes besides the checkpoint. These loads build the model class and place | ||||||
| # it themselves, so from_pretrained's own arguments (device_map, max_memory, ...) do not reach them. | ||||||
| _SHARDED_LOAD_OPTIONS = { | ||||||
| _EXPERT_PARALLEL: frozenset( | ||||||
| { | ||||||
| "trust_remote_code", | ||||||
| "attn_implementation", | ||||||
| "hf_config", | ||||||
| "freeze", | ||||||
| "require_lm_head", | ||||||
| } | ||||||
| ), | ||||||
| _FSDP2: frozenset( | ||||||
| { | ||||||
| "trust_remote_code", | ||||||
|
|
@@ -439,15 +450,11 @@ def _route(plan: DistributionPlan) -> str | None: | |||||
| f"{plan!r} is for {plan.world_size} ranks, but this run has {dist.size()}." | ||||||
| ) | ||||||
| if plan.is_expert_parallel: | ||||||
| raise NotImplementedError( | ||||||
| f"{plan!r} shards the routed experts, which load_pretrained has no load for. Plan " | ||||||
| "ep_size=1 (MODELOPT_EP_DEGREE=1) to give every rank the whole model, or shard it " | ||||||
| "with FSDP2." | ||||||
| ) | ||||||
| return _EXPERT_PARALLEL | ||||||
| return _FSDP2 if plan.is_fsdp_dense else None | ||||||
|
|
||||||
|
|
||||||
| def _load_sharded(route, ckpt_path, auto_class, model_type, kwargs, hub_options) -> nn.Module: | ||||||
| def _load_sharded(route, plan, ckpt_path, auto_class, model_type, kwargs, hub_options) -> nn.Module: | ||||||
| """The sharded load ``route`` names, refusing what it cannot honor before it starts.""" | ||||||
| from modelopt.torch.models.hf import checkpoint_has_mtp | ||||||
| from modelopt.torch.utils import distributed as dist | ||||||
|
|
@@ -487,6 +494,15 @@ def _load_sharded(route, ckpt_path, auto_class, model_type, kwargs, hub_options) | |||||
| ) | ||||||
| trust_remote_code = kwargs.pop("trust_remote_code", False) | ||||||
| hf_config = kwargs.pop("hf_config", None) | ||||||
| if route == _EXPERT_PARALLEL: | ||||||
| return load_and_prepare_ep( | ||||||
| ckpt_path, | ||||||
| plan, | ||||||
| trust_remote_code=trust_remote_code, | ||||||
| hf_config=hf_config, | ||||||
| hub_options=hub_options, | ||||||
| **kwargs, | ||||||
| ) | ||||||
|
Comment on lines
+497
to
+505
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] The FSDP2 route below reads its config with Related: the docstring at line 584 now reads "A sharded load builds its model from the config 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. Done. |
||||||
| if hf_config is None: | ||||||
| hf_config = AutoConfig.from_pretrained( | ||||||
| ckpt_path, trust_remote_code=trust_remote_code, **hub_options | ||||||
|
|
@@ -539,20 +555,22 @@ def load_pretrained( | |||||
| the model over the run's ranks by. Every rank takes rank 0's, or, when rank 0 passes none, | ||||||
| :func:`resolve_distribution_plan`'s. By the plan: | ||||||
|
|
||||||
| * ``dense="fsdp"``: a causal LM FSDP2-sharded over the whole process group, each rank reading its | ||||||
| own layers (:func:`parallel_load_and_prepare_fsdp2`, whose ``cpu_offload``, ``mp_policy`` and | ||||||
| ``broadcast_chunk_size`` ``kwargs`` may set). A single process launched with | ||||||
| * ``ep_size > 1``: the routed experts sharded as they load, the dense part replicated or | ||||||
| FSDP2-sharded as the plan says (:func:`load_and_prepare_ep`, whose | ||||||
| ``freeze`` and ``require_lm_head`` ``kwargs`` may set); | ||||||
| * otherwise ``dense="fsdp"``: a causal LM FSDP2-sharded over the whole process group, each rank | ||||||
| reading its own layers (:func:`parallel_load_and_prepare_fsdp2`, whose ``cpu_offload``, | ||||||
| ``mp_policy`` and ``broadcast_chunk_size`` ``kwargs`` may set). A single process launched with | ||||||
| ``torchrun --nproc_per_node=1`` has no group to shard through, so the load sets up one of one | ||||||
| rank, which :func:`~modelopt.torch.utils.distributed.distributed_run` destroys with the run; a | ||||||
| process started without a launcher cannot set one up and is refused. It reads the | ||||||
| checkpoint's tensors as stored, so it refuses a quantized checkpoint. | ||||||
| checkpoint's tensors as stored, so it refuses a quantized checkpoint; | ||||||
| * otherwise ``auto_class.from_pretrained(ckpt_path, **kwargs)`` (``auto_class`` defaults to | ||||||
| ``AutoModelForCausalLM``), placed as ``kwargs`` say. Every rank of a distributed run gets the | ||||||
| whole model, ``device_map`` defaulting to the rank's GPU. | ||||||
|
|
||||||
| A plan with ``ep_size > 1`` raises: no load here shards the routed experts. On a distributed run | ||||||
| every rank loads the plan rank 0 was handed. A rank of a multi-rank launch whose process group | ||||||
| is not set up is refused on every route: set it up first, with | ||||||
| On a distributed run every rank loads the plan rank 0 was handed. A rank of a multi-rank launch | ||||||
| whose process group is not set up is refused on every route: set it up first, with | ||||||
| :func:`~modelopt.torch.utils.distributed.distributed_run`. The plan is recorded on the model | ||||||
| (:func:`~modelopt.torch.utils.distributed.set_distribution_plan`), where calibration and export | ||||||
| read it back. | ||||||
|
|
@@ -569,11 +587,12 @@ def load_pretrained( | |||||
| * A Hub model ID is made local first (:func:`ensure_local_checkpoint`, which takes the | ||||||
| ``revision``, ``token``, ``cache_dir`` and ``local_files_only`` ``kwargs``): the export reads | ||||||
| the source checkpoint from disk. Under ``torch.distributed`` every rank must call this. The | ||||||
| config read and the whole-model load still take all but ``revision``: an ``auto_map`` entry | ||||||
| can name custom code in another Hub repo, which Transformers fetches with them. A sharded | ||||||
| load builds its model from the config with ``from_config``, which takes none, so such code | ||||||
| comes with the environment's Hub settings. A ``subfolder`` is part of where the checkpoint | ||||||
| is: the config, the loading hook and the record below read it, as the load does. | ||||||
| config read, the whole-model load and the expert-parallel load still take all but | ||||||
| ``revision``: an ``auto_map`` entry can name custom code in another Hub repo, which | ||||||
| Transformers fetches with them. The FSDP2 load builds its model from the config with | ||||||
| ``from_config``, which takes none, so such code comes with the environment's Hub settings. | ||||||
| A ``subfolder`` is part of where the checkpoint is: the config, the loading hook and the | ||||||
| record below read it, as the load does. | ||||||
| * The model type's loading hook runs | ||||||
| (:func:`~modelopt.torch.models.hf.prepare_model_for_loading`), e.g. to keep the MTP head a | ||||||
| Nemotron-H checkpoint carries. ``model_type`` selects it; when omitted, the type of the | ||||||
|
|
@@ -674,7 +693,7 @@ def handed() -> DistributionPlan | None: | |||||
| ).model_type | ||||||
| ) | ||||||
| if route is not None: | ||||||
| model = _load_sharded(route, ckpt_path, auto_class, model_type, kwargs, hub_options) | ||||||
| model = _load_sharded(route, plan, ckpt_path, auto_class, model_type, kwargs, hub_options) | ||||||
| # A sharded load finds the unplaced keys while it places the weights. | ||||||
| keys = model._modelopt_unplaced_source_keys | ||||||
| else: | ||||||
|
|
@@ -1320,6 +1339,9 @@ def _decide_distribution( | |||||
| ) | ||||||
| ep_size = _ep_degree(ep_size) | ||||||
| dense = _dense_from_env(world_size) if dense is None else dense | ||||||
| # A replication both knobs ask for is the caller's choice, and loads; one reached from "auto" | ||||||
| # would leave an expert-parallel MoE whole on every rank unasked, and is refused below. | ||||||
| asked_to_replicate = ep_size == 1 and dense == "replicate" | ||||||
| if ( | ||||||
| auto_class not in (None, AutoModelForCausalLM) | ||||||
| and ep_size in ("auto", 1) | ||||||
|
|
@@ -1417,6 +1439,39 @@ def _decide_distribution( | |||||
| raise NotImplementedError( | ||||||
| f"The dense part of {hf_config.model_type} cannot be sharded {unshardable}.{remedy}" | ||||||
| ) | ||||||
| if experts and asked_to_replicate: | ||||||
| print( | ||||||
|
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 uses bare It is correct today only because
Suggested change
(dropping the now-redundant |
||||||
| f"{hf_config.model_type}: ep_size=1 and dense='replicate' as asked, so every rank " | ||||||
| "holds the whole MoE.", | ||||||
| flush=True, | ||||||
| ) | ||||||
| elif experts and ep_size == 1 and dense == "replicate": | ||||||
| # Name a way to shard the experts only where one exists: a count the config spells none of | ||||||
| # the ways ModelOpt reads leaves no EP degree to name, as does one sharing no factor above 1 | ||||||
| # with the world size. | ||||||
| if not num_experts: | ||||||
| shard = ( | ||||||
| "Its config names its routed-expert count none of the ways ModelOpt reads, so " | ||||||
| "no EP degree can shard its experts. Shard" | ||||||
| ) | ||||||
| elif math.gcd(num_experts, world_size) == 1: | ||||||
| shard = ( | ||||||
| f"No EP degree above 1 divides both its {num_experts} experts and the world size, " | ||||||
| f"so none can shard its experts on {world_size} ranks. Shard" | ||||||
| ) | ||||||
| else: | ||||||
| shard = ( | ||||||
| f"Shard its experts (MODELOPT_EP_DEGREE above 1, dividing both its {num_experts} " | ||||||
| "experts and the world size), shard" | ||||||
| ) | ||||||
| raise RuntimeError( | ||||||
| f"{hf_config.model_type} can be expert-parallel, but the plan resolved to ep_size=1 at " | ||||||
| f"world_size={world_size}, so every rank would hold the whole MoE. {shard} the whole " | ||||||
| "model with FSDP2 (MODELOPT_FSDP_SIZE=world, for an unquantized checkpoint), load it in " | ||||||
| "a single process, or replicate it on every rank outright: MODELOPT_EP_DEGREE=1 with " | ||||||
| "MODELOPT_FSDP_SIZE=1, or " | ||||||
| f"load_pretrained(..., distribution=DistributionPlan({world_size}, 1, 'replicate'))." | ||||||
| ) | ||||||
| return DistributionPlan(world_size, ep_size, dense) | ||||||
|
|
||||||
|
|
||||||
|
|
@@ -1474,6 +1529,10 @@ def resolve_distribution_plan( | |||||
|
|
||||||
| ``config``, the config the load will use when the caller has one, is decided from instead of | ||||||
| the checkpoint's, which is then not read: each rank takes a copy, as building writes to it. | ||||||
|
|
||||||
| A plan that leaves an expert-parallel MoE whole on every rank raises, unless both knobs ask for | ||||||
| exactly that: ``ep_size=1`` with ``dense="replicate"`` (``MODELOPT_EP_DEGREE=1`` and | ||||||
| ``MODELOPT_FSDP_SIZE=1``) replicates it, noting that every rank holds the whole MoE. | ||||||
| """ | ||||||
| from modelopt.torch.utils import distributed as dist | ||||||
|
|
||||||
|
|
@@ -2240,6 +2299,7 @@ def load_and_prepare_ep( | |||||
| freeze: bool = True, | ||||||
| hf_config=None, | ||||||
| require_lm_head: bool = True, | ||||||
| hub_options: dict | None = None, | ||||||
| ) -> nn.Module: | ||||||
| """Load + shard a MoE causal LM for expert-parallel PTQ via the DP=EP all-to-all transport. | ||||||
|
|
||||||
|
|
@@ -2248,7 +2308,9 @@ def load_and_prepare_ep( | |||||
| does (:func:`~modelopt.torch.utils.distributed.set_distribution_plan`), as | ||||||
| :func:`load_pretrained` does for every load. The model builds with its LM head where an auto | ||||||
| class builds one; ``require_lm_head=False`` builds a VLM without it, through ``AutoModel`` (a | ||||||
| causal LM always keeps its head). | ||||||
| causal LM always keeps its head). ``hub_options`` (``token``, ``cache_dir``, | ||||||
| ``local_files_only``, as :func:`load_pretrained` takes them) go to its config read and | ||||||
| ``from_pretrained``, which fetch the custom code an ``auto_map`` names in another Hub repo. | ||||||
|
|
||||||
| Builds an ``(edp, ep)`` device mesh and loads experts EP-sharded over the ``ep`` submesh | ||||||
| (``enable_expert_parallel=True`` -- each rank owns ``num_experts / ep`` experts as grouped-GEMM | ||||||
|
|
@@ -2333,8 +2395,9 @@ class builds one; ``require_lm_head=False`` builds a VLM without it, through ``A | |||||
| from transformers.distributed import DistributedConfig | ||||||
|
|
||||||
| world_size, ep_degree, edp = plan.world_size, plan.ep_size, plan.edp | ||||||
| hub_options = dict(hub_options or {}) | ||||||
|
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] Through
Since the function already localizes the path itself, threading the options through closes all three in one line: def _resolve_checkpoint_dir(ckpt_path: str, rank: int, **hub_options) -> str:
"""Local dir for ``ckpt_path``; resolves an HF Hub ID (rank 0 downloads, others wait)."""
if os.path.isdir(ckpt_path):
return ckpt_path
if rank == 0:
snapshot_download(ckpt_path, **hub_options)
if is_initialized():
barrier()
return snapshot_download(ckpt_path, **hub_options)and at line 2397: ckpt_path = _resolve_checkpoint_dir(ckpt_path, torch.distributed.get_rank(), **hub_options)( |
||||||
| if hf_config is None: | ||||||
| config_kwargs: dict[str, Any] = {"trust_remote_code": trust_remote_code} | ||||||
| config_kwargs: dict[str, Any] = {"trust_remote_code": trust_remote_code, **hub_options} | ||||||
| if attn_implementation is not None: | ||||||
| config_kwargs["attn_implementation"] = attn_implementation | ||||||
| hf_config = AutoConfig.from_pretrained(ckpt_path, **config_kwargs) | ||||||
|
|
@@ -2436,6 +2499,7 @@ class builds one; ``require_lm_head=False`` builds a VLM without it, through ``A | |||||
| "device_mesh": ep_submesh, | ||||||
| "distributed_config": distributed_config, | ||||||
| "trust_remote_code": trust_remote_code, | ||||||
| **hub_options, | ||||||
| } | ||||||
| if not _dc_has_tp_plan: | ||||||
| from_pretrained_kwargs["tp_plan"] = "auto" | ||||||
|
|
||||||
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] Two small route-accuracy gaps that this dispatch inherits from the FSDP2-only code above it.
1. The
dtyperefusal gives FSDP2-only advice on the expert-parallel route. Lines 456-461 interpolaterouteinto the message but then end with "FSDP2'smp_policysets the dtype it computes in" — andmp_policyis not in_SHARDED_LOAD_OPTIONS[_EXPERT_PARALLEL], so an EP user who follows it gets the unknown-kwargsTypeErrornext. Theunknownmessage right below (lines 464-469) already route-gates its hint withif route == _FSDP2 else ""; the dtype one wants the same treatment:2.
hub_optionsdoes not reach the EP route's config read. The FSDP2 branch below reads the config with**hub_options(line 489), sotoken/cache_dir/local_files_onlyapply to it; the EP branch handshf_config=Nonedown andload_and_prepare_epreads the config itself without them. It only bites when the caller passesdistribution=explicitly (the decided path already fillskwargs["hf_config"]via thesetdefaultinload_pretrained) andtrust_remote_code=Truewith anauto_mappointing at another, gated Hub repo — narrow, butload_pretrainedhas by then already read that config withhub_optionsto getmodel_type, so the cheap fix is to hoist the existing read above the dispatch and let both routes share it:That also makes the
quantization_configasymmetry below explicit rather than incidental: the EP loader has its own_EP_SOURCE_FORMATScheck (line 2246), which is why it must stay ahead of the FSDP2-only one. Neither point blocks.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.
Point 2 is handled in [13/21] (9376c34): the expert-parallel route of
_load_shardedreads a config it is not handed with**hub_optionsbeforeapply_distributed_support, as the FSDP2 route does. Point 1 is postponed: thedtyperefusal still ends with FSDP2'smp_policyon both routes, and gating that clause onroute == _FSDP2, as the unknown-options message does, is a wording fix left to a follow-up.