| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | |
| 4 | import pytest |
| 5 | import torch |
| 6 | import torch.nn.functional as F |
| 7 | |
| 8 | from vllm.models.kimi_k3.amd.ops.attn_res import attn_res |
| 9 | from vllm.platforms import current_platform |
| 10 | |
| 11 | pytestmark = pytest.mark.skipif( |
| 12 | not current_platform.is_rocm(), |
| 13 | reason="AMD AttnRes requires ROCm", |
| 14 | ) |
| 15 | |
| 16 | |
| 17 | def _randn_with_row_padding(*shape: int, padding: int = 0) -> torch.Tensor: |
| 18 | storage = torch.randn( |
| 19 | *shape[:-1], |
| 20 | shape[-1] + padding, |
| 21 | device="cuda", |
| 22 | dtype=torch.bfloat16, |
| 23 | ) |
| 24 | return storage[..., : shape[-1]] |
| 25 | |
| 26 | |
| 27 | def _reference( |
| 28 | prefix: torch.Tensor, |
| 29 | blocks: torch.Tensor, |
| 30 | norm_weight: torch.Tensor, |
| 31 | qk_weight: torch.Tensor, |
| 32 | num_blocks: int, |
| 33 | eps: float, |
| 34 | ) -> torch.Tensor: |
| 35 | hidden_size = prefix.shape[-1] |
| 36 | values = torch.cat((blocks[:, :num_blocks], prefix.unsqueeze(1)), dim=1) |
| 37 | keys = F.rms_norm(values, (hidden_size,), norm_weight, eps) |
| 38 | probs = (keys @ qk_weight).softmax(dim=-1) |
| 39 | return torch.matmul(probs.unsqueeze(1), values).squeeze(1) |
| 40 | |
| 41 | |
| 42 | @pytest.mark.parametrize( |
| 43 | ( |
| 44 | "num_tokens", |
| 45 | "num_blocks", |
| 46 | "block_capacity", |
| 47 | "hidden_size", |
| 48 | "row_padding", |
| 49 | ), |
| 50 | [ |
| 51 | pytest.param(0, 3, 5, 128, 0, id="empty"), |
| 52 | pytest.param(1, 1, 2, 128, 0, id="decode-single"), |
| 53 | pytest.param(17, 4, 6, 1024, 7, id="decode-padded"), |
| 54 | pytest.param(320, 8, 10, 7168, 0, id="prefill-full"), |
| 55 | ], |
| 56 | ) |
| 57 | def test_amd_attn_res_matches_reference( |
| 58 | num_tokens: int, |
| 59 | num_blocks: int, |
| 60 | block_capacity: int, |
| 61 | hidden_size: int, |
| 62 | row_padding: int, |
| 63 | ) -> None: |
| 64 | eps = 1e-5 |
| 65 | prefix = _randn_with_row_padding(num_tokens, hidden_size, padding=row_padding) |
| 66 | blocks = _randn_with_row_padding( |
| 67 | num_tokens, |
| 68 | block_capacity, |
| 69 | hidden_size, |
| 70 | padding=row_padding, |
| 71 | ) |
| 72 | norm_weight = 1 + 0.1 * torch.randn( |
| 73 | hidden_size, device="cuda", dtype=torch.bfloat16 |
| 74 | ) |
| 75 | qk_weight = ( |
| 76 | torch.randn(hidden_size, device="cuda", dtype=torch.bfloat16) / hidden_size**0.5 |
| 77 | ) |
| 78 | expected = _reference( |
| 79 | prefix, |
| 80 | blocks, |
| 81 | norm_weight, |
| 82 | qk_weight, |
| 83 | num_blocks, |
| 84 | eps, |
| 85 | ) |
| 86 | original_prefix = prefix.clone() |
| 87 | original_blocks = blocks.clone() |
| 88 | |
| 89 | actual = attn_res( |
| 90 | prefix, |
| 91 | None, |
| 92 | blocks, |
| 93 | norm_weight, |
| 94 | qk_weight, |
| 95 | None, |
| 96 | num_blocks, |
| 97 | -1, |
| 98 | eps, |
| 99 | 0.0, |
| 100 | ) |
| 101 | |
| 102 | torch.testing.assert_close(actual, expected, atol=8e-2, rtol=3e-2) |
| 103 | torch.testing.assert_close(prefix, original_prefix, atol=0, rtol=0) |
| 104 | torch.testing.assert_close(blocks, original_blocks, atol=0, rtol=0) |
| 105 | assert actual.shape == prefix.shape |
| 106 | assert actual.is_contiguous() |
| 107 | |
| 108 | |
| 109 | @pytest.mark.parametrize( |
| 110 | ( |
| 111 | "num_tokens", |
| 112 | "num_blocks", |
| 113 | "hidden_size", |
| 114 | "has_delta", |
| 115 | "write_block", |
| 116 | "apply_output_norm", |
| 117 | ), |
| 118 | [ |
| 119 | pytest.param(1, 0, 128, False, True, True, id="empty-write-norm"), |
| 120 | pytest.param(7, 1, 1024, True, False, True, id="single-add-norm"), |
| 121 | pytest.param(17, 5, 7168, True, True, True, id="padded-write-add"), |
| 122 | pytest.param(3, 8, 7168, True, False, True, id="full-add-norm"), |
| 123 | pytest.param(320, 4, 7168, True, False, False, id="prefill-add"), |
| 124 | ], |
| 125 | ) |
| 126 | def test_amd_attn_res_fused_contract( |
| 127 | num_tokens: int, |
| 128 | num_blocks: int, |
| 129 | hidden_size: int, |
| 130 | has_delta: bool, |
| 131 | write_block: bool, |
| 132 | apply_output_norm: bool, |
| 133 | ) -> None: |
| 134 | torch.manual_seed(42) |
| 135 | eps = 1e-5 |
| 136 | output_eps = 2e-5 |
| 137 | block_capacity = 9 |
| 138 | prefix = _randn_with_row_padding(num_tokens, hidden_size, padding=7) |
| 139 | delta = ( |
| 140 | _randn_with_row_padding(num_tokens, hidden_size, padding=11) |
| 141 | if has_delta |
| 142 | else None |
| 143 | ) |
| 144 | blocks = _randn_with_row_padding( |
| 145 | num_tokens, block_capacity, hidden_size, padding=13 |
| 146 | ) |
| 147 | norm_weight = 1 + 0.1 * torch.randn( |
| 148 | hidden_size, device="cuda", dtype=torch.bfloat16 |
| 149 | ) |
| 150 | qk_weight = ( |
| 151 | torch.randn(hidden_size, device="cuda", dtype=torch.bfloat16) / hidden_size**0.5 |
| 152 | ) |
| 153 | output_norm_weight = ( |
| 154 | 1 + 0.1 * torch.randn(hidden_size, device="cuda", dtype=torch.bfloat16) |
| 155 | if apply_output_norm |
| 156 | else None |
| 157 | ) |
| 158 | expected_prefix = prefix.clone() |
| 159 | if delta is not None: |
| 160 | expected_prefix = expected_prefix + delta |
| 161 | values = torch.cat( |
| 162 | (blocks[:, :num_blocks].clone(), expected_prefix.unsqueeze(1)), dim=1 |
| 163 | ) |
| 164 | keys = F.rms_norm(values.float(), (hidden_size,), norm_weight.float(), eps) |
| 165 | probs = (keys @ qk_weight.float()).softmax(dim=-1) |
| 166 | expected = torch.matmul(probs.unsqueeze(1), values.float()).squeeze(1) |
| 167 | if output_norm_weight is not None: |
| 168 | expected = F.rms_norm( |
| 169 | expected, (hidden_size,), output_norm_weight.float(), output_eps |
| 170 | ) |
| 171 | expected = expected.to(prefix.dtype) |
| 172 | original_blocks = blocks.clone() |
| 173 | block_write_idx = num_blocks if write_block else -1 |
| 174 | |
| 175 | actual = attn_res( |
| 176 | prefix, |
| 177 | delta, |
| 178 | blocks, |
| 179 | norm_weight, |
| 180 | qk_weight, |
| 181 | output_norm_weight, |
| 182 | num_blocks, |
| 183 | block_write_idx, |
| 184 | eps, |
| 185 | output_eps, |
| 186 | ) |
| 187 | |
| 188 | torch.testing.assert_close(actual, expected, atol=8e-2, rtol=3e-2) |
| 189 | torch.testing.assert_close(prefix, expected_prefix, atol=0, rtol=0) |
| 190 | if write_block: |
| 191 | original_blocks[:, block_write_idx].copy_(expected_prefix) |
| 192 | torch.testing.assert_close(blocks, original_blocks, atol=0, rtol=0) |
| 193 | assert actual.is_contiguous() |