main
py 272 lines 9.35 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """FlashMLA mega attention vs the split-KV decode pipeline (DeepSeek V4.1).
4
5 Times the full set of per-layer ops each path runs in a decode step, captured
6 in a CUDA graph -- eager timing is dominated by launch overhead and flatters
7 whichever path launches fewer kernels.
8
9 mega: fused_qnorm_rope_kv_rope_quant_insert (Q pad + KV insert)
10 + mega kernel
11 split-KV: fused_qnorm_rope_kv_rope_quant_insert (Q RoPE + pad + KV insert)
12 + flash_mla_with_kvcache + fused_inv_rope_fp8_quant
13
14 Both arms fuse their Q preparation into the KV insert, so that op is charged
15 to both; the mega arm's Q side is only a zero-pad (nothing at all when the
16 shard is already at the kernel head count) because its kernel does the Q RoPE,
17 the inverse RoPE and the FP8 cast.
18
19 ``--local-heads`` is the variable that decides the outcome: the mega kernel
20 absorbs work proportional to the live head count while paying for the padded
21 one, so it wins at 64 live heads and is a wash at 16 (TP4 on a 64-head model).
22
23 Both arms use the same KV records so the comparison is kernel-for-kernel:
24 a V4.1 fp8 (528 B) sliding-window cache and a compressed cache that is either
25 V4.1 fp8 or V4.1 NVFP4 (288 B) -- FlashMLA's SM100 sparse decode reads the
26 NVFP4 compressed record in both the fused and unfused kernels.
27
28 Run: .venv/bin/python benchmarks/kernels/benchmark_dsv41_mega_attn.py
29 """
30
31 import argparse
32
33 import torch
34
35 import vllm.v1.attention.ops.flashmla as fm
36 from vllm.models.deepseek_v4.common.ops.fused_inv_rope_fp8_quant import (
37 fused_inv_rope_fp8_quant,
38 )
39 from vllm.models.deepseek_v41.common.ops import quantize_and_insert_k_cache
40 from vllm.models.deepseek_v41.common.ops.fused_compress_quant_cache import (
41 rope_quant_insert,
42 )
43 from vllm.models.deepseek_v41.common.ops.fused_layout import permute_q_to_fused
44 from vllm.models.deepseek_v41.nvidia.flash_mla_mega_attn import (
45 alloc_mega_attn_output,
46 is_flashmla_mega_attn_supported,
47 )
48 from vllm.utils.math_utils import round_up
49
50 HEAD_DIM, ROPE_DIM = 512, 64
51 V41_BYTES, V41_FP4_BYTES = 528, 288
52 SWA_BLOCK, COMPRESSED_BLOCK = 32, 128
53
54
55 def make_cos_sin_cache(max_pos, device):
56 inv_freq = 1.0 / (
57 10000 ** (torch.arange(0, ROPE_DIM, 2, device=device).float() / ROPE_DIM)
58 )
59 freqs = torch.outer(torch.arange(max_pos, device=device).float(), inv_freq)
60 return torch.cat([freqs.cos(), freqs.sin()], -1)
61
62
63 def empty_paged_cache(num_rows, block_size, bytes_per_token, device):
64 """A zeroed paged cache view [num_blocks, block_size, bytes_per_token]."""
65 num_blocks = (num_rows + block_size - 1) // block_size + 1
66 page = round_up(block_size * bytes_per_token, 512)
67 backing = torch.zeros(num_blocks, page, dtype=torch.uint8, device=device)
68 return backing, backing.as_strided(
69 (num_blocks, block_size, bytes_per_token), (page, bytes_per_token, 1)
70 )
71
72
73 def fill_cache(view, bytes_per_token, cos_sin, device):
74 n = view.shape[0] * view.shape[1]
75 rows = torch.randn(n, HEAD_DIM, device=device, dtype=torch.bfloat16)
76 slots = torch.arange(n, dtype=torch.int64, device=device)
77 if bytes_per_token == V41_FP4_BYTES:
78 # A page-aligned cache has more slots than the cos_sin table has rows;
79 # wrap so the insert never indexes past it.
80 positions = slots % cos_sin.shape[0]
81 rope_quant_insert(rows, positions, cos_sin, view, slots, 1)
82 else:
83 quantize_and_insert_k_cache(
84 rows,
85 view.reshape(view.shape[0], -1),
86 slots,
87 block_size=view.shape[1],
88 bytes_per_token=bytes_per_token,
89 )
90
91
92 def time_graph(fn, iters=100, warmup=5):
93 """Capture ``fn`` in a CUDA graph and time its replay."""
94 s = torch.cuda.Stream()
95 s.wait_stream(torch.cuda.current_stream())
96 with torch.cuda.stream(s):
97 for _ in range(3):
98 fn()
99 torch.cuda.current_stream().wait_stream(s)
100 g = torch.cuda.CUDAGraph()
101 with torch.cuda.graph(g):
102 fn()
103 for _ in range(warmup):
104 g.replay()
105 torch.accelerator.synchronize()
106 start = torch.cuda.Event(enable_timing=True)
107 end = torch.cuda.Event(enable_timing=True)
108 start.record()
109 for _ in range(iters):
110 g.replay()
111 end.record()
112 torch.accelerator.synchronize()
113 return start.elapsed_time(end) * 1000 / iters
114
115
116 def bench_decode(s_q, device, topk_extra, extra_bytes, local_heads):
117 padded_heads, scale = 64, HEAD_DIM**-0.5
118 n_wv_group = padded_heads // 8
119 cos_sin = make_cos_sin_cache(65536, device)
120 positions = torch.randint(0, 65536, (s_q,), device=device, dtype=torch.int64)
121 pos32 = positions.to(torch.int32)
122
123 # Per-step inputs: the wq_b output (local heads) and this step's KV row.
124 q_local = torch.randn(
125 s_q, local_heads, HEAD_DIM, device=device, dtype=torch.bfloat16
126 )
127 q_fused = permute_q_to_fused(q_local)
128 kv = torch.randn(s_q, HEAD_DIM, device=device, dtype=torch.bfloat16)
129
130 n_swa = s_q * 128 + 128
131 swa_backing, swa = empty_paged_cache(n_swa, SWA_BLOCK, V41_BYTES, device)
132 fill_cache(swa, V41_BYTES, cos_sin, device)
133 _, extra = empty_paged_cache(65536, COMPRESSED_BLOCK, extra_bytes, device)
134 fill_cache(extra, extra_bytes, cos_sin, device)
135
136 slot_mapping = torch.arange(s_q, dtype=torch.int64, device=device)
137 swa_idx = torch.randint(0, n_swa, (s_q, 128), device=device, dtype=torch.int32)
138 swa_len = torch.full((s_q,), 128, device=device, dtype=torch.int32)
139 ex_idx = torch.randint(
140 0, 65536, (s_q, topk_extra), device=device, dtype=torch.int32
141 )
142 ex_len = torch.full((s_q,), topk_extra, device=device, dtype=torch.int32)
143 sink = torch.zeros(padded_heads, device=device)
144 sched = fm.FlashMLASchedMeta()
145 out = alloc_mega_attn_output(s_q, n_wv_group, device)
146 swa4 = swa.unsqueeze(-2)
147 extra4 = extra.unsqueeze(-2)
148 swa_2d = swa_backing
149
150 def mega():
151 # One launch zero-pads the fused-layout Q and inserts the SWA KV.
152 q_pad = torch.ops._C.fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert(
153 q_fused,
154 kv,
155 swa_2d,
156 slot_mapping,
157 positions,
158 cos_sin,
159 0 if padded_heads == local_heads else padded_heads,
160 1e-6,
161 SWA_BLOCK,
162 False, # apply_q_norm
163 True, # kv_mxfp8
164 False, # apply_q_rope: the mega kernel rotates Q itself
165 True, # is_q_interleaved
166 )
167 torch.ops._flashmla_C.fused_norm_rope_attn_rope_cast_decode(
168 q_fused if padded_heads == local_heads else q_pad,
169 swa4,
170 swa_idx,
171 scale,
172 HEAD_DIM,
173 sink,
174 swa_len,
175 extra4,
176 ex_idx,
177 ex_len,
178 False,
179 0.0,
180 pos32,
181 False,
182 64,
183 cos_sin,
184 n_wv_group,
185 32,
186 True,
187 True,
188 True,
189 out.data,
190 out.scale,
191 )
192
193 def split_kv():
194 q_pad = torch.ops._C.fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert(
195 q_local,
196 kv,
197 swa_2d,
198 slot_mapping,
199 positions,
200 cos_sin,
201 padded_heads,
202 1e-6,
203 SWA_BLOCK,
204 False,
205 True,
206 )
207 o = fm.flash_mla_with_kvcache(
208 q=q_pad.unsqueeze(1),
209 k_cache=swa4,
210 block_table=None,
211 head_dim_v=HEAD_DIM,
212 tile_scheduler_metadata=sched,
213 cache_seqlens=None,
214 is_fp8_kvcache=True,
215 indices=swa_idx.view(s_q, 1, -1),
216 topk_length=swa_len,
217 softmax_scale=scale,
218 attn_sink=sink,
219 extra_k_cache=extra4,
220 extra_indices_in_kvcache=ex_idx.view(s_q, 1, -1),
221 extra_topk_length=ex_len,
222 )[0]
223 fused_inv_rope_fp8_quant(
224 o.squeeze(1)[:, :local_heads],
225 positions,
226 cos_sin,
227 n_groups=local_heads // 8,
228 heads_per_group=8,
229 quant_group_size=32,
230 tma_aligned_scales=True,
231 )
232
233 return time_graph(mega), time_graph(split_kv)
234
235
236 def main():
237 parser = argparse.ArgumentParser()
238 parser.add_argument(
239 "--decode-s-q",
240 nargs="+",
241 type=int,
242 default=[1, 2, 4, 8, 16, 32, 64, 128, 256, 512],
243 )
244 parser.add_argument("--topk-extra", type=int, default=512)
245 parser.add_argument("--local-heads", type=int, default=16)
246 parser.add_argument(
247 "--extra-bytes",
248 type=int,
249 choices=[V41_BYTES, V41_FP4_BYTES],
250 default=V41_BYTES,
251 help="compressed-cache record: 528 (V4.1 fp8) or 288 (V4.1 NVFP4)",
252 )
253 args = parser.parse_args()
254 ok, reason = is_flashmla_mega_attn_supported()
255 if not ok:
256 raise SystemExit(reason)
257 device = torch.device("cuda")
258 torch.manual_seed(0)
259 print(
260 f"decode, cudagraph (topk_swa=128, topk_extra={args.topk_extra}, "
261 f"local_heads={args.local_heads}, extra={args.extra_bytes}B)"
262 )
263 print(f"{'s_q':>6} {'mega_us':>9} {'split_kv_us':>12} {'speedup':>8}")
264 for s_q in args.decode_s_q:
265 m, s = bench_decode(
266 s_q, device, args.topk_extra, args.extra_bytes, args.local_heads
267 )
268 print(f"{s_q:>6d} {m:9.1f} {s:12.1f} {s / m:7.2f}x")
269
270
271 if __name__ == "__main__":
272 main()