[ROCm] [CI] Support cached K/V (key/value=None) in Triton prefix-prefill (#48257)

Signed-off-by: Stefan Koncarevic <stefan.koncarevic@amd.com> Co-authored-by: Andreas Karatzas <akaratza@amd.com>

stefankoncarevic committed Jul 30, 2026 at 06:31 UTC b88916617d3d2249bff0dae5cecb6b727c980a20
2 files changed +525 -40
tests/kernels/attention/test_prefix_prefill.py
+336
@@ -323,6 +323,342 @@ def test_contexted_kv_attention(
323 torch.testing.assert_close(output, output_ref, atol=atol, rtol=0)
324
325
326 +@pytest.mark.parametrize("num_heads", NUM_HEADS)
327 +@pytest.mark.parametrize("num_queries_per_kv", NUM_QUERIES_PER_KV)
328 +@pytest.mark.parametrize("head_size", HEAD_SIZES)
329 +@pytest.mark.parametrize("dtype", DTYPES)
330 +@pytest.mark.parametrize("kv_cache_dtype", KV_CACHE_DTYPES)
331 +@pytest.mark.parametrize("device", CUDA_DEVICES)
332 +@torch.inference_mode()
333 +def test_contexted_kv_attention_cached_kv(
334 + num_heads: int,
335 + num_queries_per_kv: int,
336 + head_size: int,
337 + dtype: torch.dtype,
338 + kv_cache_dtype: str,
339 + device: str,
340 + block_size: int = 32,
341 +) -> None:
342 + # Exercises the KV_FROM_CACHE path of context_attention_fwd: the current
343 + # chunk K/V are not passed as dense tensors (k=v=None); they are read back
344 + # from the paged KV cache, as done by layers that re-attend an already
345 + # cached sequence with the query only (e.g. IQuest LoopCoder's
346 + # attn(q, None, None)). The whole sequence therefore lives in the cache and
347 + # the result must still match a dense causal SDPA reference.
348 + if "fp8" in kv_cache_dtype and not current_platform.has_device_capability(89):
349 + pytest.skip(
350 + "Triton limitation: fp8e4nv data type is not supported on CUDA arch < 89"
351 + )
352 +
353 + set_random_seed(0)
354 + torch.set_default_device(device)
355 + torch.accelerator.set_device_index(device)
356 +
357 + MAX_SEQ_LEN = 1024
358 + MAX_CTX_LEN = 1024
359 + BS = 10
360 + cache_size = 640
361 + max_block_per_request = 64
362 + query_lens = [random.randint(16, MAX_SEQ_LEN) for _ in range(BS)]
363 + # ensure one sequence in batch is a decode
364 + query_lens[-1] = 1
365 + ctx_lens = [random.randint(16, MAX_CTX_LEN) for _ in range(BS)]
366 + seq_lens = [a + b for a, b in zip(query_lens, ctx_lens)]
367 + num_kv_heads = num_heads // num_queries_per_kv
368 +
369 + num_tokens = sum(query_lens)
370 + query = torch.empty(num_tokens, num_heads, head_size, dtype=dtype)
371 + query.uniform_(-1e-3, 1e-3)
372 + output = torch.empty(num_tokens, num_heads, head_size, dtype=dtype)
373 +
374 + kv = torch.empty(sum(seq_lens), 2, num_kv_heads, head_size, dtype=dtype)
375 + kv.uniform_(-1e-3, 1e-3)
376 + key, value = kv.unbind(dim=1)
377 +
378 + if kv_cache_dtype == "auto":
379 + cache_dtype = dtype
380 + else:
381 + cache_dtype = STR_DTYPE_TO_TORCH_DTYPE[kv_cache_dtype]
382 + k_cache = torch.zeros(
383 + cache_size, block_size, num_kv_heads, head_size, dtype=cache_dtype
384 + )
385 + v_cache = torch.zeros(
386 + cache_size, block_size, num_kv_heads, head_size, dtype=cache_dtype
387 + )
388 + values = torch.arange(0, cache_size, dtype=torch.int32)
389 + values = values[torch.randperm(cache_size)]
390 + block_table = values[: BS * max_block_per_request].view(BS, max_block_per_request)
391 + b_seq_len = torch.tensor(seq_lens, dtype=torch.int32)
392 + b_start_loc = torch.cumsum(torch.tensor([0] + query_lens), dim=0).to(torch.int32)
393 + max_input_len = MAX_SEQ_LEN
394 + b_seq_start_loc = torch.cumsum(torch.tensor([0] + seq_lens[:-1]), dim=0).to(
395 + torch.int32
396 + )
397 + # Unlike the dense test, write the WHOLE sequence (context + current chunk)
398 + # into the paged cache, since the current chunk is read back from the cache.
399 + for i in range(BS):
400 + cur = 0
401 + block_id = 0
402 + while cur < seq_lens[i]:
403 + start_loc = b_seq_start_loc[i] + cur
404 + if cur + block_size > seq_lens[i]:
405 + end_loc = b_seq_start_loc[i] + seq_lens[i]
406 + else:
407 + end_loc = start_loc + block_size
408 + start_slot = block_table[i, block_id] * block_size
409 + end_slot = start_slot + end_loc - start_loc
410 + k_cache.view(-1, num_kv_heads, head_size)[start_slot:end_slot].copy_(
411 + key[start_loc:end_loc]
412 + )
413 + v_cache.view(-1, num_kv_heads, head_size)[start_slot:end_slot].copy_(
414 + value[start_loc:end_loc]
415 + )
416 + cur += block_size
417 + block_id += 1
418 + # transpose to the paged cache layouts the kernel expects:
419 + # K_cache[num_blocks, num_kv_heads, head_size/8, block_size, 8]
420 + # V_cache[num_blocks, num_kv_heads, head_size, block_size]
421 + k_cache = (
422 + k_cache.view(-1, block_size, num_kv_heads, head_size // 8, 8)
423 + .permute(0, 2, 3, 1, 4)
424 + .contiguous()
425 + )
426 + v_cache = (
427 + v_cache.view(-1, block_size, num_kv_heads, head_size)
428 + .permute(0, 2, 3, 1)
429 + .contiguous()
430 + )
431 + k_scale = v_scale = torch.tensor(1.0, dtype=torch.float32, device=device)
432 +
433 + # Cached-K/V path: current-chunk k and v are None.
434 + context_attention_fwd(
435 + query,
436 + None,
437 + None,
438 + output,
439 + kv_cache_dtype,
440 + k_cache,
441 + v_cache,
442 + block_table,
443 + b_start_loc,
444 + b_seq_len,
445 + MAX_CTX_LEN,
446 + max_input_len,
447 + k_scale,
448 + v_scale,
449 + sliding_window=0,
450 + )
451 + torch.accelerator.synchronize()
452 +
453 + scale = float(1.0 / (head_size**0.5))
454 +
455 + query_sdpa = query.view(num_tokens, num_kv_heads, num_queries_per_kv, head_size)
456 + query_sdpa = query_sdpa.permute(1, 2, 0, 3).reshape(
457 + 1, num_heads, num_tokens, head_size
458 + )
459 + key_sdpa = key[:, :, None, :].expand(
460 + key.shape[0], num_kv_heads, num_queries_per_kv, key.shape[-1]
461 + )
462 + key_sdpa = key_sdpa.permute(1, 2, 0, 3).reshape(
463 + 1, num_heads, sum(seq_lens), head_size
464 + )
465 + value_sdpa = value[:, :, None, :].expand(
466 + value.shape[0], num_kv_heads, num_queries_per_kv, value.shape[-1]
467 + )
468 + value_sdpa = value_sdpa.permute(1, 2, 0, 3).reshape(
469 + 1, num_heads, sum(seq_lens), head_size
470 + )
471 +
472 + attn_mask = create_causal_attention_mask_for_sdpa(
473 + query_lens, seq_lens, 0, device=device, dtype=dtype
474 + )
475 + output_ref = F.scaled_dot_product_attention(
476 + query_sdpa,
477 + key_sdpa,
478 + value_sdpa,
479 + attn_mask=attn_mask,
480 + dropout_p=0.0,
481 + scale=scale,
482 + )
483 + output_ref = output_ref.view(num_heads, num_tokens, head_size)
484 + output_ref = output_ref.permute(1, 0, 2).contiguous()
485 + atol = 1e-3 if "fp8" in kv_cache_dtype else 1e-4
486 + torch.testing.assert_close(output, output_ref, atol=atol, rtol=0)
487 +
488 +
489 +@pytest.mark.parametrize("device", CUDA_DEVICES)
490 +@torch.inference_mode()
491 +def test_contexted_kv_attention_cached_kv_block_table_boundary(device: str) -> None:
492 + # Boundary guard for the KV_FROM_CACHE block-table load. With an
493 + # exact-sized block table (row length == number of blocks for the
494 + # sequence) and a query that ends the sequence, the last K/V tile has
495 + # padded lanes whose absolute positions step past the sequence end. Those
496 + # lanes must not read a block-table entry past this batch's row.
497 + #
498 + # seq_len is a whole number of blocks, so bn_logical for a padded lane
499 + # lands exactly on num_blocks (one past the last valid row entry), and
500 + # query_len is not tile-aligned so the overshoot is actually exercised.
501 + # The result must still match a dense causal SDPA reference.
502 + set_random_seed(0)
503 + torch.set_default_device(device)
504 + torch.accelerator.set_device_index(device)
505 +
506 + dtype = torch.float16
507 + kv_cache_dtype = "auto"
508 + num_heads = 4
509 + num_queries_per_kv = 1
510 + num_kv_heads = num_heads // num_queries_per_kv
511 + head_size = 32
512 + block_size = 32
513 + num_blocks = 4
514 + seq_len = num_blocks * block_size # 128 == exact multiple of block_size
515 + query_len = 99 # < seq_len and not a multiple of the kernel tile
516 +
517 + query = torch.empty(query_len, num_heads, head_size, dtype=dtype)
518 + query.uniform_(-1e-3, 1e-3)
519 + output = torch.empty(query_len, num_heads, head_size, dtype=dtype)
520 +
521 + kv = torch.empty(seq_len, 2, num_kv_heads, head_size, dtype=dtype)
522 + kv.uniform_(-1e-3, 1e-3)
523 + key, value = kv.unbind(dim=1)
524 +
525 + k_cache = torch.zeros(num_blocks, block_size, num_kv_heads, head_size, dtype=dtype)
526 + v_cache = torch.zeros(num_blocks, block_size, num_kv_heads, head_size, dtype=dtype)
527 + # Exact-sized block table: row length == num_blocks (identity mapping).
528 + block_table = torch.arange(num_blocks, dtype=torch.int32).view(1, num_blocks)
529 + b_seq_len = torch.tensor([seq_len], dtype=torch.int32)
530 + b_start_loc = torch.tensor([0, query_len], dtype=torch.int32)
531 +
532 + # Write the whole sequence (context + current chunk) into the paged cache.
533 + for cur in range(0, seq_len, block_size):
534 + block_id = cur // block_size
535 + end = min(cur + block_size, seq_len)
536 + start_slot = block_table[0, block_id] * block_size
537 + end_slot = start_slot + (end - cur)
538 + k_cache.view(-1, num_kv_heads, head_size)[start_slot:end_slot].copy_(
539 + key[cur:end]
540 + )
541 + v_cache.view(-1, num_kv_heads, head_size)[start_slot:end_slot].copy_(
542 + value[cur:end]
543 + )
544 +
545 + k_cache = (
546 + k_cache.view(-1, block_size, num_kv_heads, head_size // 8, 8)
547 + .permute(0, 2, 3, 1, 4)
548 + .contiguous()
549 + )
550 + v_cache = (
551 + v_cache.view(-1, block_size, num_kv_heads, head_size)
552 + .permute(0, 2, 3, 1)
553 + .contiguous()
554 + )
555 + k_scale = v_scale = torch.tensor(1.0, dtype=torch.float32, device=device)
556 +
557 + # Cached-K/V path: current-chunk k and v are None.
558 + context_attention_fwd(
559 + query,
560 + None,
561 + None,
562 + output,
563 + kv_cache_dtype,
564 + k_cache,
565 + v_cache,
566 + block_table,
567 + b_start_loc,
568 + b_seq_len,
569 + seq_len,
570 + query_len,
571 + k_scale,
572 + v_scale,
573 + sliding_window=0,
574 + )
575 + torch.accelerator.synchronize()
576 +
577 + scale = float(1.0 / (head_size**0.5))
578 + query_sdpa = query.view(query_len, num_kv_heads, num_queries_per_kv, head_size)
579 + query_sdpa = query_sdpa.permute(1, 2, 0, 3).reshape(
580 + 1, num_heads, query_len, head_size
581 + )
582 + key_sdpa = key[:, :, None, :].expand(
583 + seq_len, num_kv_heads, num_queries_per_kv, head_size
584 + )
585 + key_sdpa = key_sdpa.permute(1, 2, 0, 3).reshape(1, num_heads, seq_len, head_size)
586 + value_sdpa = value[:, :, None, :].expand(
587 + seq_len, num_kv_heads, num_queries_per_kv, head_size
588 + )
589 + value_sdpa = value_sdpa.permute(1, 2, 0, 3).reshape(
590 + 1, num_heads, seq_len, head_size
591 + )
592 +
593 + attn_mask = create_causal_attention_mask_for_sdpa(
594 + [query_len], [seq_len], 0, device=device, dtype=dtype
595 + )
596 + output_ref = F.scaled_dot_product_attention(
597 + query_sdpa,
598 + key_sdpa,
599 + value_sdpa,
600 + attn_mask=attn_mask,
601 + dropout_p=0.0,
602 + scale=scale,
603 + )
604 + output_ref = output_ref.view(num_heads, query_len, head_size)
605 + output_ref = output_ref.permute(1, 0, 2).contiguous()
606 + torch.testing.assert_close(output, output_ref, atol=1e-4, rtol=0)
607 +
608 +
609 +@pytest.mark.parametrize("device", CUDA_DEVICES)
610 +@torch.inference_mode()
611 +def test_contexted_kv_attention_cached_kv_alibi_unsupported(device: str) -> None:
612 + # The cached-K/V (k=None) path is not supported together with ALiBi; the
613 + # entry point must reject it up-front with a clear NotImplementedError
614 + # rather than launching the kernel with an unsupported combination.
615 + set_random_seed(0)
616 + torch.set_default_device(device)
617 + torch.accelerator.set_device_index(device)
618 +
619 + num_heads = 4
620 + num_kv_heads = 4
621 + head_size = 16
622 + x = 8
623 + block_size = 16
624 + num_blocks = 4
625 + query_len = 8
626 +
627 + query = torch.empty(query_len, num_heads, head_size, dtype=torch.float16)
628 + query.uniform_(-1e-3, 1e-3)
629 + output = torch.empty_like(query)
630 + k_cache = torch.zeros(
631 + num_blocks, num_kv_heads, head_size // x, block_size, x, dtype=torch.float16
632 + )
633 + v_cache = torch.zeros(
634 + num_blocks, num_kv_heads, head_size, block_size, dtype=torch.float16
635 + )
636 + block_table = torch.arange(num_blocks, dtype=torch.int32).view(1, num_blocks)
637 + b_seq_len = torch.tensor([query_len], dtype=torch.int32)
638 + b_start_loc = torch.tensor([0, query_len], dtype=torch.int32)
639 + k_scale = v_scale = torch.tensor(1.0, dtype=torch.float32, device=device)
640 + alibi_slopes = torch.ones(num_heads, dtype=torch.float32, device=device)
641 +
642 + with pytest.raises(NotImplementedError):
643 + context_attention_fwd(
644 + query,
645 + None,
646 + None,
647 + output,
648 + "auto",
649 + k_cache,
650 + v_cache,
651 + block_table,
652 + b_start_loc,
653 + b_seq_len,
654 + query_len,
655 + query_len,
656 + k_scale,
657 + v_scale,
658 + alibi_slopes=alibi_slopes,
659 + )
660 +
661 +
662 @pytest.mark.parametrize("num_heads", NUM_HEADS)
663 @pytest.mark.parametrize("num_queries_per_kv", NUM_QUERIES_PER_KV)
664 @pytest.mark.parametrize("head_size", HEAD_SIZES)
vllm/v1/attention/ops/prefix_prefill.py
+189 -40
@@ -35,6 +35,74 @@ float8_info = torch.finfo(current_platform.fp8_dtype())
35 # ],
36 # key=["BLOCK_SIZE", "MAX_Q_LEN", "MAX_CTX_LEN"]
37 # )
38 +@triton.jit
39 +def _paged_kv_cache_offsets(
40 + B_Loc,
41 + cur_batch,
42 + token_indices,
43 + token_valid,
44 + offs_d,
45 + cur_kv_head,
46 + x,
47 + stride_b_loc_b,
48 + stride_b_loc_s,
49 + stride_k_cache_bs,
50 + stride_k_cache_h,
51 + stride_k_cache_d,
52 + stride_k_cache_bl,
53 + stride_k_cache_x,
54 + stride_v_cache_bs,
55 + stride_v_cache_h,
56 + stride_v_cache_d,
57 + stride_v_cache_bl,
58 + PHYSICAL_BLOCK_SIZE: tl.constexpr,
59 + MASK_BLOCK_TABLE: tl.constexpr = False,
60 +):
61 + """Compute paged K/V cache element offsets for a tile of token positions.
62 +
63 + `token_indices` holds the absolute sequence positions of the tile. The
64 + logical block for each token is looked up in `B_Loc` (handling cross-block
65 + tiles), then the physical element offsets are built with the same layout the
66 + kernel uses everywhere:
67 + K cache: [num_blocks, num_kv_heads, head_size // x, block_size, x]
68 + V cache: [num_blocks, num_kv_heads, head_size, block_size]
69 + Returns `(off_k, off_v)` with shapes [D, N] and [N, D] respectively.
70 +
71 + When `MASK_BLOCK_TABLE` is set, the block-table load is masked with
72 + `token_valid` so padded lanes (a tile that extends past the end of the
73 + sequence) cannot read a block-table entry past this batch's row. Invalid
74 + lanes resolve to block 0 (always in-bounds) and are dropped by the caller's
75 + later K/V load mask. When it is not set, `token_valid` is unused and the
76 + load is emitted exactly as before.
77 + """
78 + bn_logical = token_indices // PHYSICAL_BLOCK_SIZE
79 + if MASK_BLOCK_TABLE:
80 + bn = tl.load(
81 + B_Loc + cur_batch * stride_b_loc_b + bn_logical * stride_b_loc_s,
82 + mask=token_valid,
83 + other=0,
84 + ).to(tl.int64)
85 + else:
86 + bn = tl.load(
87 + B_Loc + cur_batch * stride_b_loc_b + bn_logical * stride_b_loc_s
88 + ).to(tl.int64)
89 + internal = token_indices % PHYSICAL_BLOCK_SIZE
90 + off_k = (
91 + bn[None, :] * stride_k_cache_bs
92 + + cur_kv_head * stride_k_cache_h
93 + + (offs_d[:, None] // x) * stride_k_cache_d
94 + + internal[None, :] * stride_k_cache_bl
95 + + (offs_d[:, None] % x) * stride_k_cache_x
96 + )
97 + off_v = (
98 + bn[:, None] * stride_v_cache_bs
99 + + cur_kv_head * stride_v_cache_h
100 + + offs_d[None, :] * stride_v_cache_d
101 + + internal[:, None] * stride_v_cache_bl
102 + )
103 + return off_k, off_v
104 +
105 +
106 @triton.jit
107 def _fwd_kernel(
108 Q,
@@ -94,6 +162,11 @@ def _fwd_kernel(
162 MAX_CTX_LEN: tl.constexpr = 0,
163 FP8_MIN: tl.constexpr = float8_info.min,
164 FP8_MAX: tl.constexpr = float8_info.max,
165 + # When True, the current-chunk K/V are not provided as dense tensors
166 + # (`K`/`V` are unused placeholders); instead they are read back from the
167 + # paged KV cache. This supports layers that re-attend an already-cached
168 + # sequence with query only (e.g. IQuest LoopCoder's `attn(q, None, None)`).
169 + KV_FROM_CACHE: tl.constexpr = False,
170 ):
171 cur_batch = tl.program_id(0)
172 cur_head = tl.program_id(1)
@@ -162,33 +235,34 @@ def _fwd_kernel(
235 # replace one physical block every 17 32-Tile blocks
236 # Calculate the logical block index of each of the 32 tokens
237 # in the current Tile (handling cross-block cases).
238 + # Physical-block K/V cache offsets for the context tokens in this tile
239 + # (handles cross-block tiles via B_Loc). Same addressing is reused for
240 + # the current-chunk cache read below.
241 token_indices = start_n + offs_bs_n
166 - bn_logical_indices = token_indices // PHYSICAL_BLOCK_SIZE
167 -
168 - # 2. Vectorized loading of physical block IDs from B_Loc
169 - bn = tl.load(
170 - B_Loc + cur_batch * stride_b_loc_b + bn_logical_indices * stride_b_loc_s
171 - ).to(tl.int64)
172 -
173 - # 3. Calculate the exact offset of
174 - # each token within its physical block.
175 - internal_offsets = token_indices % PHYSICAL_BLOCK_SIZE
176 -
177 - # Addressing of K (5D)
178 - off_k = (
179 - bn[None, :] * stride_k_cache_bs
180 - + cur_kv_head * stride_k_cache_h
181 - + (offs_d[:, None] // x) * stride_k_cache_d
182 - + internal_offsets[None, :] * stride_k_cache_bl
183 - + (offs_d[:, None] % x) * stride_k_cache_x
184 - )
185 -
186 - # Addressing of V (4D)
187 - off_v = (
188 - bn[:, None] * stride_v_cache_bs
189 - + cur_kv_head * stride_v_cache_h
190 - + offs_d[None, :] * stride_v_cache_d
191 - + internal_offsets[:, None] * stride_v_cache_bl
242 + # Context tokens are always followed by the current chunk, so the tile
243 + # never steps past this batch's block-table row; the block-table load
244 + # stays unmasked (MASK_BLOCK_TABLE=False) and offs_bs_n is an unused
245 + # placeholder for token_valid.
246 + off_k, off_v = _paged_kv_cache_offsets(
247 + B_Loc,
248 + cur_batch,
249 + token_indices,
250 + offs_bs_n,
251 + offs_d,
252 + cur_kv_head,
253 + x,
254 + stride_b_loc_b,
255 + stride_b_loc_s,
256 + stride_k_cache_bs,
257 + stride_k_cache_h,
258 + stride_k_cache_d,
259 + stride_k_cache_bl,
260 + stride_k_cache_x,
261 + stride_v_cache_bs,
262 + stride_v_cache_h,
263 + stride_v_cache_d,
264 + stride_v_cache_bl,
265 + PHYSICAL_BLOCK_SIZE,
266 )
267
268 if (
@@ -300,12 +374,54 @@ def _fwd_kernel(
374 ):
375 start_n = tl.multiple_of(start_n, BLOCK_N)
376 # -- compute qk ----
303 - k = tl.load(
304 - k_ptrs + (cur_batch_in_all_start_index + start_n) * stride_kbs,
305 - mask=dim_mask[:, None]
306 - & ((start_n + offs_n[None, :]) < cur_batch_query_len),
307 - other=0.0,
308 - )
377 + if KV_FROM_CACHE:
378 + # Current-chunk K/V are not passed densely; read them from the paged
379 + # cache at their absolute positions [ctx_len, ctx_len + query_len),
380 + # reusing the same addressing as the context loop above.
381 + cache_token_idx = cur_batch_ctx_len + start_n + offs_n
382 + # Padded lanes of the last tile can index past the sequence end;
383 + # mask the block-table load so they don't read past this batch's
384 + # row (they are dropped by the K/V load mask below anyway).
385 + cache_token_valid = (start_n + offs_n) < cur_batch_query_len
386 + off_k_cur, off_v_cur = _paged_kv_cache_offsets(
387 + B_Loc,
388 + cur_batch,
389 + cache_token_idx,
390 + cache_token_valid,
391 + offs_d,
392 + cur_kv_head,
393 + x,
394 + stride_b_loc_b,
395 + stride_b_loc_s,
396 + stride_k_cache_bs,
397 + stride_k_cache_h,
398 + stride_k_cache_d,
399 + stride_k_cache_bl,
400 + stride_k_cache_x,
401 + stride_v_cache_bs,
402 + stride_v_cache_h,
403 + stride_v_cache_d,
404 + stride_v_cache_bl,
405 + PHYSICAL_BLOCK_SIZE,
406 + MASK_BLOCK_TABLE=True,
407 + )
408 + k_cur_load = tl.load(
409 + K_cache + off_k_cur,
410 + mask=dim_mask[:, None]
411 + & ((start_n + offs_n[None, :]) < cur_batch_query_len),
412 + other=0.0,
413 + )
414 + if k_cur_load.dtype.is_fp8():
415 + k = (k_cur_load.to(tl.float32) * tl.load(k_scale)).to(q.dtype)
416 + else:
417 + k = k_cur_load
418 + else:
419 + k = tl.load(
420 + k_ptrs + (cur_batch_in_all_start_index + start_n) * stride_kbs,
421 + mask=dim_mask[:, None]
422 + & ((start_n + offs_n[None, :]) < cur_batch_query_len),
423 + other=0.0,
424 + )
425
426 qk = tl.zeros([BLOCK_M, BLOCK_N], dtype=tl.float32)
427 qk = tl.dot(q, k, acc=qk, input_precision=IN_PRECISION)
@@ -333,12 +449,25 @@ def _fwd_kernel(
449 acc = acc * alpha[:, None]
450
451 # update acc
336 - v = tl.load(
337 - v_ptrs + (cur_batch_in_all_start_index + start_n) * stride_vbs,
338 - mask=dim_mask[None, :]
339 - & ((start_n + offs_n[:, None]) < cur_batch_query_len),
340 - other=0.0,
341 - )
452 + if KV_FROM_CACHE:
453 + # off_v_cur was computed together with off_k_cur above.
454 + v_cur_load = tl.load(
455 + V_cache + off_v_cur,
456 + mask=dim_mask[None, :]
457 + & ((start_n + offs_n[:, None]) < cur_batch_query_len),
458 + other=0.0,
459 + )
460 + if v_cur_load.dtype.is_fp8():
461 + v = (v_cur_load.to(tl.float32) * tl.load(v_scale)).to(q.dtype)
462 + else:
463 + v = v_cur_load
464 + else:
465 + v = tl.load(
466 + v_ptrs + (cur_batch_in_all_start_index + start_n) * stride_vbs,
467 + mask=dim_mask[None, :]
468 + & ((start_n + offs_n[:, None]) < cur_batch_query_len),
469 + other=0.0,
470 + )
471 p = p.to(v.dtype)
472
473 acc = tl.dot(p, v, acc=acc, input_precision=IN_PRECISION)
@@ -703,8 +832,27 @@ def context_attention_fwd(
832 FP8 KV Cache prefill kernel"
833 )
834
835 + assert (k is None) == (v is None), (
836 + "k and v must both be None (cached-K/V path) or both be tensors"
837 + )
838 + kv_from_cache = k is None
839 + if kv_from_cache:
840 + if alibi_slopes is not None:
841 + raise NotImplementedError(
842 + "context_attention_fwd with cached K/V (key=None) is not "
843 + "supported together with ALiBi slopes."
844 + )
845 + # k_cache: [num_blocks, num_kv_heads, head_size // x, block_size, x]
846 + num_kv_heads = k_cache.shape[1]
847 + Lq = Lk = Lv = q.shape[-1]
848 + # q is a never-dereferenced placeholder for the dense K/V pointer args.
849 + k = q
850 + v = q
851 + else:
852 + num_kv_heads = k.shape[1]
853 + Lq, Lk, Lv = q.shape[-1], k.shape[-1], v.shape[-1]
854 +
855 # shape constraints
707 - Lq, Lk, Lv = q.shape[-1], k.shape[-1], v.shape[-1]
856 assert Lq == Lk and Lk == Lv
857 # round up Lk to a power of 2 - this is required for Triton block size
858 Lk_padded = triton.next_power_of_2(Lk)
@@ -712,7 +860,7 @@ def context_attention_fwd(
860 if sm_scale is None:
861 sm_scale = 1.0 / (Lq**0.5)
862 batch, head = b_seq_len.shape[0], q.shape[1]
715 - num_queries_per_kv = q.shape[1] // k.shape[1]
863 + num_queries_per_kv = q.shape[1] // num_kv_heads
864
865 assert batch + 1 == len(b_start_loc)
866
@@ -873,6 +1021,7 @@ def context_attention_fwd(
1021 num_stages=1,
1022 USE_SINKS=sinks is not None,
1023 CAUSAL=causal,
1024 + KV_FROM_CACHE=kv_from_cache,
1025 **extra_kargs,
1026 )
1027 return