| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | """CPU-only tests for FlashInfer sparse MLA backend constraints.""" |
| 4 | |
| 5 | from types import SimpleNamespace |
| 6 | from unittest.mock import patch |
| 7 | |
| 8 | import pytest |
| 9 | import torch |
| 10 | |
| 11 | from vllm.v1.attention.backends.mla.flashinfer_mla_sparse import ( |
| 12 | _DEFAULT_WORKSPACE_BUFFER_SIZE, |
| 13 | FlashInferMLASparseTRTLLMBackend, |
| 14 | _required_workspace_bytes, |
| 15 | compute_trtllm_sparse_mla_workspace_bytes, |
| 16 | ) |
| 17 | |
| 18 | # Reporter config from vllm#50781: GLM-5.2 (64 q heads), TP=8, DCP=8, |
| 19 | # max_num_batched_tokens=16384, FlashInfer 0.6.14. |
| 20 | _REPORTER_HEADS_PER_RANK = 64 // 8 |
| 21 | _REPORTER_DCP = 8 |
| 22 | _REPORTER_MAX_TOKENS = 16384 |
| 23 | # From the reported crash: trtllm-gen requested exactly this many bytes for |
| 24 | # trtllm_gen_softmax_workspace after an 8 MiB counter carve had left |
| 25 | # 404,750,336 of the 413,138,944-byte default buffer. |
| 26 | _REPORTER_OBSERVED_SOFTMAX_BYTES = 1_611_661_312 |
| 27 | _REPORTER_OBSERVED_PRECARVE_BYTES = 8_388_608 |
| 28 | |
| 29 | |
| 30 | def test_reporter_softmax_carve_is_reproduced_byte_exactly(): |
| 31 | # The crashing step scheduled 12288 tokens (a single 12K-token request); |
| 32 | # the softmax slab formula must reproduce the kernel's request exactly: |
| 33 | # 8 * (8 heads/rank * 8 dcp) * 12288 tokens * 256 + 1 MiB guard. |
| 34 | softmax_bytes = compute_trtllm_sparse_mla_workspace_bytes( |
| 35 | base_workspace_bytes=0, |
| 36 | dcp_world_size=_REPORTER_DCP, |
| 37 | num_heads_per_rank=_REPORTER_HEADS_PER_RANK, |
| 38 | max_num_batched_tokens=12288, |
| 39 | ) |
| 40 | assert softmax_bytes == _REPORTER_OBSERVED_SOFTMAX_BYTES |
| 41 | |
| 42 | |
| 43 | def test_reporter_config_covers_observed_overflow(): |
| 44 | computed = compute_trtllm_sparse_mla_workspace_bytes( |
| 45 | base_workspace_bytes=_DEFAULT_WORKSPACE_BUFFER_SIZE, |
| 46 | dcp_world_size=_REPORTER_DCP, |
| 47 | num_heads_per_rank=_REPORTER_HEADS_PER_RANK, |
| 48 | max_num_batched_tokens=_REPORTER_MAX_TOKENS, |
| 49 | ) |
| 50 | assert computed >= ( |
| 51 | _REPORTER_OBSERVED_SOFTMAX_BYTES + _REPORTER_OBSERVED_PRECARVE_BYTES |
| 52 | ) |
| 53 | |
| 54 | |
| 55 | def test_non_dcp_size_is_unchanged(): |
| 56 | computed = compute_trtllm_sparse_mla_workspace_bytes( |
| 57 | base_workspace_bytes=_DEFAULT_WORKSPACE_BUFFER_SIZE, |
| 58 | dcp_world_size=1, |
| 59 | num_heads_per_rank=128, |
| 60 | max_num_batched_tokens=65536, |
| 61 | ) |
| 62 | assert computed == _DEFAULT_WORKSPACE_BUFFER_SIZE |
| 63 | |
| 64 | |
| 65 | def test_combined_pcp_dcp_rejects_flashinfer_sparse(): |
| 66 | config = SimpleNamespace( |
| 67 | parallel_config=SimpleNamespace( |
| 68 | prefill_context_parallel_size=4, |
| 69 | decode_context_parallel_size=4, |
| 70 | ), |
| 71 | model_config=None, |
| 72 | ) |
| 73 | with patch("vllm.config.get_current_vllm_config", return_value=config): |
| 74 | reason = FlashInferMLASparseTRTLLMBackend.supports_combination( |
| 75 | head_size=576, |
| 76 | dtype=torch.bfloat16, |
| 77 | kv_cache_dtype="fp8", |
| 78 | block_size=64, |
| 79 | use_mla=True, |
| 80 | has_sink=False, |
| 81 | use_sparse=True, |
| 82 | use_mm_prefix=False, |
| 83 | device_capability=SimpleNamespace(major=10), |
| 84 | ) |
| 85 | |
| 86 | assert reason is not None |
| 87 | assert "use FLASHMLA_SPARSE" in reason |
| 88 | |
| 89 | |
| 90 | @pytest.mark.parametrize("pcp_size,dcp_size", [(4, 1), (1, 4)]) |
| 91 | def test_single_context_parallel_mode_allows_flashinfer_sparse( |
| 92 | pcp_size: int, dcp_size: int |
| 93 | ): |
| 94 | config = SimpleNamespace( |
| 95 | parallel_config=SimpleNamespace( |
| 96 | prefill_context_parallel_size=pcp_size, |
| 97 | decode_context_parallel_size=dcp_size, |
| 98 | ), |
| 99 | model_config=None, |
| 100 | ) |
| 101 | with patch("vllm.config.get_current_vllm_config", return_value=config): |
| 102 | reason = FlashInferMLASparseTRTLLMBackend.supports_combination( |
| 103 | head_size=576, |
| 104 | dtype=torch.bfloat16, |
| 105 | kv_cache_dtype="fp8", |
| 106 | block_size=64, |
| 107 | use_mla=True, |
| 108 | has_sink=False, |
| 109 | use_sparse=True, |
| 110 | use_mm_prefix=False, |
| 111 | device_capability=SimpleNamespace(major=10), |
| 112 | ) |
| 113 | |
| 114 | assert reason is None |
| 115 | |
| 116 | |
| 117 | def test_default_constant_matches_envs_default(monkeypatch): |
| 118 | from vllm import envs |
| 119 | |
| 120 | monkeypatch.delenv("VLLM_FLASHINFER_WORKSPACE_BUFFER_SIZE", raising=False) |
| 121 | assert _DEFAULT_WORKSPACE_BUFFER_SIZE == envs.VLLM_FLASHINFER_WORKSPACE_BUFFER_SIZE |
| 122 | |
| 123 | |
| 124 | def test_env_unset_returns_computed(monkeypatch): |
| 125 | monkeypatch.delenv("VLLM_FLASHINFER_WORKSPACE_BUFFER_SIZE", raising=False) |
| 126 | required = _required_workspace_bytes( |
| 127 | dcp_world_size=_REPORTER_DCP, |
| 128 | num_heads_per_rank=_REPORTER_HEADS_PER_RANK, |
| 129 | max_num_batched_tokens=_REPORTER_MAX_TOKENS, |
| 130 | ) |
| 131 | assert required == compute_trtllm_sparse_mla_workspace_bytes( |
| 132 | _DEFAULT_WORKSPACE_BUFFER_SIZE, |
| 133 | _REPORTER_DCP, |
| 134 | _REPORTER_HEADS_PER_RANK, |
| 135 | _REPORTER_MAX_TOKENS, |
| 136 | ) |
| 137 | |
| 138 | |
| 139 | def test_env_override_below_computed_is_respected_with_warning(monkeypatch): |
| 140 | override = 100 * 1024 * 1024 |
| 141 | monkeypatch.setenv("VLLM_FLASHINFER_WORKSPACE_BUFFER_SIZE", str(override)) |
| 142 | with patch( |
| 143 | "vllm.v1.attention.backends.mla.flashinfer_mla_sparse.logger" |
| 144 | ) as mock_logger: |
| 145 | required = _required_workspace_bytes( |
| 146 | dcp_world_size=_REPORTER_DCP, |
| 147 | num_heads_per_rank=_REPORTER_HEADS_PER_RANK, |
| 148 | max_num_batched_tokens=_REPORTER_MAX_TOKENS, |
| 149 | ) |
| 150 | assert required == override |
| 151 | mock_logger.warning_once.assert_called_once() |
| 152 | |
| 153 | |
| 154 | def test_env_override_above_computed_is_respected_without_warning(monkeypatch): |
| 155 | override = 8 * 1024 * 1024 * 1024 |
| 156 | monkeypatch.setenv("VLLM_FLASHINFER_WORKSPACE_BUFFER_SIZE", str(override)) |
| 157 | with patch( |
| 158 | "vllm.v1.attention.backends.mla.flashinfer_mla_sparse.logger" |
| 159 | ) as mock_logger: |
| 160 | required = _required_workspace_bytes( |
| 161 | dcp_world_size=_REPORTER_DCP, |
| 162 | num_heads_per_rank=_REPORTER_HEADS_PER_RANK, |
| 163 | max_num_batched_tokens=_REPORTER_MAX_TOKENS, |
| 164 | ) |
| 165 | assert required == override |
| 166 | mock_logger.warning_once.assert_not_called() |