[MoE] Fix flashinfer TRT-LLM BF16 expert weight reload on refit - #33743
Open
Kh4L wants to merge 1 commit into
Open
[MoE] Fix flashinfer TRT-LLM BF16 expert weight reload on refit#33743Kh4L wants to merge 1 commit into
Kh4L wants to merge 1 commit into
Conversation
Kh4L
requested review from
Alisehen,
AniZpZ,
BBuf,
Edwardf0t1,
FlamingoPg,
Fridge003,
HaiShaw,
OrangeRedeng,
Ying1123,
b8zhong,
ch-wan,
hnyls2002,
ispobock,
merrymercy and
mmangkad
as code owners
August 5, 2026 18:52
Kh4L
added a commit
to Kh4L/NemoRL
that referenced
this pull request
Aug 5, 2026
SGLang auto-selects flashinfer_trtllm on sm100 for bf16 MoE, but stock 0.5.12.post1 rewrites expert weights into a block layout that its weight-update path never undoes, so the first refit bucket dies with "size of tensor a (64) must match the size of tensor b (2048) at non-singleton dimension 2". Until now the recipe pinned moe_runner_backend to triton to avoid the block layout entirely. Apply the upstream fix (sgl-project/sglang#33743, fixing #27787) to the installed sglang during the image build. sglang arrives as a PyPI wheel whose srt/ tree is pure Python, so the fix needs no forked wheel; the applier patches every venv and the uv cache archive they link into, and hard-fails unless all of them carry it. The vendored file is the v0.5.12.post1 backport of that PR -- the PR targets main, where the hot-update paths have since moved -- and is executable-code-identical to it. Delete it once the fix is in the pinned SGLang release. Switch the 32n4g recipe to flashinfer_trtllm, which on GB200 at tp=2 decodes 11-30% faster than triton across batch sizes 1-32, and +17% at the ~2 concurrent requests per engine this recipe produces. Validated on GB200: bit-exact generation, max abs logprob delta 0.0, over an adversarially bucketed push of all 18867 checkpoint tensors for flashinfer_trtllm, flashinfer_trtllm_routed and triton; and 4/4 refits in a 32-node async-GRPO run (status=passed, world_size=65 engines=32). Also record the two load-bearing launch overrides the refit guide omitted. Signed-off-by: Serge Panev <spanev@nvidia.com>
Kh4L
force-pushed
the
moe-trtllm-bf16-hot-reload-layout
branch
from
August 5, 2026 19:19
4bae2c9 to
3acb407
Compare
Collaborator
|
@Kh4L could you fix the conflicts? thanks! |
Kh4L
force-pushed
the
moe-trtllm-bf16-hot-reload-layout
branch
from
August 6, 2026 18:57
3acb407 to
14712d8
Compare
Collaborator
|
@Kh4L could you rebase/merge with main again? github action was broken |
Kh4L
added a commit
to Kh4L/NemoRL
that referenced
this pull request
Aug 10, 2026
…atch sglang 0.5.13 pulls its companions with it: sglang-kernel 0.4.3, flashinfer 0.6.12, transformers 5.8.1 and kernels >=0.14.1,<0.15. None of them resolve alone -- uv rejects the set until all seven move together. 0.5.13 still carries the bug (unquant.py gates the inverse transform on is_flashinfer_trtllm_routed() alone), and sgl-project/sglang#33743 is still open, so the vendored patch stays. But it has to be retargeted rather than carried over, because 0.5.13 sits between v0.5.12.post1 and upstream main: - it keeps the hot-update entry points in model_executor/model_runner.py, so that hunk is the v0.5.12.post1 one (main moved them to weight_updater.py) - it has already moved to the four-argument _maybe_get_cached_w3_w1_permute_indices(..., is_gated_act_gemm=...), so base_config.py and unquant.py come from the PR's main-branch version Force-applying the old backport with fuzz would have reverted that call to three arguments and changed the permutation for non-gated MoE models -- wrong weights, and no shape check would have caught it. One hunk exists in neither upstream form: replacing the inline flashinfer block in process_weights_after_loading with the extracted helper. Reconciled by hand against the 0.5.13 tree, then verified to apply cleanly to a pristine checkout, round-trip byte-exact, call the helper exactly once, and expose all six patch-added methods. The patch REPLACES rather than joins the old one: the applier globs *.patch and applies every file it finds, so two variants targeting the same source would conflict. Not yet exercised on hardware at 0.5.13 -- the GB200 evidence in the README is against the v0.5.12.post1 backport. Signed-off-by: Serge Panev <spanev@nvidia.com>
…paths
process_weights_after_loading rewrites BF16 MoE expert weights into the
flashinfer TRT-LLM BlockMajorK layout whenever use_flashinfer_trtllm_moe is
set, and that flag covers both flashinfer_trtllm and flashinfer_trtllm_routed.
The inverse hook was gated on is_flashinfer_trtllm_routed() alone, so with
--moe-runner-backend flashinfer_trtllm the destination stayed in block layout
and the hot copy raised:
The size of tensor a (64) must match the size of tensor b (2048) at
non-singleton dimension 2
torch names the copy_ destination first, so the 64 is ours: block_k is 128
bytes and the conversion runs on a uint8 view, giving 128 / 2 bytes-per-bf16.
Nobody opts into this -- flashinfer_trtllm is auto-selected on sm100 for bf16
MoE models when moe_runner_backend is left at "auto".
Gate the inverse on the same flag that gates the transform, so the two cannot
drift apart again, and add repack_weights_after_hot_update to re-derive the
layout once the copies are done. update_weights_from_disk and the
checkpoint-engine IPC path already re-run process_weights_after_loading;
update_weights_from_tensor, update_weights_from_distributed and the bucketed
variants call model.load_weights() directly and do not. Widening the gate on
its own would therefore only trade the loud copy failure for a parameter left
in canonical layout while the kernel reads BlockMajorK.
The restore inverts the data rather than only reinterpreting the shape. RL
callers batch weights (weight_sync/utils.py), so one refit is many update
RPCs, and with a shape-only restore an earlier bucket's re-derive would block
expert slots a later bucket has not written yet a second time.
Reproduced against the real code path on CPU with Qwen3-30B-A3B geometry
(hidden 2048, moe_intermediate 768, tp=2 -> 384 per partition):
before: postprocess (4,32,768,64) -> restore no-op -> copy raises
after: restore -> (4,768,2048) -> copy ok -> repack -> (4,32,768,64)
Fixes sgl-project#27787
Kh4L
force-pushed
the
moe-trtllm-bf16-hot-reload-layout
branch
from
August 12, 2026 21:43
14712d8 to
5cfc305
Compare
Collaborator
|
/tag-and-rerun-ci |
Collaborator
|
/rerun-failed-ci |
b8zhong
approved these changes
Aug 14, 2026
Collaborator
|
/rerun-failed-ci |
Collaborator
|
@Kh4L can you help resolve the conflicts. Thanks! |
Collaborator
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
process_weights_after_loadingrewrites BF16 MoE expert weights into the flashinfer TRT-LLM BlockMajorK layout wheneveruse_flashinfer_trtllm_moeis set, and that flag covers bothflashinfer_trtllmandflashinfer_trtllm_routed. The inverse hook, however, was gated onis_flashinfer_trtllm_routed()alone. So with--moe-runner-backend flashinfer_trtllmthe destination parameter stayed in block layout and the hot copy raised:torch names the
copy_destination first, so the 64 is ours:block_kis 128 bytes and the conversion runs on auint8view, giving 128 / 2 bytes-per-bf16.Nobody opts into this —
flashinfer_trtllmis auto-selected on sm100 for BF16 MoE models whenmoe_runner_backendis left at"auto", so an RL weight refit on Blackwell hits it by default.Fixes #27787
Modifications
use_flashinfer_trtllm_moe), so the two cannot drift apart again.repack_weights_after_hot_updatehook to re-derive the layout once the copies are done.update_weights_from_diskand the checkpoint-engine IPC path already re-runprocess_weights_after_loading;update_weights_from_tensor,update_weights_from_distributedand the bucketed variants callmodel.load_weights()directly and do not. Widening the gate on its own would therefore only trade the loud copy failure for a parameter left in canonical layout while the kernel reads BlockMajorK. The re-derive is called from afinallyon all four update paths, so a mid-update exception cannot leave parameters canonical while the kernel expects block layout.Files touched:
layers/quantization/unquant.py,layers/quantization/base_config.py(no-op base hook),model_executor/model_runner_components/weight_updater.py, plus a new CPU unit test.Note on the rebase onto current main: the only conflict was add/add with #33905 (XPU MoE row-stride padding), which inserts helpers at the same module-level position. Both blocks are kept and there is no interaction — the XPU padding applies under an XPU default device, the BlockMajorK rewrite under flashinfer on sm100, and #33905 only touches
create_weights, which this PR does not.Accuracy Tests
Reproduced against the real code path on CPU with Qwen3-30B-A3B geometry (hidden 2048, moe_intermediate 768, tp=2 -> 384 per partition):
test/registered/unit/layers/quantization/test_flashinfer_trtllm_bf16_moe_reload.pycovers this end to end and is registered as a CPU test (11 cases): the block layout is applied on cold load and changes the shape; the restore runs for the non-routed backend and inverts the layout rather than reshaping it; a reload reproduces the cold-load layout exactly, including through the bucketed update path; the repack is a no-op when nothing was reverted and when the backend is inactive; non-bijective permutations and unexpected element counts are rejected; and thefinallyre-derive still runs whenload_weightsraises.Speed Tests and Profiling
Not applicable to the forward path — this PR only changes the weight-update path. The re-derive runs once per update RPC, after the copies land; no kernel or forward-pass code is touched.
Checklist
Review and Merge Process
/tag-and-rerun-ci,/tag-run-ci-label,/rerun-failed-ciCI States
Latest PR Test (Base): ✅ Run #31643837888
Latest PR Test (Extra): ❌ Run #31643837810