Repository navigation
[Kernel][LoRA] Add deterministic split-K=8 LoRA shrink for batch invariance - #59377
Conversation
…riance Under VLLM_BATCH_INVARIANT=1 the LoRA shrink kernel runs with SPLIT_K=1 because split-K normally reduces with float atomics. The opt-in VLLM_LORA_DETERMINISTIC_SPLIT_K=8 keeps K parallelism without atomics: _lora_shrink_kernel stores unscaled FP32 partials for 8 fixed K splits (BLOCK_K=256 for every M) through a STORE_PARTIALS branch in do_shrink_kernel, and a reducer adds the splits in a fixed order and applies scaling once. Partials are allocated per call with torch.empty. The default path (split-K 0, BI on or off) launches exactly as before. S=8 is rejected at import without VLLM_BATCH_INVARIANT=1 or together with VLLM_LORA_ENABLE_DUAL_STREAM=1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Shenglei Fu <117230642+ShengleiFu@users.noreply.github.com>
|
Documentation preview: https://vllm--59377.org.readthedocs.build/en/59377/ |
yewentao256
left a comment
There was a problem hiding this comment.
Thanks for the work!
Please do the following:
- shrink the diff, idealy < 200 LOC
- e2e accuracy using
lm_eval... - e2e benchmark using
vllm bench serve...
When doing e2e test, please attach with raw output log in case some agents fake the data. DO NOT use AI generated Table here
For example:
"lm_eval --model local-completions --model_args "base_url=http://127.0.0.1:9256/v1/completions,model=$MODEL,num_concurrent=1024" --tasks gsm8k --trust_remote_code"
# Now
|Tasks|Version| Filter |n-shot| Metric | |Value | |Stderr|
|-----|------:|----------------|-----:|-----------|---|-----:|---|-----:|
|gsm8k| 3|flexible-extract| 5|exact_match|↑ |0.9212|± |0.0074|
| | |strict-match | 5|exact_match|↑ |0.9212|± |0.0074|
# main
|Tasks|Version| Filter |n-shot| Metric | |Value | |Stderr|
|-----|------:|----------------|-----:|-----------|---|-----:|---|-----:|
|gsm8k| 3|flexible-extract| 5|exact_match|↑ |0.9151|± |0.0077|
| | |strict-match | 5|exact_match|↑ |0.9158|± |0.0076|vllm bench serve --model zai-org/GLM-5.2-FP8 --dataset-name random --host 127.0.0.1 --port 9256 --random-input-len 8192 --random-output-len 1 --request-rate inf --num-prompts 128 --num-warmups 16
# Now
============ Serving Benchmark Result ============
Successful requests: 128
Failed requests: 0
Benchmark duration (s): 3.08
Total input tokens: 1048576
Total generated tokens: 128
Request throughput (req/s): 41.50
Output token throughput (tok/s): 41.50
Peak output token throughput (tok/s): 76.00
Peak concurrent requests: 128.00
Total token throughput (tok/s): 340041.81
---------------Time to First Token----------------
Mean TTFT (ms): 1669.15
Median TTFT (ms): 1669.02
P99 TTFT (ms): 2724.38
-----Time per Output Token (excl. 1st token)------
Mean TPOT (ms): 0.00
Median TPOT (ms): 0.00
P99 TPOT (ms): 0.00
---------------Inter-token Latency----------------
Mean ITL (ms): 0.00
Median ITL (ms): 0.00
P99 ITL (ms): 0.00
==================================================
# main
============ Serving Benchmark Result ============
Successful requests: 128
Failed requests: 0
Benchmark duration (s): 3.23
Total input tokens: 1048576
Total generated tokens: 128
Request throughput (req/s): 39.63
Output token throughput (tok/s): 39.63
Peak output token throughput (tok/s): 50.00
Peak concurrent requests: 128.00
Total token throughput (tok/s): 324671.20
---------------Time to First Token----------------
Mean TTFT (ms): 1668.69
Median TTFT (ms): 1649.00
P99 TTFT (ms): 3126.43
-----Time per Output Token (excl. 1st token)------
Mean TPOT (ms): 0.00
Median TPOT (ms): 0.00
P99 TPOT (ms): 0.00
---------------Inter-token Latency----------------
Mean ITL (ms): 0.00
Median ITL (ms): 0.00
P99 ITL (ms): 0.00
==================================================| VLLM_LORA_DETERMINISTIC_SPLIT_K: int = 0 | ||
| """``8`` runs the batch-invariant LoRA shrink as a fixed split-K=8 two-pass | ||
| kernel (requires ``VLLM_BATCH_INVARIANT=1``); ``0`` (default) disables it.""" |
There was a problem hiding this comment.
Usually we don't want to introduce more envs, could we make it by default?
| "VLLM_LORA_DETERMINISTIC_SPLIT_K": lambda: _parse_lora_deterministic_split_k( | ||
| os.getenv("VLLM_LORA_DETERMINISTIC_SPLIT_K", "0") | ||
| ), |
Drop VLLM_LORA_DETERMINISTIC_SPLIT_K and its import-time guards: the fixed 8-way split-K shrink is now the batch-invariant LoRA shrink path. It skips PDL, so LoRA dual-stream no longer needs to be rejected. Move the tests into test_punica_ops.py as in-process checks against an FP64 reference and across batch sizes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Shenglei Fu <117230642+ShengleiFu@users.noreply.github.com>
|
This pull request has merge conflicts that must be resolved before it can be |
|
Thanks for the review! Addressed:
E2E on H100 80GB, Qwen3-4B + LoRA adapter (nissenj/Qwen3-4B-lora), VLLM_BATCH_INVARIANT=1 vllm serve Qwen3-4B --enable-lora --lora-modules bench-adapter=<adapter> \
--max-loras 1 --max-lora-rank 16 --max-model-len 2048 --max-num-seqs 32 \
--no-enable-prefix-caching --gpu-memory-utilization 0.82 --seed 42 --port 8000
# (--max-model-len 9216 for the 8192-token prefill bench)lm_eval lm_eval --model local-completions --model_args "base_url=http://127.0.0.1:8000/v1/completions,model=bench-adapter,tokenizer=<Qwen3-4B>,num_concurrent=32,max_gen_toks=256" --tasks gsm8k --num_fewshot 5 --seed 42
# Now
|Tasks|Version| Filter |n-shot| Metric | |Value | |Stderr|
|-----|------:|----------------|-----:|-----------|---|-----:|---|-----:|
|gsm8k| 3|flexible-extract| 5|exact_match|↑ |0.7961|± |0.0111|
| | |strict-match | 5|exact_match|↑ |0.7930|± |0.0112|
# main
|Tasks|Version| Filter |n-shot| Metric | |Value | |Stderr|
|-----|------:|----------------|-----:|-----------|---|-----:|---|-----:|
|gsm8k| 3|flexible-extract| 5|exact_match|↑ |0.7930|± |0.0112|
| | |strict-match | 5|exact_match|↑ |0.7923|± |0.0112|vllm bench serve, 128 in / 128 out vllm bench serve --model Qwen3-4B --lora-modules bench-adapter --port 8000 --dataset-name random \
--random-input-len 128 --random-output-len 128 --num-prompts 64 --max-concurrency 8 --ignore-eos --seed 42
# Now
============ Serving Benchmark Result ============
Successful requests: 64
Failed requests: 0
Maximum request concurrency: 8
Benchmark duration (s): 17.16
Total input tokens: 8192
Total generated tokens: 8192
Request throughput (req/s): 3.73
Output token throughput (tok/s): 477.35
Peak output token throughput (tok/s): 515.00
Peak concurrent requests: 16.00
Total token throughput (tok/s): 954.70
---------------Time to First Token----------------
Mean TTFT (ms): 127.25
Median TTFT (ms): 129.14
P99 TTFT (ms): 144.29
-----Time per Output Token (excl. 1st token)------
Mean TPOT (ms): 15.88
Median TPOT (ms): 15.95
P99 TPOT (ms): 15.98
---------------Inter-token Latency----------------
Mean ITL (ms): 15.88
Median ITL (ms): 15.89
P99 ITL (ms): 18.22
==================================================
# main
============ Serving Benchmark Result ============
Successful requests: 64
Failed requests: 0
Maximum request concurrency: 8
Benchmark duration (s): 19.60
Total input tokens: 8192
Total generated tokens: 8192
Request throughput (req/s): 3.27
Output token throughput (tok/s): 418.05
Peak output token throughput (tok/s): 440.00
Peak concurrent requests: 16.00
Total token throughput (tok/s): 836.11
---------------Time to First Token----------------
Mean TTFT (ms): 120.20
Median TTFT (ms): 122.15
P99 TTFT (ms): 138.25
-----Time per Output Token (excl. 1st token)------
Mean TPOT (ms): 18.33
Median TPOT (ms): 18.33
P99 TPOT (ms): 18.38
---------------Inter-token Latency----------------
Mean ITL (ms): 18.33
Median ITL (ms): 18.33
P99 ITL (ms): 19.89
==================================================vllm bench serve, 8192 in / 1 out vllm bench serve --model Qwen3-4B --lora-modules bench-adapter --port 8000 --
--random-input-len 8192 --random-output-len 1 --request-rate inf --num-prompts 128 --seed 42
# Now
============ Serving Benchmark Result ============
Successful requests: 128
Failed requests: 0
Benchmark duration (s): 27.29
Total input tokens: 1048576
Total generated tokens: 128
Request throughput (req/s): 4.69
Output token throughput (tok/s): 4.69
Peak output token throughput (tok/s): 5.00
Peak concurrent requests: 128.00
Total token throughput (tok/s): 38427.01
---------------Time to First Token----------------
Mean TTFT (ms): 13749.14
Median TTFT (ms): 13737.77
P99 TTFT (ms): 26979.98
-----Time per Output Token (excl. 1st token)------
Mean TPOT (ms): 0.00
Median TPOT (ms): 0.00
P99 TPOT (ms): 0.00
---------------Inter-token Latency----------------
Mean ITL (ms): 0.00
Median ITL (ms): 0.00
P99 ITL (ms): 0.00
==================================================
# main
============ Serving Benchmark Result ============
Successful requests: 128
Failed requests: 0
Benchmark duration (s): 27.96
Total input tokens: 1048576
Total generated tokens: 128
Request throughput (req/s): 4.58
Output token throughput (tok/s): 4.58
Peak output token throughput (tok/s): 5.00
Peak concurrent requests: 128.00
Total token throughput (tok/s): 37504.56
---------------Time to First Token----------------
Mean TTFT (ms): 14101.88
Median TTFT (ms): 14097.32
P99 TTFT (ms): 27649.76
-----Time per Output Token (excl. 1st token)------
Mean TPOT (ms): 0.00
Median TPOT (ms): 0.00
P99 TPOT (ms): 0.00
---------------Inter-token Latency----------------
Mean ITL (ms): 0.00
Median ITL (ms): 0.00
P99 ITL (ms): 0.00
==================================================B200, this PR vs main (8fb16ea)lm_eval lm_eval --model local-completions --model_args "base_url=http://127.0.0.1:8000/v1/completions,model=bench-adapter,tokenizer=<Qwen3-4B>,num_concurrent=32,max_gen_toks=256" --tasks gsm8k --num_fewshot 5 --seed 42
# Now
|Tasks|Version| Filter |n-shot| Metric | |Value | |Stderr|
|-----|------:|----------------|-----:|-----------|---|-----:|---|-----:|
|gsm8k| 3|flexible-extract| 5|exact_match|↑ |0.7938|± |0.0111|
| | |strict-match | 5|exact_match|↑ |0.7930|± |0.0112|
# main
|Tasks|Version| Filter |n-shot| Metric | |Value | |Stderr|
|-----|------:|----------------|-----:|-----------|---|-----:|---|-----:|
|gsm8k| 3|flexible-extract| 5|exact_match|↑ |0.7877|± |0.0113|
| | |strict-match | 5|exact_match|↑ |0.7885|± |0.0112|vllm bench serve, 128 in / 128 out vllm bench serve --model Qwen3-4B --lora-modules bench-adapter --port 8000 --dataset-name random \
--random-input-len 128 --random-output-len 128 --num-prompts 64 --max-concurrency 8 --ignore-eos --seed 42
# Now
============ Serving Benchmark Result ============
Successful requests: 64
Failed requests: 0
Maximum request concurrency: 8
Benchmark duration (s): 15.77
Total input tokens: 8192
Total generated tokens: 8192
Request throughput (req/s): 4.06
Output token throughput (tok/s): 519.62
Peak output token throughput (tok/s): 561.00
Peak concurrent requests: 16.00
Total token throughput (tok/s): 1039.24
---------------Time to First Token----------------
Mean TTFT (ms): 159.06
Median TTFT (ms): 155.13
P99 TTFT (ms): 188.50
-----Time per Output Token (excl. 1st token)------
Mean TPOT (ms): 14.25
Median TPOT (ms): 14.25
P99 TPOT (ms): 14.31
---------------Inter-token Latency----------------
Mean ITL (ms): 14.25
Median ITL (ms): 14.25
P99 ITL (ms): 18.66
==================================================
# main
============ Serving Benchmark Result ============
Successful requests: 64
Failed requests: 0
Maximum request concurrency: 8
Benchmark duration (s): 18.50
Total input tokens: 8192
Total generated tokens: 8192
Request throughput (req/s): 3.46
Output token throughput (tok/s): 442.76
Peak output token throughput (tok/s): 472.00
Peak concurrent requests: 16.00
Total token throughput (tok/s): 885.53
---------------Time to First Token----------------
Mean TTFT (ms): 147.17
Median TTFT (ms): 142.94
P99 TTFT (ms): 197.13
-----Time per Output Token (excl. 1st token)------
Mean TPOT (ms): 17.04
Median TPOT (ms): 17.04
P99 TPOT (ms): 17.10
---------------Inter-token Latency----------------
Mean ITL (ms): 17.04
Median ITL (ms): 17.03
P99 ITL (ms): 20.58
==================================================vllm bench serve, 8192 in / 1 out vllm bench serve --model Qwen3-4B --lora-modules bench-adapter --port 8000 --dataset-name random \
--random-input-len 8192 --random-output-len 1 --request-rate inf --num-prompts 128 --seed 42
# Now
============ Serving Benchmark Result ============
Successful requests: 128
Failed requests: 0
Benchmark duration (s): 27.31
Total input tokens: 1048576
Total generated tokens: 128
Request throughput (req/s): 4.69
Output token throughput (tok/s): 4.69
Peak output token throughput (tok/s): 6.00
Peak concurrent requests: 128.00
Total token throughput (tok/s): 38404.04
---------------Time to First Token----------------
Mean TTFT (ms): 13921.23
Median TTFT (ms): 13917.67
P99 TTFT (ms): 27080.74
-----Time per Output Token (excl. 1st token)------
Mean TPOT (ms): 0.00
Median TPOT (ms): 0.00
P99 TPOT (ms): 0.00
---------------Inter-token Latency----------------
Mean ITL (ms): 0.00
Median ITL (ms): 0.00
P99 ITL (ms): 0.00
==================================================
# main
============ Serving Benchmark Result ============
Successful requests: 128
Failed requests: 0
Benchmark duration (s): 27.56
Total input tokens: 1048576
Total generated tokens: 128
Request throughput (req/s): 4.64
Output token throughput (tok/s): 4.64
Peak output token throughput (tok/s): 6.00
Peak concurrent requests: 128.00
Total token throughput (tok/s): 38052.37
---------------Time to First Token----------------
Mean TTFT (ms): 14051.87
Median TTFT (ms): 14045.95
P99 TTFT (ms): 27383.82
-----Time per Output Token (excl. 1st token)------
Mean TPOT (ms): 0.00
Median TPOT (ms): 0.00
P99 TPOT (ms): 0.00
---------------Inter-token Latency----------------
Mean ITL (ms): 0.00
Median ITL (ms): 0.00
P99 ITL (ms): 0.00
================================================== |
…nistic-splitk # Conflicts: # docs/features/batch_invariance.md
yewentao256
left a comment
There was a problem hiding this comment.
LGTM, thanks for the work!
Nit: I think it is better to put batch invariance tests in determinism tests folder
|
✅ @ShengleiFu, CI is now available for this PR.
|
|
Hi @ShengleiFu, the pre-commit checks have failed. Please run: uv pip install pre-commit>=4.5.1
pre-commit install
pre-commit run --all-filesThen, commit the changes and push to your branch. For future commits, |
…rminism Run them in the H100 and B200 batch-invariance steps and trigger those steps on vllm/lora/ops/triton_ops changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Shenglei Fu <117230642+ShengleiFu@users.noreply.github.com>
|
/ci run |
|
❌ This PR is 15 commits behind upstream |
|
/ci run |
|
✅ Triggered Buildkite CI #92763 for commit |
CI selector (shadow): 33 test steps (50 jobs) instead of 54 (76 jobs)Shadow mode: this changes nothing about what CI runs. It shows what the evidence-based selector would pick for this PR, next to today's rules. How it works. Feedback welcome: reply here if it would skip a step this change needs, or runs something unrelated.
Selector would run (33)
Would skip (today's rules run them) (25)
Would add (today's rules do not run them) (4)
AMD mirrors: would skip (23)
AMD mirrors: would add (4)
5 changed files · base |
|
Hi @yewentao256 , it seems the CI failures are not related to this pr, could you help take a look and see if we can merge it, thanks! |
|
/ci retry |
|
✅ Queued 3 failed job(s) for retry in Buildkite CI #92763. |
|
/ci retry |
|
✅ Queued 1 failed job(s) for retry in Buildkite CI #92763. |
Purpose
With
VLLM_BATCH_INVARIANT=1the LoRA shrink kernel runs withSPLIT_K=1, because split-K normally reduces with float atomics. This PR makes the batch-invariant shrink keep K parallelism without atomics:_lora_shrink_kernelstores unscaled FP32 partials for 8 fixed K splits (BLOCK_K=256 for every M) and a small reducer adds them in a fixed order and applies scaling once.This PR:
STORE_PARTIALSbranch todo_shrink_kernel/_lora_shrink_kerneland a fixed-order reducer, used wheneverVLLM_BATCH_INVARIANT=1(no new env var);torch.empty, so each CUDA graph keeps its scratch in its own pool;Batch-invariant LoRA outputs are still invariant across batch composition, but they change relative to the previous
SPLIT_K=1path because the reduction order differs.Not a duplicate: #50278 forces
split_k=1on the default (non-BI) path; #58533 changes PDL overlap on the dual-stream path.Test Plan
pytest tests/lora/test_punica_ops.py -k batch_invariant(FP64 reference over K tail / empty split / 3 slices / no-LoRA rows; bitwise-equal target row across M = 31…129)pytest tests/lora/test_punica_ops.py -k "test_kernels and shrink"(non-BI path)lm_evalgsm8k andvllm bench serve,VLLM_BATCH_INVARIANT=1, Qwen3-4B + LoRA adapter, requests targeting the adapter, this PR vs mainTest Result
pytest tests/lora/test_punica_ops.py -k batch_invariant: 6 passed;-k "test_kernels and shrink": 2136 passed (H100 and B200)lm_evalandvllm bench servelogs (B200 vs main, H100 vs the previous base) are in the comment below.AI assistance (Claude) was used to write this change; the submitter reviewed every line and ran the e2e evaluations.
🤖 Generated with Claude Code