Skip to content

[Perf][Hybrid] 3D-grid tiling of the state-copy Triton kernels - #49436

Merged
tdoublep merged 18 commits into
vllm-project:mainfrom
fuscof-ibm:postprocess_vectorize_tiled
Aug 10, 2026
Merged

tdoublep merged 18 commits into
vllm-project:mainfrom
fuscof-ibm:postprocess_vectorize_tiled

Conversation

@fuscof-ibm

@fuscof-ibm fuscof-ibm commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Purpose

Follow-up to #48110.

#48110 optimized the copy of the states for hybrid models using 64bit load/stores. However the state copy is still underutilizing the HBM memory at small batch and the kernel requires 8B-aligned state tensors.

This PR further improves the performance by adding a 3D grid and lift the hard 8B alignment precondition by using a head/body/tail pattern. Those are the main changes:

  1. Tiles the temporal copy across CTAs. The uint64 body range of each temporal state is partitioned across TEMPORAL_TILES CTAs along a new program_id(2) axis. Grid goes from (num_reqs, total_states) to (num_reqs, total_states, TEMPORAL_TILES). Small-batch runs (reqs=1..16) now fill the SMs instead of leaving them idle.
  2. Extends the 64bit load/store path to the SD conv state. SD Conv copies previously went through a separate byte-wise route; they now share _copy_mamba_state_block and the same fused launch, with tile_idx > 0 early-returning so conv still runs single-CTA (it's ~60–80 KiB per block, compared to 1-4MB of the temporal part).
  3. Drops the hard 8B alignment precondition. _memcpy_u64_tiled now splits the copy into head (≤7 B to align dst to 8 B) / body (u64-vectorized) / tail (≤7 B). The setup-time asserts become one-time warning_once calls — misalignment is a throughput hint, not a correctness failure.
  4. Refactors the shared body into _memcpy_u64_tiled so postprocess_mamba_fused_kernel and precopy_mamba_align_fused_kernel so both kernel can use it and the 64 aligned copies can be tested in isolation.
  5. Tunes TEMPORAL_TILES = 16 based on a sweep over {8, 16, 32} on GB200 with Qwen3.5-9B and Nemotron-3-Super-120B-A12B.

Files touched: vllm/v1/worker/mamba_utils.py, plus two test files.

Net Effect: 3D-grid tiling lifts small-batch kernel bandwidth by 6–7.7× at reqs=1 and pins post-copy throughput at ~72–84% of GB200 HBM3e peak bandwidth across reqs ∈ {1..128} (vs. main's 11–79% swing). End-to-end on Qwen3.5-9B + MTP + prefix caching: +1.1–1.8% output tok/s and −0.7 to −1.6% median TPOT / ITL / E2EL across conc ∈ {16, 32, 64}, with MTP acceptance unchanged and no regressions.

Test Plan

Unit tests (new):

python -m pytest tests/kernels/mamba/test_memcpy_u64_tiled.py -v
python -m pytest tests/kernels/mamba/test_precopy_mamba_align.py -v
  • test_memcpy_u64_tiled.py: byte-identity check of _memcpy_u64_tiled vs. a torch slice copy across copy_size ∈ [0, 4 KiB], all sub-8B src/dst alignment offsets, and NUM_TILES ∈ {1, 16}. Guards head/body/tail correctness and the tile partitioning at the boundaries.
  • test_precopy_mamba_align.py: existing V1-vs-V2 copy-spec equivalence test, now parametrized over TEMPORAL_TILES ∈ {1, 16}.

Integration tests (existing hybrid coverage):

python -m pytest tests/models/language/generation/test_hybrid.py -v

Microbenchmark

Reused the benchmarks/bench_copy_mamba_state_block.py developed for #48110 and extended to support a) multiple models and b) the 3D grid launch. In particular we use 36a924e for 3D grids (as in this PR) and 5cce1bc for 2D grid (as in main).

Sweep num_reqs ∈ {1, 4, 8, 16, 32, 64, 128} on GB200 to confirm tiling improves small-batch throughput without regressing at large batch.

End-to-end

Exact same benchmark as in #48110, but using MRv2.

For the main we use

vllm bench serve against vllm serve with MTP and prefix caching enabled (which exercises the state copies):

VLLM_USE_V2_MODEL_RUNNER=1 \
vllm serve \
    --model Qwen/Qwen3.5-9B \
    -tp 1 -pp 1 -dp 1 \
    --language-model-only \
    --reasoning-parser qwen3 \
    --speculative-config '{"method":"qwen3_next_mtp","num_speculative_tokens":2}' \
    --enable-prefix-caching

Benchmark workload:

    vllm bench serve \
        --dataset-name random \
        --seed 42 \
        --temperature 0 \
        --random-input-len "$INPUT_LEN" \
        --random-output-len "$OUTPUT_LEN" \
        --num-prompts "$NUM_PROMPTS" \
        --request-rate inf \
        --max-concurrency "$conc" \
        --ignore-eos \
        --percentile-metrics "ttft,tpot,itl,e2el" \
        --metric-percentiles "50,90,99"

Workload: --dataset-name random, INPUT_LEN=500, OUTPUT_LEN=5330, NUM_PROMPTS=500.

The workload is chosen to exercise the _copy_mamba_state_block which happens when a block is crossed.
For Qwen3.5-9B the block size is 533. The output length is chosen to be 10x the block size.

Hardware: NVIDIA GB200. Concurrency sweep {16,32,64}. At each concurrency we ran the benchmark 4 times per branch and discarded the 1st run to strip warmup/JIT effects; reported numbers aggregate the remaining 3 runs

Test Result

Unit Tests

Unit and integration tests: all passing

Microbenchmark

Raw results:

microbenchmarks_results.txt

Throughput

Peak = ~8 TB/s for GB200 HBM3e

Qwen3.5-9B

reqs MAIN (GB/s) PR (GB/s) Gain MAIN % peak PR % peak
1 585.7 4519.1 7.72× 7.3% 56.5%
4 2073.5 5683.7 2.74× 25.9% 71.0%
8 3610.2 6265.6 1.74× 45.1% 78.3%
16 5360.5 6521.5 1.22× 67.0% 81.5%
32 6292.4 6659.7 1.06× 78.7% 83.2%
64 5925.7 6713.5 1.13× 74.1% 83.9%
128 6216.5 6748.3 1.09× 77.7% 84.4%

On Qwen3.5-9B the PR delivers > 70% of the peak starting from reqs=4 while main requires reqs=23 to achieve that value.

Nemotron-3-Super-120B-A12B-NVFP4

reqs MAIN (GB/s) PR (GB/s) Gain MAIN % peak PR % peak
1 949.9 5740.9 6.04× 11.9% 71.8%
4 3107.1 6320.0 2.03× 38.8% 79.0%
8 4918.8 6504.7 1.32× 61.5% 81.3%
16 6524.3 6617.2 1.01× 81.6% 82.7%
32 5790.2 6669.8 1.15× 72.4% 83.4%
64 6263.5 6692.7 1.07× 78.3% 83.7%
128 6315.3 6702.9 1.06× 78.9% 83.8%

Unlike main, on Nemotron-3-Super this PR achieves an almost flat curve starting from reqs=1.


Kernel Latency

Qwen3.5-9B — Kernel Latency (μs)

reqs MAIN PR Speedup
1 178.58 23.15 7.71×
4 201.78 73.61 2.74×
8 231.78 133.55 1.74×
16 312.20 256.62 1.22×
32 531.92 502.58 1.06×
64 1129.67 997.11 1.13×
128 2153.65 1983.94 1.09×

This PR is provides lower latencies at all regimes, with larger gains below reqs=16`.

Nemotron-3-Super-120B-A12B-NVFP4 — Kernel Latency (μs)

reqs MAIN PR Speedup
1 360.15 59.59 6.04×
4 440.41 216.52 2.03×
8 556.40 420.74 1.32×
16 838.95 827.17 1.01×
32 1890.63 1641.31 1.15×
64 3495.56 3271.35 1.07×

End-to-end

Raw results:

tiled_16.txt
tiled_32.txt
tiled_64.txt

Values below are mean ± stddev across 3 runs (sample stddev, n=3).
Δ rows compare PR to main; for latency metrics negative is better, for throughput positive is better.

Metric Branch conc=16 conc=32 conc=64
Output tok/s main 6219.19 ± 10.28 9744.67 ± 5.37 13848.81 ± 37.01
Output tok/s PR 6331.49 ± 9.81 9864.17 ± 10.30 14004.27 ± 20.58
Δ throughput +1.81 % +1.23 % +1.12 %
Median TPOT (ms) main 2.407 ± 0.006 3.027 ± 0.006 4.113 ± 0.025
Median TPOT (ms) PR 2.373 ± 0.006 2.987 ± 0.006 4.063 ± 0.032
Δ median TPOT −1.39 % −1.32 % −1.22 %
Median ITL (ms) main 7.003 ± 0.006 8.623 ± 0.012 11.773 ± 0.032
Median ITL (ms) PR 6.890 ± 0.010 8.517 ± 0.015 11.693 ± 0.021
Δ median ITL −1.62 % −1.24 % −0.68 %
Mean E2EL (ms) main 13224.01 ± 15.90 16889.25 ± 7.79 23644.57 ± 32.34
Mean E2EL (ms) PR 13010.79 ± 8.04 16680.60 ± 25.10 23416.33 ± 40.85
Δ mean E2EL −1.61 % −1.24 % −0.97 %
Acceptance rate (%) main 98.68 ± 0.05 98.61 ± 0.03 98.60 ± 0.09
Acceptance rate (%) PR 98.67 ± 0.03 98.51 ± 0.08 98.62 ± 0.05

Summary

  • Consistent ~1.1–1.8% throughput improvement across all concurrency levels, well above the ~0.1–0.3% run-to-run noise (visible from the stddevs).
  • Latency reductions track throughput gains: TPOT, ITL, and E2EL all improve by ~0.7–1.6%, monotonically decreasing as concurrency rises.
  • Gain shrinks as concurrency grows: at conc=16 tiling helps more when the 2D grid is smaller
  • Speculative decoding is unaffected: acceptance rate stays ~98.5–98.7% and acceptance length stays 2.97 on both branches — this is purely a postprocess optimization with no semantic effect on drafts.
  • No regressions on any metric at any concurrency level.

Duplicate-work check

Per AGENTS.md:

gh pr list --repo vllm-project/vllm --state open --search "postprocess_mamba"
gh pr list --repo vllm-project/vllm --state open --search "mamba tiled"
gh pr list --repo vllm-project/vllm --state open --search "_copy_mamba_state_block"

This PR is a strict follow-on to the earlier postprocess_mamba uint64 vectorization PR — same file, same kernel, but pushes past the single-CTA-per-copy ceiling and drops the alignment precondition. Not duplicating any open work.
Tested on MRv2.


Essential Elements of an Effective PR Description Checklist
  • The purpose of the PR, such as "Fix some issue (link existing issues this PR will resolve)".
  • The test plan, such as providing test command.
  • The test results, such as pasting the results comparison before and after, or e2e results
  • (Optional) The necessary documentation update, such as updating supported_models.md and examples for a new model.

@mergify mergify Bot added the v1 label Jul 22, 2026
@fuscof-ibm
fuscof-ibm force-pushed the postprocess_vectorize_tiled branch from 25f2c3e to c4f8598 Compare July 22, 2026 16:06
@fuscof-ibm
fuscof-ibm marked this pull request as ready for review July 23, 2026 08:00

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

Comment thread vllm/v1/worker/mamba_utils.py
Comment thread vllm/v1/worker/mamba_utils.py
Comment thread vllm/v1/worker/mamba_utils.py
Comment thread vllm/v1/worker/mamba_utils.py
@ZJY0516

ZJY0516 commented Jul 26, 2026

Copy link
Copy Markdown
Member

@claude review

Comment thread vllm/v1/worker/mamba_utils.py Outdated
Comment thread vllm/v1/worker/mamba_utils.py
Comment thread tests/kernels/mamba/test_memcpy_u64_tiled.py
@fuscof-ibm
fuscof-ibm force-pushed the postprocess_vectorize_tiled branch from 8444ec5 to 2d335a2 Compare July 27, 2026 12:29
@mergify

mergify Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

This pull request has merge conflicts that must be resolved before it can be
merged. Please rebase the PR, @fuscof-ibm.

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

Signed-off-by: Francesco Fusco <ffu@zurich.ibm.com>
Signed-off-by: Francesco Fusco <ffu@zurich.ibm.com>
Signed-off-by: Francesco Fusco <ffu@zurich.ibm.com>
Signed-off-by: Francesco Fusco <ffu@zurich.ibm.com>
Signed-off-by: Francesco Fusco <ffu@zurich.ibm.com>
Byte-identity check against a torch slice copy across copy_size (0 to
4 MiB), src/dst sub-8B alignment offsets, and NUM_TILES ∈ {1..16}.
Guards the head/body/tail memcpy and tile partitioning shared by the
fused mamba state-copy kernels.

Signed-off-by: Francesco Fusco <ffu@zurich.ibm.com>
Sweep temporal_tiles ∈ {1, 2, 4, 8, 16, 32} in the V1-copy-spec
equivalence test to guard the u64 body tile partitioning against
off-by-one at the tile boundary, missing head/tail owner, or duplicate
stores when multiple tiles run.

Signed-off-by: Francesco Fusco <ffu@zurich.ibm.com>
We sweep the number of tiles from 8 to 32 and 16 is the sweet spot for
H100/GB200 on Qwen3.5-9B and Nemotron-3-Super-120B-A12B

Signed-off-by: Francesco Fusco <ffu@zurich.ibm.com>
Signed-off-by: Francesco Fusco <ffu@zurich.ibm.com>
Signed-off-by: Francesco Fusco <ffu@zurich.ibm.com>
Signed-off-by: Francesco Fusco <ffu@zurich.ibm.com>
Signed-off-by: Francesco Fusco <ffu@zurich.ibm.com>
Signed-off-by: Francesco Fusco <ffu@zurich.ibm.com>
@fuscof-ibm
fuscof-ibm force-pushed the postprocess_vectorize_tiled branch from b4d2eec to 2475ea7 Compare August 4, 2026 18:19
Parametrize test_memcpy_u64_tiled.py on COPY_BLOCK_SIZE ∈ {8, 1024}.

At the production value 1024 the existing copy_size cases (≤ 4 KiB)
collapse to tile 0 under NUM_TILES=16 because per_tile_u64 rounds up to
COPY_BLOCK_SIZE; tiles 1–15 iterate zero times, so the tile-boundary
math is never exercised end-to-end. Adding COPY_BLOCK_SIZE=8 makes the
same copy sizes span all 16 tiles with several inner iterations each,
without inflating tensor allocations (128 KB would have been necessary
with a COPY_BLOCK_SIZE of 1024)
The partitioning arithmetic is COPY_BLOCK_SIZE-agnostic, so testing at
8 proves it for 1024.

Signed-off-by: Francesco Fusco <ffu@zurich.ibm.com>
Co-authored-by: Claude <noreply@anthropic.com>
@fuscof-ibm
fuscof-ibm force-pushed the postprocess_vectorize_tiled branch from 2475ea7 to 9ebcaa2 Compare August 4, 2026 18:37
Signed-off-by: Francesco Fusco <ffu@zurich.ibm.com>
@tdoublep
tdoublep self-requested a review August 4, 2026 20:12

@tdoublep tdoublep left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@tdoublep tdoublep added the ready ONLY add when PR is ready to merge/full CI is needed label Aug 4, 2026
@tdoublep
tdoublep enabled auto-merge (squash) August 4, 2026 20:13
@mergify mergify Bot removed the needs-rebase label Aug 4, 2026
Reduces tests/kernels/mamba/test_memcpy_u64_tiled.py from 180 to 120
parametrized cases (~15s GB200 wallclock saved) by dropping redundant
crossproduct entries -- under the 40-min mamba kernels CI budget.

- Collapse NUM_TILES x COPY_BLOCK_SIZE 2x2 into 3 tuples via joint
  parametrize: drop (NUM_TILES=1, COPY_BLOCK_SIZE=8). At single-tile
  the block size is unobservable -- tile partitioning collapses to the
  full range on tile 0, so (1, 8) is byte-identical to (1, 1024).
  Kept: (1, 1024), (_TEMPORAL_TILES, 1024), (_TEMPORAL_TILES, 8).

- Drop copy_size=17. copy_size=15 already exercises the tail-mask
  path with 7 tail bytes; the mask is length-agnostic so a 1-byte tail
  hits the same branch. The (7,7) shared-misalignment pair still covers
  the head_bytes=1 corner with a non-empty body.

Signed-off-by: Francesco Fusco <ffu@zurich.ibm.com>
…test

Halves test_precopy_matches_v1_copy_specs from 144 to 72 parametrized
cases in tests/kernels/mamba/test_precopy_mamba_align.py (~16s on a
GB200).

The inner-memcpy boundary math is still exercised directly in
test_memcpy_u64_tiled.py, which parametrizes over both COPY_BLOCK_SIZE
values on the device function.

Signed-off-by: Francesco Fusco <ffu@zurich.ibm.com>
auto-merge was automatically disabled August 5, 2026 15:37

Head branch was pushed to by a user without write access

@tdoublep

Copy link
Copy Markdown
Member

/ci retry

@github-actions

Copy link
Copy Markdown

✅ No earlier CI build exists for this PR. Use /ci run first.

@tdoublep

Copy link
Copy Markdown
Member

/ci run

@github-actions

Copy link
Copy Markdown

✅ Triggered Buildkite CI #83111 for commit 6748a62bea87.

@fuscof-ibm

Copy link
Copy Markdown
Contributor Author

/ci run

1 similar comment
@tdoublep

Copy link
Copy Markdown
Member

/ci run

@github-actions

Copy link
Copy Markdown

✅ Triggered Buildkite CI #83145 for commit fee7490c6f1e.

@tdoublep
tdoublep merged commit fac808b into vllm-project:main Aug 10, 2026
87 checks passed
zyp2014 pushed a commit to zyp2014/vllm that referenced this pull request Aug 21, 2026
…project#49436)

Signed-off-by: Francesco Fusco <ffu@zurich.ibm.com>
Co-authored-by: Claude <noreply@anthropic.com>
Aiakos1818 pushed a commit to Aiakos1818/vllm that referenced this pull request Sep 14, 2026
Ports of upstream PRs that apply to this fork's Qwen3.8-27B GDN + MTP3
stack. Each was rebased by hand onto v0.27.1 + the sm75/qwen3.8 base.

- vllm-project#51812 [Bugfix] Align Qwen GDN gates with speculative tokens (v0.28):
  gather `a`/`b` with the same `spec_token_indx` as mixed_qkv before the
  fused recurrent update, so a mixed batch (non-spec tokens ahead of spec
  tokens) no longer feeds another token's gates to the kernel.
- vllm-project#56196 [Bugfix] Mamba: store a short prefill chunk's conv state in its
  own block: with prefix caching a chunk shorter than the conv state wrote
  its new state through the pointer it read the initial state from,
  overwriting the shared prefix block. The destination block is now
  resolved once from `block_idx_last_scheduled_token` and used in every
  branch. Ported with its regression test.
- vllm-project#49436 [Perf] 3D-grid tiling of the state-copy Triton kernels (v0.28):
  new `_memcpy_u64_tiled` head/body/tail device function partitions the
  temporal state's u64 body across `_TEMPORAL_TILES=16` CTAs (small-batch
  HBM fill), lifts the 8B-alignment assert to a warning, and collapses to
  a single-CTA copy for conv states. Ported with its tests.

Not ported: vllm-project#52789 (internal prefill checkpoints for Mamba prefix
caching). It is Kimi-K3 KDA only — `num_prefill_checkpoint_blocks` is set
solely by `vllm/models/kimi_k3/nvidia/kda.py` under the flashkda backend,
so the scheduler/manager infrastructure is inert for GDN models. The
analogous capability here is the durable-anchor work in the kv commit.

Tests: 149 CPU (host-tier/spill/prefix/mamba-align/api) pass; kernel
suites pass on the 2080Ti (memcpy 120, precopy 75, causal_conv1d 156; the
8 float64-reference `varlen` failures are pre-existing on this machine and
fail identically without the port). The vllm-project#56196 test fails on the unpatched
kernel and passes with it.

E2E on the 9.6e9 profile (real NVMe SSD tier): S resident 384,704 ->
V0 deep revert cached 350,400 -> T evict -> R SSD restore cached 382,400
(sha db8b8e836881534b, same as the pre-port baseline) -> V2 post-restore
revert cached 350,400; stores=3 restores=2.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready ONLY add when PR is ready to merge/full CI is needed v1

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants