Skip to content

[MoE] Fill expert padding before post-load processing, not in the loader - #64352

Open
hao-aaron wants to merge 17 commits into
vllm-project:mainfrom
hao-aaron:moe-pad-fill
Open

hao-aaron wants to merge 17 commits into
vllm-project:mainfrom
hao-aaron:moe-pad-fill

Conversation

@hao-aaron

@hao-aaron hao-aaron commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator

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

  • Reload of padded MoE experts matches a cold start. gpt-oss-20b (MXFP4, experts padded 2880 → 3072): after a weight update, 96 padded expert parameters differ from a fresh server on main, and 0 with this PR (verifier NCCL sync); 280 → 0 for a disk reload at TP2.
  • Cold start unchanged where main initialized the padding. Every parameter and buffer after a cold start is bitwise identical to main on gpt-oss-20b, Qwen3-30B-A3B (BF16 TP4, FP8 TP1) and Qwen3-30B-A3B-NVFP4 (TP4). Where main left padding uninitialized (e.g. unquantized MoE weights from torch.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.
  • Covers MoE expert padding in every loader that goes through 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 after create_weights: a padding_value attribute if set, else 1 for a floating-point parameter created with ones (its bytes equal the dtype's 1.0, e.g. torch.ones FP8 block scales), else 0. Uninitialized (torch.empty) storage never sets the value, even when it happens to hold a constant.
  • Online quantization is unaffected. Layers post-processed during loading are not filled again: with online fp8_per_block (padded experts), cold start and reloads are bitwise identical to main.
  • Cost: 4–7 ms per post-load processing pass on the models above, plus the marking in the loaders (gpt-oss-20b sync 0.41 → 0.45 s); nothing on the forward path.

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:

main this PR
parameters ≠ fresh 96 (w13/w2 bias and weight scale) 0
non-parameter tensors moved / extra 72 / 96 72 / 96
sync time (one sync) 0.41 s 0.45 s

The 72/96 non-parameter tensors are moe_quant_config and TRT-LLM gemm1_* constants rebuilt by post-processing: a separate issue (#63090, aoshen02#180), unchanged here.

Disk reload (reload_weights, layerwise) on current main, reload to a perturbed checkpoint twice and back to the original:

Model, backend main this PR
gpt-oss-20b TP2, TRT-LLM MXFP4 280–288 expert tensors ≠ fresh; back-to-original differs from cold start in 480 tensors 0 expert tensors ≠ fresh; back-to-original: no parameter differs
Qwen3-30B-A3B BF16 TP4, TRT-LLM (intermediate 192 → 256) pass; GSM8K 0.890 → 0.893 pass; GSM8K 0.892 → 0.892
Qwen3-30B-A3B BF16 TP4, online fp8_per_block (192 → 256) no parameter ≠ fresh; cold start = this PR no parameter ≠ fresh; 144 padded regions per rank recorded
Qwen3-30B-A3B-FP8 TP1 (no padding) unchanged unchanged
Qwen3-30B-A3B-NVFP4 TP4, TRT-LLM (no padding recorded) 960 side tensors extra / 384 moved identical to 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 serve random 1024 in / 256 out at concurrency 64 (640 prompts), KV cache from the server log.

Model GSM8K main → PR Total throughput tok/s, main → PR KV cache tokens, main → PR
gpt-oss-20b TP2, TRT-LLM MXFP4 0.861, 0.857 → 0.858, 0.864 70,881, 72,503 → 70,193, 73,090 10,970,211 → 10,971,579
Qwen3-30B-A3B BF16 TP4, TRT-LLM 0.894, 0.891 → 0.893, 0.892 48,643, 49,148 → 49,190, 49,289 6,374,288 → 6,374,288

The two main runs 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 main side 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.

passed failed
main 354 1 (test_kv_scale_reload)
this PR 359 1 (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_padding writes each parameter's initial value (zeros, ones, torch.randn → 0, explicit padding_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 by fill_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 with mark_padding; post-load processing re-lays out the whole tensor. Cold start and direct reload match a fresh cold start (fails without the fill in process_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 main e0d00e4b8, slurm 42842).

FAILED test_utils.py::test_padding_records_only_the_latest_load
  assert layer.w.tolist() == [7] * 6 + [0] * 2   ->  At index 4 diff: 0.0 != 7
FAILED test_gpt_oss_weight_loading.py::test_streamed_mxfp4_expert_splits_like_the_checkpoint_loader
  rank 1 got 64 of its 128 rows (split by the padded 96 rows per rank)

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 from validation/make_perturbed_checkpoint.py --variant 1.

Details

Root cause. Backends pad expert dims for kernel alignment, create_weights allocates 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 with torch.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 to fill_ it.

Change.

  • mark_padding(layer, name, region, value) records a padded region of layer.<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.
  • RoutedExperts records instead of writing (_split_padding replaces _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_expert keeps main's signature and finds its parameter from the storage, so other callers record too), and block-aligned FP8 TP, whose in-loader fill_ and pad_value plumbing 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 an nn.Parameter not registered on the layer warns once.
  • The contract is documented on QuantizeMethodBase.process_weights_after_loading and fill_padding: code that runs post-processing itself must call fill_padding(layer) first.

Replaces #50523 and aoshen02#179.

#62332 (refits into already-shuffled FlashInfer layouts) is orthogonal.

Limitations.

  • Only MoE expert padding is recorded. Loaders that concatenate zeros onto the checkpoint tensor (DeepSeek-V4/V4.1 shared experts and drafters, Ling-3 shared experts, dots3 dense/shared MLP, GLM5-Next NoPE kv_a rows) are correct today, since the loader writes the zeros on every load; they need mark_padding once the load plan ([RFC]: Load plan — an engine-neutral API for sharded weight transfer #64418) stops running loaders.
  • Code that runs post-processing itself must call fill_padding first (tests that call quant_method.process_weights_after_loading directly do so now). The sharded-RDT baked replay runs no loaders, so it fills nothing. It does bake padded unquantized MoE (weight_loader_numel excludes 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.
  • Padding is filled when loaders ran for that load. When transfer engines move to the load plan ([RFC]: Load plan — an engine-neutral API for sharded weight transfer #64418), RDT and nccl_m2n will no longer run loaders, so the plan needs an explicit fill_padding step before post-processing.
  • A model built on 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 set padding_value=1 on unit scales if that path needs it.
  • A padded NVFP4 MoE was not available to test; Qwen3-30B-A3B-NVFP4 at TP4 records no padding.
  • The evidence is tensor-level (bitwise against a fresh server), not accuracy. On every padded configuration tried, end-to-end output after a reload is also broken by the separate side-tensor issue above ([Model Loader] Keep side tensors on startup storage across layerwise reload #63090, [Model Loader] Write weight-derived tensors back in place on reload aoshen02/vllm#180), on main and 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.
  • DeepSeek-V4.1 reload was not validated: layerwise reload runs out of memory on 4 GPUs on main as well.
  • Out of scope: padding left unrecorded on purpose, because nothing reads it or every load rewrites it:
    • Vocab padding rows (VocabParallelEmbedding / ParallelLMHead): the loader zero-fills them, no token indexes them, and logits are sliced and masked.
    • DeepSeek-V4/V4.1 attention-sink padded heads: the loader writes -inf, and the outputs of padded heads are dropped.
    • Kimi-K3 KDA in_proj_qkvgfab padding rows and MiMo-V2 fused qkv pad rows: the forward splits them off.
    • Inkling gate padded expert rows and EP-alignment padding experts: never routed to.
    • Padding created by post-load processing itself (FlashInfer FP4/MXFP8 alignment, Marlin tiles, b12x, CUTLASS NVFP4, ModelOpt, online quantization): rebuilt from zeros on every run.
    • Stride-only padding (ROCm _maybe_pad_weight, gfx11 cache-aliasing padding): never read.
    • DBRX, Humming MoE (refused by direct reload), Cohere Compass, GGUF, bitsandbytes, HQQ, FP-Quant: not RL targets.

Pull Request Checklist
  • I used vLLM's /pr-checklist skill. (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

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>
hao-aaron and others added 2 commits October 28, 2026 20:33
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>
@hao-aaron
hao-aaron marked this pull request as ready for review October 28, 2026 22:19
@hao-aaron
hao-aaron requested a review from mgoin as a code owner October 28, 2026 22:19

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@hao-aaron hao-aaron added the ready ONLY add when PR is ready to merge/full CI is needed label Oct 28, 2026
@hao-aaron
hao-aaron requested a review from wzhao18 as a code owner October 28, 2026 22:19
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>
@mergify

mergify Bot commented Oct 28, 2026

Copy link
Copy Markdown
Contributor

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-files

Then, commit the changes and push to your branch.

For future commits, pre-commit will run automatically on changed files before each commit.

@hao-aaron

Copy link
Copy Markdown
Collaborator Author

/ci run

@github-actions

Copy link
Copy Markdown

✅ Triggered Buildkite CI #98145 for commit 55d1267e2283.

@aoshen02

Copy link
Copy Markdown
Contributor

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:

  1. Pad values come from the parameter name. routed_experts.py:565 uses 1 when the name contains scale and the dtype is floating point, otherwise 0. That matches create_weights today by convention only: NVFP4 FP8 block scales get 1, uint8 MX scales get 0, and a backend that initializes differently or names a scale differently silently diverges from cold start. Cold-start identity was checked on gpt-oss, Qwen3 BF16/FP8 and DeepSeek-V4.1, not on a padded NVFP4 MoE. Could the value be declared where the parameter is created (e.g. a weight attr set in create_weights) instead of inferred?

  2. A missed record is silent. When param is None or not found in self._parameters (:562), the padding is not recorded and stays stale with no error, which is exactly the bug this PR fixes. Skipping meta tensors is fine, but otherwise I'd assert, or at least warn once.

  3. Every post-processing caller must call fill_padding first. That holds for the two loader paths, but sharded_rdt_engine.py:1396 already runs quant_method.process_weights_after_loading without it, and several tests had to be edited to add it. For post-processing that keeps shape, stride and dtype, the layout check is the only guard against filling processed bytes. Please state this contract where process_weights_after_loading is documented, so new callers do not miss it.

  4. The block-aligned FP8 TP path writes its padding twice (:417 fill_ plus a record). No other path keeps an in-loader fallback, so I'd drop one of the two for consistency.

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>
@mergify mergify Bot removed the needs-rebase label Oct 31, 2026
@hao-aaron

Copy link
Copy Markdown
Collaborator Author

/ci run

@github-actions

Copy link
Copy Markdown

✅ Triggered Buildkite CI #98858 for commit 1d3d59d316ae.

ℹ️ This PR is 1 commit behind upstream main. Merged upstream main into this branch as 1d3d59d316ae before starting CI. Pull the branch before pushing more changes.

@mergify

mergify Bot commented Oct 31, 2026

Copy link
Copy Markdown
Contributor

This pull request has merge conflicts that must be resolved before it can be
merged. Please rebase the PR, @hao-aaron.

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

@mergify mergify Bot added the needs-rebase label Oct 31, 2026
Signed-off-by: Aaron Hao <hao-aaron@users.noreply.github.com>

# Conflicts:
#	vllm/model_executor/model_loader/utils.py
@hao-aaron

Copy link
Copy Markdown
Collaborator Author

/ci run

@github-actions

github-actions Bot commented Nov 1, 2026

Copy link
Copy Markdown

✅ Triggered Buildkite CI #98975 for commit 292132564b26.

@mergify mergify Bot removed the needs-rebase label Nov 1, 2026
…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>
@hao-aaron

Copy link
Copy Markdown
Collaborator Author

/ci run

@github-actions

github-actions Bot commented Nov 1, 2026

Copy link
Copy Markdown

✅ Triggered Buildkite CI #98981 for commit 877a17f46e73.

ℹ️ This PR is 1 commit behind upstream main. Merged upstream main into this branch as 877a17f46e73 before starting CI. Pull the branch before pushing more changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Aaron Hao <hao-aaron@users.noreply.github.com>
@hao-aaron

Copy link
Copy Markdown
Collaborator Author

/ci run

@github-actions

github-actions Bot commented Nov 1, 2026

Copy link
Copy Markdown

✅ Triggered Buildkite CI #98984 for commit c777c75cefe8.

@hao-aaron

Copy link
Copy Markdown
Collaborator Author

/ci retry

@github-actions

github-actions Bot commented Nov 1, 2026

Copy link
Copy Markdown

✅ Queued 4 failed job(s) for retry in Buildkite CI #98984.

@hao-aaron

Copy link
Copy Markdown
Collaborator Author

/ci retry

@github-actions

github-actions Bot commented Nov 1, 2026

Copy link
Copy Markdown

✅ Queued 4 failed job(s) for retry in Buildkite CI #98984.

aoshen02 added a commit to hao-aaron/vllm that referenced this pull request Nov 2, 2026
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>
@hao-aaron

Copy link
Copy Markdown
Collaborator Author

/ci run

@github-actions

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown

✅ Triggered Buildkite CI #99031 for commit 26cc90fe7b00.

ℹ️ This PR is 11 commits behind upstream main. Merged upstream main into this branch as 26cc90fe7b00 before starting CI. Pull the branch before pushing more changes.

@hao-aaron

Copy link
Copy Markdown
Collaborator Author

Notes from a review pass, for reviewers. The fixes are in 624c71606 (see the description); what is left:

🤖 Generated with Claude Code

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gpt-oss Related to GPT-OSS models quantization ready ONLY add when PR is ready to merge/full CI is needed

Projects

Status: To Triage

Development

Successfully merging this pull request may close these issues.

3 participants