Repository navigation
Conversation
MoE loaders now record the padding they narrow away (`mark_padding`) instead of writing it, and `fill_padding` writes it before every post-load processing pass: cold start, layerwise reload and the sharded-RDT replay. Post-processing then sees the same padding on every load, whatever the storage held before (uninitialized layerwise tensors, bytes from an earlier load, or checkpoint bytes written by a transfer engine that does not run the loaders). Covers hidden/intermediate padding of expert weights, scales and biases, the gpt-oss combined-expert path and block-aligned FP8 TP (unit scales). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Aaron Hao <aaronhao@inferact.ai>
Review follow-ups: - Padding records live on the layer, and a load marks the layer pending; `fill_padding` fills only pending layers whose parameters still have the recorded layout. A layer post-processed during loading (online quantization) is no longer filled again in kernel layout. - The fill value is the region's `create_weights` value, read when first recorded (e.g. unit FP8 block scales stay 1); block-aligned FP8 TP keeps its explicit 0 / 1. - The loader passes its parameter instead of looking it up by storage. - Drop the sharded-RDT hook: its replay tensors are never marked. - Drop `_narrow_expert_data_for_padding`; `_split_padding` replaces it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Aaron Hao <aaronhao@inferact.ai>
Resolve conflicts with vllm-project#62407 (_in_own_storage) and vllm-project#61457 (kernel_parameters import); both sides kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Aaron Hao <aaronhao@inferact.ai>
Second review follow-ups: - Padding value is 1 for floating-point scales and 0 for everything else, instead of whatever the region held when first recorded (which was uninitialized memory after a dummy load or on an online-quant cold start). - The gpt-oss per-expert (streamed) loader records its padding too. - Block-aligned FP8 TP keeps writing its padding in the loader as on main, besides recording it, for code that runs post-processing without `fill_padding`. - A parameter whose layout changed starts its padding records over instead of raising. - Test the fill through layerwise reload into dirty storage, and the gpt-oss streamed loader. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Aaron Hao <aaronhao@inferact.ai>
|
Hi @hao-aaron, the pre-commit checks have failed. Please run: uv pip install pre-commit>=4.5.1
pre-commit install
pre-commit run --all-filesThen, commit the changes and push to your branch. For future commits, |
|
/ci run |
|
✅ Triggered Buildkite CI #98145 for commit |
|
The batching makes sense to me: zeroing the padding in the loader per expert (aoshen02#179) cost about 1.5 s per update on DeepSeek-V4.1 TP4 (~100k small memsets per rank), and one strided fill per region removes that. A few things I'd like to see changed:
Posted by Claude Code on my behalf. |
Review follow-ups: - The fill value is recorded per parameter after `create_weights`: a `padding_value` attribute if set, else the initial value if constant (e.g. unit block scales), else 0. Replaces the name-based rule. - A padded write into a parameter that is not registered on the layer warns once instead of being skipped silently. - Block-aligned FP8 TP no longer also fills in the loader; `fill_padding` is the one mechanism. - Document that callers running post-load processing themselves must call `fill_padding` first; skip parameters released by post-processing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Aaron Hao <aaronhao@inferact.ai>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Aaron Hao <aaronhao@inferact.ai>
|
/ci run |
|
✅ Triggered Buildkite CI #98858 for commit ℹ️ This PR is 1 commit behind upstream |
|
This pull request has merge conflicts that must be resolved before it can be |
Signed-off-by: Aaron Hao <hao-aaron@users.noreply.github.com> # Conflicts: # vllm/model_executor/model_loader/utils.py
|
/ci run |
|
✅ Triggered Buildkite CI #98975 for commit |
…adding - Fill padding with 1 only for floating-point params created with ones (compared against the dtype's bytes of 1.0), else 0. Taking any constant initial value read uninitialized torch.empty storage, e.g. a freed all-0x01 bool buffer from the previous layer's scan (fp8 2^-9 padding). - Route the gpt-oss Quark expert loader through _narrow_and_mark_padding so its padding (MXFP4 hidden 2880 -> 3072 on ROCm) is filled too. - Test the fill before post-load processing in the direct-reload toy model, and the Quark loader's padding. - Note that the sharded-RDT baked replay relies on unquantized MoE zeroing its intermediate padding. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Aaron Hao <hao-aaron@users.noreply.github.com>
|
/ci run |
|
✅ Triggered Buildkite CI #98981 for commit ℹ️ This PR is 1 commit behind upstream |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Aaron Hao <hao-aaron@users.noreply.github.com>
|
/ci run |
|
✅ Triggered Buildkite CI #98984 for commit |
|
/ci retry |
|
✅ Queued 4 failed job(s) for retry in Buildkite CI #98984. |
|
/ci retry |
|
✅ Queued 4 failed job(s) for retry in Buildkite CI #98984. |
Padding is in vllm-project#64352's scope: the expert loaders record the padding they leave uncovered and `fill_padding` writes it, with the cold-start value, right before post-load processing, which also keeps it compatible with the load plan (vllm-project#64418). Restores `begin_direct_reload` and removes `test_direct_reload_zeroes_padding`, which wrote `param.data` directly and depended on the zero. The `mxfp4-marlin` address case stays. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: aoshen02 <aoshen@inferact.ai>
…oader splits like the full one Records accumulated across loads, so a load that split a parameter differently from an earlier one had real rows overwritten by the earlier load's padding: gpt-oss MXFP4 at TP>=2 loaded per expert after a full-checkpoint load zeroed 96 channels of every expert on rank 0. A fill now writes only the regions recorded since the last fill. The per-expert (streamed) loader split the intermediate dim by the padded per-rank size, while the full-checkpoint loader splits it in whole MXFP4 blocks; it now splits as the full one does, so both write the same rows. GptOssRoutedExperts._copy_to_expert keeps main's (expert_data, loaded_weight) signature and finds the parameter from the storage, and the Quark loader calls it instead of a separate closure, so callers written against main record padding too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Aaron Hao <hao-aaron@users.noreply.github.com>
|
/ci run |
|
✅ Triggered Buildkite CI #99031 for commit ℹ️ This PR is 11 commits behind upstream |
|
Notes from a review pass, for reviewers. The fixes are in
🤖 Generated with Claude Code |
Overview
MoE loaders record the padding they narrow away instead of writing it, and the padding is filled right before post-load processing, on every load, with the value each parameter was created with. A reload into storage that already held data (layerwise rematerialization, or an in-place reload into post-processed bytes) then post-processes exactly the bytes a cold start would. Replaces #50523 and aoshen02#179; it also batches the per-expert zeroing of aoshen02#179, which cost about 1.5 s per update on DeepSeek-V4.1 TP4 (~100k small memsets per rank), into one strided fill per region. Part of RFC #54477.
Claims
main, and 0 with this PR (verifier NCCL sync); 280 → 0 for a disk reload at TP2.maininitialized the padding. Every parameter and buffer after a cold start is bitwise identical tomainon gpt-oss-20b, Qwen3-30B-A3B (BF16 TP4, FP8 TP1) and Qwen3-30B-A3B-NVFP4 (TP4). Wheremainleft padding uninitialized (e.g. unquantized MoE weights fromtorch.empty), it is now 0. Ones-initialized integer scales (Quark OCP_MX uint8 E8M0) now get 0 padding instead of 1; harmless, since their weight padding is zero. On DeepSeek-V4.1-Flash (TP4) every parameter is identical; only runtime buffers that change during generation (engram.staged_rows,topk_indices_buffer) differ.RoutedExperts.weight_loader(Inkling's experts bypass it and write their own padding, see [Model] Inkling, Muse Spark: write checkpoint tensors through the parameter's weight loader #64690). Hidden/intermediate rounding of weights, scales and biases, the gpt-oss combined, per-expert (streamed) and Quark paths, and block-aligned FP8 TP. The value is recorded right aftercreate_weights: apadding_valueattribute if set, else 1 for a floating-point parameter created with ones (its bytes equal the dtype's 1.0, e.g.torch.onesFP8 block scales), else 0. Uninitialized (torch.empty) storage never sets the value, even when it happens to hold a constant.fp8_per_block(padded experts), cold start and reloads are bitwise identical tomain.Validation
All on GB200. "main" is the base commit; each side is compared with a fresh start of the same tree. The verifier, gpt-oss and NVFP4 runs used the current head 95bf429 against its merge base 15cc0a8; the unit-test A/B used 8f20479, and the Qwen3 BF16/FP8 rows earlier commits of this PR (a2080a4, and d36411e for online FP8). CI covers the unit tests on the current head.
Weight update through the verifier (vllm-state-transition-verifier
run_bench.py --audit). gpt-oss-20b, trainer FSDP=2 → server TP1 over NCCL, one sync to a perturbed checkpoint (every tensor changed), every parameter, buffer and kernel-held tensor compared with a fresh server on that checkpoint:w13/w2bias and weight scale)The 72/96 non-parameter tensors are
moe_quant_configand TRT-LLMgemm1_*constants rebuilt by post-processing: a separate issue (#63090, aoshen02#180), unchanged here.Disk reload (
reload_weights, layerwise) on currentmain, reload to a perturbed checkpoint twice and back to the original:fp8_per_block(192 → 256)main; cold start bitwise identical (8084 tensors)Cold-start serving A/B (95bf429 vs 15cc0a8, same node, order main → PR → PR → main): GSM8K 5-shot greedy (1319 questions; gpt-oss via chat completions, reasoning effort low),
vllm bench serverandom 1024 in / 256 out at concurrency 64 (640 prompts), KV cache from the server log.The two
mainruns differ from each other as much as from the PR; the gpt-oss throughput gap follows run order (second run of each pair faster for both arms).Unit tests (GB200, 8f20479 vs
mainside by side in one job):tests/kernels/moe/test_moe_weight_loading_padded.py,tests/kernels/moe/test_fp8_tp_loading.py,tests/quantization/test_online.py,tests/quantization/test_auto_gptq.py,tests/model_executor/model_loader/test_gpt_oss_weight_loading.py,tests/model_executor/test_utils.py,tests/model_executor/model_loader/test_reload.py.test_kv_scale_reload)test_kv_scale_reload)No test fails only with this PR.
New tests:
test_moe_weight_loading_padded.py: loads into storage holding stale bytes; the loader leaves padding to post-processing,fill_paddingwrites each parameter's initial value (zeros, ones,torch.randn→ 0, explicitpadding_value) for every expert, a second fill after post-processing is a no-op, and a parameter whose layout changed is skipped.test_reload.py::test_padded_moe_reload_releases_each_layer: layerwise reload into rematerialized tensors holding other bytes; padding is 0 afterwards (fails without the fill in_layerwise_process).test_gpt_oss_weight_loading.py: the per-expert (streamed) loader records its padding.test_fp8_tp_loading.py: block-aligned FP8 TP padding is now written only byfill_padding(the test fails without it).test_direct_reload.py(g-padding): the toy model's loader writes a smaller checkpoint slice and records the rest withmark_padding; post-load processing re-lays out the whole tensor. Cold start and direct reload match a fresh cold start (fails without the fill inprocess_weights_after_loading).test_gpt_oss_weight_loading.py::test_quark_expert_padding_is_filled_before_post_processing: the Quark loader records its padding (per-expert and combined).test_moe_weight_loading_padded.py::test_initial_padding_value: a constant other than 1.0 (including all-0x01 bytes, as a freed bool buffer holds) gets 0.test_utils.py::test_padding_records_only_the_latest_load: a second load that splits a parameter differently is not overwritten by the first load's padding, and a parameter the second load wrote without padding is not filled.test_gpt_oss_weight_loading.py::test_streamed_mxfp4_expert_splits_like_the_checkpoint_loader: the per-expert loader splits the MXFP4 intermediate dim in whole blocks, as the full-checkpoint loader does, not by the padded per-rank size.The last two fail on the previous head with the new tests copied in (slurm 42791) and pass on
624c71606(slurm 42790):test_utils.py,test_gpt_oss_weight_loading.py,test_moe_weight_loading_padded.py,test_direct_reload.py,test_reload.py: 257 passed, 1 failed (test_kv_scale_reload, which also fails on maine0d00e4b8, slurm 42842).Verifier command:
run_bench.py --model-a <gpt-oss-20b> --update <perturbed copy> --backend nccl --layout fsdp=2 --server-gpus 0 --trainer-gpus 2,3 --warmup 0 --iters 1 --audit -- --max-model-len 4096, with the perturbed copy fromvalidation/make_perturbed_checkpoint.py --variant 1.Details
Root cause. Backends pad expert dims for kernel alignment,
create_weightsallocates the padding zero-filled, and loaders only write the checkpoint-backed slice. Post-processing then reads the whole padded tensor (TRT-LLM MXFP4 interleaves and shuffles over the padded size). A reload does not start from the allocation-time zeros: layerwise reload rematerializes withtorch.empty_strided, in-place reload loads into the previous post-processed bytes, and the block-aligned FP8 TP path only refilled its padding because its loader happened tofill_it.Change.
mark_padding(layer, name, region, value)records a padded region oflayer.<name>(offset, shape, stride relative to the parameter) on the layer, which outlives the parameter tensors across reloads. A region of one expert is recorded once for all experts. Records hold only the latest load's regions: a parameter's first record after a fill starts its records over, and a fill drops parameters this load did not mark. Two loaders may split the same parameter differently (gpt-oss full-checkpoint vs per-expert), so earlier regions may cover real rows.fill_padding(module)writes the regions recorded since the last fill, and skips a parameter whose layout no longer matches the recorded one. It runs for every module before any post-processing at cold start, and per layer before post-processing in layerwise reload. A layer that online quantization post-processed during loading is therefore not filled again in kernel layout.RoutedExpertsrecords instead of writing (_split_paddingreplaces_narrow_expert_data_for_padding), with the value recorded at construction. This covers hidden/intermediate padding, the gpt-oss combined, per-expert and Quark loaders (GptOssRoutedExperts._copy_to_expertkeeps main's signature and finds its parameter from the storage, so other callers record too), and block-aligned FP8 TP, whose in-loaderfill_andpad_valueplumbing are removed. The loader passes its parameter. The gpt-oss per-expert loader now splits the intermediate dim as the full-checkpoint loader does (whole MXFP4 blocks); it split by the padded per-rank size before, so the two loaders wrote different rows at TP >= 2 (on main too). recording is skipped on meta tensors (the layerwise dry run) and plain tensors, and a padded write into annn.Parameternot registered on the layer warns once.QuantizeMethodBase.process_weights_after_loadingandfill_padding: code that runs post-processing itself must callfill_padding(layer)first.Replaces #50523 and aoshen02#179.
materialize_meta_tensor. It fixes layerwise reload only, and only with zeros, so non-zero padding (unit scales) and in-place reload stay wrong.#62332 (refits into already-shuffled FlashInfer layouts) is orthogonal.
Limitations.
kv_arows) are correct today, since the loader writes the zeros on every load; they needmark_paddingonce the load plan ([RFC]: Load plan — an engine-neutral API for sharded weight transfer #64418) stops running loaders.fill_paddingfirst (tests that callquant_method.process_weights_after_loadingdirectly do so now). The sharded-RDT baked replay runs no loaders, so it fills nothing. It does bake padded unquantized MoE (weight_loader_numelexcludes the padding) into uninitialized storage: intermediate padding is still correct there because unquantized post-processing re-zeroes it on the AITER and FlashInfer TRT-LLM backends (_zero_intermediate_padding, now commented as relied on), but hidden and bias padding of such a layer are not refilled on that path. The records persist on the layer, so a follow-up can replay them on that path.nccl_m2nwill no longer run loaders, so the plan needs an explicitfill_paddingstep before post-processing.meta(the weight-cache IPC loader) records padding value 0 for every parameter, including ones-initialized scales. Direct reload refuses pre-processed weights, but a layerwise reload of such a model would fill scale padding with 0 rather than the 1 its cold start wrote. Quant methods can setpadding_value=1on unit scales if that path needs it.mainand with this PR alike: gpt-oss-20b GSM8K 0.33 → 0.00 after a layerwise reload on TRT-LLM MXFP4 (also on Triton MXFP4, which does not pad gpt-oss at TP1). That fix is needed too before reloaded gpt-oss matches a cold start end to end. Qwen3-30B-A3B BF16 already zeroes its intermediate padding in post-processing and keeps its accuracy on both sides.mainas well.VocabParallelEmbedding/ParallelLMHead): the loader zero-fills them, no token indexes them, and logits are sliced and masked.in_proj_qkvgfabpadding rows and MiMo-V2 fused qkv pad rows: the forward splits them off._maybe_pad_weight, gfx11 cache-aliasing padding): never read.Pull Request Checklist
I used vLLM's
/pr-checklistskill. (Mandatory for agents, optional for humans).AI assistance was used during the creation of this PR.
Design Fit: Minimizes impact on core components, reuses existing functionality, and justifies added complexity.
Testing and Validation: Validates the change and ensures any added tests are meaningful and reliable, with CI coverage or documented CI resource constraints and validation performed outside CI.
Code Quality and Style: Keeps code and comments clear and concise, and updates relevant documentation and examples.
Pull Request Contents: Includes a brief summary and relevant links, supports claims with evidence, explains root causes and implementation trade-offs, and follows the contributing guide.
AI assistance (Claude Code) was used to write the change and run the validation; the submitter reviewed every changed line.
🤖 Generated with Claude Code