Repository navigation
[OMNIML-6085] [2/3] Keep hf_ptq's previews in lockstep on a sharded model, and pin whole-model loads by plan #3139
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-base-14-hf-ptq
Are you sure you want to change the base?
Changes from all commits
20d7eee
3c5ffb9
6904778
b9bd7c3
2d54de5
739b185
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 |
|---|---|---|
|
|
@@ -22,7 +22,7 @@ | |
| import os | ||
| import warnings | ||
| from collections.abc import Iterator | ||
| from contextlib import contextmanager | ||
| from contextlib import contextmanager, nullcontext | ||
| from dataclasses import dataclass | ||
| from datetime import timedelta | ||
| from pathlib import Path | ||
|
|
@@ -56,6 +56,7 @@ | |
| ) | ||
| from modelopt.torch.quantization.mode import get_modelike_from_algo_cfg | ||
| from modelopt.torch.utils import distributed as dist_utils | ||
| from modelopt.torch.utils.distributed import DistributionPlan, distribution_plan | ||
| from modelopt.torch.utils.mlflow import Tool, resolved_recipe_texts, tracked_run | ||
| from modelopt.torch.utils.plugins.model_load_utils import is_fsdp2_text_only_vlm, load_pretrained | ||
|
|
||
|
|
@@ -154,6 +155,48 @@ def validate_fsdp2_supported(args, config): | |
| ) | ||
|
|
||
|
|
||
| def preview_generate_kwargs(model) -> dict[str, Any]: | ||
| """Extra ``generate`` kwargs that keep every rank of a sharded model decoding a preview. | ||
|
|
||
| Each rank previews the first sample of its own calibration shard, so the ranks finish at | ||
| different steps. A model its plan shards runs a collective in every forward (the routed experts' | ||
| all-to-all, FSDP2's all-gathers): a rank that stopped decoding would leave the others blocked in | ||
| it. ``synced_gpus`` keeps every rank stepping until all of them have finished. | ||
| """ | ||
| plan = distribution_plan(model) | ||
| sharded = plan.world_size > 1 and (plan.is_expert_parallel or plan.is_fsdp_dense) | ||
| return {"synced_gpus": True} if sharded else {} | ||
|
|
||
|
|
||
| @contextmanager | ||
| def keep_the_experts_implementation(model) -> Iterator[None]: | ||
| """Keep ``generate`` from switching a MoE's experts to ``batched_mm`` while it decodes. | ||
|
|
||
| On a GPU, ``generate`` runs the experts of a ``grouped_mm`` MoE on ``batched_mm`` for the | ||
| decoding stage and switches them back (``_optimize_model_for_decode``, Transformers 5.0 and | ||
| later). Two things follow. ModelOpt's quantizers sit on the ``eager``, ``grouped_mm`` and | ||
| ``torch_a2a`` forwards, which ``batched_mm`` bypasses, so the preview after PTQ would decode on | ||
| unquantized experts. And Transformers refuses the switch for an FSDP2-wrapped model, whose class | ||
| is a subclass built outside its modeling module: ``FSDPQwen3MoeForCausalLM does not support | ||
| setting experts implementation``. Every module of the model that has the hook is kept from it: | ||
| a model can generate through a submodule, as a Nemotron-VL model's ``chat`` generates through | ||
| its language model. | ||
| """ | ||
| modules = [m for m in model.modules() if hasattr(m, "_optimize_model_for_decode")] | ||
| # Put back what each module itself held, if anything: a nested use, or a hook bound per instance. | ||
| own = [(module, vars(module).get("_optimize_model_for_decode")) for module in modules] | ||
| for module in modules: | ||
| module._optimize_model_for_decode = nullcontext | ||
| try: | ||
| yield | ||
| finally: | ||
| for module, held in own: | ||
| if held is None: | ||
| del module._optimize_model_for_decode | ||
| else: | ||
| module._optimize_model_for_decode = held | ||
|
|
||
|
|
||
| def run_nemotron_vl_preview( | ||
| full_model, | ||
| tokenizer, | ||
|
|
@@ -178,32 +221,37 @@ def run_nemotron_vl_preview( | |
| """ | ||
| from vlm_utils import run_text_only_generation, run_vl_preview_generation | ||
|
|
||
| print(f"Running text-only preview generation for Nemotron VL model ({stage_name})...") | ||
| question = tokenizer.decode(input_ids[0], skip_special_tokens=True) | ||
| generation_config = { | ||
| "max_new_tokens": 100, | ||
| "do_sample": False, | ||
| "eos_token_id": tokenizer.eos_token_id, | ||
| } | ||
|
|
||
| # Try text-only generation (may fail for encoder-decoder models like Nemotron-Parse) | ||
| text_response = run_text_only_generation( | ||
| full_model, tokenizer, question, generation_config, pyt_ckpt_path, trust_remote_code | ||
| ) | ||
|
|
||
| generated_ids = None | ||
| if text_response is not None: | ||
| print(f"✅ Text-only generation successful: {text_response[:100]}...") | ||
| generated_ids = text_response | ||
| elif allow_fallback: | ||
| print("Text-only generation failed, falling back to standard generate...") | ||
| generated_ids = full_model.generate(input_ids, max_new_tokens=100) | ||
| with keep_the_experts_implementation(full_model): | ||
| print(f"Running text-only preview generation for Nemotron VL model ({stage_name})...") | ||
| question = tokenizer.decode(input_ids[0], skip_special_tokens=True) | ||
| lockstep = preview_generate_kwargs(full_model) | ||
| generation_config = { | ||
| "max_new_tokens": 100, | ||
| "do_sample": False, | ||
| "eos_token_id": tokenizer.eos_token_id, | ||
| **lockstep, | ||
| } | ||
|
|
||
| # Try text-only generation (may fail for encoder-decoder models like Nemotron-Parse) | ||
| text_response = run_text_only_generation( | ||
| full_model, tokenizer, question, generation_config, pyt_ckpt_path, trust_remote_code | ||
| ) | ||
|
|
||
| # Run additional VL test with images | ||
| print(f"Running additional VL test with images ({stage_name})...") | ||
| run_vl_preview_generation(full_model, tokenizer, pyt_ckpt_path, stage_name, trust_remote_code) | ||
| generated_ids = None | ||
| if text_response is not None: | ||
| print(f"✅ Text-only generation successful: {text_response[:100]}...") | ||
| generated_ids = text_response | ||
| elif allow_fallback: | ||
| print("Text-only generation failed, falling back to standard generate...") | ||
| generated_ids = full_model.generate(input_ids, max_new_tokens=100, **lockstep) | ||
|
|
||
| # Run additional VL test with images | ||
| print(f"Running additional VL test with images ({stage_name})...") | ||
| run_vl_preview_generation( | ||
| full_model, tokenizer, pyt_ckpt_path, stage_name, trust_remote_code, lockstep | ||
| ) | ||
|
|
||
| return generated_ids | ||
| return generated_ids | ||
|
|
||
|
|
||
| def _is_multimodal_config(config): | ||
|
|
@@ -604,8 +652,19 @@ def get_model( | |
| offload_folder=None, | ||
| max_cpu_memory_gb=None, | ||
| max_gpu_memory_gb=None, | ||
| distribution: DistributionPlan | None = None, | ||
| ): | ||
| print(f"Initializing model from {ckpt_path}") | ||
| # The plan to load the whole model by on every rank; a single process's by default. Only a plan | ||
| # the caller gave reaches load_pretrained: without one it decides itself as before, and refuses | ||
| # MODELOPT_* knobs asking to shard over a launch whose process group is not set up. | ||
| plan = distribution or DistributionPlan() | ||
| if plan.is_expert_parallel or plan.is_fsdp_dense: | ||
| raise ValueError( | ||
| f"{plan!r} shards the model over the ranks, but get_model loads the whole model on " | ||
| "every rank: load it with load_pretrained." | ||
| ) | ||
| routing = {} if distribution is None else {"distribution": distribution} | ||
|
|
||
| _disk_offload = offload_folder is not None | ||
| if _disk_offload and max_cpu_memory_gb is None: | ||
|
|
@@ -616,9 +675,25 @@ def get_model( | |
| UserWarning, | ||
| ) | ||
|
|
||
| device_map = "auto" | ||
| if device == "cpu": | ||
| device_map = "cpu" | ||
| # Under torchrun each rank asks for its own "cuda:<local_rank>". Do not fall through to "auto": | ||
| # accelerate runs its placement heuristic INDEPENDENTLY in every rank, and on a node whose GPUs | ||
| # are all idle every rank scores them identically and picks the SAME card. All ranks' weights, | ||
| # and with them every quantizer's _amax buffer, then live on one GPU; the first NCCL collective | ||
| # binds the whole data-parallel group to that single device and aborts with "Duplicate GPU | ||
| # detected : rank 1 and rank 0 both on CUDA device ...". Every load path below honours the pin | ||
| # except those that load with no device map (Nemotron VL): each rank builds a whole copy in host | ||
| # memory and only then moves it to its own GPU, so the node needs one host copy per rank. | ||
| # A dense model too large to replicate per rank wants --use_fsdp2, not "auto" sharding: under | ||
| # data-parallel calibration each rank must own its own GPU. A single process keeps "auto". | ||
| # hf_ptq hands in its run's torch.device, which never equals a string such as "cpu". | ||
| on_cpu = torch.device(device).type == "cpu" | ||
| if plan.world_size > 1 and not on_cpu and ":" not in str(device): | ||
| raise ValueError( | ||
| f"{plan!r} has each rank hold its own copy of the model on its own GPU, but " | ||
| f"device={device!r} names no GPU index; pass the rank's own GPU, cuda:<local_rank>." | ||
| ) | ||
| pinned = {"": str(device)} if not on_cpu and plan.world_size > 1 else None | ||
|
Comment on lines
+689
to
+695
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 guard catches a device that names no GPU, but not the failure the comment block above it is written to prevent: a device that names the wrong GPU.
Two small things that would close it:
Worth doing in this PR rather than in [3/3], since this is where the only caller's device will come from.
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. Left as is: no caller can reach the wrong-GPU case, and the proposed rank check would refuse valid launches.
Collaborator
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.
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. Refused now, in 28b6aba: |
||
| device_map = "cpu" if on_cpu else pinned or "auto" | ||
|
Comment on lines
+689
to
+696
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]
Using the same predicate makes the assumption explicit and lets replicated = plan.world_size > 1 and not (plan.is_expert_parallel or plan.is_fsdp_dense)
pinned = {"": str(device)} if not on_cpu and replicated else NoneRelatedly, the
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. Left as is: |
||
|
|
||
| # Prepare config kwargs for loading | ||
| config_kwargs = {"trust_remote_code": trust_remote_code} if trust_remote_code else {} | ||
|
|
@@ -654,13 +729,24 @@ def get_model( | |
| # modules together for checkpoints that fit; larger ones can still spill and split a | ||
| # tied pair, and need an explicit single-device map. Multi-GPU only: a single-GPU split | ||
| # cannot separate a tied pair, and sequential would needlessly cap max_memory there. | ||
| if device != "cpu" and torch.cuda.device_count() > 1 and is_diffusion_gemma(hf_config): | ||
| if ( | ||
| not pinned | ||
| and not on_cpu | ||
| and torch.cuda.device_count() > 1 | ||
| and is_diffusion_gemma(hf_config) | ||
| ): | ||
| print( | ||
| "Detected DiffusionGemma model. Using device_map='sequential'; the balanced " | ||
| "'auto' mapping can split its tied encoder/decoder weights across GPUs." | ||
| ) | ||
| use_seq_device_map = True | ||
|
|
||
| if use_seq_device_map and pinned: | ||
| raise ValueError( | ||
| f"--use_seq_device_map spreads one model over every GPU, but each rank of this run " | ||
| f"holds its own copy on its own GPU ({device}). Drop it, or shard the model with " | ||
| "--use_fsdp2." | ||
| ) | ||
| if use_seq_device_map: | ||
| device_map = "sequential" | ||
| # If we use sequential, set max_memory limit to ensure that the model does not occupy the full GPU | ||
|
|
@@ -712,6 +798,7 @@ def has_pack_quantized_config(config): | |
| ckpt_path, | ||
| AutoModelForCausalLM, | ||
| model_type=hf_config.model_type, | ||
| **routing, | ||
| device_map=device_map, | ||
| **model_kwargs, | ||
| ) | ||
|
|
@@ -723,7 +810,8 @@ def has_pack_quantized_config(config): | |
| ckpt_path, | ||
| AutoModelForCausalLM, | ||
| model_type=hf_config.model_type, | ||
| device_map="auto", | ||
| **routing, | ||
| device_map=pinned or "auto", | ||
| trust_remote_code=trust_remote_code, | ||
| dtype="auto", | ||
| **experts_kwargs, | ||
|
|
@@ -750,7 +838,8 @@ def has_pack_quantized_config(config): | |
| ckpt_path, | ||
| AutoModelForCausalLM, | ||
| model_type=hf_config.model_type, | ||
| device_map="cpu" if device == "cpu" else "sequential", | ||
| **routing, | ||
| device_map="cpu" if on_cpu else pinned or "sequential", | ||
| **model_kwargs, | ||
| ) | ||
| else: | ||
|
|
@@ -836,7 +925,9 @@ def has_pack_quantized_config(config): | |
| f"Offload folder: {offload_folder}\n" | ||
| "Weights exceeding GPU+CPU budgets will be streamed from disk." | ||
| ) | ||
| else: | ||
| elif not pinned: | ||
| # A pinned copy goes onto its rank's GPU whole, by a map that ignores max_memory: sizing | ||
| # it over the node's GPUs would print a limit the load does not apply. | ||
| inferred_device_map = infer_auto_device_map(model, max_memory=max_memory) | ||
| if "cpu" in inferred_device_map.values(): | ||
| for _device in max_memory: | ||
|
|
@@ -858,6 +949,7 @@ def has_pack_quantized_config(config): | |
| ckpt_path, | ||
| auto_model_module, | ||
| model_type=hf_config.model_type, | ||
| **routing, | ||
| device_map=device_map, | ||
| **model_kwargs2, | ||
| ) | ||
|
|
@@ -866,11 +958,11 @@ def has_pack_quantized_config(config): | |
| _unpack_compressed_linear_weights(model, ckpt_path) | ||
|
|
||
| # If device_map was disabled (None), manually move model to target device | ||
| if device_map is None and device != "cpu": | ||
| if device_map is None and not on_cpu: | ||
| print(f"Moving model to {device} device...") | ||
| model = model.to(device) | ||
|
|
||
| if device == "cuda" and not is_model_on_gpu(model): | ||
| if torch.device(device).type == "cuda" and not is_model_on_gpu(model): | ||
| print("Warning: Some parameters are not on a GPU. Calibration can be slow or hit OOM") | ||
|
|
||
| return model | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -43,7 +43,9 @@ | |||||||||||||||||
| get_tokenizer, | ||||||||||||||||||
| is_nemotron_vl, | ||||||||||||||||||
| is_trtllm_enc_dec_export, | ||||||||||||||||||
| keep_the_experts_implementation, | ||||||||||||||||||
| mlflow_run, | ||||||||||||||||||
| preview_generate_kwargs, | ||||||||||||||||||
| recipe_layerwise_blocks, | ||||||||||||||||||
| run_nemotron_vl_preview, | ||||||||||||||||||
| save_source_config, | ||||||||||||||||||
|
|
@@ -99,7 +101,7 @@ | |||||||||||||||||
| get_max_batch_size, | ||||||||||||||||||
| get_supported_datasets, | ||||||||||||||||||
| ) | ||||||||||||||||||
| from modelopt.torch.utils.distributed import DistributionPlan, set_distribution_plan | ||||||||||||||||||
| from modelopt.torch.utils.distributed import DistributionPlan, abort, set_distribution_plan | ||||||||||||||||||
| from modelopt.torch.utils.memory_monitor import launch_memory_monitor | ||||||||||||||||||
| from modelopt.torch.utils.mlflow import add_mlflow_args, resolve_mlflow_args | ||||||||||||||||||
| from modelopt.torch.utils.plugins.hf_checkpoint_utils import ( | ||||||||||||||||||
|
|
@@ -116,6 +118,10 @@ | |||||||||||||||||
| RAND_SEED = 1234 | ||||||||||||||||||
| # Most tokens per calibration batch when layerwise calibration picks the batch size (64 x 512). | ||||||||||||||||||
| LAYERWISE_CALIB_TOKENS = 32768 | ||||||||||||||||||
| _PREVIEW_STOPS_THE_RUN = ( | ||||||||||||||||||
| "The preview after quantization failed, which stops the run before the export, so no " | ||||||||||||||||||
| "checkpoint is written; --skip_generate exports without the preview." | ||||||||||||||||||
| ) | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
||||||||||||||||||
| def _layerwise_calib_batch_size(calib_seq: int, num_samples: int) -> int: | ||||||||||||||||||
|
|
@@ -809,11 +815,13 @@ def pre_quantize( | |||||||||||||||||
| trust_remote_code=args.trust_remote_code, | ||||||||||||||||||
| ) | ||||||||||||||||||
| else: | ||||||||||||||||||
| generated_ids_before_ptq = full_model.generate( | ||||||||||||||||||
| preview_input_ids, | ||||||||||||||||||
| attention_mask=preview_attention_mask, | ||||||||||||||||||
| max_new_tokens=100, | ||||||||||||||||||
| ) | ||||||||||||||||||
| with keep_the_experts_implementation(full_model): | ||||||||||||||||||
| generated_ids_before_ptq = full_model.generate( | ||||||||||||||||||
| preview_input_ids, | ||||||||||||||||||
| attention_mask=preview_attention_mask, | ||||||||||||||||||
| max_new_tokens=100, | ||||||||||||||||||
| **preview_generate_kwargs(full_model), | ||||||||||||||||||
| ) | ||||||||||||||||||
|
|
||||||||||||||||||
| return preview_input_ids, preview_attention_mask, generated_ids_before_ptq | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
@@ -866,13 +874,20 @@ def post_quantize( | |||||||||||||||||
| # part of the model to CPU (seen on unified-memory single-GPU hosts) can make a | ||||||||||||||||||
| # quantized layer run on CPU, which some kernels (e.g. NVFP4 dynamic block | ||||||||||||||||||
| # quantization) don't support. Don't let that discard the completed calibration. | ||||||||||||||||||
| lockstep = preview_generate_kwargs(full_model) | ||||||||||||||||||
| try: | ||||||||||||||||||
| generated_ids_after_ptq = full_model.generate( | ||||||||||||||||||
| preview_input_ids, | ||||||||||||||||||
| attention_mask=preview_attention_mask, | ||||||||||||||||||
| max_new_tokens=100, | ||||||||||||||||||
| ) | ||||||||||||||||||
| with keep_the_experts_implementation(full_model): | ||||||||||||||||||
| generated_ids_after_ptq = full_model.generate( | ||||||||||||||||||
| preview_input_ids, | ||||||||||||||||||
| attention_mask=preview_attention_mask, | ||||||||||||||||||
| max_new_tokens=100, | ||||||||||||||||||
| **lockstep, | ||||||||||||||||||
| ) | ||||||||||||||||||
| except Exception as e: | ||||||||||||||||||
|
Collaborator
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.
Under
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. Changed in 7772c0d: under lockstep, a failed post-PTQ preview now stops the run with a message that names |
||||||||||||||||||
| if lockstep: | ||||||||||||||||||
| # The ranks decode in lockstep: a rank that skipped the preview would go on to the | ||||||||||||||||||
| # export's collectives while the others still wait in generate's, so the run stops. | ||||||||||||||||||
| raise RuntimeError(_PREVIEW_STOPS_THE_RUN) from e | ||||||||||||||||||
|
Comment on lines
886
to
+890
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. [CRITICAL Algorithm] The raise does not stop the run — the rank wedges in This is the fix for round 3's finding, and the direction is right, but on a sharded launch the raise never reaches the launcher.
So in the asymmetric case this branch targets (one rank's decode OOMs on its own shard's sample): the peers sit in The same gap applies to the other two lockstep raises this PR adds ( Fix: follow the convention the repo already uses for every other distributed entrypoint ( # examples/hf_ptq/hf_ptq.py, in main()
try:
...
except BaseException:
# A per-rank failure leaves the peers inside a collective this rank will never reach
# (generate's lockstep all-reduce, calibration's a2a), so cleanup's barrier and
# destroy_process_group would stall with the traceback still unprinted. Abort instead and
# let the launcher terminate the peers. SystemExit is re-raised by abort().
if args.dist_state.world_size > 1:
dist_utils.abort()
raise
finally:
cleanup_distributed(args)
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. Fixed in a8b2bae, at |
||||||||||||||||||
| warnings.warn(f"Post-quantization generation sanity check failed, skipping it: {e}") | ||||||||||||||||||
|
Comment on lines
886
to
891
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. [CRITICAL Distributed] The post-PTQ preview now runs Under This is the same hazard the PR deliberately fixes for the VL previews —
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. Kept as is, on purpose; a maintainer should decide whether to change it.
The expert-parallel route (#2754) does raise on a failed preview, since it runs inside
Comment on lines
886
to
891
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. [CRITICAL Algorithm] Still open at I'm not re-litigating the argument here; it was made in round 2 and answered in your reply ("kept, by the maintainer's decision"). The maintainer has now decided the other way: cjluo-nv's review lists "Fix The one-line recap of why: under except Exception as e:
if lockstep:
# The peers are waiting in generate's lockstep all-reduce; warning here would
# leave them there until the group timeout.
raise
warnings.warn(f"Post-quantization generation sanity check failed, skipping it: {e}")If keeping the completed calibration on a sharded run is the blocker, the
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. Changed in 7772c0d: under lockstep, a failed post-PTQ preview now stops the run with a message that names |
||||||||||||||||||
| elif is_nemotron_vl_model and tokenizer is not None: | ||||||||||||||||||
| generated_ids_after_ptq = run_nemotron_vl_preview( | ||||||||||||||||||
|
|
@@ -1577,6 +1592,14 @@ def main(args: argparse.Namespace): | |||||||||||||||||
| tokenizer, | ||||||||||||||||||
| device, | ||||||||||||||||||
| ) | ||||||||||||||||||
| except BaseException: | ||||||||||||||||||
| # A rank that fails alone, such as in a sharded model's lockstep preview, leaves the others | ||||||||||||||||||
| # in a collective it never reaches, and cleanup's barrier and destroy_process_group would | ||||||||||||||||||
| # wait on them with the traceback unprinted. abort() prints it and exits, so the launcher | ||||||||||||||||||
| # stops the other ranks. | ||||||||||||||||||
| if args.dist_state.world_size > 1: | ||||||||||||||||||
| abort() | ||||||||||||||||||
| raise | ||||||||||||||||||
| finally: | ||||||||||||||||||
| cleanup_distributed(args) | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
||||||||||||||||||
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.
[IMPORTANT Compatibility] Defaulting to
DistributionPlan()and always forwarding it asdistribution=silently disablesload_pretrained's env-driven routing, including its explicit refusals.Until now
get_modelpassed nodistribution, soload_pretrainedreachedresolve_distribution_plan(model_load_utils.py:833-844) and consultedMODELOPT_EP_DEGREE/MODELOPT_FSDP_SIZE. Two behaviours change:torchrunwithout--use_fsdp2the process group is not set up, soaloneis true. Previouslydistribution is Nonehit the guard atmodel_load_utils.py:748-762, which raises "MODELOPT_FSDP_SIZE=…ask to shard the model over this N-rank launch, whose process group is not set up; load inside distributed_run()". With a one-rank plan handed in,handed()short-circuits that branch entirely: the env var is ignored without a word,plan.world_size == 1sopinnedisNone, and every rank falls through todevice_map="auto"— producing precisely the "Duplicate GPU detected" failure the new comment block below describes. That is the opposite of the intent, and CONTRIBUTING/CLAUDE guidance is to fail loudly rather than silently ignore an option or route.device_mapwould be refused by the sharded route), so forcing the whole-model plan is defensible — but it should be explicit, not a side effect.Suggestion: keep the forced whole-model plan, and refuse rather than ignore a sharding request the caller made through the environment — e.g. when
distribution is NoneandMODELOPT_EP_DEGREE/MODELOPT_FSDP_SIZEask for a sharded layout, raise pointing at--use_fsdp2. A short comment on this line saying why a plan is always handed in (to keep the whole-model route rather than let rank 0's GPU memory choose) would also stop the next reader from "fixing" it back todistribution.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.
Fixed in 06d7121:
get_modelforwardsdistributiononly when its caller gave one, so without a planload_pretraineddecides as before and refuses aMODELOPT_EP_DEGREE/MODELOPT_FSDP_SIZEthat asks to shard over a launch whose process group is not set up. Pinning still reads the plan (a single process's by default), and a comment on that line says why. New testtest_without_a_plan_the_load_decides_and_refuses_sharding_knobs_it_cannot_honor(WORLD_SIZE=2,MODELOPT_FSDP_SIZE=world, no group) fails with the old forwarding.