Kernels

Add flash_attn_with_kvcache paged-decode kernel (port from huggingface/transformers#45977)

#4
by ArthurZ HF Staff - opened
sdpa-metal/flash_attn_with_kvcache.metal ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Paged-attention decode kernel — Flash-Decoding style single-pass online
2
+ // softmax, reading K/V from a paged cache via a block-table indirection.
3
+ //
4
+ // One simdgroup (32 threads) per (request, head) pair. Each thread handles a
5
+ // stride-PD_TG_THREADS slice of head_dim. The per-position score is reduced
6
+ // across the simdgroup with simd_sum.
7
+ //
8
+ // Layout:
9
+ // q: (B, H_Q, head_dim) fp32 (one query token per request)
10
+ // k_cache: (num_slots, H_KV, head_dim) fp32 (paged storage)
11
+ // v_cache: (num_slots, H_KV, head_dim) fp32
12
+ // block_table: (B, max_blocks) int32 (slot = bt[b, t/bs] * bs + t%bs)
13
+ // seq_lens: (B,) int32 (valid KV positions per request)
14
+ // out: (B, H_Q, head_dim) fp32
15
+ //
16
+ // Dispatch: grid = (B, H_Q, 1), tg = (32, 1, 1)
17
+ //
18
+ // Threadgroup memory layout (fp32 floats):
19
+ // q_sh: head_dim
20
+ // o_sh: head_dim
21
+ // score_sh: 1 (broadcast slot for the per-position score)
22
+ // m_sh: 1 (broadcast slot for the running max)
23
+ // l_sh: 1 (broadcast slot for the running denominator)
24
+ //
25
+ // Ported from huggingface/transformers' gguf-dequant-kernels package
26
+ // (paged_decode_attention_f32). Bit-equivalent to a reference SDPA over the
27
+ // gathered K/V at (B=4, H_Q=16, head_dim=128, S=50). Runs at ~134 µs/call on
28
+ // M3 Max for that shape; well below the ~16 ms/layer it costs to gather the
29
+ // cache into a contiguous tensor and run torch SDPA over it.
30
+
31
+ #include <metal_stdlib>
32
+ using namespace metal;
33
+
34
+ #define PD_TG_THREADS 32
35
+
36
+ kernel void paged_decode_attention_f32(
37
+ device const float *q [[buffer(0)]], // (B, H_Q, head_dim)
38
+ device const float *k_cache [[buffer(1)]], // (num_slots, H_KV, head_dim)
39
+ device const float *v_cache [[buffer(2)]], // (num_slots, H_KV, head_dim)
40
+ device const int *block_table [[buffer(3)]], // (B, max_blocks)
41
+ device const int *seq_lens [[buffer(4)]], // (B,)
42
+ device float *out [[buffer(5)]], // (B, H_Q, head_dim)
43
+ constant uint &B [[buffer(6)]],
44
+ constant uint &H_Q [[buffer(7)]],
45
+ constant uint &H_KV [[buffer(8)]],
46
+ constant uint &head_dim [[buffer(9)]],
47
+ constant uint &block_size [[buffer(10)]],
48
+ constant uint &max_blocks [[buffer(11)]],
49
+ constant float &scale [[buffer(12)]],
50
+ threadgroup float *shmem [[threadgroup(0)]],
51
+ uint3 gid [[threadgroup_position_in_grid]],
52
+ uint3 tid3 [[thread_position_in_threadgroup]])
53
+ {
54
+ const uint tid = tid3.x;
55
+ const uint b = gid.x;
56
+ const uint h_q = gid.y;
57
+ if (b >= B || h_q >= H_Q) return;
58
+
59
+ const uint num_kv_groups = H_Q / H_KV;
60
+ const uint h_kv = h_q / num_kv_groups;
61
+
62
+ const uint q_row_off = (b * H_Q + h_q) * head_dim;
63
+ const uint S = (uint)seq_lens[b];
64
+
65
+ threadgroup float *q_sh = shmem;
66
+ threadgroup float *o_sh = shmem + head_dim;
67
+ threadgroup float *score_sh = shmem + 2 * head_dim;
68
+ threadgroup float *m_sh = shmem + 2 * head_dim + 1;
69
+ threadgroup float *l_sh = shmem + 2 * head_dim + 2;
70
+
71
+ for (uint i = tid; i < head_dim; i += PD_TG_THREADS) {
72
+ q_sh[i] = q[q_row_off + i];
73
+ o_sh[i] = 0.f;
74
+ }
75
+ if (tid == 0) {
76
+ *m_sh = -INFINITY;
77
+ *l_sh = 0.f;
78
+ }
79
+ threadgroup_barrier(metal::mem_flags::mem_threadgroup);
80
+
81
+ for (uint pos = 0; pos < S; pos++) {
82
+ const uint blk_idx = pos / block_size;
83
+ const uint in_blk = pos % block_size;
84
+ const int bt_val = block_table[b * max_blocks + blk_idx];
85
+ if (bt_val < 0) continue; // padding slot
86
+ const uint slot = (uint)bt_val * block_size + in_blk;
87
+ const uint k_off = (slot * H_KV + h_kv) * head_dim;
88
+
89
+ float partial = 0.f;
90
+ for (uint i = tid; i < head_dim; i += PD_TG_THREADS) {
91
+ partial += q_sh[i] * k_cache[k_off + i];
92
+ }
93
+ partial = simd_sum(partial);
94
+ if (tid == 0) *score_sh = partial * scale;
95
+ threadgroup_barrier(metal::mem_flags::mem_threadgroup);
96
+
97
+ const float score = *score_sh;
98
+ const float m_old = *m_sh;
99
+ const float m_new = max(m_old, score);
100
+ const float alpha = exp(m_old - m_new);
101
+ const float p_val = exp(score - m_new);
102
+
103
+ if (tid == 0) {
104
+ *l_sh = (*l_sh) * alpha + p_val;
105
+ *m_sh = m_new;
106
+ }
107
+
108
+ const uint v_off = (slot * H_KV + h_kv) * head_dim;
109
+ for (uint i = tid; i < head_dim; i += PD_TG_THREADS) {
110
+ o_sh[i] = o_sh[i] * alpha + p_val * v_cache[v_off + i];
111
+ }
112
+ threadgroup_barrier(metal::mem_flags::mem_threadgroup);
113
+ }
114
+
115
+ const float l_final = *l_sh;
116
+ for (uint i = tid; i < head_dim; i += PD_TG_THREADS) {
117
+ out[q_row_off + i] = o_sh[i] / l_final;
118
+ }
119
+ }
120
+
121
+
122
+ // Single-dispatch paged KV cache write. Computes write_slot from
123
+ // block_table + seq_lens on-device and writes new K/V into the paged cache.
124
+ // Used by ``flash_attn_with_kvcache`` to fold the per-step cache write into
125
+ // the same Metal pass.
126
+ //
127
+ // k_cache / v_cache : (num_slots, H_KV, D) fp32 in-place
128
+ // new_k / new_v : (B, H_KV, D) fp32 (per-request new token)
129
+ // block_table : (B, max_blocks) int32
130
+ // seq_lens : (B,) int32 (length AFTER write)
131
+
132
+ kernel void kv_paged_write_f32(
133
+ device float *k_cache [[buffer(0)]],
134
+ device float *v_cache [[buffer(1)]],
135
+ device const float *new_k [[buffer(2)]],
136
+ device const float *new_v [[buffer(3)]],
137
+ device const int *block_table [[buffer(4)]],
138
+ device const int *seq_lens [[buffer(5)]],
139
+ constant uint &B [[buffer(6)]],
140
+ constant uint &H_KV [[buffer(7)]],
141
+ constant uint &D [[buffer(8)]],
142
+ constant uint &block_size [[buffer(9)]],
143
+ constant uint &max_blocks [[buffer(10)]],
144
+ uint3 gid [[thread_position_in_grid]])
145
+ {
146
+ const uint b = gid.x, h = gid.y, d = gid.z;
147
+ if (b >= B || h >= H_KV || d >= D) return;
148
+ const int seq_len = seq_lens[b];
149
+ if (seq_len <= 0) return;
150
+ const uint pos = (uint)(seq_len - 1);
151
+ const uint block_in_seq = pos / block_size;
152
+ if (block_in_seq >= max_blocks) return;
153
+ const uint block_id = (uint)block_table[b * max_blocks + block_in_seq];
154
+ const uint write_slot = block_id * block_size + (pos % block_size);
155
+ const uint64_t new_off = (uint64_t)b * H_KV * D + (uint64_t)h * D + d;
156
+ const uint64_t cache_off = (uint64_t)write_slot * H_KV * D + (uint64_t)h * D + d;
157
+ k_cache[cache_off] = new_k[new_off];
158
+ v_cache[cache_off] = new_v[new_off];
159
+ }