Repository navigation
[OMNIML-6084] [17/21] Export an expert-parallel model through the no-gather writer - #2753
shengliangxu wants to merge 21 commits into
Conversation
|
Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually. Contributors can view more details about this message here. |
4e11e09 to
be3cc39
Compare
|
|
/ok to test be3cc39 |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## shengliangx/hfep-12c-nogather-export #2753 +/- ##
=======================================================================
Coverage ? 62.01%
=======================================================================
Files ? 656
Lines ? 73553
Branches ? 0
=======================================================================
Hits ? 45613
Misses ? 27940
Partials ? 0
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
be3cc39 to
bdf08a2
Compare
|
/ok to test bdf08a2 |
bdf08a2 to
136543b
Compare
136543b to
405ef82
Compare
|
/ok to test 405ef82 |
405ef82 to
6fdea4b
Compare
6fdea4b to
2b63470
Compare
|
/ok to test 2b63470 |
|
/ok to test 951faa3 |
|
/ok to test 0bb80b2 |
|
/ok to test 6c7c6c0 |
|
/ok to test 1f71c36 |
|
/ok to test 777ff13 |
|
/ok to test f02e2d7 |
|
/ok to test 332db97 |
|
/ok to test 7cb7af4 |
An EP-sharded model could not be exported at all. transformers pre-slices each expert parameter across the EP group, so save_pretrained sees a tensor-parallel model and refuses outright. Routing now sends an expert-parallel model to the no-gather writer unconditionally, since it has no fallback; a model sharded by FSDP2 alone keeps its opt-in. What expert parallelism changes in the writer and around it: - The global expert index. Under FSDP2 alone a per-expert key's index is the DTensor's own dim-0 offset, but under EP transformers has already sliced the parameter to this rank's group, so that offset is group-relative and every group would write experts 0..n-1 over each other -- a silently wrong checkpoint. The coordinate now comes from the routed experts' ParallelState (expert_parallel_state), the same state calibration reads. - Per-expert weight quantizers. The fold indexed them by the DTensor's global dim-0 offset, the FSDP2 convention where the quantizer list stays global; under EP the list holds only this rank's experts. What tells the two apart is the DTensor's global dim-0 extent -- equal to the list's length under FSDP2, larger under EP -- and an assert pins the local invariant. (Seen as an IndexError on every rank exporting GLM-5.3-Flash under EP16.) - The rank. It was get_rank() only under FSDP2 and 0 otherwise, so under EP every process took the rank-0 branch and rewrote config.json at once, and the closing barrier was skipped, so the job hung instead of exiting. - Carried weights. Under EP every other rank's experts read as unplaced on each rank, and every rank believed it was the single sender of the carried set; the writer's point-to-point protocol then deadlocked (GLM-5.3-Flash, EP16: 15 of 16 ranks parked in the barrier). Rank 0 alone writes them now. - transformers stamps a DistributedConfig on the config when sharding; it is not JSON-serializable and describes this run's mesh, so it is dropped first. - A rank whose export raises prints its traceback before the finally clause's barrier, where it would otherwise block on ranks that never fail and the job would hang with no error in the log. _hf_shard_flags derives (fsdp2_sharded, ep_sharded) once for all of these gates, kept separate because the FSDP2-dense + EP-expert hybrid is both at once. _process_hf_model_for_export names the in-place processing half the writer path runs: prepare, pack quantized weights with sharded experts left fused, return the quant config. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…json A distributed export wrote a config.json that differed from a single-GPU export of the same checkpoint in two keys: - base_model_ep_plan, which the EP loader swaps to the torch_a2a styles. Those styles exist only while ModelOpt is imported, so loading the export expert-parallel anywhere else fails on an unknown style. transformers drops the tp and pp plans when it serializes a config, but not the EP plan. - distributed_config: null. The writer nulled the DistributedConfig that transformers stamps on the config (it is not JSON-serializable), and the null was written out. Nulling it in place also left transformers' tp_plan property raising on the live model, since it reads distributed_config.enable_expert_parallel whenever the attribute exists. While config.json is written, each config -- the root and every nested one, where a VLM keeps its plan -- now holds the value its source config.json declares, or loses the attribute when the source has none. The run's values come back afterwards. Found comparing a Qwen3-30B-A3B export from the EP path with a single-GPU export of the same checkpoint. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Its docstring and test named the speculative-decoding exporters as a second caller; in this stack the no-gather writer is the only one. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Kimi-K3's expert-parallel prepare hook installs _checkpoint_conversion_mapping on the root and text configs, renaming the source's per-expert w1/w3/w2 so transformers can fuse them on load. Those regexes name the source's tensors, not the export's, and the distributed writer serialized them into config.json at both levels: the 16-node Kimi-K3 export carried them while its base_model_ep_plan and distributed_config were already gone. Restore them with the other run attributes, so a reader of the export never sees renames meant for a different checkpoint. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The export reverts in-memory names to the checkpoint's by replaying the conversions from_pretrained recorded. A load that rewrote a converter lists every checkpoint tensor it consumes as a source: a dequantizing FP8 load adds each weight's weight_scale_inv, and an on-the-fly compressed-tensors decompressor turns each .weight into weight_packed / weight_scale / weight_global_scale. The same expert leaf then repeats, the expert reverse bailed with an arity mismatch, and the whole reverse was skipped, leaving the export on in-memory names. On Kimi-K3 that wrote the routed experts as block_sparse_moe.experts.N.gate_proj/up_proj/down_proj, while the model's own modeling code builds them as w1/w3/w2 (gate_proj and friends belong to its dense MLP class), so a stock load matched none of the 989,184 expert tensors. A converter rewritten by an FP8 dequantizing load fails the same way. Read a module's leaf through the regex anchor and the compressed-tensors sibling suffixes, and keep each leaf once, in order. The dense split rule gets the same treatment, for a fused dense converter a dequantizing load rewrote. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
They were kept out of config.json because Kimi-K3's prepare hook installed _checkpoint_conversion_mapping on its configs. The hook registers its per-expert conversion with transformers' conversion registry instead now, and nothing in the expert-parallel path sets that attribute any more. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
A FineGrainedFP8 dequantize=True load adds a converter that decodes each weight in place, and its reverse re-encodes it (Fp8Quantize), which the quant-aware reverse does not support. It then gave up on the whole mapping, so MiniMax-M2.7 exported its routed experts under in-memory names (mlp.experts.N.gate_proj) instead of the checkpoint's (block_sparse_moe.experts.N.w1). Such a converter renames nothing, and the export writes ModelOpt's own quantized weights and scales rather than re-encoding the source's format, so it contributes no reverse rule now. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
transformers 5.5 and 5.6 reverse Fp8Dequantize into their identity op rather than Fp8Quantize, so on CI's minimum transformers the dequantizing converter still aborted the reverse. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
CI's partial-install torch lane runs tests/unit/torch without the hf extras. These two tests import transformers inside the test body, so they failed there instead of skipping like the rest of the file. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
A barrier alone let the ranks whose writes succeeded return, and the run carried on as if the checkpoint were whole while another rank's export raised. The sharded export now ends by gathering every rank's outcome (all_reduce), so every rank raises with the failing rank's error. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…on plan _hf_shard_flags reports FSDP2 and expert-parallel sharding from the model's recorded plan, rather than from an FSDP2 module or a seeded expert-parallel group, so the export's route, its single writer of the carried weights and its fail-together finish all follow the plan. The writer takes its EP coordinate from the experts' group only when the plan is expert-parallel, and raises if the experts carry none: without it every EP rank would write experts 0..n-1 over the others'. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Each of its three cases spawned two ranks, about 14 s of the linux unit job apiece. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
With fewer expert-parallel ranks than the world, FSDP2 shards the dense part within each expert-parallel group and replicates it across the groups. The no-gather writer, which every expert-parallel export takes, writes a dense tensor only from a dim-0 shard over the whole world, so it refused that layout -- after the export had folded the quantizers. The export now refuses the plan before it folds anything and names the knobs that avoid it. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
… failed Each rank folds only its own shard, so processing can fail on one rank alone, for instance when that rank's experts mix quantized and unquantized ones. That rank went on to the closing export_ok consensus while its peers started the writer's collectives, and the two did not match. Processing now ends in its own consensus, so every rank raises before any of them writes. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…rallel export Each rank holds only its own experts' weight quantizers, so the keep-fused fold's all-or-none check saw only that rank's share. A rank whose experts were all unquantized and one whose experts were all quantized both passed, folded differently, and only the quantized one entered the writer's input-scale all-reduce, out of step with its peer's ownership gather. An expert-parallel export now gathers every rank's per-projection state before anything is folded, and every rank refuses a projection whose experts are not all quantized or all unquantized. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The two-rank test gave both projections the same states and never had every rank mixed, so an agreement that read only the first projection, or accepted a projection mixed on every rank, still passed. Both cases are now refused before processing, as the agreement intends. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The tests of the closing consensus called _finish_export_together directly, so they re-tested all_reduce: had export_hf_checkpoint gone back to a plain barrier, every test would still have passed. One pooled export now fails on rank 0 alone, after every rank wrote its shards, and both ranks must raise with its error. Its collectives run on a group that gives up in seconds, so a rank left waiting fails the test instead of hanging it. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…red ones Merged: - test_a_dequantizing_fp8_load_still_reverses_the_other_conversions becomes the fp8-dequantize case of test_experts_revert_through_converters_loading_rewrote, as a real dequantizing load leaves both converters: the case appends the in-place Fp8Dequantize converter and checks that a bf16 weight is not re-encoded. Aborting the reverse on that converter, and on transformers 5.5 not recognizing its identity reverse, still fail the case. - test_a_model_without_expert_parallel_experts_has_no_expert_state folds into test_the_routed_experts_state_is_found_by_its_expert_parallel_group. Dropped, each caught by a remaining test when its code is broken: - test_process_hf_model_for_export_folds_in_place_and_returns_the_quant_config: returning no quant config, folding nothing, or never keeping experts fused fails test_nogather_export_writes_what_the_single_process_export_writes. - test_the_export_takes_how_the_model_is_sharded_from_its_plan: dropping either flag, swapping them, or reading a pipeline's plan fails the after-the-write failure test, the replicated-dense refusal or test_export_diffusers_pipeline_config. - The one-rank-mixed case of the expert-quantization agreement: every mutation of the agreement it caught, the ranks-disagree and every-rank-mixed cases catch too. The agreement cases now name the refused projection instead of pinning each rank's state in the message, and the processing-failure job checks that rank 0's error names rank 1. Both jobs, and the after-the-write job in place of its own group, run their collectives inside bounded_collectives, and the processing-failure job uses try/except rather than pytest.raises. Kept, since nothing else fails when their code is broken: the agreement's down-proj-disagrees case (an agreement that reads only the first projection) and its all-unquantized case (one that refuses an agreed unquantized projection). Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
test_dense_split_names_each_part_once_through_load_siblings imported _dense_split_rule inside the test, next to the file's module-level quant_aware_conversion imports. Its transformers imports stay local, with the note the file already uses: CI's transformers-free lane collects this file. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…cope The expert-parallel export tests imported mock, the distributed writer, unified_export_hf and the distribution-plan helpers inside each test. Move those to the module's import block; the transformers imports stay local, with the file's existing note, since the transformers-free lane collects it. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
|
/ok to test 6922256 |
What does this PR do?
Type of change: New feature
An expert-parallel model could not be exported at all. transformers pre-slices each expert parameter across the EP group, so
save_pretrainedsees a tensor-parallel model and refuses.export_hf_checkpointnow sends an expert-parallel model to the no-gather writer ([16/21]) unconditionally, since it has no fallback. It reads that the model is expert-parallel from the model's distribution plan ([4/21]), as it reads FSDP2 sharding ([16/21]). What expert parallelism changes in and around the writer:ParallelState([9/21]), the same state calibration reads. If the experts carry none, the writer raises.config.json, and the closing barrier was skipped, so the job hung.distributed_config, and the source's ownbase_model_ep_planrather than thetorch_a2aone the loader swapped in, which only exists while ModelOpt is imported.weight_scale_inv,weight_packed, ...) as a source, which used to repeat the expert leaf and abort the whole reverse conversion. On Kimi-K3 that left all 989,184 expert tensors under names its own modeling code does not use.Fp8Quantize), which the reverse could not do, so it gave up on the whole mapping, and MiniMax-M2.7 exportedmlp.experts.N.gate_projinstead ofblock_sparse_moe.experts.N.w1. That converter renames nothing, so it now contributes no reverse rule. This was also main's behavior for single-process FP8 sources.all_reduce, [4/21]), so every rank raises with the failing rank's error.Usage
None:
export_hf_checkpoint(model, export_dir=...)on a model fromload_and_prepare_ep, under torchrun.Testing
tests/unit/torch/export/test_unified_export_hf.py:tests/unit/torch/export/test_quant_aware_conversion.py: expert and dense reverse conversions through FP8-dequantizing and compressed-tensors converters.tests/unit/torch/utils/test_distributed_run.py: the expert-parallel coordinate the writer reads.tests/unit/torch/export/test_distributed_writer.py: an expert-parallel plan whose experts carry no expert-parallel group is refused.Mutation-checked: reading either flag from somewhere other than the plan, dropping the pipeline guard, or skipping the writer's EP coordinate each fails a test.
tests/unit/torch/export/test_unified_export_hf.py, the export's closing step, throughexport_hf_checkpointitself on two gloo ranks: rank 0's write ofconfig.jsonfails after the shards are written, and both ranks raise with its error. Ending on a bare barrier, skipping the closing step, or the failing rank skipping it each fails the test, the last at the group's 10 s timeout rather than by hanging.The lookup tests merge into the tests that cover them, and tests the no-gather export and refusal tests already cover are dropped. Each drop is checked by mutation.
Locally on this revision, the reordered series rebased on main 264fd26: the full unit suite on transformers 5.18, and on 5.5 apart from the tests whose ONNX, TensorRT or diffusers packages that environment lacks; CI's transformers-free unit lane; pre-commit over the whole PR against main; and the Sphinx docs build. The same at every PR of the stack.
The GPU results here were measured on earlier revisions of this branch; this revision's GPU tests run in CI only.
On GPUs, expert-parallel exports through this path:
block_sparse_moe.experts.N.w1/w2/w3. Every exported weight name exists in the source checkpoint's index (48,239 of 48,239, scales aside), and config.json carries no run attributes. vLLM (0.30 dev build, TP4) serves the export. On gsm8k (5-shot, 100 samples) it scores 0.94 / 0.93 (flexible / strict) against the FP8 source's 0.93 / 0.95, within one standard error.Details in [21/21].
Before your PR is "Ready for review"
CONTRIBUTING.md: N/AAdditional Information
Part 17 of 21 of expert-parallel PTQ for Hugging Face MoE models; merges after [16/21].
Summary by CodeRabbit
New Features
Bug Fixes