[LoRA] Fix experimental fast-path multi-adapter correctness + flashinfer 0.6.12 compatibility - #28091
Merged
Merged
Conversation
jybsuper
requested review from
BBuf,
DarkSharpness,
Fridge003,
HydraQYH,
Ying1123,
celve,
lifuhuang,
yuan-luo and
yushengsu-thu
as code owners
June 12, 2026 21:33
Contributor
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
jybsuper
force-pushed
the
lora-main-fixes
branch
from
June 17, 2026 03:38
fb17e3b to
1de241a
Compare
jybsuper
requested review from
Edwardf0t1,
HaiShaw,
ch-wan,
ispobock and
merrymercy
as code owners
June 18, 2026 01:45
Collaborator
Author
|
/rerun-failed-ci |
jybsuper
force-pushed
the
lora-main-fixes
branch
2 times, most recently
from
June 18, 2026 06:33
956c4c4 to
34f58fe
Compare
Collaborator
Author
|
/rerun-failed-ci |
… 0.6.12 flashinfer 0.6.12 (the version main now pins) changed the internal quantization/routing APIs the forked trtllm fused-moe sources were built against, breaking the JIT compile: - get_sf_out_offset_128x4/_8x4 dropped the std::optional batch index; pass 0 instead of std::nullopt (identical offsets on both versions, the batch term was value_or(0) * stride). - Routing::run gained an int32_t* expertIds param (pre-computed routing); 3-way-merged the upstream 0.6.11.post1 -> 0.6.12 diff into the forked runner.h / trtllm_fused_moe_runner.cu / trtllm_fused_moe_kernel_launcher.cu (all merge conflicts were reformat-only on our side) and pass expertIds=nullptr in the lora packed-routing path to keep its exact pre-0.6.12 behavior (mPtrScores=nullptr + mPtrTopKPacked). - Adopted upstream's per-token-scaling condition (usePerTokenScalingGemm2 now requires E2m1 activations) and the FP4 launcher RoutingInputMode / topk_ids refactor. dev_kernel.cu / DevKernel.h were unchanged upstream and keep our fork as-is.
…ngle resident adapter slot With --max-loras-per-batch >= 2, the SGLANG_OPT_LORA_CUBLAS* dense fast paths produced gibberish: they are single-adapter kernels that run the whole batch against one weight slot (weights[0], or weight_indices[0] + scalings[0]), with no per-token adapter dispatch. With one resident slot (mlpb=1, the validated configuration) that is always correct; with two resident adapters, requests routed to the slot-1 adapter silently compute their delta with slot-0 weights (and mixed-rank pairs corrupt further via the slot-0 scaling), even when a request only references one adapter. Gate all 7 cuBLAS sites (sgemm_lora_a/b, gate_up_lora_b, qkv_lora_b, and the 3 kv_b_lora_absorbed paths) on weights.shape[0] == 1 - the number of resident buffer slots, a static server-level property, so the branch is also cuda-graph safe (no batch-composition-dependent control flow). With mlpb=1 the validated fast path is unchanged; with mlpb>=2 every dense LoRA op falls through to the per-segment Triton kernels, which read weight_indices/lora_ranks/scalings per token segment and handle mixed-rank multi-adapter batches correctly.
…fe by hoisting side-stream allocations Root cause of the multi-adapter (--max-loras-per-batch >= 2) '!!!!' decode collapse on TP/EP > 1 with the two-stream overlap: the gate_up LoRA chain allocated its routing tensors (virtual topk ids, align outputs, sanitize output) and the shrink intermediate INSIDE the side-stream context while the decode graph was being captured. During capture the caching allocator's cross-stream tracking is inactive, so those side-stream blocks can be pool-reused by later allocations on other streams with no ordering edge in the graph, corrupting replays. With a single resident adapter the racing values are constant batch-to-batch and the corruption is invisible, which is why mlpb=1 has always been stable. Evidence chain: kernels + EP logic + side-stream placement + graph replay all pass composition fuzz in isolation; replacing the in-op lora_ready event with a hard pre-op join does NOT fix the server (not an event/timing race); PDL off does not fix it; moving the identical chain onto the main stream DOES fix it. Only the allocation stream differs. Fix: add a 'routing' stage to merged_experts_fused_moe_lora_add that pre-warms the routing cache on the caller's (main) stream before the side-stream fork (routing needs only topk_ids + token_lora_mapping, both ready pre-fork), and pre-allocate the shrink intermediate on main, so the side-stream block launches kernels only. Applied to both the FP8 and FP4 two-stream entries. Extends the existing SGLANG_OPT_LORA_OVERLAP_MAIN_ALLOC lesson (dense shrink outputs) to the MoE chain. The two-stream overlap runs fully for multi-adapter serving. Validated on GB300: TP4/EP4, max-loras-per-batch=4, 4 real adapters (mixed ranks 16/32), two-stream ON: 12/12 sequential + 12/12 mixed-concurrent behavioral checks, deterministic; 4-adapter mixed throughput 2551/4585/ 7865/12603 tok/s at bs16/32/64/128 = ~77% of the no-LoRA ceiling (vs 67-71% with the overlap disabled).
…RA dispatch A main-side refactor renamed the routed-MoE topk packer to fused_pack_topk and moved it to jit_kernel/trtllm_lora_temp/topk_pack.py, dropping the old _pack_topk_for_flashinfer_routed name from flashinfer_trtllm. The experimental LoRA dispatch sites (sgl_fp8_moe / moe_overlap / lora_dispatch) still imported the old name -> ImportError at the first MoE-LoRA forward (all quants; the kimi fp4 path just hit it first). Import fused_pack_topk from its canonical location in the three consumers rather than re-exporting the old name through flashinfer_trtllm, where it is unused and would be stripped by the ruff F401 --fix pre-commit hook.
… cuda-graph capture The shared-expert add overlaps the routed MoE by doing output.add_(shared_output) on the producer (main) stream, ordered against the alt-stream routed output via base_ready/add_done events. That cross-stream choreography is not cuda-graph capture-safe -- on replay the add races the routed write and corrupts output (qwen3.5-FP8 + --enable-lora + cuda graph -> garbage decode). Under capture, fall back to the serial caller-side add (qwen2_moe reclaims the staging). Eager keeps the overlap.
jybsuper
force-pushed
the
lora-main-fixes
branch
from
June 19, 2026 03:45
d96ad14 to
d9a8cfd
Compare
yushengsu-thu
approved these changes
Jun 19, 2026
yushengsu-thu
enabled auto-merge (squash)
June 19, 2026 22:27
Fridge003
approved these changes
Jun 19, 2026
Fridge003
disabled auto-merge
June 19, 2026 23:20
sekfung
pushed a commit
to sekfung/sglang
that referenced
this pull request
Jun 20, 2026
…fer 0.6.12 compatibility (sgl-project#28091)
29 tasks
Chronostasys
pushed a commit
to MindLab-Research/sglang
that referenced
this pull request
Aug 24, 2026
…fer 0.6.12 compatibility (sgl-project#28091)
jakki-amd
pushed a commit
to jakki-amd/sglang
that referenced
this pull request
Sep 9, 2026
…fer 0.6.12 compatibility (sgl-project#28091)
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
Follow-up fixes for the experimental fast LoRA path introduced in #27329 (
--moe-runner-backend experimental_sgl_trtllm+SGLANG_EXPERIMENTAL_LORA_OPTI=1).Three problems are fixed:
get_sf_out_offset_128x4/_8x4signature change, newexpertIdsparameter in the routing runner). This blocked the experimental path on images shipping flashinfer ≥ 0.6.12 and on sm103 (GB300).--max-loras-per-batch ≥ 2with adapters of different ranks → gibberish). TheSGLANG_OPT_LORA_CUBLAS=1fast paths unconditionally useweights[0]/weight_indices[0]/scalings[0], i.e. slot-0's weights and scaling get applied to every request in the batch once the cuBLAS size thresholds engage. Short probes pass (below thresholds), real traffic garbles — and which adapter "wins" depends on slot assignment, so the failure is intermittent.!!!!). Root cause: the gate_up LoRA chain allocated its routing tensors and shrink intermediate inside the side-stream context during graph capture. The caching allocator's cross-stream tracking is disabled while capturing, so those pool blocks get reused by later allocations on other streams with no graph-edge ordering → replay corruption. Benign with one resident adapter (the corrupted buffers hold constants), catastrophic with several.Modifications
One commit per fix (+ a formatting-only commit):
fix(lora-experimental): rebase kimi NVFP4 JIT kernels onto flashinfer 0.6.123-way merge of the upstream 0.6.11.post1 → 0.6.12 diff into the forked
trtllm_fused_moe_kernel_launcher.cu/trtllm_fused_moe_runner.cu/runner.h(newexpertIdsrouting parameter, passed asnullptrin the LoRA packed-routing call);get_sf_out_offset_*(std::nullopt, …)→(/*batchIdx=*/0, …)at 6 sites;supported_major_versions = [10, 12]so sm103 compiles.fix(lora-experimental): restrict cuBLAS dense-LoRA fast paths to a single resident adapter slotAll 7 cuBLAS dispatch sites (
sgemm_lora_a,sgemm_lora_b,qkv_lora_b,gate_up_lora_b,kv_b_lora_absorbed×3) additionally requireweights.shape[0] == 1. With multiple resident slots the per-segment Triton kernels (which indexweight_indices/lora_ranks/scalingsper request and are multi-slot correct) are used instead. Single-adapter deployments keep the cuBLAS fast path unchanged.fix(lora-experimental): make the MoE two-stream overlap multi-slot safe by hoisting side-stream allocationsNew
stage="routing"prewarm in the virtual-experts op: the routing buffers for both the shrink and expand stages are computed (and therefore allocated) on the main stream before the side-stream fork, and the gate_up shrink intermediate is preallocated on main and passed in asintermediate_buffer. The side stream now performs zero allocations under capture; the two-stream overlap stays fully enabled for multi-slot batches.style(lora-experimental): apply pre-commit formatting(formatting only; verified the.cufiles are byte-identical after whitespace stripping and the.pyfiles have identical ASTs).Accuracy Tests
Hardware: 4×GB300 (sm103), image
lmsysorg/sglang:dev-cu13(flashinfer 0.6.12). Model: Qwen3.5-35B-A3B-FP8. Four behavioral test adapters (ranks 16 and 32, target modules incl. mambain_proj_qkvz/out_proj, expert weights, andlm_head) that deterministically prefix every output word with the adapter name — any cross-adapter contamination or corruption is immediately visible.With
--max-loras-per-batch 4, all four adapters resident, TP4/EP4:!!!!The flashinfer 0.6.12 rebase was additionally compile-verified on sm103 and exercised end-to-end by every run below.
Speed Tests and Profiling
Same hardware. All numbers are e2e decode throughput (tok/s) at batch size 16/32/64/128, input=output=2048 random tokens,
ignore_eos, greedy, one batch per measurement;%= of the no-LoRA base for the same topology measured on the same machine (default MoE runner backend, which resolves toflashinfer_trtllmon sm100+ for this model). Repeat-run spread ≤3% worst case, <1% typical. Commit labels: pre-fixes = parent of this series' correctness fixes (5f84d344), cuBLAS gate =752b4690, full series =82de4dcc.No-LoRA bases (default backend):
Single resident adapter (
--max-loras-per-batch 1), TP4/EP4 — fix overhead is zero (the cuBLAS gate keeps the single-slot fast path; warm-cache runs, gate/full = mean of 2 runs):Four resident adapters (
--max-loras-per-batch 4), mixed 4-adapter traffic:!!!!) — n/aTakeaways: the correctness fixes have no measurable overhead in any configuration (the routing hoist is a consistent +1–6% where it differs at all, since the side stream no longer stalls on allocation/routing), so they are unconditional — no
ep>1/mlpb>1gating. Multi-adapter mixed traffic retains 70–79% of the no-LoRA base.Reproduction commands
No-LoRA base (per topology,
<TP_ARGS>∈--tp 4 --ep 4 --enable-flashinfer-allreduce-fusion|--tp 4 --enable-flashinfer-allreduce-fusion|--tp 1):LoRA runs — same command, plus the experimental envs and LoRA flags (
<MLPB>= 1 or 4):Benchmark client (
mode=nonefor the base rows,singlefor mlpb=1 rows — all requests on one adapter,mix4for mlpb=4 rows — round-robin over the four adapters):e2e_tput = bs*2048 / wallover one batch per batch size.Checklist
CI States
Latest PR Test (Base): ✅ Run #27804052947
Latest PR Test (Extra): ❌ Run #27804052809