| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | """fp8 query and index-K cache on the Triton indexer must agree with bf16. |
| 4 | |
| 5 | The MiniMax M3 lightning-indexer scores only feed a top-k block ranking (the |
| 6 | attention itself reads the main KV cache), so an fp8 (e4m3) side cache trades |
| 7 | ~2% relative score error for half the per-step index-K read bandwidth — the |
| 8 | dominant linear-in-context decode cost at long context. #45892 enabled fp8 |
| 9 | index caches on the SM100 MSA path; this covers the Triton impl used |
| 10 | everywhere else (e.g. SM120), for both the decode and prefill scorers. |
| 11 | """ |
| 12 | |
| 13 | import pytest |
| 14 | import torch |
| 15 | |
| 16 | from vllm.models.minimax_m3.common.ops.index_topk import ( |
| 17 | SPARSE_BLOCK_SIZE, |
| 18 | minimax_m3_index_decode, |
| 19 | minimax_m3_index_score, |
| 20 | minimax_m3_index_topk, |
| 21 | ) |
| 22 | from vllm.platforms import current_platform |
| 23 | |
| 24 | FP8_CUDA_SUPPORTED = current_platform.is_cuda() and current_platform.supports_fp8() |
| 25 | PAGE = SPARSE_BLOCK_SIZE |
| 26 | HEAD_DIM = 128 |
| 27 | HEADS = 1 |
| 28 | TOPK = 16 |
| 29 | NUM_PAGES = 100 |
| 30 | SEQ_LEN = 96 * PAGE + 37 |
| 31 | PLANTED = (7, 40, 88) |
| 32 | |
| 33 | |
| 34 | def _make_inputs(device: str) -> tuple[torch.Tensor, torch.Tensor]: |
| 35 | torch.manual_seed(3) |
| 36 | keys = torch.randn(NUM_PAGES * PAGE, HEAD_DIM, dtype=torch.bfloat16, device=device) |
| 37 | # RMS-normalize like the real (post index_k_norm) keys. |
| 38 | keys = keys * keys.float().pow(2).mean(-1, keepdim=True).add(1e-6).rsqrt().to( |
| 39 | torch.bfloat16 |
| 40 | ) |
| 41 | q = torch.randn(1, HEADS, HEAD_DIM, dtype=torch.bfloat16, device=device) |
| 42 | for b in PLANTED: |
| 43 | keys[b * PAGE + 5] = (q[0, 0] * 3).to(torch.bfloat16) |
| 44 | return q, keys |
| 45 | |
| 46 | |
| 47 | def _check_selection(top_bf16: set[int], top_fp8: set[int]) -> None: |
| 48 | # Planted (clearly relevant) blocks must never be lost. |
| 49 | assert all(b in top_fp8 for b in PLANTED) |
| 50 | # Selection may differ only at the noise floor (near-tied filler blocks). |
| 51 | assert len(top_bf16 & top_fp8) >= TOPK - 2 |
| 52 | |
| 53 | |
| 54 | @pytest.mark.skipif(not FP8_CUDA_SUPPORTED, reason="CUDA FP8 support required") |
| 55 | @torch.inference_mode() |
| 56 | def test_fp8_index_query_and_cache_decode_topk_matches_bf16() -> None: |
| 57 | dev = "cuda" |
| 58 | q, keys = _make_inputs(dev) |
| 59 | block_table = torch.arange(NUM_PAGES, dtype=torch.int32, device=dev).unsqueeze(0) |
| 60 | seq_lens = torch.tensor([SEQ_LEN], dtype=torch.int32, device=dev) |
| 61 | |
| 62 | def select(cache: torch.Tensor) -> set[int]: |
| 63 | query = q.to(cache.dtype) |
| 64 | idx = minimax_m3_index_decode( |
| 65 | query, |
| 66 | cache.view(NUM_PAGES, PAGE, HEAD_DIM).contiguous(), |
| 67 | block_table, |
| 68 | seq_lens, |
| 69 | SEQ_LEN, |
| 70 | TOPK, |
| 71 | 0, |
| 72 | 0, |
| 73 | HEADS, |
| 74 | 1, |
| 75 | 1, |
| 76 | ) |
| 77 | return {int(b) for b in idx[0, 0].tolist() if b >= 0} |
| 78 | |
| 79 | _check_selection(select(keys), select(keys.to(torch.float8_e4m3fn))) |
| 80 | |
| 81 | |
| 82 | @pytest.mark.skipif(not FP8_CUDA_SUPPORTED, reason="CUDA FP8 support required") |
| 83 | @torch.inference_mode() |
| 84 | def test_fp8_index_query_and_cache_prefill_topk_matches_bf16() -> None: |
| 85 | dev = "cuda" |
| 86 | q, keys = _make_inputs(dev) |
| 87 | block_table = torch.arange(NUM_PAGES, dtype=torch.int32, device=dev).unsqueeze(0) |
| 88 | seq_lens = torch.tensor([SEQ_LEN], dtype=torch.int32, device=dev) |
| 89 | # single decode-like query token at the end of the sequence |
| 90 | cu_seqlens_q = torch.tensor([0, 1], dtype=torch.int32, device=dev) |
| 91 | prefix_lens = torch.tensor([SEQ_LEN - 1], dtype=torch.int32, device=dev) |
| 92 | |
| 93 | def select(cache: torch.Tensor) -> set[int]: |
| 94 | query = q.to(cache.dtype) |
| 95 | score = minimax_m3_index_score( |
| 96 | query, |
| 97 | cache.view(NUM_PAGES, PAGE, HEAD_DIM).contiguous(), |
| 98 | block_table, |
| 99 | cu_seqlens_q, |
| 100 | seq_lens, |
| 101 | prefix_lens, |
| 102 | 1, |
| 103 | SEQ_LEN, |
| 104 | HEADS, |
| 105 | ) |
| 106 | idx = minimax_m3_index_topk(score, cu_seqlens_q, prefix_lens, 1, TOPK, 0, 0) |
| 107 | return {int(b) for b in idx[0, 0].tolist() if b >= 0} |
| 108 | |
| 109 | _check_selection(select(keys), select(keys.to(torch.float8_e4m3fn))) |