[CPU][MLA] Fix prefill backend selection so MLA runs end-to-end on CPU (#51471)

Signed-off-by: baoloongmao <baoloongmao@tencent.com> Co-authored-by: Codex <codex@openai.com>

maobaolong committed Aug 28, 2026 at 12:36 UTC 6f7df92a8e6cdc74a725b8f10b4d0b48ba2b37ef
9 files changed +442 -69
tests/v1/attention/test_cpu_mla_backend.py
+164 -2
@@ -19,6 +19,51 @@ import pytest
19 import torch
20
21 from vllm.platforms import current_platform
22 +from vllm.v1.attention.backends.mla.prefill.cpu_sdpa import (
23 + CPUSDPAMLAPrefillBackend,
24 +)
25 +from vllm.v1.attention.backends.mla.prefill.selector import (
26 + get_mla_prefill_backend,
27 +)
28 +
29 +
30 +def _reference_ragged_attention(
31 + q: torch.Tensor,
32 + k: torch.Tensor,
33 + v: torch.Tensor,
34 + q_cu_seq_lens: torch.Tensor,
35 + kv_cu_seq_lens: torch.Tensor,
36 + *,
37 + scale: float,
38 + causal: bool,
39 +) -> tuple[torch.Tensor, torch.Tensor]:
40 + outputs: list[torch.Tensor] = []
41 + lses: list[torch.Tensor] = []
42 + num_reqs = q_cu_seq_lens.numel() - 1
43 + for req_idx in range(num_reqs):
44 + q_start = int(q_cu_seq_lens[req_idx])
45 + q_end = int(q_cu_seq_lens[req_idx + 1])
46 + kv_start = int(kv_cu_seq_lens[req_idx])
47 + kv_end = int(kv_cu_seq_lens[req_idx + 1])
48 +
49 + q_r = q[q_start:q_end].transpose(0, 1).float()
50 + k_r = k[kv_start:kv_end].transpose(0, 1).float()
51 + v_r = v[kv_start:kv_end].transpose(0, 1).float()
52 +
53 + scores = torch.matmul(q_r, k_r.transpose(-2, -1)) * scale
54 + if causal:
55 + causal_mask = torch.ones(
56 + (q_end - q_start, kv_end - kv_start),
57 + dtype=torch.bool,
58 + device=scores.device,
59 + ).triu(1)
60 + scores.masked_fill_(causal_mask.unsqueeze(0), float("-inf"))
61 +
62 + probs = torch.softmax(scores, dim=-1)
63 + outputs.append(torch.matmul(probs, v_r).transpose(0, 1).to(v.dtype))
64 + lses.append(torch.logsumexp(scores, dim=-1))
65 +
66 + return torch.cat(outputs, dim=0), torch.cat(lses, dim=1)
67
68
69 @pytest.mark.skipif(not current_platform.is_cpu(), reason="CPU only")
@@ -58,7 +103,7 @@ def test_kv_cache_cpu_write() -> None:
103
104 @pytest.mark.cpu_model
105 @pytest.mark.skipif(not current_platform.is_cpu(), reason="CPU only")
61 -def test_cpu_mla_backend_smoke() -> None:
106 +def test_cpu_mla_backend_smoke(tmp_path) -> None:
107 # Shrink the architecture aggressively so the test is CI-friendly.
108 from vllm import LLM, SamplingParams
109
@@ -70,19 +115,136 @@ def test_cpu_mla_backend_smoke() -> None:
115 }
116 llm = LLM(
117 model="deepseek-ai/DeepSeek-V2-Lite",
118 + skip_tokenizer_init=True,
119 trust_remote_code=True,
120 load_format="dummy",
121 enforce_eager=True,
122 max_model_len=128,
123 max_num_seqs=2,
124 block_size=16,
125 + gpu_memory_utilization=0.25,
126 hf_overrides=hf_overrides,
127 + kv_transfer_config={
128 + "kv_connector": "ExampleConnector",
129 + "kv_role": "kv_both",
130 + "kv_connector_extra_config": {
131 + "shared_storage_path": str(tmp_path),
132 + },
133 + },
134 )
135
136 sampling_params = SamplingParams(max_tokens=4, temperature=0.0)
83 - outputs = llm.generate(["Hello", "MLA on CPU"], sampling_params)
137 + outputs = llm.generate([[1, 2, 3, 4], [5, 6, 7]], sampling_params, use_tqdm=False)
138 assert len(outputs) == 2
139 for output in outputs:
140 # `dummy` weights do not produce meaningful text, but the number
141 # of generated tokens must match what we asked for.
142 assert len(output.outputs[0].token_ids) == 4
143 +
144 + prefix_hit_prompt = [1] * 20
145 + outputs = llm.generate([prefix_hit_prompt], sampling_params, use_tqdm=False)
146 + assert len(outputs[0].outputs[0].token_ids) == 4
147 + assert llm.reset_prefix_cache(reset_connector=False)
148 +
149 + # ExampleConnector persists KV externally at block granularity. With a
150 + # 20-token prompt and block_size=16, the second run reloads 16 cached
151 + # tokens and forces the 4-token suffix through MLA prefill with context.
152 + outputs = llm.generate([prefix_hit_prompt], sampling_params, use_tqdm=False)
153 + assert len(outputs[0].outputs[0].token_ids) == 4
154 +
155 +
156 +@pytest.mark.skipif(not current_platform.is_cpu(), reason="CPU only")
157 +def test_cpu_mla_prefill_backend_selected() -> None:
158 + # On CPU (no device capability) the MLA prefill backend must be the
159 + # SDPA-based CPU backend, not flash-attn which is unavailable on CPU.
160 + backend_cls = get_mla_prefill_backend(None)
161 + assert backend_cls is CPUSDPAMLAPrefillBackend
162 +
163 +
164 +@pytest.mark.skipif(not current_platform.is_cpu(), reason="CPU only")
165 +def test_cpu_mla_prefill_new_tokens() -> None:
166 + # The SDPA prefill must exactly match a reference causal attention over
167 + # the ragged (varlen) q/k/v layout, with per-request padding handled.
168 + backend = CPUSDPAMLAPrefillBackend(
169 + num_heads=4,
170 + scale=0.25,
171 + kv_lora_rank=16,
172 + qk_nope_head_dim=16,
173 + qk_rope_head_dim=0,
174 + v_head_dim=16,
175 + vllm_config=None,
176 + )
177 +
178 + class _PrefillMeta:
179 + query_start_loc = torch.tensor([0, 3, 6], dtype=torch.int32)
180 +
181 + backend.prepare_metadata(_PrefillMeta())
182 + q = torch.randn(6, 4, 16, dtype=torch.bfloat16)
183 + k = torch.randn(6, 4, 16, dtype=torch.bfloat16)
184 + v = torch.randn(6, 4, 16, dtype=torch.bfloat16)
185 + out = backend.run_prefill_new_tokens(q, k, v, return_softmax_lse=False)
186 + ref, _ = _reference_ragged_attention(
187 + q,
188 + k,
189 + v,
190 + _PrefillMeta.query_start_loc,
191 + _PrefillMeta.query_start_loc,
192 + scale=0.25,
193 + causal=True,
194 + )
195 + assert out.float().allclose(ref.float(), atol=2e-2)
196 + assert out.shape == (6, 4, 16)
197 +
198 + out_lse, lse = backend.run_prefill_new_tokens(q, k, v, return_softmax_lse=True)
199 + ref_out, ref_lse = _reference_ragged_attention(
200 + q,
201 + k,
202 + v,
203 + _PrefillMeta.query_start_loc,
204 + _PrefillMeta.query_start_loc,
205 + scale=0.25,
206 + causal=True,
207 + )
208 + assert out_lse.float().allclose(ref_out.float(), atol=2e-2)
209 + assert lse.float().allclose(ref_lse.float(), atol=2e-2)
210 + assert lse.shape == (4, 6)
211 +
212 +
213 +@pytest.mark.skipif(not current_platform.is_cpu(), reason="CPU only")
214 +def test_cpu_mla_prefill_context_chunk() -> None:
215 + backend = CPUSDPAMLAPrefillBackend(
216 + num_heads=4,
217 + scale=0.25,
218 + kv_lora_rank=16,
219 + qk_nope_head_dim=16,
220 + qk_rope_head_dim=0,
221 + v_head_dim=16,
222 + vllm_config=None,
223 + )
224 +
225 + class _PrefillMeta:
226 + query_start_loc = torch.tensor([0, 5], dtype=torch.int32)
227 +
228 + class _Chunk:
229 + query_start_loc = torch.tensor([0, 2, 5], dtype=torch.int32)
230 + cu_seq_lens = torch.tensor([0, 3, 7], dtype=torch.int32)
231 +
232 + backend.prepare_metadata(_PrefillMeta())
233 + q = torch.randn(5, 4, 16, dtype=torch.bfloat16)
234 + k = torch.randn(7, 4, 16, dtype=torch.bfloat16)
235 + v = torch.randn(7, 4, 16, dtype=torch.bfloat16)
236 +
237 + out, lse = backend.run_prefill_context_chunk(_Chunk(), q, k, v)
238 + ref_out, ref_lse = _reference_ragged_attention(
239 + q,
240 + k,
241 + v,
242 + _Chunk.query_start_loc,
243 + _Chunk.cu_seq_lens,
244 + scale=0.25,
245 + causal=False,
246 + )
247 + assert out.float().allclose(ref_out.float(), atol=2e-2)
248 + assert lse.float().allclose(ref_lse.float(), atol=2e-2)
249 + assert out.shape == (5, 4, 16)
250 + assert lse.shape == (4, 5)
tests/v1/attention/test_mla_prefill_selector.py
+26 -1
@@ -58,10 +58,31 @@ def _make_vllm_config(
58 class TestGetMLAPrefillBackend:
59 """Tests for get_mla_prefill_backend (public API)."""
60
61 - def test_no_device_capability_returns_flash_attn(self):
61 + def test_cpu_uses_sdpa_prefill(self):
62 vllm_config = _make_vllm_config()
63
64 with patch("vllm.platforms.current_platform") as mock_platform:
65 + mock_platform.is_cpu.return_value = True
66 +
67 + backend = get_mla_prefill_backend(vllm_config)
68 + assert backend is MLAPrefillBackendEnum.CPU.get_class()
69 +
70 + def test_no_device_capability_returns_flash_attn(self):
71 + vllm_config = _make_vllm_config()
72 +
73 + class FlashAttnBackend:
74 + @staticmethod
75 + def get_name():
76 + return "FLASH_ATTN"
77 +
78 + with (
79 + patch("vllm.platforms.current_platform") as mock_platform,
80 + patch.object(
81 + MLAPrefillBackendEnum.FLASH_ATTN,
82 + "get_class",
83 + return_value=FlashAttnBackend,
84 + ),
85 + ):
86 mock_platform.get_device_capability.return_value = None
87 mock_platform.is_cpu.return_value = False
88
@@ -80,6 +101,7 @@ class TestGetMLAPrefillBackend:
101 )
102
103 with patch("vllm.platforms.current_platform") as mock_platform:
104 + mock_platform.is_cpu.return_value = False
105 mock_platform.get_device_capability.return_value = DeviceCapability(
106 major=9, minor=0
107 )
@@ -98,6 +120,7 @@ class TestGetMLAPrefillBackend:
120 )
121
122 with patch("vllm.platforms.current_platform") as mock_platform:
123 + mock_platform.is_cpu.return_value = False
124 mock_platform.get_device_capability.return_value = DeviceCapability(
125 major=9, minor=0
126 )
@@ -111,6 +134,7 @@ class TestGetMLAPrefillBackend:
134 )
135
136 with patch("vllm.platforms.current_platform") as mock_platform:
137 + mock_platform.is_cpu.return_value = False
138 mock_platform.get_device_capability.return_value = DeviceCapability(
139 major=10, minor=0
140 )
@@ -153,6 +177,7 @@ class TestGetMLAPrefillBackend:
177 return_value=3,
178 ),
179 ):
180 + mock_platform.is_cpu.return_value = False
181 mock_platform.get_device_capability.return_value = DeviceCapability(
182 major=9, minor=0
183 )
vllm/_custom_ops.py
+25
@@ -174,6 +174,31 @@ def mla_decode_kvcache_cpu(
174 torch.ops._C.mla_decode_kvcache(out, query, kv_cache, scale, block_tables, seq_lens)
175
176
177 +def gather_mla_context_cache_cpu(
178 + *,
179 + src_cache: torch.Tensor,
180 + dst: torch.Tensor,
181 + block_table: torch.Tensor,
182 + starts: torch.Tensor,
183 + cu_seq_lens: torch.Tensor,
184 +) -> None:
185 + page_size = src_cache.shape[1]
186 + flat_cache = src_cache.view(-1, src_cache.shape[-1])
187 + seq_lens = cu_seq_lens[1:] - cu_seq_lens[:-1]
188 +
189 + out_start = 0
190 + for req_idx, (start, seq_len) in enumerate(zip(starts.tolist(), seq_lens.tolist())):
191 + if seq_len <= 0:
192 + continue
193 + positions = torch.arange(start, start + seq_len, device=block_table.device)
194 + block_ids = block_table[req_idx, positions // page_size].to(torch.long)
195 + offsets = positions % page_size
196 + slots = block_ids * page_size + offsets
197 + out_end = out_start + seq_len
198 + dst[out_start:out_end].copy_(flat_cache[slots])
199 + out_start = out_end
200 +
201 +
202 # merge attn states ops
203 def merge_attn_states(
204 output: torch.Tensor,
vllm/model_executor/layers/attention/mla_attention.py
+12
@@ -2785,6 +2785,18 @@ class MLACommonBaseImpl(MLAAttentionImpl[A], Generic[A]):
2785 batch_size=chunk.num_requests,
2786 seq_starts=chunk.starts,
2787 )
2788 + elif current_platform.is_cpu():
2789 + assert not is_quantized_kv_cache(self.kv_cache_dtype), (
2790 + "CPU MLA context gather fallback only supports "
2791 + f"non-quantized KV cache, got {self.kv_cache_dtype}"
2792 + )
2793 + ops.gather_mla_context_cache_cpu(
2794 + src_cache=kv_c_and_k_pe_cache,
2795 + dst=workspace[:toks],
2796 + block_table=block_table,
2797 + starts=chunk.starts,
2798 + cu_seq_lens=chunk.cu_seq_lens,
2799 + )
2800 elif not use_fp8_prefill:
2801 ops.gather_and_maybe_dequant_cache(
2802 src_cache=kv_c_and_k_pe_cache,
vllm/v1/attention/backends/mla/prefill/cpu_native.py deleted
-61
@@ -1,61 +0,0 @@
1 -# SPDX-License-Identifier: Apache-2.0
2 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 -"""Inert MLA prefill backend for the CPU platform.
4 -
5 -CPU's MLA attention impl (``CPUMLAImpl``) fully overrides ``forward_mha`` with a
6 -kernel that attends directly against the paged latent KV cache (covering both
7 -fresh prefill and cached-prefix continuation in one pass), so it never calls
8 -into a pluggable ``MLAPrefillBackend``'s ``run_prefill_new_tokens``/
9 -``run_prefill_context_chunk``. This class exists only to satisfy
10 -``MLACommonMetadataBuilder``'s structural requirement that
11 -``MLAAttention.prefill_backend`` be a real, cloneable object.
12 -"""
13 -
14 -from typing import TYPE_CHECKING
15 -
16 -import torch
17 -
18 -from vllm.v1.attention.backends.mla.prefill.base import MLAPrefillBackend
19 -
20 -if TYPE_CHECKING:
21 - from vllm.model_executor.layers.attention.mla_attention import (
22 - MLACommonPrefillMetadata,
23 - )
24 -
25 -
26 -class CPUNativeMLAPrefillBackend(MLAPrefillBackend):
27 - """Placeholder MLA prefill backend for CPU; never actually invoked."""
28 -
29 - @staticmethod
30 - def get_name() -> str:
31 - return "CPU_NATIVE"
32 -
33 - def run_prefill_new_tokens(
34 - self,
35 - q: torch.Tensor,
36 - k: torch.Tensor,
37 - v: torch.Tensor,
38 - return_softmax_lse: bool,
39 - out: torch.Tensor | None = None,
40 - output_scale: torch.Tensor | None = None,
41 - ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
42 - raise AssertionError(
43 - "CPUNativeMLAPrefillBackend.run_prefill_new_tokens is unreachable: "
44 - "CPUMLAImpl.forward_mha fully overrides the dense-MHA prefill path "
45 - "and never calls the pluggable prefill backend."
46 - )
47 -
48 - def run_prefill_context_chunk(
49 - self,
50 - chunk: "MLACommonPrefillMetadata.ContextChunk",
51 - q: torch.Tensor,
52 - k: torch.Tensor,
53 - v: torch.Tensor,
54 - out: torch.Tensor | None = None,
55 - ) -> tuple[torch.Tensor, torch.Tensor]:
56 - raise AssertionError(
57 - "CPUNativeMLAPrefillBackend.run_prefill_context_chunk is "
58 - "unreachable: CPUMLAImpl.forward_mha fully overrides the "
59 - "dense-MHA prefill path and never calls the pluggable prefill "
60 - "backend."
61 - )
vllm/v1/attention/backends/mla/prefill/cpu_sdpa.py new
+137
@@ -0,0 +1,137 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +"""CPU MLA prefill backend implemented with dense per-request attention.
4 +
5 +This backend is selected on CPU devices, where flash-attn is unavailable. It
6 +computes MLA prefill attention by slicing the ragged (varlen) q/k/v tensors
7 +per request and evaluating dense attention in PyTorch.
8 +"""
9 +
10 +from typing import TYPE_CHECKING
11 +
12 +import torch
13 +
14 +from vllm.v1.attention.backends.mla.prefill.base import MLAPrefillBackend
15 +
16 +if TYPE_CHECKING:
17 + from vllm.model_executor.layers.attention.mla_attention import (
18 + MLACommonPrefillMetadata,
19 + )
20 +
21 +
22 +class CPUSDPAMLAPrefillBackend(MLAPrefillBackend):
23 + """MLA prefill backend for CPU using dense PyTorch attention.
24 +
25 + flash-attn is not installable on CPU, so this backend provides an
26 + equivalent prefill implementation using per-request dense attention.
27 + """
28 +
29 + @staticmethod
30 + def get_name() -> str:
31 + return "CPU_SDPA_MLA"
32 +
33 + @classmethod
34 + def is_available(cls) -> bool:
35 + # SDPA is always available on CPU.
36 + return True
37 +
38 + def supports_out(self) -> bool:
39 + return True
40 +
41 + def _ragged_sdpa(
42 + self,
43 + q: torch.Tensor,
44 + k: torch.Tensor,
45 + v: torch.Tensor,
46 + q_cu_seq_lens: torch.Tensor,
47 + kv_cu_seq_lens: torch.Tensor,
48 + causal: bool,
49 + return_softmax_lse: bool,
50 + ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
51 + # q/k/v: [num_tokens, num_heads, head_dim] (ragged, concatenated).
52 + # q_cu_seq_lens/kv_cu_seq_lens: [num_reqs + 1] row offsets for each
53 + # request's query and key/value tokens.
54 + num_reqs = q_cu_seq_lens.numel() - 1
55 + assert num_reqs == kv_cu_seq_lens.numel() - 1
56 +
57 + out_chunks: list[torch.Tensor] = []
58 + lse_chunks: list[torch.Tensor] = []
59 + for r in range(num_reqs):
60 + q_start = int(q_cu_seq_lens[r])
61 + q_end = int(q_cu_seq_lens[r + 1])
62 + kv_start = int(kv_cu_seq_lens[r])
63 + kv_end = int(kv_cu_seq_lens[r + 1])
64 +
65 + q_r = q[q_start:q_end].transpose(0, 1).float() # [NH, Sq, Hq]
66 + k_r = k[kv_start:kv_end].transpose(0, 1).float() # [NH, Sk, Hq]
67 + v_r = v[kv_start:kv_end].transpose(0, 1).float() # [NH, Sk, Hv]
68 +
69 + attn_scores = torch.matmul(q_r, k_r.transpose(-2, -1)) * self.scale
70 + if causal:
71 + causal_mask = torch.ones(
72 + (q_end - q_start, kv_end - kv_start),
73 + dtype=torch.bool,
74 + device=attn_scores.device,
75 + ).triu(1)
76 + attn_scores.masked_fill_(causal_mask.unsqueeze(0), float("-inf"))
77 +
78 + attn_probs = torch.softmax(attn_scores, dim=-1, dtype=torch.float32)
79 + out_r = torch.matmul(attn_probs, v_r).transpose(0, 1).to(v.dtype)
80 + out_chunks.append(out_r) # [Sq, NH, Hv]
81 + if return_softmax_lse:
82 + lse_chunks.append(torch.logsumexp(attn_scores, dim=-1))
83 +
84 + out = torch.cat(out_chunks, dim=0)
85 + if not return_softmax_lse:
86 + return out
87 + lse = torch.cat(lse_chunks, dim=1)
88 + return out, lse
89 +
90 + def run_prefill_new_tokens(
91 + self,
92 + q: torch.Tensor,
93 + k: torch.Tensor,
94 + v: torch.Tensor,
95 + return_softmax_lse: bool,
96 + out: torch.Tensor | None = None,
97 + output_scale: torch.Tensor | None = None,
98 + ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
99 + assert output_scale is None, (
100 + "CPU MLA prefill backend does not support fused FP8 output"
101 + )
102 + out_t = self._ragged_sdpa(
103 + q,
104 + k,
105 + v,
106 + self._prefill_metadata.query_start_loc,
107 + self._prefill_metadata.query_start_loc,
108 + causal=True,
109 + return_softmax_lse=return_softmax_lse,
110 + )
111 + if out is not None and not return_softmax_lse:
112 + assert isinstance(out_t, torch.Tensor)
113 + out.copy_(out_t)
114 + return out
115 + return out_t
116 +
117 + def run_prefill_context_chunk(
118 + self,
119 + chunk: "MLACommonPrefillMetadata.ContextChunk",
120 + q: torch.Tensor,
121 + k: torch.Tensor,
122 + v: torch.Tensor,
123 + out: torch.Tensor | None = None,
124 + ) -> tuple[torch.Tensor, torch.Tensor]:
125 + assert out is None, "CPU MLA context prefill does not support in-place output"
126 + result = self._ragged_sdpa(
127 + q,
128 + k,
129 + v,
130 + chunk.query_start_loc,
131 + chunk.cu_seq_lens,
132 + causal=False,
133 + return_softmax_lse=True,
134 + )
135 + assert isinstance(result, tuple)
136 + out, lse = result
137 + return out, lse
vllm/v1/attention/backends/mla/prefill/registry.py
+1 -3
@@ -52,9 +52,7 @@ class MLAPrefillBackendEnum(Enum, metaclass=_MLAPrefillBackendEnumMeta):
52 "vllm.v1.attention.backends.mla.prefill.aiter_flash_attn."
53 "AiterFlashAttnPrefillBackend"
54 )
55 - CPU_NATIVE = (
56 - "vllm.v1.attention.backends.mla.prefill.cpu_native.CPUNativeMLAPrefillBackend"
57 - )
55 + CPU = "vllm.v1.attention.backends.mla.prefill.cpu_sdpa.CPUSDPAMLAPrefillBackend"
56 # Placeholder for third-party/custom backends - must be registered before use
57 # set to None to avoid alias with other backend, whose value is an empty string
58 CUSTOM = None
vllm/v1/attention/backends/mla/prefill/selector.py
+4 -2
@@ -107,10 +107,12 @@ def get_mla_prefill_backend(
107 """
108 from vllm.platforms import current_platform
109
110 + if current_platform.is_cpu():
111 + logger.info_once("Using CPU SDPA MLA prefill backend.")
112 + return MLAPrefillBackendEnum.CPU.get_class()
113 +
114 device_capability = current_platform.get_device_capability()
115 if device_capability is None:
112 - if current_platform.is_cpu():
113 - return MLAPrefillBackendEnum.CPU_NATIVE.get_class()
116 logger.info_once(
117 "Device capability not available, using FlashAttention MLA prefill backend."
118 )
vllm/v1/attention/ops/merge_attn_states.py
+73
@@ -6,6 +6,68 @@ import torch
6 from vllm.platforms import current_platform
7
8
9 +def _merge_attn_states_torch(
10 + output: torch.Tensor,
11 + prefix_output: torch.Tensor,
12 + prefix_lse: torch.Tensor,
13 + suffix_output: torch.Tensor,
14 + suffix_lse: torch.Tensor,
15 + output_lse: torch.Tensor | None = None,
16 + prefill_tokens_with_context: int | None = None,
17 + output_scale: torch.Tensor | None = None,
18 +) -> None:
19 + assert output_scale is None, "CPU fallback does not support FP8 output"
20 +
21 + num_tokens = output.shape[0]
22 + if prefill_tokens_with_context is None:
23 + prefill_tokens_with_context = num_tokens
24 +
25 + prefix_lse = torch.where(
26 + torch.isinf(prefix_lse) & (prefix_lse > 0),
27 + torch.full_like(prefix_lse, float("-inf")),
28 + prefix_lse,
29 + )
30 + suffix_lse = torch.where(
31 + torch.isinf(suffix_lse) & (suffix_lse > 0),
32 + torch.full_like(suffix_lse, float("-inf")),
33 + suffix_lse,
34 + )
35 +
36 + if prefill_tokens_with_context < num_tokens:
37 + output[prefill_tokens_with_context:].copy_(
38 + suffix_output[prefill_tokens_with_context:]
39 + )
40 + if output_lse is not None:
41 + output_lse[:, prefill_tokens_with_context:].copy_(
42 + suffix_lse[:, prefill_tokens_with_context:]
43 + )
44 +
45 + if prefill_tokens_with_context == 0:
46 + return
47 +
48 + prefix_lse = prefix_lse[:, :prefill_tokens_with_context].float()
49 + suffix_lse = suffix_lse[:, :prefill_tokens_with_context].float()
50 + max_lse = torch.maximum(prefix_lse, suffix_lse)
51 + prefix_se = torch.exp(prefix_lse - max_lse)
52 + suffix_se = torch.exp(suffix_lse - max_lse)
53 + out_se = prefix_se + suffix_se
54 + both_empty = max_lse == float("-inf")
55 +
56 + prefix_scale = (prefix_se / out_se).transpose(0, 1).unsqueeze(-1)
57 + suffix_scale = (suffix_se / out_se).transpose(0, 1).unsqueeze(-1)
58 + merged = (
59 + prefix_output[:prefill_tokens_with_context].float() * prefix_scale
60 + + suffix_output[:prefill_tokens_with_context].float() * suffix_scale
61 + )
62 + merged.masked_fill_(both_empty.transpose(0, 1).unsqueeze(-1), 0.0)
63 + output[:prefill_tokens_with_context].copy_(merged.to(output.dtype))
64 +
65 + if output_lse is not None:
66 + merged_lse = torch.log(out_se) + max_lse
67 + merged_lse.masked_fill_(both_empty, float("-inf"))
68 + output_lse[:, :prefill_tokens_with_context].copy_(merged_lse)
69 +
70 +
71 def merge_attn_states(
72 output: torch.Tensor,
73 prefix_output: torch.Tensor,
@@ -94,6 +156,17 @@ def merge_attn_states(
156 prefill_tokens_with_context,
157 output_scale,
158 )
159 + elif current_platform.is_cpu():
160 + return _merge_attn_states_torch(
161 + output,
162 + prefix_output,
163 + prefix_lse,
164 + suffix_output,
165 + suffix_lse,
166 + output_lse,
167 + prefill_tokens_with_context,
168 + output_scale,
169 + )
170 else:
171 from vllm.v1.attention.ops.triton_merge_attn_states import (
172 merge_attn_states,