| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | |
| 4 | import torch |
| 5 | |
| 6 | import vllm.model_executor.offloader as offloader |
| 7 | from vllm.models.deepseek_v4.amd.rocm import ( |
| 8 | DeepseekV4ROCMAiterMLAAttention, |
| 9 | ) |
| 10 | |
| 11 | |
| 12 | class _WeightOnlyLinear(torch.nn.Module): |
| 13 | def __init__(self, weight: torch.Tensor) -> None: |
| 14 | super().__init__() |
| 15 | self.weight = torch.nn.Parameter(weight, requires_grad=False) |
| 16 | |
| 17 | |
| 18 | class _Compressor(torch.nn.Module): |
| 19 | def __init__(self, weight: torch.Tensor) -> None: |
| 20 | super().__init__() |
| 21 | self.fused_wkv_wgate = _WeightOnlyLinear(weight) |
| 22 | |
| 23 | |
| 24 | class _IndexerWeightsProjection(torch.nn.Module): |
| 25 | def forward(self, hidden_states: torch.Tensor) -> tuple[torch.Tensor, None]: |
| 26 | return hidden_states.sum(dim=-1, keepdim=True), None |
| 27 | |
| 28 | |
| 29 | class _Indexer(torch.nn.Module): |
| 30 | def __init__(self, compressor_weight: torch.Tensor) -> None: |
| 31 | super().__init__() |
| 32 | self.compressor = _Compressor(compressor_weight) |
| 33 | self.weights_proj = _IndexerWeightsProjection() |
| 34 | |
| 35 | |
| 36 | def _make_attention( |
| 37 | main_weight: torch.Tensor, |
| 38 | indexer_weight: torch.Tensor, |
| 39 | ) -> DeepseekV4ROCMAiterMLAAttention: |
| 40 | attention = DeepseekV4ROCMAiterMLAAttention.__new__(DeepseekV4ROCMAiterMLAAttention) |
| 41 | torch.nn.Module.__init__(attention) |
| 42 | attention.compressor = _Compressor(main_weight) |
| 43 | attention.indexer = _Indexer(indexer_weight) |
| 44 | attention.register_buffer("_fused_compressor_weight", None, persistent=False) |
| 45 | attention._fused_compressor_split_sizes = None |
| 46 | return attention |
| 47 | |
| 48 | |
| 49 | def test_prepare_compressor_gemm_fusion_aliases_original_weights(): |
| 50 | main_value = torch.arange(12, dtype=torch.float32).reshape(4, 3) |
| 51 | indexer_value = torch.arange(6, dtype=torch.float32).reshape(2, 3) + 100 |
| 52 | attention = _make_attention(main_value, indexer_value) |
| 53 | main_weight = attention.compressor.fused_wkv_wgate.weight |
| 54 | indexer_weight = attention.indexer.compressor.fused_wkv_wgate.weight |
| 55 | |
| 56 | assert attention.prepare_compressor_gemm_fusion() is True |
| 57 | |
| 58 | fused_weight = attention._fused_compressor_weight |
| 59 | assert fused_weight is not None |
| 60 | assert attention._fused_compressor_split_sizes == (4, 2) |
| 61 | torch.testing.assert_close(fused_weight[:4], main_value) |
| 62 | torch.testing.assert_close(fused_weight[4:], indexer_value) |
| 63 | |
| 64 | fused_storage = fused_weight.untyped_storage().data_ptr() |
| 65 | assert main_weight.untyped_storage().data_ptr() == fused_storage |
| 66 | assert indexer_weight.untyped_storage().data_ptr() == fused_storage |
| 67 | assert main_weight.storage_offset() == 0 |
| 68 | assert indexer_weight.storage_offset() == main_weight.numel() |
| 69 | |
| 70 | state_dict = attention.state_dict() |
| 71 | assert "_fused_compressor_weight" not in state_dict |
| 72 | torch.testing.assert_close( |
| 73 | state_dict["compressor.fused_wkv_wgate.weight"], main_value |
| 74 | ) |
| 75 | torch.testing.assert_close( |
| 76 | state_dict["indexer.compressor.fused_wkv_wgate.weight"], |
| 77 | indexer_value, |
| 78 | ) |
| 79 | |
| 80 | fused_data_ptr = fused_weight.data_ptr() |
| 81 | assert attention.prepare_compressor_gemm_fusion() is False |
| 82 | assert attention._fused_compressor_weight.data_ptr() == fused_data_ptr |
| 83 | |
| 84 | |
| 85 | def test_prepare_compressor_gemm_fusion_skips_weight_offloading(monkeypatch): |
| 86 | attention = _make_attention(torch.ones(4, 3), torch.ones(2, 3)) |
| 87 | main_weight = attention.compressor.fused_wkv_wgate.weight |
| 88 | indexer_weight = attention.indexer.compressor.fused_wkv_wgate.weight |
| 89 | |
| 90 | monkeypatch.setattr(offloader, "get_offloader", object) |
| 91 | |
| 92 | assert attention.prepare_compressor_gemm_fusion() is False |
| 93 | assert attention._fused_compressor_weight is None |
| 94 | assert main_weight.untyped_storage().data_ptr() != ( |
| 95 | indexer_weight.untyped_storage().data_ptr() |
| 96 | ) |
| 97 | |
| 98 | |
| 99 | def test_fused_input_projection_uses_one_mm_and_returns_expected_tuple( |
| 100 | monkeypatch, |
| 101 | ): |
| 102 | main_weight = torch.tensor([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]], dtype=torch.float32) |
| 103 | indexer_weight = torch.tensor([[0.0, 0.0, 1.0]], dtype=torch.float32) |
| 104 | attention = _make_attention(main_weight, indexer_weight) |
| 105 | assert attention.prepare_compressor_gemm_fusion() is True |
| 106 | |
| 107 | hidden_states = torch.tensor( |
| 108 | [[2.0, 3.0, 5.0], [7.0, 11.0, 13.0]], dtype=torch.float32 |
| 109 | ) |
| 110 | qr_kv = torch.full((2, 4), 17.0) |
| 111 | monkeypatch.setattr( |
| 112 | DeepseekV4ROCMAiterMLAAttention, |
| 113 | "_fused_wqa_wkv_gemm", |
| 114 | lambda self, hidden: qr_kv, |
| 115 | ) |
| 116 | |
| 117 | mm_calls: list[tuple[torch.Tensor, torch.Tensor, torch.dtype | None]] = [] |
| 118 | |
| 119 | def cpu_mm_with_out_dtype( |
| 120 | left: torch.Tensor, |
| 121 | right: torch.Tensor, |
| 122 | *, |
| 123 | out_dtype: torch.dtype | None = None, |
| 124 | ) -> torch.Tensor: |
| 125 | mm_calls.append((left, right, out_dtype)) |
| 126 | return torch.matmul(left, right).to(out_dtype) |
| 127 | |
| 128 | monkeypatch.setattr(torch, "mm", cpu_mm_with_out_dtype) |
| 129 | |
| 130 | qr_out, kv_score, indexer_kv_score, indexer_weights = ( |
| 131 | attention._run_parallel_input_projections(hidden_states) |
| 132 | ) |
| 133 | |
| 134 | assert qr_out is qr_kv |
| 135 | torch.testing.assert_close(kv_score, hidden_states[:, :2]) |
| 136 | torch.testing.assert_close(indexer_kv_score, hidden_states[:, 2:]) |
| 137 | torch.testing.assert_close(indexer_weights, hidden_states.sum(dim=-1, keepdim=True)) |
| 138 | assert len(mm_calls) == 1 |
| 139 | assert mm_calls[0][0] is hidden_states |
| 140 | assert mm_calls[0][2] is torch.float32 |
| 141 | assert mm_calls[0][1].shape == (3, 3) |