-
Notifications
You must be signed in to change notification settings - Fork 143
Expand file tree
/
Copy pathattention.py
More file actions
563 lines (465 loc) · 19.1 KB
/
Copy pathattention.py
File metadata and controls
563 lines (465 loc) · 19.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
import triton
import triton.language as tl
from myvllm.utils import get_context
import torch
import torch.nn as nn
@triton.jit
def store_kvcache_kernel(
key_ptr, # pointer to what we want to store
value_ptr,
k_cache_ptr, # pointer to where we want to store
v_cache_ptr,
slot_mapping_ptr,
num_kv_heads: tl.constexpr,
head_dim: tl.constexpr,
block_size: tl.constexpr
):
"""
Store keys and values into paged KV cache.
Each token is mapped to a slot via slot_mapping.
Grid layout: (num_tokens, num_kv_heads)
Cache layout: (num_blocks, block_size, num_kv_heads, head_dim)
"""
# thread ID, in dimension 0
token_idx = tl.program_id(0) # each GPU thread processes one token
# slot ID, where in cache to store this token
slot_idx = tl.load(slot_mapping_ptr + token_idx)
if slot_idx == -1:
return
# Calculate which block and position within block
block_idx = slot_idx // block_size
block_offset = slot_idx % block_size
# Process each head
# program_id(0) = which token
# program_id(1) = which head
head_idx = tl.program_id(1)
# it creates a vector [0, 1, ..., head_dim-1]
# Load key and value for this token and head
head_offsets = tl.arange(0, head_dim)
# Input: (num_tokens, num_kv_heads, head_dim)
# example: input_offset = 5 * (8 * 128) + 3 * 128 + [0, 1, 2, ..., 127]
# = 5120 + 384 + [0, 1, 2, ..., 127]
# = [5504, 5505, 5506, ..., 5631]
input_offset = (token_idx * num_kv_heads * head_dim + # skip previous tokens
head_idx * head_dim + # skip previous heads
head_offsets)
# Cache: (num_blocks, block_size, num_kv_heads, head_dim)
cache_offset = (block_idx * block_size * num_kv_heads * head_dim + # skip previous blocks
block_offset * num_kv_heads * head_dim + # skip previous positions in block
head_idx * head_dim + # skip previous heads
head_offsets)
# load key and value value floats from the pointers's memory
key = tl.load(key_ptr + input_offset)
value = tl.load(value_ptr + input_offset)
# store into cache
tl.store(k_cache_ptr + cache_offset, key)
tl.store(v_cache_ptr + cache_offset, value)
def store_kvcache(
key: torch.Tensor,
value: torch.Tensor,
k_cache: torch.Tensor,
v_cache: torch.Tensor,
slot_mapping: torch.Tensor,
block_size: int
):
"""
Store key-value pairs into paged cache.
Args:
key: (num_tokens, num_kv_heads, head_dim)
value: (num_tokens, num_kv_heads, head_dim)
k_cache: (num_blocks, block_size, num_kv_heads, head_dim)
v_cache: (num_blocks, block_size, num_kv_heads, head_dim)
slot_mapping: (num_tokens,) - maps each token to a cache slot
block_size: number of tokens per block
"""
num_tokens, num_kv_heads, head_dim = key.shape
# Make contiguous if needed
if not key.is_contiguous():
key = key.contiguous()
if not value.is_contiguous():
value = value.contiguous()
assert k_cache.shape == v_cache.shape, "K and V cache shapes must match"
assert slot_mapping.numel() == num_tokens, "Slot mapping size must match number of tokens"
grid = (num_tokens, num_kv_heads)
# launch num_tokens x num_kv_heads threads
store_kvcache_kernel[grid](
key, # tensors are automatically converted to pointers by triton
value,
k_cache,
v_cache,
slot_mapping,
num_kv_heads=num_kv_heads,
head_dim=head_dim,
block_size=block_size
)
@triton.jit
def flash_attention_varlen_kernel(
Q, K, V, O,
cu_seqlens_q_ptr,
scale,
num_heads: tl.constexpr,
num_kv_heads: tl.constexpr,
head_dim: tl.constexpr,
BLOCK_M: tl.constexpr,
BLOCK_N: tl.constexpr,
):
"""
Flash Attention kernel for variable-length sequences.
Each program processes one block of queries for one head in one sequence.
"""
# Program IDs
start_m = tl.program_id(0) # block index
off_h = tl.program_id(1) # head index
seq_idx = tl.program_id(2) # sequence index
# Determine which KV head to use (for GQA)
kv_head_idx = off_h // (num_heads // num_kv_heads)
# Load sequence boundaries
seq_start = tl.load(cu_seqlens_q_ptr + seq_idx)
seq_end = tl.load(cu_seqlens_q_ptr + seq_idx + 1)
seq_len = seq_end - seq_start
# Early exit if this block is beyond sequence length
if start_m * BLOCK_M >= seq_len:
return
# Offset for this block of queries
offs_m = start_m * BLOCK_M + tl.arange(0, BLOCK_M)
offs_d = tl.arange(0, head_dim)
# Query pointers: Q has shape (total_tokens, num_heads, head_dim)
q_ptrs = Q + (seq_start + offs_m[:, None]) * num_heads * head_dim + off_h * head_dim + offs_d[None, :]
# Load Q block - shape (BLOCK_M, head_dim)
mask_m = offs_m < seq_len
q = tl.load(q_ptrs, mask=mask_m[:, None], other=0.0)
# Initialize output accumulators
l_i = tl.zeros([BLOCK_M], dtype=tl.float32)
m_i = tl.zeros([BLOCK_M], dtype=tl.float32) - 1e10
acc = tl.zeros([BLOCK_M, head_dim], dtype=tl.float32)
# Number of blocks to process
num_blocks = tl.cdiv(seq_len, BLOCK_N)
# Loop over K, V blocks
for block_n in range(num_blocks):
start_n = block_n * BLOCK_N
offs_n = start_n + tl.arange(0, BLOCK_N)
# Mask for valid positions
mask_n = offs_n < seq_len
# K pointers: K has shape (total_tokens, num_kv_heads, head_dim)
k_ptrs = K + (seq_start + offs_n[None, :]) * num_kv_heads * head_dim + kv_head_idx * head_dim + offs_d[:, None]
# Load K block - shape (head_dim, BLOCK_N)
k = tl.load(k_ptrs, mask=mask_n[None, :], other=0.0)
# Compute QK^T - shape (BLOCK_M, BLOCK_N)
qk = tl.dot(q, k)
qk = qk * scale
# Apply causal mask: only attend to positions <= current position
mask_causal = (offs_m[:, None] + seq_start) >= (offs_n[None, :] + seq_start)
qk = tl.where(mask_causal & mask_n[None, :], qk, -1e10)
# Online softmax update
m_ij = tl.max(qk, axis=1)
m_i_new = tl.maximum(m_i, m_ij)
alpha = tl.exp(m_i - m_i_new)
p = tl.exp(qk - m_i_new[:, None])
# Rescale previous accumulator
acc = acc * alpha[:, None]
# Load V block - shape (BLOCK_N, head_dim)
v_ptrs = V + (seq_start + offs_n[:, None]) * num_kv_heads * head_dim + kv_head_idx * head_dim + offs_d[None, :]
v = tl.load(v_ptrs, mask=mask_n[:, None], other=0.0)
# Accumulate weighted values
acc = acc + tl.dot(p.to(v.dtype), v)
# Update normalizer
l_i = l_i * alpha + tl.sum(p, axis=1)
m_i = m_i_new
# Final normalization
acc = acc / l_i[:, None]
# Store output: O has shape (total_tokens, num_heads, head_dim)
o_ptrs = O + (seq_start + offs_m[:, None]) * num_heads * head_dim + off_h * head_dim + offs_d[None, :]
tl.store(o_ptrs, acc.to(O.dtype.element_ty), mask=mask_m[:, None])
def flash_attention_prefill(
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor,
cu_seqlens: torch.Tensor,
scale: float,
num_heads: int,
num_kv_heads: int,
head_dim: int,
) -> torch.Tensor:
"""
Optimized Flash Attention for prefill phase with variable-length sequences.
Args:
q: (total_tokens, num_heads, head_dim)
k: (total_tokens, num_kv_heads, head_dim)
v: (total_tokens, num_kv_heads, head_dim)
cu_seqlens: cumulative sequence lengths
scale: attention scale factor
Returns:
output: (total_tokens, num_heads, head_dim)
"""
# Make tensors contiguous
q = q.contiguous()
k = k.contiguous()
v = v.contiguous()
# Allocate output
output = torch.empty_like(q)
# Conservative block sizes to avoid OOM on shared memory
# Shared memory usage ~ BLOCK_M * BLOCK_N * 4 bytes (for float32 attention scores)
# + BLOCK_M * head_dim * 4 (for Q)
# + BLOCK_N * head_dim * 4 (for K, V)
# Want to keep total < 48KB for most GPUs
if head_dim <= 64:
BLOCK_M = 64
BLOCK_N = 64
elif head_dim <= 128:
BLOCK_M = 32
BLOCK_N = 32
else:
BLOCK_M = 16
BLOCK_N = 16
# Number of sequences
num_seqs = cu_seqlens.shape[0] - 1
# Find max sequence length to determine grid size
cu_seqlens_cpu = cu_seqlens.cpu()
max_seq_len = (cu_seqlens_cpu[1:] - cu_seqlens_cpu[:-1]).max().item()
# Calculate grid dimensions - launch all kernels at once
grid = (triton.cdiv(max_seq_len, BLOCK_M), num_heads, num_seqs)
flash_attention_varlen_kernel[grid](
q, k, v, output,
cu_seqlens,
scale,
num_heads=num_heads,
num_kv_heads=num_kv_heads,
head_dim=head_dim,
BLOCK_M=BLOCK_M,
BLOCK_N=BLOCK_N,
)
return output
@triton.jit
def paged_attention_decode_kernel(
output_ptr,
query_ptr,
k_cache_ptr,
v_cache_ptr,
block_tables_ptr,
context_lens_ptr,
scale: tl.constexpr,
num_heads: tl.constexpr,
num_kv_heads: tl.constexpr,
head_dim: tl.constexpr,
block_size: tl.constexpr,
max_num_blocks: tl.constexpr,
BLOCK_N: tl.constexpr,
):
"""
Optimized paged attention kernel for decode phase.
Processes KV cache in chunks.
"""
batch_idx = tl.program_id(0)
head_idx = tl.program_id(1)
# Determine which KV head this query head uses (for GQA)
kv_head_idx = head_idx // (num_heads // num_kv_heads)
# Load context length
context_len = tl.load(context_lens_ptr + batch_idx)
# Load query: (batch_size, num_heads, head_dim)
offs_d = tl.arange(0, head_dim)
q_offset = batch_idx * num_heads * head_dim + head_idx * head_dim + offs_d
q = tl.load(query_ptr + q_offset)
# Initialize accumulators
acc = tl.zeros([head_dim], dtype=tl.float32)
l_i = 0.0
m_i = -1e10
# Calculate total number of chunks to process
max_chunks = tl.cdiv(max_num_blocks * block_size, BLOCK_N)
# Process all tokens in chunks
for chunk_idx in range(max_chunks):
# Global token index for this chunk
token_start = chunk_idx * BLOCK_N
# Only process if within valid range
if token_start < context_len:
# Determine which tokens in this chunk are valid
offs_n = token_start + tl.arange(0, BLOCK_N)
mask_n = offs_n < context_len
# Compute attention scores for this chunk
qk = tl.zeros([BLOCK_N], dtype=tl.float32) - 1e10
# Load K for each valid position and compute scores
for i in range(BLOCK_N):
token_idx = token_start + i
if token_idx < context_len:
block_num = token_idx // block_size
block_offset = token_idx % block_size
if block_num < max_num_blocks:
# Look up physical block
block_table_offset = batch_idx * max_num_blocks + block_num
physical_block_idx = tl.load(block_tables_ptr + block_table_offset)
if physical_block_idx != -1:
# Load K
k_offset = (physical_block_idx * block_size * num_kv_heads * head_dim +
block_offset * num_kv_heads * head_dim +
kv_head_idx * head_dim + offs_d)
k_vec = tl.load(k_cache_ptr + k_offset)
# Compute score for this token
score = tl.sum(q * k_vec) * scale
# Update qk array at position i using tl.where
mask_i = tl.arange(0, BLOCK_N) == i
qk = tl.where(mask_i, score, qk)
# Apply mask to invalid positions
qk = tl.where(mask_n, qk, -1e10)
# Online softmax
m_ij = tl.max(qk)
m_i_new = tl.maximum(m_i, m_ij)
alpha = tl.exp(m_i - m_i_new)
p = tl.exp(qk - m_i_new)
# Rescale accumulator
acc = acc * alpha
l_i = l_i * alpha
# Load V and accumulate
for i in range(BLOCK_N):
token_idx = token_start + i
if token_idx < context_len:
block_num = token_idx // block_size
block_offset = token_idx % block_size
if block_num < max_num_blocks:
# Look up physical block
block_table_offset = batch_idx * max_num_blocks + block_num
physical_block_idx = tl.load(block_tables_ptr + block_table_offset)
if physical_block_idx != -1:
# Load V
v_offset = (physical_block_idx * block_size * num_kv_heads * head_dim +
block_offset * num_kv_heads * head_dim +
kv_head_idx * head_dim + offs_d)
v_vec = tl.load(v_cache_ptr + v_offset)
# Extract weight for this token from p
mask_i = tl.arange(0, BLOCK_N) == i
weight = tl.sum(tl.where(mask_i, p, 0.0))
acc = acc + weight * v_vec
l_i = l_i + weight
m_i = m_i_new
# Normalize
output = acc / l_i
# Store output
output_offset = batch_idx * num_heads * head_dim + head_idx * head_dim + offs_d
tl.store(output_ptr + output_offset, output)
def paged_attention_decode(
query: torch.Tensor,
k_cache: torch.Tensor,
v_cache: torch.Tensor,
block_tables: torch.Tensor,
context_lens: torch.Tensor,
scale: float,
num_heads: int,
num_kv_heads: int,
head_dim: int,
block_size: int
) -> torch.Tensor:
"""
Compute attention in decode mode using paged KV cache.
Args:
query: (batch_size, num_heads, head_dim)
k_cache: (num_blocks, block_size, num_kv_heads, head_dim)
v_cache: (num_blocks, block_size, num_kv_heads, head_dim)
block_tables: (batch_size, max_num_blocks)
context_lens: (batch_size,)
scale: attention scale factor
Returns:
output: (batch_size, num_heads, head_dim)
"""
batch_size = query.shape[0]
max_num_blocks = block_tables.shape[1]
# Make contiguous
query = query.contiguous()
output = torch.empty_like(query)
# Chunk size for processing KV tokens
BLOCK_N = 64 if head_dim <= 128 else 32
grid = (batch_size, num_heads)
paged_attention_decode_kernel[grid](
output,
query,
k_cache,
v_cache,
block_tables,
context_lens,
scale=scale,
num_heads=num_heads,
num_kv_heads=num_kv_heads,
head_dim=head_dim,
block_size=block_size,
max_num_blocks=max_num_blocks,
BLOCK_N=BLOCK_N,
)
return output
class Attention(nn.Module):
def __init__(
self,
num_heads: int,
head_dim: int,
scale: float = 1.0,
num_kv_heads: int = None,
block_size: int = 16,
):
super().__init__()
self.num_heads = num_heads
self.head_dim = head_dim
self.scale = scale
self.num_kv_heads = num_kv_heads if num_kv_heads is not None else num_heads
self.block_size = block_size
self.k_cache = self.v_cache = torch.tensor([])
def forward(self, q: torch.Tensor, k: torch.Tensor, v: torch.Tensor) -> torch.Tensor:
context = get_context()
k_cache, v_cache = self.k_cache, self.v_cache
# Store current k, v into cache if cache is allocated
if k_cache.numel() > 0 and v_cache.numel() > 0 and context.slot_mapping is not None:
# Ensure k, v are in the right shape: (num_tokens, num_kv_heads, head_dim)
if k.dim() == 4:
# Batched: (B, N, num_kv_heads, head_dim) -> reshape to (B*N, num_kv_heads, head_dim)
B, N, num_kv_heads, head_dim = k.shape
k_to_store = k.reshape(B * N, num_kv_heads, head_dim).contiguous()
v_to_store = v.reshape(B * N, num_kv_heads, head_dim).contiguous()
else:
# Already in correct shape (num_tokens, num_kv_heads, head_dim)
k_to_store = k.contiguous()
v_to_store = v.contiguous()
store_kvcache(k_to_store, v_to_store, k_cache, v_cache, context.slot_mapping, self.block_size)
scale = self.scale / (self.head_dim ** 0.5)
if context.is_prefill:
# Prefill: use flash attention
# Varlen mode: (total_tokens, num_heads, head_dim)
cu_seqlens = context.cu_seqlens_q
if cu_seqlens is None:
raise ValueError("cu_seqlens_q must be provided for varlen attention")
o = flash_attention_prefill(q, k, v, cu_seqlens, scale,
self.num_heads, self.num_kv_heads, self.head_dim)
# Output: (total_tokens, num_heads, head_dim) -> (total_tokens, num_heads * head_dim)
return o.reshape(o.shape[0], self.num_heads * self.head_dim)
else:
o = paged_attention_decode(
q,
k_cache,
v_cache,
context.block_tables,
context.context_lens,
scale,
self.num_heads,
self.num_kv_heads,
self.head_dim,
self.block_size
)
# o: (batch_size, num_heads, head_dim) -> (batch_size, num_heads * head_dim)
return o.reshape(o.shape[0], self.num_heads * self.head_dim)
if __name__ == "__main__":
# Example usage
layer = Attention(num_heads=8, head_dim=64).cuda()
B, N, D = 4, 1024, 512
q = torch.randn(B, N, D).cuda()
k = torch.randn(B, N, D).cuda()
v = torch.randn(B, N, D).cuda()
layer.k_cache = torch.zeros(B, N, D).cuda()
layer.v_cache = torch.zeros(B, N, D).cuda()
slot_mapping = torch.arange(N).cuda()
for _ in range(10): # Warm-up iterations
_ = layer(q, k, v)
import time
times = []
for _ in range(100): # Timing iterations
torch.cuda.synchronize()
start_time = time.time()
output_tensor = layer(q, k, v)
torch.cuda.synchronize()
end_time = time.time()
times.append(end_time - start_time)
avg_time = sum(times) / len(times)
print(f"Average inference time over 100 runs: {avg_time * 1000:.4f} ms")