Skip to content

[MoE] Fix flashinfer TRT-LLM BF16 expert weight reload on refit - #33743

Open
Kh4L wants to merge 1 commit into
sgl-project:mainfrom
Kh4L:moe-trtllm-bf16-hot-reload-layout
Open

[MoE] Fix flashinfer TRT-LLM BF16 expert weight reload on refit#33743
Kh4L wants to merge 1 commit into
sgl-project:mainfrom
Kh4L:moe-trtllm-bf16-hot-reload-layout

Conversation

@Kh4L

@Kh4L Kh4L commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Motivation

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, however, was gated on is_flashinfer_trtllm_routed() alone. So with --moe-runner-backend flashinfer_trtllm the destination parameter 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", so an RL weight refit on Blackwell hits it by default.

Fixes #27787

Modifications

  1. Gate the inverse on the same flag that gates the transform (use_flashinfer_trtllm_moe), so the two cannot drift apart again.
  2. The restore inverts the data, not just the shape. RL callers batch weights, so one refit is many update RPCs; with a shape-only restore an earlier bucket's re-derive would block expert slots that a later bucket has not written yet a second time.
  3. New repack_weights_after_hot_update hook 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 re-derive is called from a finally on 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):

before: postprocess (4,32,768,64) -> restore no-op   -> copy raises
after:  restore    -> (4,768,2048) -> copy ok -> repack -> (4,32,768,64)

test/registered/unit/layers/quantization/test_flashinfer_trtllm_bf16_moe_reload.py covers 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 the finally re-derive still runs when load_weights raises.

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

  1. Ping Merge Oncalls to start the process. See the PR Merge Process.
  2. Get approvals from CODEOWNERS and other reviewers.
  3. Trigger CI tests with comments or contact authorized users to do so.
    • Common commands include /tag-and-rerun-ci, /tag-run-ci-label, /rerun-failed-ci
  4. After green CI and required approvals, ask Merge Oncalls or people with Write permission to merge the PR.

CI States

Latest PR Test (Base): ✅ Run #31643837888
Latest PR Test (Extra): ❌ Run #31643837810

@github-actions github-actions Bot added the quant LLM Quantization label Aug 5, 2026
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
Kh4L force-pushed the moe-trtllm-bf16-hot-reload-layout branch from 4bae2c9 to 3acb407 Compare August 5, 2026 19:19
@nvpohanh

nvpohanh commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

@Kh4L could you fix the conflicts? thanks!

@b8zhong b8zhong self-assigned this Aug 6, 2026
@Kh4L
Kh4L force-pushed the moe-trtllm-bf16-hot-reload-layout branch from 3acb407 to 14712d8 Compare August 6, 2026 18:57
@nvpohanh

nvpohanh commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

@Kh4L could you rebase/merge with main again? github action was broken

@nvpohanh nvpohanh added the bug Something isn't working label Aug 10, 2026
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
Kh4L force-pushed the moe-trtllm-bf16-hot-reload-layout branch from 14712d8 to 5cfc305 Compare August 12, 2026 21:43
@nvpohanh

Copy link
Copy Markdown
Collaborator

/tag-and-rerun-ci

@nvpohanh

Copy link
Copy Markdown
Collaborator

/rerun-failed-ci

@nvpohanh

Copy link
Copy Markdown
Collaborator

/rerun-failed-ci

@kpham-sgl

Copy link
Copy Markdown
Collaborator

@Kh4L can you help resolve the conflicts. Thanks!

@kaixih

kaixih commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

@Kh4L can you check if we are resolving the same issue in #25692? if yes, can you please check which one is better?

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

Labels

bug Something isn't working quant LLM Quantization run-ci

Projects

None yet

5 participants