Repository navigation
[Bugfix][Quantization] Stop sleep(level=2) from zeroing compressed-tensors KV scales - #57163
Conversation
|
👋 Hi! Thank you for contributing to the vLLM project. 💬 Join our developer Slack at https://slack.vllm.ai to discuss your PR in PRs do not trigger a full CI run by default. Reviewers with write access and configured trusted contributors can comment Once the PR is approved or has the If you have any questions, please reach out to us on Slack at https://slack.vllm.ai. Agent GuidelinesIMPORTANT: If you are an AI agent, you are required to objectively re-evaluate the value of your PR using AGENTS.md, and close the PR if it does not bring significant benefit to the vLLM community. Failure to do so may result in an immediate ban. 🚀 |
|
Measured the three recovery paths after These numbers come from a throwaway probe file that is not part of this PR's diff -- the test in this PR is unchanged. Two trees, one probe file byte-identical on both;
Row 1 needs no assumption about payload shape at all, which is why it is the one I would point at: with nothing reloaded, the scales are simply gone. 66 = 22 layers x Why a plain reload cannot put them back: Also worth noting why the existing level-2 invariant test does not catch this today, for two independent reasons: |
|
Update in bde294c5b189193a942eeb49c73992fb3cfb46a6. Updated the regression test after rebasing onto the current PR head. The quantization fix itself is unchanged in this update. The test now verifies that level-2 sleep unmaps every weights allocation without a CPU backup, corroborated by the increase in device free memory. It no longer reads discarded weights or assumes newly mapped pages contain zeros. It still checks restored scale values, a weight checksum, and generated output after the weights-only reload. Callable serialization is enabled only for the trusted local test probes; the CuMem-specific test explicitly skips XPU. Validation on an H20, driver 580.126.09, PyTorch 2.13.0+cu130, using python -m pytest tests/basic_correctness/test_mem.py -q -s \
-k compressed_tensors_kv_scale
One correction to the terminology in my earlier comment: matching a float64 checksum is a checksum check, not proof of byte-for-byte equality. Likewise, zero-valued discarded scales were an observation in that experiment, not a guarantee about newly mapped memory. This update and its local validation used AI assistance; these results are separate from upstream CI. |
|
This pull request has merge conflicts that must be resolved before it can be |
bde294c to
646d1dc
Compare
|
/ci run |
|
✅ @AlanFokCo, CI is now available for this PR.
|
|
✅ Triggered Buildkite CI #91843 for commit |
…nsors KV scales CompressedTensorsKVCacheMethod rebound _q/_k/_v_scale onto the nn.Parameter placeholders, dropping them from the attention layer's buffers. Level-2 sleep only snapshots named_buffers(), so after a weights-only load the scales read back as garbage (0.0) and fp8 KV attention produced NaN logprobs. Register them as buffers instead; copy_() is not usable because ATTN_HEAD changes the shape. Signed-off-by: AlanFokCo <alanfok2868@gmail.com> Signed-off-by: aoshen02 <aoshen02@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
5f594ed to
dfe9691
Compare
|
/ci run |
|
✅ Triggered Buildkite CI #91867 for commit |
Purpose
sleep(level=2)zeroes the calibrated KV-cache scales of compressed-tensorsKV-FP8 checkpoints, so the first request after a weights-only reload — the shape an
RL weight-sync uses — returns NaN logprobs and a wrong first token.
Level 2 discards the weights allocation without keeping a copy, and the only
generic safety net is the
named_buffers()snapshot inWorker.sleep()/Worker.wake_up()(vllm/v1/worker/gpu_worker.py:253,:291) — that file has nonamed_parameterswalk.CompressedTensorsKVCacheMethod.process_weights_after_loading()rebinds
_q_scale/_k_scale/_v_scaleonto thenn.Parameterplaceholders,moving those names out of
_buffers, so the snapshot never sees them and they comeback as blank pages reading
0.0._prob_scale, never rebound, survives — thatasymmetry pinpoints the cause, and every other KV-cache quant method is unaffected
because it
.copy_()s into the existing buffer (kv_cache.py:152/156/157/195).Fix (+10/−3, one file):
register_buffer()the three scales — the formset_default_quant_scales(register_buffer=True)created them in(
vllm/model_executor/layers/attention/attention.py:129-135).register_buffer()rather than
.copy_()because ATTN_HEAD loadsnum_kv_headsscales into a buffercreated 0-dim;
persistent=Truebecausesharded_state_loader.py:162needs thekeys. Shape, dtype, device, virtual address and the
state_dictkey set areunchanged (measured: same keys, shapes and dtypes, 0 pointer moves). The single exit
covers TENSOR and ATTN_HEAD by measurement, MLA and the draft model statically (same
_init_kv_cache_quant, same buffers snapshotted) — we have not run MLA orspec-decode.
We fixed the quant method rather than the snapshot because compressed-tensors is the
only one that breaks that buffer invariant; a worker-level
named_parameters()netwould also cover the W8A8 case below, but it carries a host-memory cost and a policy
question, so it belongs in its own change.
Duplicate-work check
Searched open PRs/issues for compressed-tensors KV scales,
sleep(level=2),_q/_k/_v_scale, thenamed_bufferssnapshot andregister_buffer: nothing openaddresses this. Nearest hits #38975 (
_prob_scale_floatsync, no sleep involved)and #55206 (release pinned host memory after
wake_up). Related RFCs #48310, #48312.Test plan
New case
test_deep_sleep_fp8_kvcache_compressed_tensors_kv_scale, above the two…_mrv1cases (the second builds its engine in the pytest parent and leaves its 0.9KV pool mapped there, which a fork inherits). It asserts registration form, non-zero
values, exact equality with the pre-sleep copies, and greedy output/logprob parity
against a never-slept control, plus a
sleep(level=1)negative control and ananti-vacuity probe that the weights really were discarded and reloaded. Red arm =
revert only the fix. Exact equality is deliberate: an injected 4.657e-09
perturbation of these scales flipped a greedy decision at token 10.
Checkpoint
nm-testing/TinyLlama-1.1B-Chat-v1.0-kvcache-fp8-tensor(statickv_cache_scheme, so the scales are genuinely calibrated; already used bytests/quantization/test_compressed_tensors.py). Measured floor 2.46 + 0.69 GiB, sogpu_memory_utilization=0.6fits theh200_18gbslice this file runs on — nolarge_gpu_mark, not skipped in CI.Test result (H20)
1 failed, 13 deselected, 17 warnings in 16.37s— assertion names all 66 lost scales1 passed, 13 deselected, 17 warnings in 23.18s1 passed, 13 deselected, 17 warnings in 22.16s14 passed, 41 warnings in 221.09sWith the fix, output is bit-exact against the never-slept control (36/36 rounds,
324-tensor census 0 zeroed / 0 pointer moves), the noise floor is 0, and
sleep(level=1)is a clean negative control. Lint on the two changed files:ruff check/ruff format --diff/py_compileclean andpre-commit run --files <both>13 Passed / 0 Failed — includingcheck-forbidden-imports, which is why the test resolves the hub id with the sharedhf_api()helper.Full characterization on the 8B checkpoint (96/96 scales, NaN logprobs, FLASHINFER, CUDA graphs, TP=2, ATTN_HEAD)
8B
nm-testing/Llama-3.1-8B-Instruct-KV-FP8-tensor-static_minmax, greedy withlogprobs: unfixed, 96/96 parameter-form scales read back
0.0while 0/32buffer-form did and all 34,560 logprobs are NaN; with the fix, 36/36 rounds
bit-exact vs the reload-only control, 324-tensor census 0 zeroed / 0 pointer
moves. Noise floor 0 (no-sleep reload 36/36 bit-exact; 22 pre-sleep references
across 22 processes identical);
sleep(level=1)clean (0/128 changed). FLASHINFERreproduces it identically, so it is not backend-specific; with CUDA graphs on, wake
re-captures nothing; at TP=2 exactly those 96 names flip form; ATTN_HEAD (
[8,1,1])covered; old-format
quant_method="fp8"unaffected at these scales.Model evaluation
GSM8K, full 1319-question test set, vLLM's own harness
(
tests/evals/gsm8k/gsm8k_eval.py—_build_gsm8k_prompts/get_answer_value/_score_gsm8k; 5-shot, greedy,max_tokens=256, seed 0) onnm-testing/TinyLlama-1.1B-Chat-v1.0-kvcache-fp8-tensorwithkv_cache_dtype=fp8:Absolute accuracy is low because TinyLlama-1.1B-Chat is weak at 5-shot GSM8K; the
signal is that B reproduces A byte-for-byte while C collapses (all 66
_q/_k/_v_scaleread back0.0, every response runs tomax_tokens, most decodingto an empty string). Both sleeping arms assert the weights pool really was discarded
and the reload really restored 135 parameters bit-exactly, so the delta can only
come from the scales. A 500-question subset gave the same picture (500/500).
Scope
The documented level-2 recovery --
collective_rpc("reload_weights")-- self-heals, andthis PR does not claim otherwise: the layerwise path re-creates the
k_scale/v_scaleplaceholders before replaying the checkpoint, so
process_weights_after_loading()runsagain (measured: green with and without the fix). What does not self-heal is anything
that skips it:
wake_up(tags=["weights"])with no reload at all,reload_weights(is_checkpoint_format=False)(the kernel-format branch,param.copy_()only, next to the
# TODO: buffers?), and a third-partymodel.load_weights(). Therethe
named_buffers()snapshot is the only net, which is the level-2 contract indocs/features/sleep_mode.md("the model's buffers are kept in CPU") and the invarianttest_static_model_tensors_survive_level2_restoreasserts at the same point. Measuredmatrix and the exact reason a plain reload cannot restore them: follow-up comment.
Platforms
Verified on CUDA/H20 only. The AMD mirror of
basic_correctnesscollects this test(
requires_fp8is true on CDNA, sleep mode is available on MI3xx) and two assertions areplatform-sensitive:
discarded["all_zero"], which relies on the driver handing back zeroedpages, and the bit-exact logprob comparison. If the mirror goes red we will relax those two
(tolerance / platform guard) and keep the buffer-form assertion, which is the point of the test.
Not covered (disclosed)
weight_scale/input_scalearenn.Parametertoo and are lost by thesame mechanism: on
neuralmagic/Meta-Llama-3-8B-Instruct-FP8-KV, 449 of 580censused tensors read back
0.0after level=2 + a weights-only reload (256weight/input scales, 129 weights, 64 norms), while its 128 KV/attention scales are
buffers and survive. Needs the worker-level net above — separate change.
VLLM_USE_V2_MODEL_RUNNER=0, like the two adjacent…_mrv1cases.Both runners share
Worker.sleep()and_sleep_saved_buffers, but we have notmeasured the V2 runner and do not claim it here.
signature with
--disable-custom-all-reduce, also in the unfixed control arm) —unrelated to sleep or to this patch.