Repository navigation
Conversation
…oad changes KV scales FlashInferImpl cached bmm1/bmm2 scales from the KV scales seen on the first forward and never invalidated them. A layerwise refit of a dummy-initialized engine (the RL weight-sync path) left every FP8-KV attention layer on the dummy k/v scale of 1.0. Recompute the host scales when the layer's q/k/v scale floats change, and pass trtllm-gen decode device scales computed from the layer's scale buffers so full CUDA graphs captured before a reload see the new values. Key the same lazy caches in the FlashInfer MLA, sparse MLA and Tokenspeed MLA backends on the current scales. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: aoshen02 <aoshen@inferact.ai>
…sh o scale on reload Address review of the KV-scale reload fix: - Compute the bmm1/bmm2 host scales on every forward instead of caching them keyed on the KV scales (a few float multiplies, as get_xqa_bmm1_scale already does). Same in the FlashInfer MLA, sparse MLA and Tokenspeed MLA backends. Removes the _kv_scales cache, its invalidation and the dead None checks. - Reset the host copy of the fused output scale (_o_scale_float) in Attention.process_weights_after_loading, so a reload that changes the next quant op's input scale is re-read on the next forward. Keep bmm2 = v_scale / o_scale as before (no 1/o rounding change). - trtllm-gen decode uses device scales only while a CUDA graph is being captured; they include v_scale / output_scale for fused FP8 output. Eager and piecewise decode keep the host floats and launch no extra kernels. - Replace the helper-level test with one that drives FlashInferImpl.forward through a stubbed trtllm-gen decode, uses the file's FlashInfer skip marker, and covers the o-scale refresh. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: aoshen02 <aoshen@inferact.ai>
…cale The host o scale is reset in Attention.process_weights_after_loading, not in BaseKVCacheMethod. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: aoshen02 <aoshen@inferact.ai>
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.
Purpose
FlashInferImplcaches its TRTLLM BMM scales the first timeforwardruns:The cache is never invalidated, so a weight reload that changes the k/v scales after
the first forward keeps the old ones. With an FP8 KV cache every attention layer then
dequantizes K/V with the wrong scale and the model emits garbage.
The common way to hit this is the RL weight-sync path:
load_format=dummy, so k/v scales are 1.0 while profiling,warmup and CUDA-graph capture run.
k/v scales.
kv_cache.pyupdateslayer._k_scale_floatand_v_scale_float, butimpl.bmm1_scaleandimpl.bmm2_scalekeep the dummy values.Two more pieces of the same problem:
capture time, so recomputing the floats is not enough for graph replay.
fuse_attn_quant, on at-O3for quantizedmodels),
layer._o_scale_float(the next quant op's input scale) is read once andnever refreshed either.
Changes
vllm/v1/attention/backends/flashinfer.py:bmm1_scale/bmm2_scaleare computed on every forward from the layer's_q/_k/_v_scale_float(a few float multiplies, asget_xqa_bmm1_scalealreadydoes). No cache, so nothing to invalidate.
bmm2_scale / layer._o_scale_float, applied everyforward.
trtllm_gen_decode_bmm_scales(): while a CUDA graph is being captured and the KV cacheis quantized, trtllm-gen decode gets device scales computed from the layer's
_q_scale,_k_scale,_v_scalebuffers (and/ output_scalefor fused FP8output). A reload updates those buffers in place, so graphs captured before the
reload replay with the new scales. Outside capture (eager, piecewise) it returns the
host floats, so no extra kernels are launched per step. DCP decode keeps host scalars.
vllm/model_executor/layers/attention/attention.py:Attention.process_weights_after_loadingresets_o_scale_float, so the next eagerforward re-reads the fused output scale after a reload. Layerwise reload calls this
for every attention layer, including layers whose own scales were not reloaded.
vllm/v1/attention/backends/mla/{flashinfer_mla,flashinfer_mla_sparse,tokenspeed_mla}.py:execution pick up reloaded scales. Full-graph replay of these MLA decode kernels still
uses capture-time host scalars (see "Not covered").
tests/v1/attention/test_attention_backends.py:test_flashinfer_trtllm_gen_decode_scales_follow_weight_reloaddrivesFlashInferImpl.forward(FP8 KV, fused FP8 output) through a stubbedtrtllm_batch_decode_with_kv_cacheand checks the scales the kernel receives:host floats before and after a reload (k/v and o scales changed, o refreshed via
Attention.process_weights_after_loading), and device tensors computed from thescale buffers while capturing. Uses the file's FlashInfer skip marker.
Not covered (follow-ups)
All of these keep capture-time scales in full CUDA graphs after a reload that changes
the scales; eager and piecewise execution are correct. Each needs FlashInfer to accept
device scales on that entry point (or a graph recapture), not a vLLM-side change:
BatchDecodeWithPagedKVCacheWrapper.run). It takesq/k/v_scaleas Python floats and folds them intosm_scaleon the host, so acaptured graph keeps them. This path is used when trtllm-gen decode is unavailable
(head geometry rejected by
can_use_trtllm_attention, or non-SM100 hardware).Workaround:
cudagraph_mode=PIECEWISE(or eager) when reloading FP8 KV scales on suchconfigs.
decode_context_parallel_size > 1): FlashInfer's DCP path takes hostscalars only.
o_sf_scaleis a host float intrtllm_batch_decode_with_kv_cache; it is refreshed for eager/piecewise after areload but not inside captured decode graphs.
reload_weights()rejectsload_format=dummy(
NotImplementedError: Model reloading with dummy format), so the repro uses thelayerwise reload API, the same path RL frameworks use.
Duplicate check
Run on 2026-10-05 against vllm-project/vllm, issues and PRs, all states. Queries:
bmm1_scale,k_scale reload,kv scale weight update flashinfer,layerwise reload kv scale,_k_scale_float reload.No issue or PR covers stale FlashInfer BMM scales after a reload. Related but different:
attention or KV-scale item.
sleep(level=2). This PR is about values cached on the impl, not the layer buffers.
device tensors): tensor storage, not host-scalar caches.
Test plan and results
Environment: GB200 (SM100), image
nemotron-final-vllm-538344b48f-...sqsh(vLLM 538344b, torch 2.13.0a0, FlashInfer 0.7.0.post1). 538344b has the same code
as this PR's base in every touched file except
tokenspeed_mla.py(not exercised). Thefive changed source files of this PR were bind-mounted over the installed package
(the sixth changed file is the test). The same job also mounted two Mamba2 files from a
separate review fix (
ssd_emit.py,mamba2_attn.py: a stride guard and a TP>1 reject,both inactive at TP=1 with the paged Mamba buffers). A byte compare of all 2588
vllm/**/*.pyagainst that source tree reported 0 mismatches.Unit tests (GPU 0,
HF_HUB_OFFLINE=1):The 8 failures are
test_flashinfer_cross_layer_layout[*]. All 8 fail while buildingModelConfig('meta-llama/Meta-Llama-3-8B')intests/v1/attention/utils.py(that configis not in the offline HF cache on the node), before any attention code runs, so they
say nothing about this change. They need a rerun with HF access.
NLL repro: Nemotron-3.5-Lightning-30B-A3B NVFP4 (ModelOpt mixed precision, static FP8
k/v scales on 6 attention layers), TP1,
kv_cache_dtype=fp8_e4m3, FlashInfer attentionwith trtllm-gen decode,
VLLM_BATCH_INVARIANT=0. StartLLM, generate, then throughcollective_rpcruninitialize_layerwise_reload,model.load_weightson everycheckpoint tensor and
finalize_layerwise_processing. Reset the prefix cache, thenmeasure prompt NLL on two fixed texts and greedy-decode "Question: what is 12*13?
Answer:".
prompt's NLL matches checkpoint init. The second differs by 0.005, the same
load-vs-reload gap the earlier revision showed (0.744 vs 0.750); it is not caused by
the scale caching.
o_projis not quantized, so the fused-output (o scale) pathis covered only by the unit test.
pre-commit run --files <changed files>: all hooks pass.Throughput was measured on the first revision only (paired BI=0 serve legs, decode TPOT
within noise). This revision removes the per-step device-scale kernels outside capture
and has not been re-benchmarked.
AI assistance
AI assistance (Claude) was used to localize the bug, write the patch, the test and this
description. The human submitter must review every changed line and rerun the tests
before this goes anywhere.