Skip to content

Commit 3cf0b07

Browse files
committed
fix kernel
1 parent f654c54 commit 3cf0b07

3 files changed

Lines changed: 170 additions & 85 deletions

File tree

lmdeploy/pytorch/kernels/cuda/v4_compressor.py

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,7 @@ def _score_kv_kernel(
130130
is_decoding: tl.constexpr,
131131
BLOCK_D: tl.constexpr,
132132
):
133-
"""Compute compressed kv via softmax-weighted sum, write back to
134-
kv[:head_dim].
133+
"""Compute compressed kv via softmax-weighted sum, write to compressed_kv.
135134
136135
Compression points are at abs_pos = n*ratio - 1 (0-indexed).
137136
For ratio=4, this means kv positions 3, 7, 11, 15, ...
@@ -149,15 +148,15 @@ def _score_kv_kernel(
149148
Manual softmax (Triton tl.cat only supports 1D).
150149
no-overlap: single [ratio, BLOCK_D] block, tl.softmax.
151150
- Replaces current token's slot in the loaded block via tl.where.
152-
- Write-back: kv[seq_start, :head_dim] (masked by emit).
151+
- Write-back: compressed_kv[seq_start] (masked by emit).
153152
154153
== PREFILL PATH (is_decoding=False) ==
155154
Grid: (num_groups, B). One CTA per compression point per batch.
156155
- Compression points aligned to abs_pos:
157156
first_compress = ceil((start_pos+1) / ratio) * ratio - 1
158157
= ((start_pos + ratio) // ratio) * ratio - 1
159158
- Each CTA handles compress_abs = first_compress + group_id * ratio.
160-
- Write-back position: kv[seq_start + (compress_abs - start_pos), :head_dim].
159+
- Write-back position: compressed_kv[seq_start + (compress_abs - start_pos)].
161160
162161
Prefill overlap data windows per compression point at compress_abs:
163162
prev window: abs_pos in [compress_abs - 2*ratio + 1, compress_abs - ratio]
@@ -220,19 +219,19 @@ def _score_kv_kernel(
220219
pos_in_ratio = start_pos % ratio
221220
if overlap:
222221
cur_kv = tl.load(
223-
kv_ptr + seq_start * kv_stride_s + (head_dim + d_off + offs_d) * kv_stride_d).to(
222+
kv_ptr + seq_start * kv_stride_s + (head_dim + offs_d) * kv_stride_d).to(
224223
tl.float32)
225224
cur_score = tl.load(
226-
score_ptr + seq_start * score_stride_s + (head_dim + d_off + offs_d) *
225+
score_ptr + seq_start * score_stride_s + (head_dim + offs_d) *
227226
score_stride_d).to(tl.float32)
228227
cur_ape = tl.load(
229-
ape_ptr + pos_in_ratio * ape_stride_r + (head_dim + d_off + offs_d) *
228+
ape_ptr + pos_in_ratio * ape_stride_r + (head_dim + offs_d) *
230229
ape_stride_d).to(tl.float32)
231230
else:
232231
cur_kv = tl.load(kv_ptr + seq_start * kv_stride_s + offs_d * kv_stride_d).to(tl.float32)
233232
cur_score = tl.load(score_ptr + seq_start * score_stride_s + offs_d * score_stride_d).to(tl.float32)
234233
cur_ape = tl.load(
235-
ape_ptr + pos_in_ratio * ape_stride_r + (d_off + offs_d) * ape_stride_d).to(
234+
ape_ptr + pos_in_ratio * ape_stride_r + offs_d * ape_stride_d).to(
236235
tl.float32)
237236
cur_score = cur_score + cur_ape
238237

@@ -252,11 +251,11 @@ def _score_kv_kernel(
252251
curr_row_ids = (head + ratio + row_ids) % cap
253252
curr_kv = tl.load(
254253
kv_state_ptr + state_id * kvc_stride_n + curr_row_ids[:, None] * kvc_stride_r +
255-
(head_dim + d_off + offs_d[None, :]) * kvc_stride_d).to(tl.float32)
254+
(head_dim + offs_d[None, :]) * kvc_stride_d).to(tl.float32)
256255
curr_score = tl.load(
257256
score_state_ptr + state_id * scorec_stride_n +
258257
curr_row_ids[:, None] * scorec_stride_r +
259-
(head_dim + d_off + offs_d[None, :]) * scorec_stride_d).to(tl.float32)
258+
(head_dim + offs_d[None, :]) * scorec_stride_d).to(tl.float32)
260259

261260
# replace current token's slot in curr window
262261
row_mask = row_ids == pos_in_ratio
@@ -341,13 +340,13 @@ def _score_kv_kernel(
341340
_curr_abs_pos = curr_abs_base + row_ids
342341
curr_kv = tl.load(
343342
kv_ptr + (_curr_kv_pos + row_ids[:, None]) * kv_stride_s +
344-
(head_dim + d_off + offs_d[None, :]) * kv_stride_d).to(tl.float32)
343+
(head_dim + offs_d[None, :]) * kv_stride_d).to(tl.float32)
345344
_curr_score_raw = tl.load(
346345
score_ptr + (_curr_kv_pos + row_ids[:, None]) * score_stride_s +
347-
(head_dim + d_off + offs_d[None, :]) * score_stride_d).to(tl.float32)
346+
(head_dim + offs_d[None, :]) * score_stride_d).to(tl.float32)
348347
_curr_ape = tl.load(
349348
ape_ptr + (_curr_abs_pos % ratio)[:, None] * ape_stride_r +
350-
(head_dim + d_off + offs_d[None, :]) * ape_stride_d).to(tl.float32)
349+
(head_dim + offs_d[None, :]) * ape_stride_d).to(tl.float32)
351350
curr_score = _curr_score_raw + _curr_ape
352351

353352
global_max = tl.maximum(tl.max(prev_score, 0), tl.max(curr_score, 0))

lmdeploy/pytorch/models/deepseek_v4.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ def forward(self,
220220
kv_flat = kv.view(-1, kv.size(-1))
221221
score_flat = score.view(-1, score.size(-1))
222222

223-
state_ids = slot.long().to(torch.int32)
223+
state_ids = slot
224224

225225
# ---- Phase C: Get state views ----
226226
state_cache = caches.named_state_caches[self.state_cache_name][self.layer_id]

tests/pytorch/kernel/test_v4_compressor.py

Lines changed: 157 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -116,41 +116,15 @@ def _run_test(self, kv_seqlens_list, q_seqlens_list, ratio, head_dim, num_states
116116
@pytest.mark.parametrize('overlap', [True], indirect=True)
117117
@pytest.mark.parametrize('ratio', [4], indirect=True)
118118
@pytest.mark.parametrize('kv_seqlens_list, q_seqlens_list', [
119-
# decode: history + 1
120-
([13, 17, 9], [1, 1, 1]),
121-
# decode: various history lengths
122-
([5, 21, 33], [1, 1, 1]),
123-
# prefill: kv_seqlens == q_seqlens (no history)
124-
([8, 16], [8, 16]),
125-
# prefill: long (more than max_write)
126-
([32, 64], [32, 64]),
127-
# prefill: with history (multi-turn, start_pos > 0)
128-
([20, 48], [8, 16]),
129-
([128, 256], [16, 32]),
119+
([13, 17, 9], [1, 1, 1]), # decode
120+
([8, 16], [8, 16]), # prefill: no history
121+
([20, 48], [8, 16]), # prefill: with history
130122
])
131123
def test_overlap(self, kv_seqlens_list, q_seqlens_list, ratio, head_dim,
132124
num_states, overlap, device, dtype):
133125
self._run_test(kv_seqlens_list, q_seqlens_list, ratio, head_dim,
134126
num_states, overlap, device, dtype)
135127

136-
# ---- overlap=False, ratio=4 ----
137-
138-
@pytest.mark.parametrize('overlap', [False], indirect=True)
139-
@pytest.mark.parametrize('ratio', [4], indirect=True)
140-
@pytest.mark.parametrize('kv_seqlens_list, q_seqlens_list', [
141-
# decode
142-
([13, 9, 128], [1, 1, 1]),
143-
# prefill
144-
([4, 8], [4, 8]),
145-
([16], [16]),
146-
# prefill: with history
147-
([20, 40], [8, 16]),
148-
])
149-
def test_no_overlap(self, kv_seqlens_list, q_seqlens_list, ratio, head_dim,
150-
num_states, overlap, device, dtype):
151-
self._run_test(kv_seqlens_list, q_seqlens_list, ratio, head_dim,
152-
num_states, overlap, device, dtype)
153-
154128
# ---- overlap=False, ratio=128 (r128 compress path) ----
155129

156130
@pytest.mark.parametrize('overlap', [False], indirect=True)
@@ -439,12 +413,8 @@ def _run_decode_test(self, kvlen, ratio, head_dim, num_states, overlap, device,
439413
@pytest.mark.parametrize('overlap', [True], indirect=True)
440414
@pytest.mark.parametrize('ratio', [4], indirect=True)
441415
@pytest.mark.parametrize('kv_seqlens_list, q_seqlens_list', [
442-
# prefill: no history (start_pos=0)
443-
([8, 16], [8, 16]),
444-
([4, 12], [4, 12]),
445-
# prefill: with history (start_pos>0)
446-
([20, 48], [8, 16]),
447-
([12, 24], [8, 16]),
416+
([8, 16], [8, 16]), # no history
417+
([12, 24], [8, 16]), # with history
448418
])
449419
def test_prefill_overlap(self, kv_seqlens_list, q_seqlens_list, ratio, head_dim,
450420
num_states, overlap, device, dtype):
@@ -456,43 +426,13 @@ def test_prefill_overlap(self, kv_seqlens_list, q_seqlens_list, ratio, head_dim,
456426
@pytest.mark.parametrize('overlap', [True], indirect=True)
457427
@pytest.mark.parametrize('ratio', [4], indirect=True)
458428
@pytest.mark.parametrize('kvlen', [
459-
4, # first emit (start_pos=3)
460-
8, # second emit (start_pos=7)
461-
5, # no emit (start_pos=4, (4+1)%4=1!=0)
462-
7, # no emit
463-
12, # third emit
429+
4, # emit (start_pos=3)
430+
5, # no emit (start_pos=4)
464431
])
465432
def test_decode_overlap(self, kvlen, ratio, head_dim, num_states, overlap,
466433
device, dtype):
467434
self._run_decode_test(kvlen, ratio, head_dim, num_states, overlap, device, dtype)
468435

469-
# ---- overlap=False, ratio=4, prefill ----
470-
471-
@pytest.mark.parametrize('overlap', [False], indirect=True)
472-
@pytest.mark.parametrize('ratio', [4], indirect=True)
473-
@pytest.mark.parametrize('kv_seqlens_list, q_seqlens_list', [
474-
([4, 8], [4, 8]),
475-
([16], [16]),
476-
([20, 40], [8, 16]),
477-
])
478-
def test_prefill_no_overlap(self, kv_seqlens_list, q_seqlens_list, ratio, head_dim,
479-
num_states, overlap, device, dtype):
480-
self._run_prefill_test(kv_seqlens_list, q_seqlens_list, ratio, head_dim,
481-
num_states, overlap, device, dtype)
482-
483-
# ---- overlap=False, ratio=4, decode ----
484-
485-
@pytest.mark.parametrize('overlap', [False], indirect=True)
486-
@pytest.mark.parametrize('ratio', [4], indirect=True)
487-
@pytest.mark.parametrize('kvlen', [
488-
4, # emit
489-
8, # emit
490-
5, # no emit
491-
])
492-
def test_decode_no_overlap(self, kvlen, ratio, head_dim, num_states, overlap,
493-
device, dtype):
494-
self._run_decode_test(kvlen, ratio, head_dim, num_states, overlap, device, dtype)
495-
496436
# ---- overlap=False, ratio=128, prefill ----
497437

498438
@pytest.mark.parametrize('overlap', [False], indirect=True)
@@ -520,6 +460,153 @@ def test_decode_ratio_128(self, kvlen, ratio, head_dim, num_states, overlap,
520460
self._run_decode_test(kvlen, ratio, head_dim, num_states, overlap, device, dtype)
521461

522462

463+
class TestScoreKVLargeHeadDim:
464+
"""Test score_kv with head_dim=512 (n_tiles=4), matching real V4 model
465+
config.
466+
467+
The original tests all use head_dim=128 where n_tiles=1, so a double-d_off bug in the kernel was masked (d_off=0 for
468+
the only tile). Only overlap=True ratio=4 is tested because that is the V4 model's actual config — overlap=False
469+
ratio=4 is not a real config, and overlap=False ratio=128 prefill had no bug (offs_d without d_off prefix).
470+
"""
471+
472+
@pytest.fixture
473+
def head_dim(self):
474+
yield 512
475+
476+
@pytest.fixture
477+
def num_states(self):
478+
yield 8
479+
480+
@pytest.fixture
481+
def device(self):
482+
yield 'cuda'
483+
484+
@pytest.fixture
485+
def dtype(self):
486+
yield torch.bfloat16
487+
488+
@pytest.fixture
489+
def overlap(self, request):
490+
yield request.param
491+
492+
@pytest.fixture
493+
def ratio(self, request):
494+
yield request.param
495+
496+
def _run_prefill_test(self, kv_seqlens_list, q_seqlens_list, ratio, head_dim,
497+
num_states, overlap, device, dtype):
498+
B = len(kv_seqlens_list)
499+
coff = 1 + overlap
500+
D = coff * head_dim
501+
max_write = ratio * coff
502+
total_q = sum(q_seqlens_list)
503+
max_seqlen_q = max(q_seqlens_list)
504+
505+
kv_seqlens = torch.tensor(kv_seqlens_list, dtype=torch.int32, device=device)
506+
cu_q_seqlens = torch.tensor([0] + list(q_seqlens_list), dtype=torch.int32,
507+
device=device).cumsum(0)
508+
509+
kv = torch.randn(total_q, D, dtype=dtype, device=device)
510+
score = torch.randn(total_q, D, dtype=dtype, device=device)
511+
ape = torch.randn(ratio, D, dtype=torch.float32, device=device)
512+
513+
state_ids = torch.arange(B, dtype=torch.int32, device=device)
514+
kv_state = torch.zeros(num_states, max_write, D, dtype=torch.float32, device=device)
515+
score_state = torch.full((num_states, max_write, D), float('-inf'),
516+
dtype=torch.float32, device=device)
517+
518+
from lmdeploy.pytorch.kernels.cuda.v4_compressor import fill_compress_state
519+
has_history = any(kv_seqlens_list[b] > q_seqlens_list[b] for b in range(B))
520+
if has_history:
521+
for b in range(B):
522+
history_len = kv_seqlens_list[b] - q_seqlens_list[b]
523+
if history_len > 0:
524+
hist_kv = torch.randn(history_len, D, dtype=dtype, device=device)
525+
hist_score = torch.randn(history_len, D, dtype=dtype, device=device)
526+
hist_cu_q = torch.tensor([0, history_len], dtype=torch.int32, device=device)
527+
hist_kvlen = torch.tensor([kv_seqlens_list[b] - q_seqlens_list[b]],
528+
dtype=torch.int32, device=device)
529+
hist_sids = torch.tensor([b], dtype=torch.int32, device=device)
530+
fill_compress_state(hist_kv, hist_score, ape, kv_state, score_state,
531+
hist_sids, hist_cu_q, hist_kvlen)
532+
533+
kv_state_ref = kv_state.clone()
534+
score_state_ref = score_state.clone()
535+
ref_compressed = _reference_score_kv(kv.clone(), score.clone(), ape, kv_state_ref,
536+
score_state_ref, state_ids, cu_q_seqlens,
537+
kv_seqlens, overlap)
538+
539+
from lmdeploy.pytorch.kernels.cuda.v4_compressor import score_kv
540+
compressed_kv = torch.zeros(total_q, head_dim, dtype=dtype, device=device)
541+
kv_state_k = kv_state.clone()
542+
score_state_k = score_state.clone()
543+
score_kv(kv, score, ape, kv_state_k, score_state_k, state_ids,
544+
cu_q_seqlens, kv_seqlens, compressed_kv, overlap, max_seqlen_q)
545+
546+
torch.testing.assert_close(compressed_kv.float(),
547+
ref_compressed.float(),
548+
atol=1e-2, rtol=1e-2)
549+
550+
def _run_decode_test(self, kvlen, ratio, head_dim, num_states, overlap, device, dtype):
551+
coff = 1 + overlap
552+
D = coff * head_dim
553+
max_write = ratio * coff
554+
555+
full_kv = torch.randn(kvlen, D, dtype=dtype, device=device)
556+
full_score = torch.randn(kvlen, D, dtype=dtype, device=device)
557+
ape = torch.randn(ratio, D, dtype=torch.float32, device=device)
558+
559+
state_ids = torch.tensor([0], dtype=torch.int32, device=device)
560+
kv_state = torch.zeros(num_states, max_write, D, dtype=torch.float32, device=device)
561+
score_state = torch.full((num_states, max_write, D), float('-inf'),
562+
dtype=torch.float32, device=device)
563+
564+
from lmdeploy.pytorch.kernels.cuda.v4_compressor import fill_compress_state
565+
full_cu_q = torch.tensor([0, kvlen], dtype=torch.int32, device=device)
566+
full_kvlen = torch.tensor([kvlen], dtype=torch.int32, device=device)
567+
fill_compress_state(full_kv, full_score, ape, kv_state, score_state,
568+
state_ids, full_cu_q, full_kvlen)
569+
570+
last_kv = full_kv[-1:].clone()
571+
last_score = full_score[-1:].clone()
572+
last_cu_q = torch.tensor([0, 1], dtype=torch.int32, device=device)
573+
last_kvlen = torch.tensor([kvlen], dtype=torch.int32, device=device)
574+
575+
kv_state_ref = kv_state.clone()
576+
score_state_ref = score_state.clone()
577+
ref_compressed = _reference_score_kv(last_kv.clone(), last_score.clone(), ape,
578+
kv_state_ref, score_state_ref, state_ids,
579+
last_cu_q, last_kvlen, overlap)
580+
581+
from lmdeploy.pytorch.kernels.cuda.v4_compressor import score_kv
582+
compressed_kv = torch.zeros(1, head_dim, dtype=dtype, device=device)
583+
kv_state_k = kv_state.clone()
584+
score_state_k = score_state.clone()
585+
score_kv(last_kv, last_score, ape, kv_state_k, score_state_k, state_ids,
586+
last_cu_q, last_kvlen, compressed_kv, overlap, 1)
587+
588+
torch.testing.assert_close(compressed_kv.float(),
589+
ref_compressed.float(),
590+
atol=1e-2, rtol=1e-2)
591+
592+
@pytest.mark.parametrize('overlap', [True], indirect=True)
593+
@pytest.mark.parametrize('ratio', [4], indirect=True)
594+
@pytest.mark.parametrize('kvlen', [12])
595+
def test_decode_overlap(self, kvlen, ratio, head_dim, num_states, overlap,
596+
device, dtype):
597+
self._run_decode_test(kvlen, ratio, head_dim, num_states, overlap, device, dtype)
598+
599+
@pytest.mark.parametrize('overlap', [True], indirect=True)
600+
@pytest.mark.parametrize('ratio', [4], indirect=True)
601+
@pytest.mark.parametrize('kv_seqlens_list, q_seqlens_list', [
602+
([12, 24], [8, 16]),
603+
])
604+
def test_prefill_overlap(self, kv_seqlens_list, q_seqlens_list, ratio, head_dim,
605+
num_states, overlap, device, dtype):
606+
self._run_prefill_test(kv_seqlens_list, q_seqlens_list, ratio, head_dim,
607+
num_states, overlap, device, dtype)
608+
609+
523610
def _reference_fill_compressed_kv(compressed_kv, kv_cache, cu_q_seqlens, kv_seqlens,
524611
block_offsets, compress_ratio, block_size):
525612
"""Python reference for fill_compressed_kv, matching
@@ -710,9 +797,8 @@ def _run_decode_test(self, kvlen, compress_ratio, head_dim, block_size, device,
710797

711798
@pytest.mark.parametrize('compress_ratio', [4], indirect=True)
712799
@pytest.mark.parametrize('kv_seqlens_list, q_seqlens_list', [
713-
([8, 16], [8, 16]),
714-
([4, 12], [4, 12]),
715-
([20, 48], [8, 16]),
800+
([8, 16], [8, 16]), # no history
801+
([20, 48], [8, 16]), # with history
716802
])
717803
def test_prefill_r4(self, kv_seqlens_list, q_seqlens_list, compress_ratio,
718804
head_dim, block_size, device, dtype):
@@ -722,7 +808,7 @@ def test_prefill_r4(self, kv_seqlens_list, q_seqlens_list, compress_ratio,
722808
# ---- ratio=4, decode ----
723809

724810
@pytest.mark.parametrize('compress_ratio', [4], indirect=True)
725-
@pytest.mark.parametrize('kvlen', [4, 8, 5, 7, 12])
811+
@pytest.mark.parametrize('kvlen', [4, 5])
726812
def test_decode_r4(self, kvlen, compress_ratio, head_dim, block_size, device, dtype):
727813
self._run_decode_test(kvlen, compress_ratio, head_dim, block_size, device, dtype)
728814

0 commit comments

Comments
 (0)