flashmap 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,474 @@
1
+ # coding=utf-8
2
+ """Split-KV attention for speculative verification on sglang's Triton backend.
3
+
4
+ A verify step attends L query tokens per request (a draft chain or tree plus
5
+ its root) to the request's cached prefix and to each other. sglang's Triton
6
+ backend runs it as a prefill (extend_attention_fwd): one program per (request,
7
+ query head) walks the whole prefix serially, its 128-row query tile holding
8
+ only L = 17 real rows. At batch size 1 that is 32 programs on a 132-SM GPU, and
9
+ the time grows with the context; a tree's custom mask costs a further ~27%.
10
+
11
+ Here one program per (request, KV head, row block, split) holds all G query
12
+ heads that share the KV head (G * L rows, in blocks of up to 128 for wide
13
+ trees), so each prefix key and value is read once per KV head (and row block)
14
+ rather than once per query head, and the prefix is split across programs
15
+ (as flash-decoding does) when the batch alone cannot fill the GPU. One more
16
+ split per (request, KV head) attends the L tokens to each other, under the
17
+ tree's mask (sglang's layout: L rows of prefix + L columns per request, the
18
+ prefix all visible; or, with ``qlen_mask``, each request's L x L block alone,
19
+ EAGLE's QLEN_ONLY layout, which skips writing the prefix columns) or causally.
20
+ A second kernel merges the splits by their
21
+ log-sum-exp. Only the shapes are read on the host, so it is graph-capturable.
22
+
23
+ ``SplitKVVerify`` takes extend_attention_fwd's arguments and returns False,
24
+ having done nothing, for a case it does not handle (the caller then runs
25
+ extend_attention_fwd); tree_worker.py installs it.
26
+ """
27
+
28
+ import os
29
+
30
+ import torch
31
+ import triton
32
+ import triton.language as tl
33
+
34
+
35
+ @triton.jit
36
+ def _verify_partial_kernel(
37
+ q_ptr,
38
+ k_ext_ptr,
39
+ v_ext_ptr,
40
+ k_buf_ptr,
41
+ v_buf_ptr,
42
+ kv_indptr,
43
+ kv_indices,
44
+ mask_ptr,
45
+ mask_indptr,
46
+ part_out_ptr,
47
+ part_lse_ptr,
48
+ sm_scale,
49
+ k_scale,
50
+ v_scale,
51
+ stride_qt,
52
+ stride_qh,
53
+ stride_kt,
54
+ stride_kh,
55
+ stride_vt,
56
+ stride_vh,
57
+ stride_kbt,
58
+ stride_kbh,
59
+ stride_vbt,
60
+ stride_vbh,
61
+ H_KV: tl.constexpr,
62
+ G: tl.constexpr,
63
+ L: tl.constexpr,
64
+ S: tl.constexpr,
65
+ D: tl.constexpr,
66
+ DV: tl.constexpr,
67
+ BLOCK_D: tl.constexpr,
68
+ BLOCK_DV: tl.constexpr,
69
+ M: tl.constexpr,
70
+ RB: tl.constexpr,
71
+ BLOCK_L: tl.constexpr,
72
+ BLOCK_N: tl.constexpr,
73
+ MIN_CHUNK: tl.constexpr,
74
+ USE_MASK: tl.constexpr,
75
+ QLEN_MASK: tl.constexpr,
76
+ ):
77
+ # Program (b, h * RB + rb, s): request b, KV head h, rows rb * M .. + M;
78
+ # splits s < S cover the prefix, split S the L verify tokens. Row r is
79
+ # query head h * G + r // L at token r % L of the request.
80
+ b = tl.program_id(0)
81
+ h = tl.program_id(1) // RB
82
+ rb = tl.program_id(1) % RB
83
+ s = tl.program_id(2)
84
+ rows = rb * M + tl.arange(0, M)
85
+ row_ok = rows < G * L
86
+ g = rows // L
87
+ t = rows % L
88
+ d = tl.arange(0, BLOCK_D)
89
+ dv = tl.arange(0, BLOCK_DV)
90
+ q = tl.load(
91
+ q_ptr
92
+ + (b * L + t)[:, None] * stride_qt
93
+ + (h * G + g)[:, None] * stride_qh
94
+ + d[None, :],
95
+ mask=row_ok[:, None] & (d[None, :] < D),
96
+ other=0.0,
97
+ )
98
+ kv_start = tl.load(kv_indptr + b)
99
+ prefix = tl.load(kv_indptr + b + 1) - kv_start
100
+ m_i = tl.full([M], float("-inf"), dtype=tl.float32)
101
+ l_i = tl.zeros([M], dtype=tl.float32)
102
+ acc = tl.zeros([M, BLOCK_DV], dtype=tl.float32)
103
+ live = s >= 0 # a split that holds keys
104
+ if s < S:
105
+ chunk = tl.cdiv(tl.maximum(tl.cdiv(prefix, S), MIN_CHUNK), BLOCK_N) * BLOCK_N
106
+ lo = s * chunk
107
+ hi = tl.minimum(lo + chunk, prefix)
108
+ live = hi > lo
109
+ for start in range(lo, hi, BLOCK_N):
110
+ n = start + tl.arange(0, BLOCK_N)
111
+ n_ok = n < hi
112
+ loc = tl.load(kv_indices + kv_start + n, mask=n_ok, other=0)
113
+ k = tl.load(
114
+ k_buf_ptr + loc[None, :] * stride_kbt + h * stride_kbh + d[:, None],
115
+ mask=n_ok[None, :] & (d[:, None] < D),
116
+ other=0.0,
117
+ )
118
+ qk = tl.dot(q, k) * (sm_scale * k_scale)
119
+ qk = tl.where(n_ok[None, :], qk, float("-inf"))
120
+ v = tl.load(
121
+ v_buf_ptr + loc[:, None] * stride_vbt + h * stride_vbh + dv[None, :],
122
+ mask=n_ok[:, None] & (dv[None, :] < DV),
123
+ other=0.0,
124
+ )
125
+ m_new = tl.maximum(m_i, tl.max(qk, 1))
126
+ alpha = tl.exp(m_i - m_new)
127
+ p = tl.exp(qk - m_new[:, None])
128
+ l_i = l_i * alpha + tl.sum(p, 1)
129
+ acc = acc * alpha[:, None] + tl.dot(p.to(v.dtype), v)
130
+ m_i = m_new
131
+ acc = acc * v_scale
132
+ else:
133
+ # Names differ from the prefix branch's: Triton requires a name assigned
134
+ # in both branches of a runtime `if` to have one type.
135
+ j = tl.arange(0, BLOCK_L)
136
+ j_ok = j < L
137
+ k_ext = tl.load(
138
+ k_ext_ptr + (b * L + j)[None, :] * stride_kt + h * stride_kh + d[:, None],
139
+ mask=j_ok[None, :] & (d[:, None] < D),
140
+ other=0.0,
141
+ )
142
+ qk_ext = tl.dot(q, k_ext) * sm_scale
143
+ if USE_MASK:
144
+ if QLEN_MASK:
145
+ # Request b's L x L block, row t for verify token t.
146
+ offs = b * L * L + t[:, None] * L + j[None, :]
147
+ else:
148
+ # Row t of request b's mask: prefix + L entries, the last L for the verify tokens.
149
+ offs = (
150
+ tl.load(mask_indptr + b)
151
+ + t[:, None] * (prefix + L)
152
+ + prefix
153
+ + j[None, :]
154
+ )
155
+ allowed = (
156
+ tl.load(mask_ptr + offs, mask=row_ok[:, None] & j_ok[None, :], other=0)
157
+ != 0
158
+ )
159
+ else:
160
+ allowed = j[None, :] <= t[:, None]
161
+ allowed = allowed & row_ok[:, None] & j_ok[None, :]
162
+ qk_ext = tl.where(allowed, qk_ext, float("-inf"))
163
+ v_ext = tl.load(
164
+ v_ext_ptr + (b * L + j)[:, None] * stride_vt + h * stride_vh + dv[None, :],
165
+ mask=j_ok[:, None] & (dv[None, :] < DV),
166
+ other=0.0,
167
+ )
168
+ m_i = tl.max(qk_ext, 1)
169
+ p_ext = tl.exp(qk_ext - tl.where(m_i == float("-inf"), 0.0, m_i)[:, None])
170
+ l_i = tl.sum(p_ext, 1)
171
+ acc = tl.dot(p_ext.to(v_ext.dtype), v_ext)
172
+ part = (b * H_KV + h) * (S + 1) + s
173
+ lse = tl.where(l_i > 0, m_i + tl.log(l_i), float("-inf"))
174
+ tl.store(part_lse_ptr + part * (RB * M) + rows, lse, mask=row_ok)
175
+ if live:
176
+ out = acc / tl.where(l_i > 0, l_i, 1.0)[:, None]
177
+ tl.store(
178
+ part_out_ptr + (part * (RB * M) + rows)[:, None] * BLOCK_DV + dv[None, :],
179
+ out,
180
+ mask=row_ok[:, None],
181
+ )
182
+
183
+
184
+ @triton.jit
185
+ def _verify_merge_kernel(
186
+ part_out_ptr,
187
+ part_lse_ptr,
188
+ o_ptr,
189
+ stride_ot,
190
+ stride_oh,
191
+ H_KV: tl.constexpr,
192
+ G: tl.constexpr,
193
+ L: tl.constexpr,
194
+ S: tl.constexpr,
195
+ DV: tl.constexpr,
196
+ BLOCK_DV: tl.constexpr,
197
+ ROWS: tl.constexpr,
198
+ BLOCK_L: tl.constexpr,
199
+ ):
200
+ # Program (b, query head): the splits' outputs for its L rows (of ROWS per
201
+ # partial), weighted by their share of the softmax mass.
202
+ b = tl.program_id(0)
203
+ head = tl.program_id(1)
204
+ h = head // G
205
+ t = tl.arange(0, BLOCK_L)
206
+ t_ok = t < L
207
+ rows = (head % G) * L + t
208
+ dv = tl.arange(0, BLOCK_DV)
209
+ m = tl.full([BLOCK_L], float("-inf"), dtype=tl.float32)
210
+ den = tl.zeros([BLOCK_L], dtype=tl.float32)
211
+ acc = tl.zeros([BLOCK_L, BLOCK_DV], dtype=tl.float32)
212
+ for s in tl.static_range(S + 1):
213
+ part = (b * H_KV + h) * (S + 1) + s
214
+ lse = tl.load(part_lse_ptr + part * ROWS + rows, mask=t_ok, other=float("-inf"))
215
+ live = lse > float("-inf")
216
+ out = tl.load(
217
+ part_out_ptr + (part * ROWS + rows)[:, None] * BLOCK_DV + dv[None, :],
218
+ mask=live[:, None],
219
+ other=0.0,
220
+ )
221
+ m_new = tl.maximum(m, lse)
222
+ base = tl.where(m_new == float("-inf"), 0.0, m_new)
223
+ alpha = tl.exp(m - base)
224
+ w = tl.exp(lse - base)
225
+ acc = acc * alpha[:, None] + out * w[:, None]
226
+ den = den * alpha + w
227
+ m = m_new
228
+ out = acc / tl.where(den > 0, den, 1.0)[:, None]
229
+ tl.store(
230
+ o_ptr + (b * L + t)[:, None] * stride_ot + head * stride_oh + dv[None, :],
231
+ out.to(o_ptr.dtype.element_ty),
232
+ mask=t_ok[:, None] & (dv[None, :] < DV),
233
+ )
234
+
235
+
236
+ def row_blocks(rows: int) -> tuple:
237
+ """(rows per program, programs) covering ``rows`` query rows: blocks of 64
238
+ or 128, whichever pads less (ties to 128, fewer programs re-reading K/V)."""
239
+ best = None
240
+ for size in (128, 64):
241
+ size = min(size, max(16, triton.next_power_of_2(rows)))
242
+ blocks = triton.cdiv(rows, size)
243
+ if best is None or size * blocks < best[0] * best[1]:
244
+ best = (size, blocks)
245
+ return best
246
+
247
+
248
+ def _env_int(name: str, default: int) -> int:
249
+ return int(os.environ.get(name, default))
250
+
251
+
252
+ class SplitKVVerify:
253
+ """extend_attention_fwd for a verify batch (L tokens per request), split
254
+ over the prefix. Scratch for the splits' partial outputs grows with the
255
+ batch, outside graph capture (a capture runs warm-up passes first); a
256
+ replaced buffer is kept, as captured graphs still point at it."""
257
+
258
+ MAX_SCRATCH_BYTES = 512 << 20
259
+ # Custom masks arrive as each request's L x L block (EAGLE's QLEN_ONLY) rather
260
+ # than sglang's full prefix + L rows. Whoever builds the masks sets this
261
+ # before graphs are captured; extend_attention_fwd cannot read that layout,
262
+ # so a batch this kernel cannot take then raises instead of falling back.
263
+ qlen_mask = False
264
+
265
+ def __init__(self):
266
+ # Splits per (request, KV head): enough programs for `waves` waves of
267
+ # the GPU's SMs, at most MAX; MIN_CHUNK keys at least per split, so
268
+ # short prefixes use fewer of them.
269
+ self.waves = _env_int("FLASHMAP_SPLITKV_WAVES", 2)
270
+ self.max_splits = _env_int("FLASHMAP_SPLITKV_MAX_SPLITS", 16)
271
+ self.min_chunk = _env_int("FLASHMAP_SPLITKV_MIN_CHUNK", 64)
272
+ self.block_n = _env_int("FLASHMAP_SPLITKV_BLOCK_N", 64)
273
+ self.num_warps = _env_int("FLASHMAP_SPLITKV_WARPS", 8)
274
+ self.part_out = None
275
+ self.part_lse = None
276
+ self._retired = []
277
+ self._sms = None
278
+
279
+ def splits(self, bs: int, h_kv: int, device) -> int:
280
+ if self._sms is None:
281
+ self._sms = torch.cuda.get_device_properties(device).multi_processor_count
282
+ want = max(1, self.waves * self._sms // (bs * h_kv))
283
+ return min(self.max_splits, 1 << (want.bit_length() - 1))
284
+
285
+ def _scratch(self, parts: int, m: int, block_dv: int, device) -> bool:
286
+ if (
287
+ self.part_out is not None
288
+ and self.part_out.shape[0] >= parts
289
+ and self.part_out.shape[1:] == (m, block_dv)
290
+ ):
291
+ return True
292
+ if (
293
+ torch.cuda.is_current_stream_capturing()
294
+ or parts * m * block_dv * 4 > self.MAX_SCRATCH_BYTES
295
+ ):
296
+ return False
297
+ if self.part_out is not None:
298
+ self._retired.append((self.part_out, self.part_lse))
299
+ self.part_out = torch.empty(
300
+ parts, m, block_dv, dtype=torch.float32, device=device
301
+ )
302
+ self.part_lse = torch.empty(parts, m, dtype=torch.float32, device=device)
303
+ return True
304
+
305
+ def __call__(
306
+ self,
307
+ q,
308
+ k,
309
+ v,
310
+ o,
311
+ k_buffer,
312
+ v_buffer,
313
+ qo_indptr,
314
+ kv_indptr,
315
+ kv_indices,
316
+ custom_mask,
317
+ is_causal,
318
+ mask_indptr,
319
+ max_len_extend,
320
+ k_scale=1.0,
321
+ v_scale=1.0,
322
+ sm_scale=None,
323
+ logit_cap=0.0,
324
+ sliding_window_size=-1,
325
+ sinks=None,
326
+ window_kv_offsets=None,
327
+ xai_temperature_len=-1,
328
+ max_bs=None,
329
+ **unused,
330
+ ) -> bool:
331
+ del window_kv_offsets, max_bs, unused
332
+ handled = self._run(
333
+ q,
334
+ k,
335
+ v,
336
+ o,
337
+ k_buffer,
338
+ v_buffer,
339
+ kv_indptr,
340
+ kv_indices,
341
+ custom_mask,
342
+ is_causal,
343
+ mask_indptr,
344
+ qo_indptr,
345
+ max_len_extend,
346
+ k_scale,
347
+ v_scale,
348
+ sm_scale,
349
+ logit_cap,
350
+ sliding_window_size,
351
+ sinks,
352
+ xai_temperature_len,
353
+ )
354
+ if not handled and custom_mask is not None and self.qlen_mask:
355
+ raise RuntimeError(
356
+ "split-KV verify declined a batch whose mask only it can read (QLEN_ONLY layout)"
357
+ )
358
+ return handled
359
+
360
+ def _run(
361
+ self,
362
+ q,
363
+ k,
364
+ v,
365
+ o,
366
+ k_buffer,
367
+ v_buffer,
368
+ kv_indptr,
369
+ kv_indices,
370
+ custom_mask,
371
+ is_causal,
372
+ mask_indptr,
373
+ qo_indptr,
374
+ max_len_extend,
375
+ k_scale,
376
+ v_scale,
377
+ sm_scale,
378
+ logit_cap,
379
+ sliding_window_size,
380
+ sinks,
381
+ xai_temperature_len,
382
+ ) -> bool:
383
+ if (logit_cap or 0) > 0 or sinks is not None or (sliding_window_size or -1) > 0:
384
+ return False
385
+ if (xai_temperature_len or -1) > 0 or (custom_mask is None and not is_causal):
386
+ return False
387
+ if not (
388
+ isinstance(k_scale, (int, float)) and isinstance(v_scale, (int, float))
389
+ ):
390
+ return False
391
+ bs = qo_indptr.shape[0] - 1
392
+ length = int(max_len_extend)
393
+ h_q, d = q.shape[1], q.shape[2]
394
+ h_kv, dv = k_buffer.shape[1], v_buffer.shape[-1]
395
+ if (
396
+ bs < 1
397
+ or q.shape[0] != bs * length
398
+ or h_q % h_kv
399
+ or k_buffer.dtype != q.dtype
400
+ ):
401
+ return False
402
+ group = h_q // h_kv
403
+ m, blocks = row_blocks(group * length)
404
+ k = k.view(-1, h_kv, d)
405
+ v = v.view(-1, h_kv, dv)
406
+ splits = self.splits(bs, h_kv, q.device)
407
+ block_dv = triton.next_power_of_2(dv)
408
+ if not self._scratch(bs * h_kv * (splits + 1), m * blocks, block_dv, q.device):
409
+ return False
410
+ block_l = max(16, triton.next_power_of_2(length))
411
+ sm_scale = sm_scale or d**-0.5
412
+ use_mask = custom_mask is not None
413
+ _verify_partial_kernel[(bs, h_kv * blocks, splits + 1)](
414
+ q,
415
+ k,
416
+ v,
417
+ k_buffer,
418
+ v_buffer,
419
+ kv_indptr,
420
+ kv_indices,
421
+ custom_mask if use_mask else q,
422
+ mask_indptr if use_mask else kv_indptr,
423
+ self.part_out,
424
+ self.part_lse,
425
+ sm_scale,
426
+ float(k_scale),
427
+ float(v_scale),
428
+ q.stride(0),
429
+ q.stride(1),
430
+ k.stride(0),
431
+ k.stride(1),
432
+ v.stride(0),
433
+ v.stride(1),
434
+ k_buffer.stride(0),
435
+ k_buffer.stride(1),
436
+ v_buffer.stride(0),
437
+ v_buffer.stride(1),
438
+ H_KV=h_kv,
439
+ G=group,
440
+ L=length,
441
+ S=splits,
442
+ D=d,
443
+ DV=dv,
444
+ BLOCK_D=triton.next_power_of_2(d),
445
+ BLOCK_DV=block_dv,
446
+ M=m,
447
+ RB=blocks,
448
+ BLOCK_L=block_l,
449
+ BLOCK_N=self.block_n,
450
+ MIN_CHUNK=self.min_chunk,
451
+ USE_MASK=use_mask,
452
+ QLEN_MASK=self.qlen_mask,
453
+ num_warps=self.num_warps,
454
+ num_stages=1,
455
+ )
456
+ o = o.view(-1, h_q, dv)
457
+ _verify_merge_kernel[(bs, h_q)](
458
+ self.part_out,
459
+ self.part_lse,
460
+ o,
461
+ o.stride(0),
462
+ o.stride(1),
463
+ H_KV=h_kv,
464
+ G=group,
465
+ L=length,
466
+ S=splits,
467
+ DV=dv,
468
+ BLOCK_DV=block_dv,
469
+ ROWS=m * blocks,
470
+ BLOCK_L=block_l,
471
+ num_warps=4,
472
+ num_stages=1,
473
+ )
474
+ return True