Skip to content

[Feature] Triton INT4 per-token-head KV cache quantization - #40835

Merged
tdoublep merged 72 commits into
vllm-project:mainfrom
JartX:refactor/prefill-fastpath-per-token-head-v2
Jun 24, 2026
Merged

tdoublep merged 72 commits into
vllm-project:mainfrom
JartX:refactor/prefill-fastpath-per-token-head-v2

Conversation

@JartX

@JartX JartX commented Apr 24, 2026 •

Copy link
Copy Markdown
Contributor

Based on PR:

#40633

Changed to only INT4_PER_TOKEN_HEAD

INT4_PER_TOKEN_HEAD

Tasks Version Filter n-shot Metric Value Stderr
gsm8k 3 flexible-extract 5 exact_match ↑ 0.912 ± 0.0273
strict-match 5 exact_match ↑ 0.91 ± 0.0273

JartX added 30 commits April 15, 2026 20:38
Signed-off-by: JartX <sagformas@epdcenter.es>
Signed-off-by: JartX <sagformas@epdcenter.es>
Signed-off-by: JartX <sagformas@epdcenter.es>
Signed-off-by: JartX <sagformas@epdcenter.es>
Signed-off-by: JartX <sagformas@epdcenter.es>
Signed-off-by: JartX <sagformas@epdcenter.es>
Signed-off-by: JartX <sagformas@epdcenter.es>
Signed-off-by: JartX <sagformas@epdcenter.es>
Signed-off-by: JartX <sagformas@epdcenter.es>
Signed-off-by: JartX <sagformas@epdcenter.es>
Signed-off-by: JartX <sagformas@epdcenter.es>
Signed-off-by: JartX <sagformas@epdcenter.es>
Signed-off-by: JartX <sagformas@epdcenter.es>
Signed-off-by: JartX <sagformas@epdcenter.es>
Signed-off-by: JartX <sagformas@epdcenter.es>
Signed-off-by: JartX <sagformas@epdcenter.es>
Signed-off-by: JartX <sagformas@epdcenter.es>
Signed-off-by: JartX <sagformas@epdcenter.es>
Signed-off-by: JartX <sagformas@epdcenter.es>
Signed-off-by: JartX <sagformas@epdcenter.es>
Signed-off-by: JartX <sagformas@epdcenter.es>
Signed-off-by: JartX <sagformas@epdcenter.es>
Signed-off-by: JartX <sagformas@epdcenter.es>
Signed-off-by: JartX <sagformas@epdcenter.es>
Signed-off-by: JartX <sagformas@epdcenter.es>
Signed-off-by: JartX <sagformas@epdcenter.es>
Signed-off-by: JartX <sagformas@epdcenter.es>
Signed-off-by: JartX <sagformas@epdcenter.es>
@mergify mergify Bot removed the needs-rebase label Jun 22, 2026
@JartX

JartX commented Jun 22, 2026

Copy link
Copy Markdown
Contributor Author

Hi @tdoublep Could you please check it again? Only the int4 kernel remains, and the rest has been unified.

@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.

Thanks! I have some minor comments to try and minimize diff, but I think we are nearly there.

self._is_per_token_head = kv_cache_spec.kv_quant_mode.is_per_token_head
if self._is_per_token_head:
self._init_reorder_batch_threshold(1, supports_spec_as_decode=False)

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.

Why is this change needed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah, this one's just leftover. It used to back a flash-attn varlen fastpath that needed decodes up front, but that path is gone now that everything goes through unified_attention. Removed it.

Comment on lines +208 to +214
seq_lens_cpu = common_attn_metadata._seq_lens_cpu
qsl_cpu = (
common_attn_metadata.query_start_loc_cpu
if seq_lens_cpu is not None
else None
)

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.

Why is this needed?

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.

Do we really need this subdir - can't we just have the kernel under ops?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah, fair — it didn't really earn its own package. Moved it up to ops/int4_per_token_head.py and dropped the subdir. Done in the latest push.

Comment on lines +450 to +451
K = _cast_kv_tile(K_load, Q, k_scale, KV_QUANT_MODE)
V = _cast_kv_tile(V_load, Q, v_scale, KV_QUANT_MODE)

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.

What is reason for moving this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good eye, nothing actually calls it, it's a leftover from an earlier pass. Removed, helpers file is back to matching main. I left it orphaned during the refactor



@triton.jit
def cast_kv_tile(data, Q, tensor_scale, KV_QUANT_MODE: tl.constexpr):

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.

Maybe I'm missing something but where is this used?

k_scale_cache: torch.Tensor, # [num_blocks, block_size, num_kv_heads] float32
v_scale_cache: torch.Tensor, # [num_blocks, block_size, num_kv_heads] float32
slot_mapping: torch.Tensor, # [num_tokens]
*,

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.

Do we need to add "*"?

Comment on lines +430 to +433
full_block_f32 = 2 * kv_half_bytes // 4
slot_f32 = nkv * padded_hs * dtype_sz // 4
head_f32 = padded_hs * dtype_sz // 4
scale_off_f32 = hs * dtype_sz // 4

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.

Why do we need this diff?

head_f32 = padded_hs * dtype_sz // 4
scale_off_f32 = hs * dtype_sz // 4

# K scales: kv_half=0

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.

ditto

)
self._k_scale_cache.fill_(1.0)

# V scales: kv_half=1, offset by kv_half_bytes

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.

ditto

)
@pytest.mark.parametrize(
"base_model,test_model",
"base_model,test_model,kv_cache_dtype",

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.

How much does this increase the test time?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Switched int4 to Llama-3.2-1B-Instruct so it doesn't drag in the 8B anymore, now it's just one extra dtype on the existing param.I originally moved it up to keep it in line with int2 (which genuinely fell apart at 1B), but int4 should be fine on the small model. If logprobs end up too tight we'll bump just that case back to 8B

- Remove leftover reorder_batch_threshold init (dead since per-token-head
  moved to unified_attention)
- Remove unused seq_lens_cpu / query_start_loc_cpu metadata fields
- Flatten triton_quant_kv/ subdir into ops/int4_per_token_head.py
- Revert _cast_kv_tile move in triton_unified_attention
- Drop unused cast_kv_tile helper (triton_attention_helpers back to main)
- Drop keyword-only * from reshape_and_cache_per_token_head_quant
- Restore original _ensure_scale_caches comments
- Use Llama-3.2-1B for the int4 e2e case (was Meta-Llama-3-8B)

Signed-off-by: JartX <sagformas@epdcenter.es>
@JartX

JartX commented Jun 23, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @tdoublep, really good catches .

Changes applied

reorder_batch_threshold — removed

Dead weight left over from an old flash-attn varlen mixed-batch path that assumed decodes-first ordering. That assumption was invalidated when we moved per-token-head to unified_attention, so the parameter had no effect. Dropped entirely.

seq_lens_cpu / query_start_loc_cpu — removed

Both were being written but never read downstream. Clean removal — no functional impact.

triton_quant_kv/ subdirectory — flattened

Agreed it didn't justify its own package boundary. Moved up to ops/int4_per_token_head.py.

_cast_kv_tile placement — reverted

No good reason to move it. Restored to original location.

cast_kv_tile in helpers file — removed

Leftover from an earlier refactor, nothing was referencing it. Dropped — helpers file now matches main again.

Wildcard import (*) — removed

Comment churn in _ensure_scale_caches — reverted

Kept the original comments as-is.

@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.

Just did a last pass through the tests. Please revert the change of introducing "device" everywhere, it creates huge churn for no reason that I can see.

)

torch.set_default_device(DEVICE_TYPE)
device = "cuda"

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.

why do we need to pass the device around in addition to setting the default device?


cache_dtype: torch.dtype # torch.int8 or FP8_DTYPE
kv_cache_dtype_str: str # "int8_per_token_head" or "fp8_per_token_head"
cache_dtype: torch.dtype # torch.uint8, torch.int8, or FP8_DTYPE

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.

Why does this specific comment change?


set_random_seed(seed)
torch.set_default_device(DEVICE_TYPE)
device = "cuda"

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.

ditto

if is_int4:
deq_atol, deq_rtol = 0.5, 0.5
else:
deq_atol, deq_rtol = 0.1, 0.1

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.

where is deq_atol and deq_rtol used in non-int4 path?


torch.set_default_device(DEVICE_TYPE)
set_random_seed(42)
device = "cuda"

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.

ditto

for i in range(num_tokens):
blk = i // block_size
off = i % block_size

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.

nit: reduce unnecessary diff


torch.set_default_device(DEVICE_TYPE)
set_random_seed(0)
device = "cuda"

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.

ditto

- Drop the device="cuda" / device=device churn (set_default_device
  already handles placement; was also hardcoding cuda)
- Restore original QuantConfig dtype comments
- Move int4 tolerance into the int4 branch (deq_atol/deq_rtol were
  unused in the int8/fp8 path)
- Restore blank line in round-trip loop; recollapse calls to match main

Signed-off-by: JartX <sagformas@epdcenter.es>

@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 Jun 23, 2026
@tdoublep
tdoublep enabled auto-merge (squash) June 23, 2026 19:32
@JartX

JartX commented Jun 23, 2026

Copy link
Copy Markdown
Contributor Author

Thanks again @tdoublep, all good catches

Thanks so much for the thorough reviews. Learned quite a bit going through them — good catches all around.

I have pushed the fixes:

device= keyword — reverted across all tests

Fair call — it was pure churn. set_default_device(DEVICE_TYPE) already handles placement, so the explicit device="cuda" was redundant at best and was hardcoding CUDA on top of the abstraction. Cleaned up everywhere.

QuantConfig dtype comments — reverted

No real justification for the change. Restored originals.

deq_atol / deq_rtol — scoped to INT4 branch

Only the INT4 path actually uses them, so moved the definitions into that branch. The INT8/FP8 path keeps its plain 0.1 — no need to pull in the wider tolerances there.

Blank line / "ditto" nits — fixed

Formatting — recollapsed to match main

A few torch.zeros/randn calls and a parametrize/fixture had gotten split across lines during iteration. Recollapsed them so the formatting is consistent with the rest of the codebase.

Two CI failures from stale call sites after merging main:

- int4_per_token_head kernel called compute_kv_seq_mask without the
  seq_len arg added in main, shifting positionals and dropping
  MAX_MM_RANGES (Triton CompilationError at 238:19).
- test_int8_per_token_head_raw_cache_matches_round_reference called
  triton_reshape_and_cache_flash_per_token_head_quant without the
  required kv_quant_mode arg.

Signed-off-by: JartX <sagformas@epdcenter.es>
auto-merge was automatically disabled June 23, 2026 20:44

Head branch was pushed to by a user without write access

@tdoublep
tdoublep enabled auto-merge (squash) June 24, 2026 06:15
@tdoublep
tdoublep merged commit 70749fd into vllm-project:main Jun 24, 2026
114 checks passed
nkzhenhua pushed a commit to nkzhenhua/vllm that referenced this pull request Jun 24, 2026
…ect#40835)

Signed-off-by: JartX <sagformas@epdcenter.es>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
wincent8 pushed a commit to wincent8/vllm that referenced this pull request Jun 29, 2026
…ect#40835)

Signed-off-by: JartX <sagformas@epdcenter.es>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
@mgoin mgoin mentioned this pull request Jun 30, 2026
29 of 75 tasks
efschu pushed a commit to efschu/shvllm that referenced this pull request Jul 18, 2026
…ect#40835)

Signed-off-by: JartX <sagformas@epdcenter.es>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
michael-gpu pushed a commit to practical-inference/vllm-ampere-extended that referenced this pull request Oct 20, 2026
…ect#40835)

Signed-off-by: JartX <sagformas@epdcenter.es>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation 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.

4 participants