Kernels
hellmans EricB HF Staff commited on
Commit
cc6ca73
·
0 Parent(s):

Duplicate from kernels-community/metal-flash-sdpa

Browse files

Co-authored-by: Eric Buehler <EricB@users.noreply.huggingface.co>

Files changed (43) hide show
  1. .gitattributes +37 -0
  2. README.md +27 -0
  3. benchmark.py +304 -0
  4. build.toml +18 -0
  5. build/torch210-metal-aarch64-darwin/__init__.py +11 -0
  6. build/torch210-metal-aarch64-darwin/_custom_ops.py +117 -0
  7. build/torch210-metal-aarch64-darwin/_metal_flash_sdpa_metal_9cd92d8.abi3.so +3 -0
  8. build/torch210-metal-aarch64-darwin/_ops.py +9 -0
  9. build/torch210-metal-aarch64-darwin/metadata.json +10 -0
  10. build/torch210-metal-aarch64-darwin/metal_flash_sdpa/__init__.py +26 -0
  11. build/torch211-metal-aarch64-darwin/__init__.py +11 -0
  12. build/torch211-metal-aarch64-darwin/_custom_ops.py +117 -0
  13. build/torch211-metal-aarch64-darwin/_metal_flash_sdpa_metal_9cd92d8.abi3.so +3 -0
  14. build/torch211-metal-aarch64-darwin/_ops.py +9 -0
  15. build/torch211-metal-aarch64-darwin/metadata.json +10 -0
  16. build/torch211-metal-aarch64-darwin/metal_flash_sdpa/__init__.py +26 -0
  17. build/torch212-metal-aarch64-darwin/__init__.py +11 -0
  18. build/torch212-metal-aarch64-darwin/_custom_ops.py +117 -0
  19. build/torch212-metal-aarch64-darwin/_metal_flash_sdpa_metal_9cd92d8.abi3.so +3 -0
  20. build/torch212-metal-aarch64-darwin/_ops.py +9 -0
  21. build/torch212-metal-aarch64-darwin/metadata.json +10 -0
  22. build/torch212-metal-aarch64-darwin/metal_flash_sdpa/__init__.py +26 -0
  23. build/torch27-metal-aarch64-darwin/metal_flash_sdpa/__init__.py +11 -0
  24. build/torch27-metal-aarch64-darwin/metal_flash_sdpa/_custom_ops.py +117 -0
  25. build/torch27-metal-aarch64-darwin/metal_flash_sdpa/_metal_flash_sdpa_8aa66c2.abi3.so +3 -0
  26. build/torch27-metal-aarch64-darwin/metal_flash_sdpa/_ops.py +9 -0
  27. build/torch29-metal-aarch64-darwin/__init__.py +11 -0
  28. build/torch29-metal-aarch64-darwin/_custom_ops.py +117 -0
  29. build/torch29-metal-aarch64-darwin/_metal_flash_sdpa_metal_8924527.abi3.so +3 -0
  30. build/torch29-metal-aarch64-darwin/_ops.py +9 -0
  31. build/torch29-metal-aarch64-darwin/metadata.json +5 -0
  32. build/torch29-metal-aarch64-darwin/metal_flash_sdpa/__init__.py +26 -0
  33. flake.lock +168 -0
  34. flake.nix +17 -0
  35. sdpa-metal/common.h +7 -0
  36. sdpa-metal/scaled_dot_product_attention.metal +2101 -0
  37. sdpa-metal/scaled_dot_product_attention.mm +316 -0
  38. tests/__init__.py +0 -0
  39. tests/test_flash_attention.py +480 -0
  40. torch-ext/metal_flash_sdpa/__init__.py +11 -0
  41. torch-ext/metal_flash_sdpa/_custom_ops.py +117 -0
  42. torch-ext/torch_binding.cpp +11 -0
  43. torch-ext/torch_binding.h +16 -0
.gitattributes ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ *.so filter=lfs diff=lfs merge=lfs -text
37
+ *.metallib filter=lfs diff=lfs merge=lfs -text
README.md ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ library_name: kernels
3
+ license: apache-2.0
4
+ ---
5
+
6
+ This is the repository card of kernels-community/metal-flash-sdpa that has been pushed on the Hub. It was built to be used with the [`kernels` library](https://github.com/huggingface/kernels). This card was automatically generated.
7
+
8
+ ## How to use
9
+
10
+ ```python
11
+ # make sure `kernels` is installed: `pip install -U kernels`
12
+ from kernels import get_kernel
13
+
14
+ kernel_module = get_kernel("kernels-community/metal-flash-sdpa")
15
+ flash_attention_varlen = kernel_module.flash_attention_varlen
16
+
17
+ flash_attention_varlen(...)
18
+ ```
19
+
20
+ ## Available functions
21
+ - `flash_attention_varlen`
22
+ - `flash_attn_varlen_func`
23
+ - `ops`
24
+
25
+ ## Benchmarks
26
+
27
+ No benchmark available yet.
benchmark.py ADDED
@@ -0,0 +1,304 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Benchmark causal mask performance scaling with sequence length"""
3
+
4
+ import torch
5
+ import time
6
+ import matplotlib.pyplot as plt
7
+ import numpy as np
8
+ from typing import List
9
+ import kernels
10
+
11
+ metal_flash_sdpa = kernels.get_kernel("kernels-community/metal-flash-sdpa")
12
+
13
+
14
+ def create_cu_seqlens(seq_lengths: List[int]) -> torch.Tensor:
15
+ """Create cumulative sequence lengths tensor."""
16
+ cu_seqlens = [0]
17
+ for length in seq_lengths:
18
+ cu_seqlens.append(cu_seqlens[-1] + length)
19
+ return torch.tensor(cu_seqlens, dtype=torch.int32, device="mps")
20
+
21
+
22
+ def benchmark_flash_sdpa_causal(
23
+ batch_size: int,
24
+ num_heads: int,
25
+ seq_len: int,
26
+ head_dim: int,
27
+ dtype: torch.dtype,
28
+ num_iterations: int = 20,
29
+ ) -> float:
30
+ """Benchmark Flash SDPA with causal mask"""
31
+
32
+ seq_lengths = [seq_len] * batch_size
33
+ cu_seqlens = create_cu_seqlens(seq_lengths)
34
+ total_tokens = sum(seq_lengths)
35
+
36
+ # Create input tensors
37
+ query = torch.randn(total_tokens, num_heads, head_dim, dtype=dtype, device="mps")
38
+ key = torch.randn(total_tokens, num_heads, head_dim, dtype=dtype, device="mps")
39
+ value = torch.randn(total_tokens, num_heads, head_dim, dtype=dtype, device="mps")
40
+ out = torch.empty_like(query)
41
+
42
+ scale = 1.0 / (head_dim**0.5)
43
+
44
+ # Warmup
45
+ for _ in range(5):
46
+ metal_flash_sdpa.flash_attention_varlen(
47
+ out=out,
48
+ query=query,
49
+ key=key,
50
+ value=value,
51
+ cu_seqlens_q=cu_seqlens,
52
+ cu_seqlens_k=cu_seqlens,
53
+ max_seqlen_q=seq_len,
54
+ max_seqlen_k=seq_len,
55
+ do_causal=True,
56
+ scale=scale,
57
+ softcapping=1.0,
58
+ )
59
+ torch.mps.synchronize()
60
+
61
+ # Benchmark
62
+ start_time = time.perf_counter()
63
+ for _ in range(num_iterations):
64
+ metal_flash_sdpa.flash_attention_varlen(
65
+ out=out,
66
+ query=query,
67
+ key=key,
68
+ value=value,
69
+ cu_seqlens_q=cu_seqlens,
70
+ cu_seqlens_k=cu_seqlens,
71
+ max_seqlen_q=seq_len,
72
+ max_seqlen_k=seq_len,
73
+ do_causal=True,
74
+ scale=scale,
75
+ softcapping=1.0,
76
+ )
77
+ torch.mps.synchronize()
78
+ end_time = time.perf_counter()
79
+
80
+ return (end_time - start_time) * 1000 / num_iterations
81
+
82
+
83
+ def benchmark_naive_sdpa_causal(
84
+ batch_size: int,
85
+ num_heads: int,
86
+ seq_len: int,
87
+ head_dim: int,
88
+ dtype: torch.dtype,
89
+ num_iterations: int = 20,
90
+ ) -> float:
91
+ """Benchmark naive SDPA with causal mask"""
92
+
93
+ # Create input tensors
94
+ query = torch.randn(
95
+ batch_size, num_heads, seq_len, head_dim, dtype=dtype, device="mps"
96
+ )
97
+ key = torch.randn(
98
+ batch_size, num_heads, seq_len, head_dim, dtype=dtype, device="mps"
99
+ )
100
+ value = torch.randn(
101
+ batch_size, num_heads, seq_len, head_dim, dtype=dtype, device="mps"
102
+ )
103
+
104
+ scale = 1.0 / (head_dim**0.5)
105
+
106
+ # Precompute causal mask
107
+ mask = torch.triu(torch.ones(seq_len, seq_len, device="mps"), diagonal=1).bool()
108
+
109
+ # Warmup
110
+ for _ in range(5):
111
+ scores = torch.matmul(query, key.transpose(-2, -1)) * scale
112
+ scores = scores.masked_fill(mask, float("-inf"))
113
+ attn_weights = torch.softmax(scores, dim=-1)
114
+ out = torch.matmul(attn_weights, value)
115
+ torch.mps.synchronize()
116
+
117
+ # Benchmark
118
+ start_time = time.perf_counter()
119
+ for _ in range(num_iterations):
120
+ scores = torch.matmul(query, key.transpose(-2, -1)) * scale
121
+ scores = scores.masked_fill(mask, float("-inf"))
122
+ attn_weights = torch.softmax(scores, dim=-1)
123
+ out = torch.matmul(attn_weights, value)
124
+ torch.mps.synchronize()
125
+ end_time = time.perf_counter()
126
+
127
+ return (end_time - start_time) * 1000 / num_iterations
128
+
129
+
130
+ def run_scaling_benchmark():
131
+ """Run causal mask scaling benchmark"""
132
+
133
+ print("=" * 80)
134
+ print("Causal Mask Performance Scaling Benchmark")
135
+ print("Batch Size: 4, Head Dimension: 64")
136
+ print("=" * 80)
137
+
138
+ # Configuration
139
+ batch_size = 4
140
+ num_heads = 16
141
+ head_dim = 64
142
+ dtype = torch.float16
143
+
144
+ # Sequence lengths from 512 to 4096
145
+ seq_lengths = [512, 768, 1024, 1536, 2048, 3072, 4096]
146
+
147
+ flash_times = []
148
+ naive_times = []
149
+ speedups = []
150
+
151
+ print(f"{'Seq Len':<8} {'Flash (ms)':<12} {'Naive (ms)':<12} {'Speedup':<10}")
152
+ print("-" * 50)
153
+
154
+ for seq_len in seq_lengths:
155
+ # Benchmark Flash SDPA
156
+ flash_time = benchmark_flash_sdpa_causal(
157
+ batch_size, num_heads, seq_len, head_dim, dtype
158
+ )
159
+ flash_times.append(flash_time)
160
+
161
+ # Benchmark Naive SDPA
162
+ naive_time = benchmark_naive_sdpa_causal(
163
+ batch_size, num_heads, seq_len, head_dim, dtype
164
+ )
165
+ naive_times.append(naive_time)
166
+
167
+ speedup = naive_time / flash_time
168
+ speedups.append(speedup)
169
+
170
+ print(f"{seq_len:<8} {flash_time:<12.2f} {naive_time:<12.2f} {speedup:<10.2f}x")
171
+
172
+ return seq_lengths, flash_times, naive_times, speedups
173
+
174
+
175
+ def create_line_plot(seq_lengths, flash_times, naive_times, speedups):
176
+ """Create line graph visualization"""
177
+
178
+ # Create figure with single plot
179
+ fig, ax = plt.subplots(1, 1, figsize=(12, 8))
180
+ fig.suptitle(
181
+ "Causal Mask Performance Scaling\n(Batch Size: 4, Head Dimension: 64)",
182
+ fontsize=16,
183
+ )
184
+
185
+ # Plot execution times
186
+ ax.plot(
187
+ seq_lengths,
188
+ flash_times,
189
+ marker="o",
190
+ linewidth=3,
191
+ markersize=10,
192
+ label="Flash SDPA",
193
+ color="blue",
194
+ )
195
+ ax.plot(
196
+ seq_lengths,
197
+ naive_times,
198
+ marker="s",
199
+ linewidth=3,
200
+ markersize=10,
201
+ label="Naive SDPA",
202
+ color="red",
203
+ )
204
+
205
+ ax.set_xlabel("Sequence Length", fontsize=14)
206
+ ax.set_ylabel("Time (ms)", fontsize=14)
207
+ ax.set_title("Execution Time vs Sequence Length", fontsize=16)
208
+ ax.grid(True, alpha=0.3)
209
+ ax.legend(fontsize=12)
210
+
211
+ # Add value annotations for all points
212
+ for i, (seq_len, flash_time, naive_time) in enumerate(
213
+ zip(seq_lengths, flash_times, naive_times)
214
+ ):
215
+ ax.annotate(
216
+ f"{flash_time:.1f}ms",
217
+ xy=(seq_len, flash_time),
218
+ xytext=(5, 5),
219
+ textcoords="offset points",
220
+ fontsize=10,
221
+ color="blue",
222
+ )
223
+ ax.annotate(
224
+ f"{naive_time:.1f}ms",
225
+ xy=(seq_len, naive_time),
226
+ xytext=(5, 5),
227
+ textcoords="offset points",
228
+ fontsize=10,
229
+ color="red",
230
+ )
231
+
232
+ # Set axis limits to better show the data
233
+ ax.set_xlim(seq_lengths[0] - 100, seq_lengths[-1] + 100)
234
+ ax.set_ylim(0, max(naive_times) * 1.1)
235
+
236
+ plt.tight_layout()
237
+ plt.savefig("benchmark.png", dpi=300, bbox_inches="tight")
238
+ plt.show()
239
+
240
+
241
+ def print_analysis(seq_lengths, flash_times, naive_times, speedups):
242
+ """Print detailed analysis of the results"""
243
+
244
+ print("\n" + "=" * 80)
245
+ print("DETAILED ANALYSIS")
246
+ print("=" * 80)
247
+
248
+ # Performance scaling analysis
249
+ print("\n1. Performance Scaling:")
250
+ print(
251
+ f" • Flash SDPA: {flash_times[0]:.2f}ms → {flash_times[-1]:.2f}ms ({flash_times[-1] / flash_times[0]:.1f}x increase)"
252
+ )
253
+ print(
254
+ f" • Naive SDPA: {naive_times[0]:.2f}ms → {naive_times[-1]:.2f}ms ({naive_times[-1] / naive_times[0]:.1f}x increase)"
255
+ )
256
+
257
+ # Speedup analysis
258
+ print("\n2. Speedup Analysis:")
259
+ print(f" • Average Speedup: {np.mean(speedups):.2f}x")
260
+ print(
261
+ f" • Max Speedup: {np.max(speedups):.2f}x (at seq_len={seq_lengths[np.argmax(speedups)]})"
262
+ )
263
+ print(
264
+ f" • Min Speedup: {np.min(speedups):.2f}x (at seq_len={seq_lengths[np.argmin(speedups)]})"
265
+ )
266
+
267
+ # Efficiency analysis
268
+ print("\n3. Efficiency Analysis:")
269
+ speedup_improvement = speedups[-1] / speedups[0]
270
+ print(f" • Speedup improvement from 512→4096: {speedup_improvement:.2f}x")
271
+
272
+ if speedup_improvement > 1.1:
273
+ print(" • Flash SDPA becomes MORE efficient at longer sequences")
274
+ elif speedup_improvement < 0.9:
275
+ print(" • Flash SDPA becomes LESS efficient at longer sequences")
276
+ else:
277
+ print(" • Flash SDPA maintains consistent efficiency across sequence lengths")
278
+
279
+ # Memory complexity analysis
280
+ print("\n4. Theoretical Complexity:")
281
+ print(f" • Sequence length increased by: {seq_lengths[-1] / seq_lengths[0]:.1f}x")
282
+ print(
283
+ f" • Theoretical O(n²) complexity increase: {(seq_lengths[-1] / seq_lengths[0]) ** 2:.1f}x"
284
+ )
285
+ print(f" • Actual Flash SDPA increase: {flash_times[-1] / flash_times[0]:.1f}x")
286
+ efficiency_ratio = (flash_times[-1] / flash_times[0]) / (
287
+ (seq_lengths[-1] / seq_lengths[0]) ** 2
288
+ )
289
+ print(f" • Flash SDPA efficiency ratio: {efficiency_ratio:.3f} (lower is better)")
290
+
291
+
292
+ def main():
293
+ # Run the scaling benchmark
294
+ seq_lengths, flash_times, naive_times, speedups = run_scaling_benchmark()
295
+
296
+ # Create line plot visualization
297
+ create_line_plot(seq_lengths, flash_times, naive_times, speedups)
298
+
299
+ # Print detailed analysis
300
+ print_analysis(seq_lengths, flash_times, naive_times, speedups)
301
+
302
+
303
+ if __name__ == "__main__":
304
+ main()
build.toml ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [general]
2
+ name = "metal_flash_sdpa"
3
+ universal = false
4
+
5
+ [torch]
6
+ src = [
7
+ "torch-ext/torch_binding.cpp",
8
+ "torch-ext/torch_binding.h",
9
+ ]
10
+
11
+ [kernel.sdpa_metal]
12
+ backend = "metal"
13
+ src = [
14
+ "sdpa-metal/scaled_dot_product_attention.mm",
15
+ "sdpa-metal/scaled_dot_product_attention.metal",
16
+ "sdpa-metal/common.h",
17
+ ]
18
+ depends = [ "torch" ]
build/torch210-metal-aarch64-darwin/__init__.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from ._custom_ops import (
2
+ flash_attention_varlen,
3
+ flash_attn_varlen_func,
4
+ )
5
+ from ._ops import ops
6
+
7
+ __all__ = [
8
+ "flash_attention_varlen",
9
+ "flash_attn_varlen_func",
10
+ "ops",
11
+ ]
build/torch210-metal-aarch64-darwin/_custom_ops.py ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List, Optional
2
+
3
+ import torch
4
+
5
+ from ._ops import ops
6
+
7
+
8
+ def flash_attention_varlen(
9
+ out: torch.Tensor,
10
+ query: torch.Tensor,
11
+ key: torch.Tensor,
12
+ value: torch.Tensor,
13
+ cu_seqlens_q: torch.Tensor,
14
+ cu_seqlens_k: torch.Tensor,
15
+ max_seqlen_q: int,
16
+ max_seqlen_k: int,
17
+ do_causal: bool = False,
18
+ scale: Optional[float] = None,
19
+ softcapping: float = 1.0,
20
+ ) -> None:
21
+ """
22
+ Flash Attention with variable-length sequences.
23
+
24
+ Args:
25
+ out: Output tensor of shape [total_q_tokens, num_heads, head_dim]
26
+ query: Query tensor of shape [total_q_tokens, num_heads, head_dim]
27
+ key: Key tensor of shape [total_k_tokens, num_heads_kv, head_dim]
28
+ value: Value tensor of shape [total_k_tokens, num_heads_kv, head_dim]
29
+ cu_seqlens_q: Cumulative sequence lengths for queries, shape [batch_size + 1], dtype must be torch.int32
30
+ cu_seqlens_k: Cumulative sequence lengths for keys, shape [batch_size + 1], dtype must be torch.int32
31
+ max_seqlen_q: Maximum sequence length in the query batch
32
+ max_seqlen_k: Maximum sequence length in the key batch
33
+ do_causal: Whether to apply causal masking
34
+ scale: Attention scale factor (default: 1/sqrt(head_dim))
35
+ softcapping: Softcapping value (default: 1.0, must be 1.0 for this implementation)
36
+
37
+ Note:
38
+ - cu_seqlens_q and cu_seqlens_k must have dtype torch.int32 for Metal compatibility
39
+ - Supported head dimensions: 32, 64, 72, 80, 96, 128
40
+ - Masks are not supported
41
+ """
42
+ if scale is None:
43
+ scale = query.shape[-1] ** -0.5
44
+
45
+ ops.flash_attention_varlen(
46
+ out,
47
+ query,
48
+ key,
49
+ value,
50
+ cu_seqlens_q,
51
+ cu_seqlens_k,
52
+ max_seqlen_q,
53
+ max_seqlen_k,
54
+ do_causal,
55
+ scale,
56
+ softcapping,
57
+ )
58
+
59
+ def flash_attn_varlen_func(
60
+ q: torch.Tensor,
61
+ k: torch.Tensor,
62
+ v: torch.Tensor,
63
+ cu_seqlens_q: torch.Tensor,
64
+ cu_seqlens_k: torch.Tensor,
65
+ max_seqlen_q: int,
66
+ max_seqlen_k: int,
67
+ dropout_p: float = 0.0,
68
+ softmax_scale: Optional[float] = None,
69
+ causal: bool = False,
70
+ window_size: tuple = (-1, -1),
71
+ alibi_slopes: Optional[torch.Tensor] = None,
72
+ deterministic: bool = False,
73
+ return_attn_probs: bool = False,
74
+ ) -> torch.Tensor:
75
+ """
76
+ Flash Attention function with API compatible with the original Flash Attention.
77
+
78
+ Note: This implementation does not support:
79
+ - dropout
80
+ - window attention
81
+ - alibi slopes
82
+ - returning attention probabilities
83
+ """
84
+ if dropout_p > 0:
85
+ raise NotImplementedError("Dropout is not supported in this implementation")
86
+ if window_size != (-1, -1):
87
+ raise NotImplementedError("Window attention is not supported")
88
+ if alibi_slopes is not None:
89
+ raise NotImplementedError("ALiBi is not supported")
90
+ if return_attn_probs:
91
+ raise NotImplementedError("Returning attention probabilities is not supported")
92
+
93
+ # Create output tensor
94
+ out = torch.empty_like(q)
95
+
96
+ # Call the kernel
97
+ flash_attention_varlen(
98
+ out=out,
99
+ query=q,
100
+ key=k,
101
+ value=v,
102
+ cu_seqlens_q=cu_seqlens_q,
103
+ cu_seqlens_k=cu_seqlens_k,
104
+ max_seqlen_q=max_seqlen_q,
105
+ max_seqlen_k=max_seqlen_k,
106
+ do_causal=causal,
107
+ scale=softmax_scale,
108
+ softcapping=1.0,
109
+ )
110
+
111
+ return out
112
+
113
+
114
+ __all__ = [
115
+ "flash_attention_varlen",
116
+ "flash_attn_varlen_func",
117
+ ]
build/torch210-metal-aarch64-darwin/_metal_flash_sdpa_metal_9cd92d8.abi3.so ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b752a49d80853771401a1dca88334750d2891aaaab83944eafe6593590054ef8
3
+ size 733096
build/torch210-metal-aarch64-darwin/_ops.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from . import _metal_flash_sdpa_metal_9cd92d8
3
+ ops = torch.ops._metal_flash_sdpa_metal_9cd92d8
4
+
5
+ def add_op_namespace_prefix(op_name: str):
6
+ """
7
+ Prefix op by namespace.
8
+ """
9
+ return f"_metal_flash_sdpa_metal_9cd92d8::{op_name}"
build/torch210-metal-aarch64-darwin/metadata.json ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "metal-flash-sdpa",
3
+ "id": "_metal_flash_sdpa_metal_9cd92d8",
4
+ "version": 1,
5
+ "license": "Apache-2.0",
6
+ "python-depends": [],
7
+ "backend": {
8
+ "type": "metal"
9
+ }
10
+ }
build/torch210-metal-aarch64-darwin/metal_flash_sdpa/__init__.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ctypes
2
+ import importlib.util
3
+ import sys
4
+ from pathlib import Path
5
+ from types import ModuleType
6
+
7
+
8
+ def _import_from_path(file_path: Path) -> ModuleType:
9
+ # We cannot use the module name as-is, after adding it to `sys.modules`,
10
+ # it would also be used for other imports. So, we make a module name that
11
+ # depends on the path for it to be unique using the hex-encoded hash of
12
+ # the path.
13
+ path_hash = "{:x}".format(ctypes.c_size_t(hash(file_path.absolute())).value)
14
+ module_name = path_hash
15
+ spec = importlib.util.spec_from_file_location(module_name, file_path)
16
+ if spec is None:
17
+ raise ImportError(f"Cannot load spec for {module_name} from {file_path}")
18
+ module = importlib.util.module_from_spec(spec)
19
+ if module is None:
20
+ raise ImportError(f"Cannot load module {module_name} from spec")
21
+ sys.modules[module_name] = module
22
+ spec.loader.exec_module(module) # type: ignore
23
+ return module
24
+
25
+
26
+ globals().update(vars(_import_from_path(Path(__file__).parent.parent / "__init__.py")))
build/torch211-metal-aarch64-darwin/__init__.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from ._custom_ops import (
2
+ flash_attention_varlen,
3
+ flash_attn_varlen_func,
4
+ )
5
+ from ._ops import ops
6
+
7
+ __all__ = [
8
+ "flash_attention_varlen",
9
+ "flash_attn_varlen_func",
10
+ "ops",
11
+ ]
build/torch211-metal-aarch64-darwin/_custom_ops.py ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List, Optional
2
+
3
+ import torch
4
+
5
+ from ._ops import ops
6
+
7
+
8
+ def flash_attention_varlen(
9
+ out: torch.Tensor,
10
+ query: torch.Tensor,
11
+ key: torch.Tensor,
12
+ value: torch.Tensor,
13
+ cu_seqlens_q: torch.Tensor,
14
+ cu_seqlens_k: torch.Tensor,
15
+ max_seqlen_q: int,
16
+ max_seqlen_k: int,
17
+ do_causal: bool = False,
18
+ scale: Optional[float] = None,
19
+ softcapping: float = 1.0,
20
+ ) -> None:
21
+ """
22
+ Flash Attention with variable-length sequences.
23
+
24
+ Args:
25
+ out: Output tensor of shape [total_q_tokens, num_heads, head_dim]
26
+ query: Query tensor of shape [total_q_tokens, num_heads, head_dim]
27
+ key: Key tensor of shape [total_k_tokens, num_heads_kv, head_dim]
28
+ value: Value tensor of shape [total_k_tokens, num_heads_kv, head_dim]
29
+ cu_seqlens_q: Cumulative sequence lengths for queries, shape [batch_size + 1], dtype must be torch.int32
30
+ cu_seqlens_k: Cumulative sequence lengths for keys, shape [batch_size + 1], dtype must be torch.int32
31
+ max_seqlen_q: Maximum sequence length in the query batch
32
+ max_seqlen_k: Maximum sequence length in the key batch
33
+ do_causal: Whether to apply causal masking
34
+ scale: Attention scale factor (default: 1/sqrt(head_dim))
35
+ softcapping: Softcapping value (default: 1.0, must be 1.0 for this implementation)
36
+
37
+ Note:
38
+ - cu_seqlens_q and cu_seqlens_k must have dtype torch.int32 for Metal compatibility
39
+ - Supported head dimensions: 32, 64, 72, 80, 96, 128
40
+ - Masks are not supported
41
+ """
42
+ if scale is None:
43
+ scale = query.shape[-1] ** -0.5
44
+
45
+ ops.flash_attention_varlen(
46
+ out,
47
+ query,
48
+ key,
49
+ value,
50
+ cu_seqlens_q,
51
+ cu_seqlens_k,
52
+ max_seqlen_q,
53
+ max_seqlen_k,
54
+ do_causal,
55
+ scale,
56
+ softcapping,
57
+ )
58
+
59
+ def flash_attn_varlen_func(
60
+ q: torch.Tensor,
61
+ k: torch.Tensor,
62
+ v: torch.Tensor,
63
+ cu_seqlens_q: torch.Tensor,
64
+ cu_seqlens_k: torch.Tensor,
65
+ max_seqlen_q: int,
66
+ max_seqlen_k: int,
67
+ dropout_p: float = 0.0,
68
+ softmax_scale: Optional[float] = None,
69
+ causal: bool = False,
70
+ window_size: tuple = (-1, -1),
71
+ alibi_slopes: Optional[torch.Tensor] = None,
72
+ deterministic: bool = False,
73
+ return_attn_probs: bool = False,
74
+ ) -> torch.Tensor:
75
+ """
76
+ Flash Attention function with API compatible with the original Flash Attention.
77
+
78
+ Note: This implementation does not support:
79
+ - dropout
80
+ - window attention
81
+ - alibi slopes
82
+ - returning attention probabilities
83
+ """
84
+ if dropout_p > 0:
85
+ raise NotImplementedError("Dropout is not supported in this implementation")
86
+ if window_size != (-1, -1):
87
+ raise NotImplementedError("Window attention is not supported")
88
+ if alibi_slopes is not None:
89
+ raise NotImplementedError("ALiBi is not supported")
90
+ if return_attn_probs:
91
+ raise NotImplementedError("Returning attention probabilities is not supported")
92
+
93
+ # Create output tensor
94
+ out = torch.empty_like(q)
95
+
96
+ # Call the kernel
97
+ flash_attention_varlen(
98
+ out=out,
99
+ query=q,
100
+ key=k,
101
+ value=v,
102
+ cu_seqlens_q=cu_seqlens_q,
103
+ cu_seqlens_k=cu_seqlens_k,
104
+ max_seqlen_q=max_seqlen_q,
105
+ max_seqlen_k=max_seqlen_k,
106
+ do_causal=causal,
107
+ scale=softmax_scale,
108
+ softcapping=1.0,
109
+ )
110
+
111
+ return out
112
+
113
+
114
+ __all__ = [
115
+ "flash_attention_varlen",
116
+ "flash_attn_varlen_func",
117
+ ]
build/torch211-metal-aarch64-darwin/_metal_flash_sdpa_metal_9cd92d8.abi3.so ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1c08a5fc159619cf1c817422b75ce9e8b9976eebb67f3c9df92594e44487a759
3
+ size 733096
build/torch211-metal-aarch64-darwin/_ops.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from . import _metal_flash_sdpa_metal_9cd92d8
3
+ ops = torch.ops._metal_flash_sdpa_metal_9cd92d8
4
+
5
+ def add_op_namespace_prefix(op_name: str):
6
+ """
7
+ Prefix op by namespace.
8
+ """
9
+ return f"_metal_flash_sdpa_metal_9cd92d8::{op_name}"
build/torch211-metal-aarch64-darwin/metadata.json ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "metal-flash-sdpa",
3
+ "id": "_metal_flash_sdpa_metal_9cd92d8",
4
+ "version": 1,
5
+ "license": "Apache-2.0",
6
+ "python-depends": [],
7
+ "backend": {
8
+ "type": "metal"
9
+ }
10
+ }
build/torch211-metal-aarch64-darwin/metal_flash_sdpa/__init__.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ctypes
2
+ import importlib.util
3
+ import sys
4
+ from pathlib import Path
5
+ from types import ModuleType
6
+
7
+
8
+ def _import_from_path(file_path: Path) -> ModuleType:
9
+ # We cannot use the module name as-is, after adding it to `sys.modules`,
10
+ # it would also be used for other imports. So, we make a module name that
11
+ # depends on the path for it to be unique using the hex-encoded hash of
12
+ # the path.
13
+ path_hash = "{:x}".format(ctypes.c_size_t(hash(file_path.absolute())).value)
14
+ module_name = path_hash
15
+ spec = importlib.util.spec_from_file_location(module_name, file_path)
16
+ if spec is None:
17
+ raise ImportError(f"Cannot load spec for {module_name} from {file_path}")
18
+ module = importlib.util.module_from_spec(spec)
19
+ if module is None:
20
+ raise ImportError(f"Cannot load module {module_name} from spec")
21
+ sys.modules[module_name] = module
22
+ spec.loader.exec_module(module) # type: ignore
23
+ return module
24
+
25
+
26
+ globals().update(vars(_import_from_path(Path(__file__).parent.parent / "__init__.py")))
build/torch212-metal-aarch64-darwin/__init__.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from ._custom_ops import (
2
+ flash_attention_varlen,
3
+ flash_attn_varlen_func,
4
+ )
5
+ from ._ops import ops
6
+
7
+ __all__ = [
8
+ "flash_attention_varlen",
9
+ "flash_attn_varlen_func",
10
+ "ops",
11
+ ]
build/torch212-metal-aarch64-darwin/_custom_ops.py ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List, Optional
2
+
3
+ import torch
4
+
5
+ from ._ops import ops
6
+
7
+
8
+ def flash_attention_varlen(
9
+ out: torch.Tensor,
10
+ query: torch.Tensor,
11
+ key: torch.Tensor,
12
+ value: torch.Tensor,
13
+ cu_seqlens_q: torch.Tensor,
14
+ cu_seqlens_k: torch.Tensor,
15
+ max_seqlen_q: int,
16
+ max_seqlen_k: int,
17
+ do_causal: bool = False,
18
+ scale: Optional[float] = None,
19
+ softcapping: float = 1.0,
20
+ ) -> None:
21
+ """
22
+ Flash Attention with variable-length sequences.
23
+
24
+ Args:
25
+ out: Output tensor of shape [total_q_tokens, num_heads, head_dim]
26
+ query: Query tensor of shape [total_q_tokens, num_heads, head_dim]
27
+ key: Key tensor of shape [total_k_tokens, num_heads_kv, head_dim]
28
+ value: Value tensor of shape [total_k_tokens, num_heads_kv, head_dim]
29
+ cu_seqlens_q: Cumulative sequence lengths for queries, shape [batch_size + 1], dtype must be torch.int32
30
+ cu_seqlens_k: Cumulative sequence lengths for keys, shape [batch_size + 1], dtype must be torch.int32
31
+ max_seqlen_q: Maximum sequence length in the query batch
32
+ max_seqlen_k: Maximum sequence length in the key batch
33
+ do_causal: Whether to apply causal masking
34
+ scale: Attention scale factor (default: 1/sqrt(head_dim))
35
+ softcapping: Softcapping value (default: 1.0, must be 1.0 for this implementation)
36
+
37
+ Note:
38
+ - cu_seqlens_q and cu_seqlens_k must have dtype torch.int32 for Metal compatibility
39
+ - Supported head dimensions: 32, 64, 72, 80, 96, 128
40
+ - Masks are not supported
41
+ """
42
+ if scale is None:
43
+ scale = query.shape[-1] ** -0.5
44
+
45
+ ops.flash_attention_varlen(
46
+ out,
47
+ query,
48
+ key,
49
+ value,
50
+ cu_seqlens_q,
51
+ cu_seqlens_k,
52
+ max_seqlen_q,
53
+ max_seqlen_k,
54
+ do_causal,
55
+ scale,
56
+ softcapping,
57
+ )
58
+
59
+ def flash_attn_varlen_func(
60
+ q: torch.Tensor,
61
+ k: torch.Tensor,
62
+ v: torch.Tensor,
63
+ cu_seqlens_q: torch.Tensor,
64
+ cu_seqlens_k: torch.Tensor,
65
+ max_seqlen_q: int,
66
+ max_seqlen_k: int,
67
+ dropout_p: float = 0.0,
68
+ softmax_scale: Optional[float] = None,
69
+ causal: bool = False,
70
+ window_size: tuple = (-1, -1),
71
+ alibi_slopes: Optional[torch.Tensor] = None,
72
+ deterministic: bool = False,
73
+ return_attn_probs: bool = False,
74
+ ) -> torch.Tensor:
75
+ """
76
+ Flash Attention function with API compatible with the original Flash Attention.
77
+
78
+ Note: This implementation does not support:
79
+ - dropout
80
+ - window attention
81
+ - alibi slopes
82
+ - returning attention probabilities
83
+ """
84
+ if dropout_p > 0:
85
+ raise NotImplementedError("Dropout is not supported in this implementation")
86
+ if window_size != (-1, -1):
87
+ raise NotImplementedError("Window attention is not supported")
88
+ if alibi_slopes is not None:
89
+ raise NotImplementedError("ALiBi is not supported")
90
+ if return_attn_probs:
91
+ raise NotImplementedError("Returning attention probabilities is not supported")
92
+
93
+ # Create output tensor
94
+ out = torch.empty_like(q)
95
+
96
+ # Call the kernel
97
+ flash_attention_varlen(
98
+ out=out,
99
+ query=q,
100
+ key=k,
101
+ value=v,
102
+ cu_seqlens_q=cu_seqlens_q,
103
+ cu_seqlens_k=cu_seqlens_k,
104
+ max_seqlen_q=max_seqlen_q,
105
+ max_seqlen_k=max_seqlen_k,
106
+ do_causal=causal,
107
+ scale=softmax_scale,
108
+ softcapping=1.0,
109
+ )
110
+
111
+ return out
112
+
113
+
114
+ __all__ = [
115
+ "flash_attention_varlen",
116
+ "flash_attn_varlen_func",
117
+ ]
build/torch212-metal-aarch64-darwin/_metal_flash_sdpa_metal_9cd92d8.abi3.so ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8cc31f8453fb18e703bb77d1ea793c8039d14156078eda8c20713af96a7e2e9c
3
+ size 733128
build/torch212-metal-aarch64-darwin/_ops.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from . import _metal_flash_sdpa_metal_9cd92d8
3
+ ops = torch.ops._metal_flash_sdpa_metal_9cd92d8
4
+
5
+ def add_op_namespace_prefix(op_name: str):
6
+ """
7
+ Prefix op by namespace.
8
+ """
9
+ return f"_metal_flash_sdpa_metal_9cd92d8::{op_name}"
build/torch212-metal-aarch64-darwin/metadata.json ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "metal-flash-sdpa",
3
+ "id": "_metal_flash_sdpa_metal_9cd92d8",
4
+ "version": 1,
5
+ "license": "Apache-2.0",
6
+ "python-depends": [],
7
+ "backend": {
8
+ "type": "metal"
9
+ }
10
+ }
build/torch212-metal-aarch64-darwin/metal_flash_sdpa/__init__.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ctypes
2
+ import importlib.util
3
+ import sys
4
+ from pathlib import Path
5
+ from types import ModuleType
6
+
7
+
8
+ def _import_from_path(file_path: Path) -> ModuleType:
9
+ # We cannot use the module name as-is, after adding it to `sys.modules`,
10
+ # it would also be used for other imports. So, we make a module name that
11
+ # depends on the path for it to be unique using the hex-encoded hash of
12
+ # the path.
13
+ path_hash = "{:x}".format(ctypes.c_size_t(hash(file_path.absolute())).value)
14
+ module_name = path_hash
15
+ spec = importlib.util.spec_from_file_location(module_name, file_path)
16
+ if spec is None:
17
+ raise ImportError(f"Cannot load spec for {module_name} from {file_path}")
18
+ module = importlib.util.module_from_spec(spec)
19
+ if module is None:
20
+ raise ImportError(f"Cannot load module {module_name} from spec")
21
+ sys.modules[module_name] = module
22
+ spec.loader.exec_module(module) # type: ignore
23
+ return module
24
+
25
+
26
+ globals().update(vars(_import_from_path(Path(__file__).parent.parent / "__init__.py")))
build/torch27-metal-aarch64-darwin/metal_flash_sdpa/__init__.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from ._custom_ops import (
2
+ flash_attention_varlen,
3
+ flash_attn_varlen_func,
4
+ )
5
+ from ._ops import ops
6
+
7
+ __all__ = [
8
+ "flash_attention_varlen",
9
+ "flash_attn_varlen_func",
10
+ "ops",
11
+ ]
build/torch27-metal-aarch64-darwin/metal_flash_sdpa/_custom_ops.py ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List, Optional
2
+
3
+ import torch
4
+
5
+ from ._ops import ops
6
+
7
+
8
+ def flash_attention_varlen(
9
+ out: torch.Tensor,
10
+ query: torch.Tensor,
11
+ key: torch.Tensor,
12
+ value: torch.Tensor,
13
+ cu_seqlens_q: torch.Tensor,
14
+ cu_seqlens_k: torch.Tensor,
15
+ max_seqlen_q: int,
16
+ max_seqlen_k: int,
17
+ do_causal: bool = False,
18
+ scale: Optional[float] = None,
19
+ softcapping: float = 1.0,
20
+ ) -> None:
21
+ """
22
+ Flash Attention with variable-length sequences.
23
+
24
+ Args:
25
+ out: Output tensor of shape [total_q_tokens, num_heads, head_dim]
26
+ query: Query tensor of shape [total_q_tokens, num_heads, head_dim]
27
+ key: Key tensor of shape [total_k_tokens, num_heads_kv, head_dim]
28
+ value: Value tensor of shape [total_k_tokens, num_heads_kv, head_dim]
29
+ cu_seqlens_q: Cumulative sequence lengths for queries, shape [batch_size + 1], dtype must be torch.int32
30
+ cu_seqlens_k: Cumulative sequence lengths for keys, shape [batch_size + 1], dtype must be torch.int32
31
+ max_seqlen_q: Maximum sequence length in the query batch
32
+ max_seqlen_k: Maximum sequence length in the key batch
33
+ do_causal: Whether to apply causal masking
34
+ scale: Attention scale factor (default: 1/sqrt(head_dim))
35
+ softcapping: Softcapping value (default: 1.0, must be 1.0 for this implementation)
36
+
37
+ Note:
38
+ - cu_seqlens_q and cu_seqlens_k must have dtype torch.int32 for Metal compatibility
39
+ - Supported head dimensions: 32, 64, 72, 80, 96, 128
40
+ - Masks are not supported
41
+ """
42
+ if scale is None:
43
+ scale = query.shape[-1] ** -0.5
44
+
45
+ ops.flash_attention_varlen(
46
+ out,
47
+ query,
48
+ key,
49
+ value,
50
+ cu_seqlens_q,
51
+ cu_seqlens_k,
52
+ max_seqlen_q,
53
+ max_seqlen_k,
54
+ do_causal,
55
+ scale,
56
+ softcapping,
57
+ )
58
+
59
+ def flash_attn_varlen_func(
60
+ q: torch.Tensor,
61
+ k: torch.Tensor,
62
+ v: torch.Tensor,
63
+ cu_seqlens_q: torch.Tensor,
64
+ cu_seqlens_k: torch.Tensor,
65
+ max_seqlen_q: int,
66
+ max_seqlen_k: int,
67
+ dropout_p: float = 0.0,
68
+ softmax_scale: Optional[float] = None,
69
+ causal: bool = False,
70
+ window_size: tuple = (-1, -1),
71
+ alibi_slopes: Optional[torch.Tensor] = None,
72
+ deterministic: bool = False,
73
+ return_attn_probs: bool = False,
74
+ ) -> torch.Tensor:
75
+ """
76
+ Flash Attention function with API compatible with the original Flash Attention.
77
+
78
+ Note: This implementation does not support:
79
+ - dropout
80
+ - window attention
81
+ - alibi slopes
82
+ - returning attention probabilities
83
+ """
84
+ if dropout_p > 0:
85
+ raise NotImplementedError("Dropout is not supported in this implementation")
86
+ if window_size != (-1, -1):
87
+ raise NotImplementedError("Window attention is not supported")
88
+ if alibi_slopes is not None:
89
+ raise NotImplementedError("ALiBi is not supported")
90
+ if return_attn_probs:
91
+ raise NotImplementedError("Returning attention probabilities is not supported")
92
+
93
+ # Create output tensor
94
+ out = torch.empty_like(q)
95
+
96
+ # Call the kernel
97
+ flash_attention_varlen(
98
+ out=out,
99
+ query=q,
100
+ key=k,
101
+ value=v,
102
+ cu_seqlens_q=cu_seqlens_q,
103
+ cu_seqlens_k=cu_seqlens_k,
104
+ max_seqlen_q=max_seqlen_q,
105
+ max_seqlen_k=max_seqlen_k,
106
+ do_causal=causal,
107
+ scale=softmax_scale,
108
+ softcapping=1.0,
109
+ )
110
+
111
+ return out
112
+
113
+
114
+ __all__ = [
115
+ "flash_attention_varlen",
116
+ "flash_attn_varlen_func",
117
+ ]
build/torch27-metal-aarch64-darwin/metal_flash_sdpa/_metal_flash_sdpa_8aa66c2.abi3.so ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:29aa4a03d9cbe5d1b56aebaf0eff43eba5dc0a5796909c2ede503e9e1f80c565
3
+ size 732048
build/torch27-metal-aarch64-darwin/metal_flash_sdpa/_ops.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from . import _metal_flash_sdpa_8aa66c2
3
+ ops = torch.ops._metal_flash_sdpa_8aa66c2
4
+
5
+ def add_op_namespace_prefix(op_name: str):
6
+ """
7
+ Prefix op by namespace.
8
+ """
9
+ return f"_metal_flash_sdpa_8aa66c2::{op_name}"
build/torch29-metal-aarch64-darwin/__init__.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from ._custom_ops import (
2
+ flash_attention_varlen,
3
+ flash_attn_varlen_func,
4
+ )
5
+ from ._ops import ops
6
+
7
+ __all__ = [
8
+ "flash_attention_varlen",
9
+ "flash_attn_varlen_func",
10
+ "ops",
11
+ ]
build/torch29-metal-aarch64-darwin/_custom_ops.py ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List, Optional
2
+
3
+ import torch
4
+
5
+ from ._ops import ops
6
+
7
+
8
+ def flash_attention_varlen(
9
+ out: torch.Tensor,
10
+ query: torch.Tensor,
11
+ key: torch.Tensor,
12
+ value: torch.Tensor,
13
+ cu_seqlens_q: torch.Tensor,
14
+ cu_seqlens_k: torch.Tensor,
15
+ max_seqlen_q: int,
16
+ max_seqlen_k: int,
17
+ do_causal: bool = False,
18
+ scale: Optional[float] = None,
19
+ softcapping: float = 1.0,
20
+ ) -> None:
21
+ """
22
+ Flash Attention with variable-length sequences.
23
+
24
+ Args:
25
+ out: Output tensor of shape [total_q_tokens, num_heads, head_dim]
26
+ query: Query tensor of shape [total_q_tokens, num_heads, head_dim]
27
+ key: Key tensor of shape [total_k_tokens, num_heads_kv, head_dim]
28
+ value: Value tensor of shape [total_k_tokens, num_heads_kv, head_dim]
29
+ cu_seqlens_q: Cumulative sequence lengths for queries, shape [batch_size + 1], dtype must be torch.int32
30
+ cu_seqlens_k: Cumulative sequence lengths for keys, shape [batch_size + 1], dtype must be torch.int32
31
+ max_seqlen_q: Maximum sequence length in the query batch
32
+ max_seqlen_k: Maximum sequence length in the key batch
33
+ do_causal: Whether to apply causal masking
34
+ scale: Attention scale factor (default: 1/sqrt(head_dim))
35
+ softcapping: Softcapping value (default: 1.0, must be 1.0 for this implementation)
36
+
37
+ Note:
38
+ - cu_seqlens_q and cu_seqlens_k must have dtype torch.int32 for Metal compatibility
39
+ - Supported head dimensions: 32, 64, 72, 80, 96, 128
40
+ - Masks are not supported
41
+ """
42
+ if scale is None:
43
+ scale = query.shape[-1] ** -0.5
44
+
45
+ ops.flash_attention_varlen(
46
+ out,
47
+ query,
48
+ key,
49
+ value,
50
+ cu_seqlens_q,
51
+ cu_seqlens_k,
52
+ max_seqlen_q,
53
+ max_seqlen_k,
54
+ do_causal,
55
+ scale,
56
+ softcapping,
57
+ )
58
+
59
+ def flash_attn_varlen_func(
60
+ q: torch.Tensor,
61
+ k: torch.Tensor,
62
+ v: torch.Tensor,
63
+ cu_seqlens_q: torch.Tensor,
64
+ cu_seqlens_k: torch.Tensor,
65
+ max_seqlen_q: int,
66
+ max_seqlen_k: int,
67
+ dropout_p: float = 0.0,
68
+ softmax_scale: Optional[float] = None,
69
+ causal: bool = False,
70
+ window_size: tuple = (-1, -1),
71
+ alibi_slopes: Optional[torch.Tensor] = None,
72
+ deterministic: bool = False,
73
+ return_attn_probs: bool = False,
74
+ ) -> torch.Tensor:
75
+ """
76
+ Flash Attention function with API compatible with the original Flash Attention.
77
+
78
+ Note: This implementation does not support:
79
+ - dropout
80
+ - window attention
81
+ - alibi slopes
82
+ - returning attention probabilities
83
+ """
84
+ if dropout_p > 0:
85
+ raise NotImplementedError("Dropout is not supported in this implementation")
86
+ if window_size != (-1, -1):
87
+ raise NotImplementedError("Window attention is not supported")
88
+ if alibi_slopes is not None:
89
+ raise NotImplementedError("ALiBi is not supported")
90
+ if return_attn_probs:
91
+ raise NotImplementedError("Returning attention probabilities is not supported")
92
+
93
+ # Create output tensor
94
+ out = torch.empty_like(q)
95
+
96
+ # Call the kernel
97
+ flash_attention_varlen(
98
+ out=out,
99
+ query=q,
100
+ key=k,
101
+ value=v,
102
+ cu_seqlens_q=cu_seqlens_q,
103
+ cu_seqlens_k=cu_seqlens_k,
104
+ max_seqlen_q=max_seqlen_q,
105
+ max_seqlen_k=max_seqlen_k,
106
+ do_causal=causal,
107
+ scale=softmax_scale,
108
+ softcapping=1.0,
109
+ )
110
+
111
+ return out
112
+
113
+
114
+ __all__ = [
115
+ "flash_attention_varlen",
116
+ "flash_attn_varlen_func",
117
+ ]
build/torch29-metal-aarch64-darwin/_metal_flash_sdpa_metal_8924527.abi3.so ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c876534ff49ce35e464a4b282a44207bb291604fd3fafc3adff2c1c6a27111c6
3
+ size 732216
build/torch29-metal-aarch64-darwin/_ops.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from . import _metal_flash_sdpa_metal_8924527
3
+ ops = torch.ops._metal_flash_sdpa_metal_8924527
4
+
5
+ def add_op_namespace_prefix(op_name: str):
6
+ """
7
+ Prefix op by namespace.
8
+ """
9
+ return f"_metal_flash_sdpa_metal_8924527::{op_name}"
build/torch29-metal-aarch64-darwin/metadata.json ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ {
2
+ "version": 1,
3
+ "license": "Apache-2.0",
4
+ "python-depends": []
5
+ }
build/torch29-metal-aarch64-darwin/metal_flash_sdpa/__init__.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ctypes
2
+ import sys
3
+
4
+ import importlib
5
+ from pathlib import Path
6
+ from types import ModuleType
7
+
8
+ def _import_from_path(file_path: Path) -> ModuleType:
9
+ # We cannot use the module name as-is, after adding it to `sys.modules`,
10
+ # it would also be used for other imports. So, we make a module name that
11
+ # depends on the path for it to be unique using the hex-encoded hash of
12
+ # the path.
13
+ path_hash = "{:x}".format(ctypes.c_size_t(hash(file_path.absolute())).value)
14
+ module_name = path_hash
15
+ spec = importlib.util.spec_from_file_location(module_name, file_path)
16
+ if spec is None:
17
+ raise ImportError(f"Cannot load spec for {module_name} from {file_path}")
18
+ module = importlib.util.module_from_spec(spec)
19
+ if module is None:
20
+ raise ImportError(f"Cannot load module {module_name} from spec")
21
+ sys.modules[module_name] = module
22
+ spec.loader.exec_module(module) # type: ignore
23
+ return module
24
+
25
+
26
+ globals().update(vars(_import_from_path(Path(__file__).parent.parent / "__init__.py")))
flake.lock ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "nodes": {
3
+ "flake-compat": {
4
+ "locked": {
5
+ "lastModified": 1747046372,
6
+ "narHash": "sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX+fjA8Xf8PUmqCY=",
7
+ "owner": "edolstra",
8
+ "repo": "flake-compat",
9
+ "rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885",
10
+ "type": "github"
11
+ },
12
+ "original": {
13
+ "owner": "edolstra",
14
+ "repo": "flake-compat",
15
+ "type": "github"
16
+ }
17
+ },
18
+ "flake-compat_2": {
19
+ "locked": {
20
+ "lastModified": 1733328505,
21
+ "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=",
22
+ "owner": "edolstra",
23
+ "repo": "flake-compat",
24
+ "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec",
25
+ "type": "github"
26
+ },
27
+ "original": {
28
+ "owner": "edolstra",
29
+ "repo": "flake-compat",
30
+ "type": "github"
31
+ }
32
+ },
33
+ "flake-utils": {
34
+ "inputs": {
35
+ "systems": "systems"
36
+ },
37
+ "locked": {
38
+ "lastModified": 1731533236,
39
+ "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
40
+ "owner": "numtide",
41
+ "repo": "flake-utils",
42
+ "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
43
+ "type": "github"
44
+ },
45
+ "original": {
46
+ "owner": "numtide",
47
+ "repo": "flake-utils",
48
+ "type": "github"
49
+ }
50
+ },
51
+ "flake-utils_2": {
52
+ "inputs": {
53
+ "systems": "systems_2"
54
+ },
55
+ "locked": {
56
+ "lastModified": 1731533236,
57
+ "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
58
+ "owner": "numtide",
59
+ "repo": "flake-utils",
60
+ "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
61
+ "type": "github"
62
+ },
63
+ "original": {
64
+ "owner": "numtide",
65
+ "repo": "flake-utils",
66
+ "type": "github"
67
+ }
68
+ },
69
+ "hf-nix": {
70
+ "inputs": {
71
+ "flake-compat": "flake-compat_2",
72
+ "flake-utils": "flake-utils_2",
73
+ "nixpkgs": "nixpkgs"
74
+ },
75
+ "locked": {
76
+ "lastModified": 1751968576,
77
+ "narHash": "sha256-cmKrlWpNTG/hq1bCaHXfbdm9T+Y6V+5//EHAVc1TLBE=",
78
+ "owner": "huggingface",
79
+ "repo": "hf-nix",
80
+ "rev": "3fcd1e1b46da91b6691261640ffd6b7123d0cb9e",
81
+ "type": "github"
82
+ },
83
+ "original": {
84
+ "owner": "huggingface",
85
+ "repo": "hf-nix",
86
+ "type": "github"
87
+ }
88
+ },
89
+ "kernel-builder": {
90
+ "inputs": {
91
+ "flake-compat": "flake-compat",
92
+ "flake-utils": "flake-utils",
93
+ "hf-nix": "hf-nix",
94
+ "nixpkgs": [
95
+ "kernel-builder",
96
+ "hf-nix",
97
+ "nixpkgs"
98
+ ]
99
+ },
100
+ "locked": {
101
+ "lastModified": 1752758138,
102
+ "narHash": "sha256-twHfCnTbiVz1ScEOvtaJxjBJhqlx56YWhZOgg83qxzg=",
103
+ "owner": "huggingface",
104
+ "repo": "kernel-builder",
105
+ "rev": "39cdfec039c0d2d57aeb4bfd7bb75fcf13187103",
106
+ "type": "github"
107
+ },
108
+ "original": {
109
+ "owner": "huggingface",
110
+ "repo": "kernel-builder",
111
+ "type": "github"
112
+ }
113
+ },
114
+ "nixpkgs": {
115
+ "locked": {
116
+ "lastModified": 1747820358,
117
+ "narHash": "sha256-fTqsZsUX6M3yeEvgyQvXcbGmT2CaRVyVwsi8eK29Oj4=",
118
+ "owner": "danieldk",
119
+ "repo": "nixpkgs",
120
+ "rev": "d3c1681180717528068082103bf323147de6ab0b",
121
+ "type": "github"
122
+ },
123
+ "original": {
124
+ "owner": "danieldk",
125
+ "ref": "cudatoolkit-12.9-kernel-builder",
126
+ "repo": "nixpkgs",
127
+ "type": "github"
128
+ }
129
+ },
130
+ "root": {
131
+ "inputs": {
132
+ "kernel-builder": "kernel-builder"
133
+ }
134
+ },
135
+ "systems": {
136
+ "locked": {
137
+ "lastModified": 1681028828,
138
+ "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
139
+ "owner": "nix-systems",
140
+ "repo": "default",
141
+ "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
142
+ "type": "github"
143
+ },
144
+ "original": {
145
+ "owner": "nix-systems",
146
+ "repo": "default",
147
+ "type": "github"
148
+ }
149
+ },
150
+ "systems_2": {
151
+ "locked": {
152
+ "lastModified": 1681028828,
153
+ "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
154
+ "owner": "nix-systems",
155
+ "repo": "default",
156
+ "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
157
+ "type": "github"
158
+ },
159
+ "original": {
160
+ "owner": "nix-systems",
161
+ "repo": "default",
162
+ "type": "github"
163
+ }
164
+ }
165
+ },
166
+ "root": "root",
167
+ "version": 7
168
+ }
flake.nix ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ description = "Flake for SDPA kernel";
3
+
4
+ inputs = {
5
+ kernel-builder.url = "github:huggingface/kernel-builder";
6
+ };
7
+
8
+ outputs =
9
+ {
10
+ self,
11
+ kernel-builder,
12
+ }:
13
+ kernel-builder.lib.genFlakeOutputs {
14
+ path = ./.;
15
+ rev = self.shortRev or self.dirtyShortRev or self.lastModifiedDate;
16
+ };
17
+ }
sdpa-metal/common.h ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ #ifndef SDPA_METAL_COMMON_H
2
+ #define SDPA_METAL_COMMON_H
3
+
4
+ // Common definitions for Metal kernels
5
+ // This file is included by Metal shaders, so it should not contain C++ code
6
+
7
+ #endif // SDPA_METAL_COMMON_H
sdpa-metal/scaled_dot_product_attention.metal ADDED
@@ -0,0 +1,2101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #include <metal_stdlib>
2
+ #include <metal_simdgroup>
3
+
4
+ using namespace metal;
5
+
6
+ #define STEEL_CONST static constant constexpr const
7
+ #define STEEL_PRAGMA_UNROLL _Pragma("clang loop unroll(full)")
8
+
9
+ #if defined(__HAVE_BFLOAT__)
10
+
11
+ typedef bfloat bfloat16_t;
12
+ typedef half float16_t;
13
+
14
+ #else
15
+
16
+ typedef half float16_t;
17
+
18
+ /////////////////////////////////////////////////////////////////////////////
19
+ // Helpers
20
+ /////////////////////////////////////////////////////////////////////////////
21
+
22
+ constexpr METAL_FUNC uint16_t float_to_bfloat_bits(float x) {
23
+ // Check for nan
24
+ if ((as_type<uint32_t>(x) & ~_fp_encoding_traits<float>::sign_mask) >
25
+ _fp_encoding_traits<float>::inf_mask) {
26
+ return uint16_t(as_type<uint32_t>(0x7FC0));
27
+ }
28
+ // Take bits
29
+ uint32_t float_bits = as_type<uint32_t>(x);
30
+
31
+ // Round to nearest even
32
+ float_bits += ((float_bits >> 16) & 1) + as_type<uint32_t>(0x7FFF);
33
+
34
+ // Take upper 16 bits
35
+ return float_bits >> 16;
36
+ }
37
+
38
+ constexpr METAL_FUNC float bfloat_bits_to_float(uint16_t x) {
39
+ // Upper 16 bits are the data and lower 16 bits are 0s
40
+ return as_type<float>((uint32_t)x << 16);
41
+ }
42
+
43
+ struct _MLX_BFloat16;
44
+
45
+ template <typename T>
46
+ static constexpr constant bool can_convert_to_bfloat =
47
+ !is_same_v<T, _MLX_BFloat16> && is_convertible_v<T, float>;
48
+
49
+ template <typename T>
50
+ static constexpr constant bool can_convert_from_bfloat =
51
+ !is_same_v<T, _MLX_BFloat16> && is_convertible_v<float, T>;
52
+
53
+ /////////////////////////////////////////////////////////////////////////////
54
+ // Bfloat struct
55
+ /////////////////////////////////////////////////////////////////////////////
56
+
57
+ struct _MLX_BFloat16 {
58
+ /////////////////////////////////////////////////////////////////////////////
59
+ // Constructors
60
+ uint16_t bits_;
61
+ _MLX_BFloat16() thread = default;
62
+ _MLX_BFloat16() threadgroup = default;
63
+ _MLX_BFloat16() device = default;
64
+ _MLX_BFloat16() constant = default;
65
+
66
+ struct bits_to_bfloat_struct {};
67
+ static constexpr METAL_FUNC bits_to_bfloat_struct bits_to_bfloat() {
68
+ return bits_to_bfloat_struct();
69
+ }
70
+ constexpr METAL_FUNC _MLX_BFloat16(uint16_t bits, bits_to_bfloat_struct)
71
+ : bits_(bits) {}
72
+
73
+ /////////////////////////////////////////////////////////////////////////////
74
+ // Conversions to bfloat
75
+
76
+ template <
77
+ typename T,
78
+ typename = typename enable_if<can_convert_to_bfloat<T>>::type>
79
+ constexpr METAL_FUNC _MLX_BFloat16(T x) thread
80
+ : bits_(float_to_bfloat_bits(static_cast<float>(x))) {}
81
+
82
+ template <
83
+ typename T,
84
+ typename = typename enable_if<can_convert_to_bfloat<T>>::type>
85
+ constexpr METAL_FUNC _MLX_BFloat16(T x) threadgroup
86
+ : bits_(float_to_bfloat_bits(static_cast<float>(x))) {}
87
+
88
+ template <
89
+ typename T,
90
+ typename = typename enable_if<can_convert_to_bfloat<T>>::type>
91
+ constexpr METAL_FUNC _MLX_BFloat16(T x) device
92
+ : bits_(float_to_bfloat_bits(static_cast<float>(x))) {}
93
+
94
+ template <
95
+ typename T,
96
+ typename = typename enable_if<can_convert_to_bfloat<T>>::type>
97
+ constexpr METAL_FUNC _MLX_BFloat16(T x) constant
98
+ : bits_(float_to_bfloat_bits(static_cast<float>(x))) {}
99
+
100
+ /////////////////////////////////////////////////////////////////////////////
101
+ // Conversions from bfloat
102
+
103
+ template <
104
+ typename T,
105
+ typename = typename enable_if<can_convert_from_bfloat<T>>::type>
106
+ constexpr METAL_FUNC operator T() const thread {
107
+ return static_cast<T>(bfloat_bits_to_float(bits_));
108
+ }
109
+
110
+ template <
111
+ typename T,
112
+ typename = typename enable_if<can_convert_from_bfloat<T>>::type>
113
+ constexpr METAL_FUNC operator T() const threadgroup {
114
+ return static_cast<T>(bfloat_bits_to_float(bits_));
115
+ }
116
+
117
+ template <
118
+ typename T,
119
+ typename = typename enable_if<can_convert_from_bfloat<T>>::type>
120
+ constexpr METAL_FUNC operator T() const device {
121
+ return static_cast<T>(bfloat_bits_to_float(bits_));
122
+ }
123
+
124
+ template <
125
+ typename T,
126
+ typename = typename enable_if<can_convert_from_bfloat<T>>::type>
127
+ constexpr METAL_FUNC operator T() const constant {
128
+ return static_cast<T>(bfloat_bits_to_float(bits_));
129
+ }
130
+ };
131
+
132
+ /////////////////////////////////////////////////////////////////////////////
133
+ // Bfloat operators
134
+ /////////////////////////////////////////////////////////////////////////////
135
+
136
+ /////////////////////////////////////////////////////////////////////////////
137
+ // Unary ops
138
+ constexpr METAL_FUNC _MLX_BFloat16 operator-(_MLX_BFloat16 x) {
139
+ return -static_cast<float>(x);
140
+ }
141
+
142
+ /////////////////////////////////////////////////////////////////////////////
143
+ // Binary operators
144
+ #define bfloat_binop_base(__op__, __operator__, otype, atype, btype, ctype) \
145
+ constexpr METAL_FUNC otype __operator__(atype lhs, btype rhs) { \
146
+ return static_cast<ctype>(lhs) __op__ static_cast<ctype>(rhs); \
147
+ }
148
+
149
+ #define bfloat_binop_helper(__op__, __operator__, otype, itype, ctype) \
150
+ constexpr METAL_FUNC otype __operator__(_MLX_BFloat16 lhs, itype rhs) { \
151
+ return static_cast<ctype>(lhs) __op__ static_cast<ctype>(rhs); \
152
+ } \
153
+ constexpr METAL_FUNC otype __operator__(itype lhs, _MLX_BFloat16 rhs) { \
154
+ return static_cast<ctype>(lhs) __op__ static_cast<ctype>(rhs); \
155
+ }
156
+
157
+ /////////////////////////////////////////////////////////////////////////////
158
+ // Arithmetic Operators
159
+ #define bfloat_binop(_op_, _operator_) \
160
+ bfloat_binop_base( \
161
+ _op_, _operator_, _MLX_BFloat16, _MLX_BFloat16, _MLX_BFloat16, float); \
162
+ bfloat_binop_helper(_op_, _operator_, float, float, float); \
163
+ bfloat_binop_helper(_op_, _operator_, float, half, float); \
164
+ bfloat_binop_helper(_op_, _operator_, _MLX_BFloat16, int32_t, float); \
165
+ bfloat_binop_helper(_op_, _operator_, _MLX_BFloat16, uint32_t, float); \
166
+ bfloat_binop_helper(_op_, _operator_, _MLX_BFloat16, int64_t, float); \
167
+ bfloat_binop_helper(_op_, _operator_, _MLX_BFloat16, uint64_t, float);
168
+
169
+ bfloat_binop(+, operator+);
170
+ bfloat_binop(-, operator-);
171
+ bfloat_binop(*, operator*);
172
+ bfloat_binop(/, operator/);
173
+
174
+ /////////////////////////////////////////////////////////////////////////////
175
+ // Comparison ops
176
+ #define bfloat_compop(__op__, __operator__) \
177
+ bfloat_binop_base( \
178
+ __op__, __operator__, bool, _MLX_BFloat16, _MLX_BFloat16, float); \
179
+ bfloat_binop_helper(__op__, __operator__, bool, float, float); \
180
+ bfloat_binop_helper(__op__, __operator__, bool, half, float); \
181
+ bfloat_binop_helper(__op__, __operator__, bool, int32_t, float); \
182
+ bfloat_binop_helper(__op__, __operator__, bool, uint32_t, float); \
183
+ bfloat_binop_helper(__op__, __operator__, bool, int64_t, float); \
184
+ bfloat_binop_helper(__op__, __operator__, bool, uint64_t, float);
185
+
186
+ bfloat_compop(>, operator>);
187
+ bfloat_compop(<, operator<);
188
+ bfloat_compop(>=, operator>=);
189
+ bfloat_compop(<=, operator<=);
190
+ bfloat_compop(==, operator==);
191
+ bfloat_compop(!=, operator!=);
192
+
193
+ #undef bfloat_compop
194
+ #undef bfloat_binop_base
195
+ #undef bfloat_binop_helper
196
+ #undef bfloat_binop
197
+
198
+ /////////////////////////////////////////////////////////////////////////////
199
+ // Inplace Operators
200
+ #define bfloat_inplace_op_helper(__op__, __operator__, itype, addr_space) \
201
+ constexpr METAL_FUNC addr_space _MLX_BFloat16& __operator__( \
202
+ addr_space _MLX_BFloat16& lhs, itype rhs) { \
203
+ lhs = static_cast<float>(lhs) __op__ static_cast<float>(rhs); \
204
+ return lhs; \
205
+ } \
206
+ constexpr METAL_FUNC addr_space itype& __operator__( \
207
+ addr_space itype& lhs, _MLX_BFloat16 rhs) { \
208
+ lhs = static_cast<float>(lhs) __op__ static_cast<float>(rhs); \
209
+ return lhs; \
210
+ }
211
+
212
+ #define bfloat_inplace_op_addr_space_helper(__op__, __operator__, itype) \
213
+ bfloat_inplace_op_helper(__op__, __operator__, itype, device); \
214
+ bfloat_inplace_op_helper(__op__, __operator__, itype, thread); \
215
+ bfloat_inplace_op_helper(__op__, __operator__, itype, threadgroup);
216
+
217
+ #define bfloat_inplace_op(itype) \
218
+ bfloat_inplace_op_addr_space_helper(+, operator+=, itype); \
219
+ bfloat_inplace_op_addr_space_helper(-, operator-=, itype); \
220
+ bfloat_inplace_op_addr_space_helper(*, operator*=, itype); \
221
+ bfloat_inplace_op_addr_space_helper(/, operator/=, itype);
222
+
223
+ bfloat_inplace_op(float);
224
+ bfloat_inplace_op(half);
225
+ bfloat_inplace_op(int16_t);
226
+ bfloat_inplace_op(int32_t);
227
+ bfloat_inplace_op(int64_t);
228
+ bfloat_inplace_op(uint16_t);
229
+ bfloat_inplace_op(uint32_t);
230
+ bfloat_inplace_op(uint64_t);
231
+
232
+ #undef bfloat_inplace_op_helper
233
+ #undef bfloat_inplace_op_addr_space_helper
234
+ #undef bfloat_inplace_op
235
+
236
+ #define bfloat_inplace_op_helper(__op__, __operator__, addr_space) \
237
+ constexpr METAL_FUNC addr_space _MLX_BFloat16& __operator__( \
238
+ addr_space _MLX_BFloat16& lhs, _MLX_BFloat16 rhs) { \
239
+ lhs = static_cast<float>(lhs) __op__ static_cast<float>(rhs); \
240
+ return lhs; \
241
+ }
242
+
243
+ #define bfloat_inplace_op_addr_space_helper(__op__, __operator__) \
244
+ bfloat_inplace_op_helper(__op__, __operator__, device); \
245
+ bfloat_inplace_op_helper(__op__, __operator__, thread); \
246
+ bfloat_inplace_op_helper(__op__, __operator__, threadgroup);
247
+
248
+ bfloat_inplace_op_addr_space_helper(+, operator+=);
249
+ bfloat_inplace_op_addr_space_helper(-, operator-=);
250
+ bfloat_inplace_op_addr_space_helper(*, operator*=);
251
+ bfloat_inplace_op_addr_space_helper(/, operator/=);
252
+
253
+ #undef bfloat_inplace_op_helper
254
+ #undef bfloat_inplace_op_addr_space_helper
255
+
256
+ /////////////////////////////////////////////////////////////////////////////
257
+ // Bfloat typedef
258
+ /////////////////////////////////////////////////////////////////////////////
259
+
260
+ typedef struct _MLX_BFloat16 bfloat16_t;
261
+
262
+ #endif
263
+
264
+ // ============ "mlx/backend/metal/kernels/scaled_dot_product_attention_params.h"
265
+
266
+ struct MLXFastAttentionParams {
267
+ const int M;
268
+ const int N;
269
+ const int K;
270
+
271
+ const int ldq; // ldq == ldo
272
+ const int ldk;
273
+ const int ldv;
274
+ const int lds;
275
+ const int ldo;
276
+
277
+ const int tiles_n;
278
+ const int tiles_m;
279
+
280
+ const int batch_stride_q;
281
+ const int batch_stride_k;
282
+ const int batch_stride_v;
283
+ const int batch_stride_o;
284
+
285
+ const int swizzle_log;
286
+ const int gemm_n_iterations_aligned;
287
+ const int gemm_k_iterations_aligned;
288
+ const int gemm_sv_m_block_iterations;
289
+
290
+ const int batch_ndim;
291
+ const float alpha;
292
+ const float softcapping;
293
+ };
294
+
295
+ struct MLXScaledDotProductAttentionParams {
296
+ // Associated dimensions & transposition information
297
+ const uint QUERY_SEQUENCE_LENGTH = 1;
298
+ const uint N_Q_HEADS = 32;
299
+ const uint N_KV_HEADS = 32;
300
+ const uint KV_TILES = 1;
301
+ const float INV_ALPHA = 0.08838834764831843f;
302
+ };
303
+
304
+ // ============ "mlx/backend/metal/kernels/scaled_dot_product_attention_params.sdpa_vector"
305
+
306
+
307
+ // ============ "mlx/backend/metal/kernels/utils.h"
308
+
309
+ template <typename U>
310
+ struct Limits {
311
+ static const constant U max = metal::numeric_limits<U>::max();
312
+ static const constant U min = metal::numeric_limits<U>::min();
313
+ static const constant U finite_max = metal::numeric_limits<U>::max();
314
+ static const constant U finite_min = metal::numeric_limits<U>::min();
315
+ };
316
+
317
+ #define instantiate_default_limit(type) \
318
+ template <> \
319
+ struct Limits<type> { \
320
+ static constexpr constant type max = metal::numeric_limits<type>::max(); \
321
+ static constexpr constant type min = metal::numeric_limits<type>::min(); \
322
+ static constexpr constant type finite_max = \
323
+ metal::numeric_limits<type>::max(); \
324
+ static constexpr constant type finite_min = \
325
+ metal::numeric_limits<type>::min(); \
326
+ };
327
+
328
+ instantiate_default_limit(uint8_t);
329
+ instantiate_default_limit(uint16_t);
330
+ instantiate_default_limit(uint32_t);
331
+ instantiate_default_limit(uint64_t);
332
+ instantiate_default_limit(int8_t);
333
+ instantiate_default_limit(int16_t);
334
+ instantiate_default_limit(int32_t);
335
+ instantiate_default_limit(int64_t);
336
+
337
+ #define instantiate_float_limit(type) \
338
+ template <> \
339
+ struct Limits<type> { \
340
+ static constexpr constant type max = \
341
+ metal::numeric_limits<type>::infinity(); \
342
+ static constexpr constant type min = \
343
+ -metal::numeric_limits<type>::infinity(); \
344
+ static constexpr constant type finite_max = \
345
+ metal::numeric_limits<type>::max(); \
346
+ static constexpr constant type finite_min = \
347
+ -metal::numeric_limits<type>::max(); \
348
+ };
349
+
350
+ instantiate_float_limit(half);
351
+ instantiate_float_limit(float);
352
+ instantiate_float_limit(bfloat16_t);
353
+
354
+
355
+ // ============ "mlx/backend/metal/kernels/steel/attn/loader.h"
356
+
357
+ template <
358
+ typename T,
359
+ short BROWS,
360
+ short BCOLS,
361
+ short dst_ld,
362
+ short reduction_dim,
363
+ short tgp_size,
364
+ short alignment = 1,
365
+ short n_reads = (BCOLS * BROWS) / (tgp_size),
366
+ short TCOLS = BCOLS / n_reads,
367
+ short TROWS = tgp_size / TCOLS>
368
+ struct BlockLoader {
369
+ STEEL_CONST short n_rows = (BROWS + TROWS - 1) / TROWS;
370
+ STEEL_CONST short vec_size = n_reads;
371
+
372
+ // Leading dimension for src
373
+ const int src_ld;
374
+ const int tile_stride;
375
+
376
+ // Thread location indices
377
+ const short thread_idx;
378
+ const short bi;
379
+ const short bj;
380
+
381
+ // threadgroup and device memory
382
+ threadgroup T* dst;
383
+ const device T* src;
384
+
385
+ struct alignas(alignment * sizeof(T)) ReadVector {
386
+ uint8_t v[sizeof(T) * vec_size];
387
+ };
388
+
389
+ /* Constructor */
390
+ METAL_FUNC BlockLoader(
391
+ const device T* src_,
392
+ const int src_ld_,
393
+ threadgroup T* dst_,
394
+ ushort simd_group_id [[simdgroup_index_in_threadgroup]],
395
+ ushort simd_lane_id [[thread_index_in_simdgroup]])
396
+ : src_ld(src_ld_),
397
+ tile_stride(reduction_dim ? BCOLS : BROWS * src_ld),
398
+ thread_idx(simd_group_id * 32 + simd_lane_id),
399
+ bi(thread_idx / TCOLS),
400
+ bj(vec_size * (thread_idx % TCOLS)),
401
+ dst(dst_ + bi * dst_ld + bj),
402
+ src(src_ + bi * src_ld + bj) {}
403
+
404
+ /* Apply operation to threadgroup without bound checking */
405
+ template <typename UnaryOp>
406
+ METAL_FUNC void apply_inplace_op(thread const UnaryOp& op) const {
407
+ STEEL_PRAGMA_UNROLL
408
+ for (short i = 0; i < BROWS; i += TROWS) {
409
+ STEEL_PRAGMA_UNROLL
410
+ for (short j = 0; j < vec_size; j++) {
411
+ dst[i * dst_ld + j] = op.apply(dst[i * dst_ld + j]);
412
+ }
413
+ }
414
+ }
415
+
416
+ /* Load from device memory into threadgroup memory - without bound checking */
417
+ METAL_FUNC void load_unsafe() const {
418
+ STEEL_PRAGMA_UNROLL
419
+ for (short i = 0; i < BROWS; i += TROWS) {
420
+ *((threadgroup ReadVector*)(&dst[i * dst_ld])) =
421
+ *((const device ReadVector*)(&src[i * src_ld]));
422
+ }
423
+ }
424
+
425
+ /* Load from device memory into threadgroup memory - with bound checking */
426
+ METAL_FUNC void load_safe(short2 src_tile_dim) const {
427
+ src_tile_dim = src_tile_dim - short2(bj, bi);
428
+
429
+ // Skip loading if thread has no valid reads
430
+ if (src_tile_dim.x <= 0 || src_tile_dim.y <= 0) {
431
+ STEEL_PRAGMA_UNROLL
432
+ for (short i = 0; i < BROWS; i += TROWS) {
433
+ STEEL_PRAGMA_UNROLL
434
+ for (short j = 0; j < vec_size; j++) {
435
+ dst[i * dst_ld + j] = T(0);
436
+ }
437
+ }
438
+ return;
439
+ }
440
+
441
+ // Use fast thread memory for bound checks
442
+ bool tmp_idx[vec_size];
443
+ T tmp_val[vec_size];
444
+
445
+ STEEL_PRAGMA_UNROLL
446
+ for (short i = 0; i < BROWS; i += TROWS) {
447
+ // Make sure tmp_idx only contains valid indices
448
+ STEEL_PRAGMA_UNROLL
449
+ for (short j = 0; j < vec_size; j++) {
450
+ tmp_idx[j] = (i < src_tile_dim.y) && (j < src_tile_dim.x);
451
+ }
452
+
453
+ // Read valid indices into tmp_val
454
+ STEEL_PRAGMA_UNROLL
455
+ for (short j = 0; j < vec_size; j++) {
456
+ tmp_val[j] = src[(tmp_idx[j] ? i * src_ld + j : 0)];
457
+ }
458
+
459
+ // Zero out uneeded values
460
+ STEEL_PRAGMA_UNROLL
461
+ for (short j = 0; j < vec_size; j++) {
462
+ tmp_val[j] = tmp_idx[j] ? tmp_val[j] : T(0);
463
+ }
464
+
465
+ // Copy values to threadgroup memory
466
+ STEEL_PRAGMA_UNROLL
467
+ for (short j = 0; j < vec_size; j++) {
468
+ dst[i * dst_ld + j] = tmp_val[j];
469
+ }
470
+ }
471
+ }
472
+
473
+ /* Iteration helper */
474
+ METAL_FUNC void next() {
475
+ src += tile_stride;
476
+ }
477
+ };
478
+
479
+ template <int R, int C>
480
+ struct CShape {
481
+ STEEL_CONST int kRows = R;
482
+ STEEL_CONST int kCols = C;
483
+ };
484
+
485
+ template <
486
+ typename T,
487
+ short BROWS,
488
+ short BCOLS,
489
+ short kDstStrRow,
490
+ short kDstStrCol,
491
+ short reduction_dim,
492
+ short tgp_size,
493
+ short n_reads = (BCOLS * BROWS) / (tgp_size),
494
+ short TCOLS = BCOLS / n_reads,
495
+ short TROWS = tgp_size / TCOLS>
496
+ struct BlockLoaderT {
497
+ STEEL_CONST short n_rows = (BROWS + TROWS - 1) / TROWS;
498
+ STEEL_CONST short vec_size = n_reads;
499
+
500
+ // Leading dimension for src
501
+ const int src_ld;
502
+ const int tile_stride;
503
+
504
+ // Thread location indices
505
+ const short thread_idx;
506
+ const short bi;
507
+ const short bj;
508
+
509
+ // threadgroup and device memory
510
+ threadgroup T* dst;
511
+ const device T* src;
512
+
513
+ /* Constructor */
514
+ METAL_FUNC BlockLoaderT(
515
+ const device T* src_,
516
+ const int src_ld_,
517
+ threadgroup T* dst_,
518
+ ushort simd_group_id [[simdgroup_index_in_threadgroup]],
519
+ ushort simd_lane_id [[thread_index_in_simdgroup]])
520
+ : src_ld(src_ld_),
521
+ tile_stride(reduction_dim ? BCOLS : BROWS * src_ld),
522
+ thread_idx(simd_group_id * 32 + simd_lane_id),
523
+ bi(thread_idx / TCOLS),
524
+ bj(vec_size * (thread_idx % TCOLS)),
525
+ dst(dst_ + bi * kDstStrRow + bj * kDstStrCol),
526
+ src(src_ + bi * src_ld + bj) {}
527
+
528
+ /* Apply operation to threadgroup without bound checking */
529
+ template <typename UnaryOp>
530
+ METAL_FUNC void apply_inplace_op(thread const UnaryOp& op) const {
531
+ STEEL_PRAGMA_UNROLL
532
+ for (short i = 0; i < BROWS; i += TROWS) {
533
+ STEEL_PRAGMA_UNROLL
534
+ for (short j = 0; j < vec_size; j++) {
535
+ dst[i * kDstStrRow + j * kDstStrCol] =
536
+ op.apply(dst[i * kDstStrRow + j * kDstStrCol]);
537
+ }
538
+ }
539
+ }
540
+
541
+ /* Load from device memory into threadgroup memory - without bound checking */
542
+ METAL_FUNC void load_unsafe() const {
543
+ STEEL_PRAGMA_UNROLL
544
+ for (short i = 0; i < BROWS; i += TROWS) {
545
+ STEEL_PRAGMA_UNROLL
546
+ for (short j = 0; j < vec_size; j++) {
547
+ dst[i * kDstStrRow + j * kDstStrCol] = src[i * src_ld + j];
548
+ }
549
+ }
550
+ }
551
+
552
+ /* Load from device memory into threadgroup memory - with bound checking */
553
+ METAL_FUNC void load_safe(short2 src_tile_dim) const {
554
+ src_tile_dim = src_tile_dim - short2(bj, bi);
555
+
556
+ // Skip loading if thread has no valid reads
557
+ if (src_tile_dim.x <= 0 || src_tile_dim.y <= 0) {
558
+ STEEL_PRAGMA_UNROLL
559
+ for (short i = 0; i < BROWS; i += TROWS) {
560
+ STEEL_PRAGMA_UNROLL
561
+ for (short j = 0; j < vec_size; j++) {
562
+ dst[i * kDstStrRow + j * kDstStrCol] = T(0);
563
+ }
564
+ }
565
+ return;
566
+ }
567
+
568
+ // Use fast thread memory for bound checks
569
+ bool tmp_idx[vec_size];
570
+ T tmp_val[vec_size];
571
+
572
+ STEEL_PRAGMA_UNROLL
573
+ for (short i = 0; i < BROWS; i += TROWS) {
574
+ // Make sure tmp_idx only contains valid indices
575
+ STEEL_PRAGMA_UNROLL
576
+ for (short j = 0; j < vec_size; j++) {
577
+ tmp_idx[j] = (i < src_tile_dim.y) && (j < src_tile_dim.x);
578
+ }
579
+
580
+ // Read valid indices into tmp_val
581
+ STEEL_PRAGMA_UNROLL
582
+ for (short j = 0; j < vec_size; j++) {
583
+ tmp_val[j] = src[(tmp_idx[j] ? i * src_ld + j : 0)];
584
+ }
585
+
586
+ // Zero out uneeded values
587
+ STEEL_PRAGMA_UNROLL
588
+ for (short j = 0; j < vec_size; j++) {
589
+ tmp_val[j] = tmp_idx[j] ? tmp_val[j] : T(0);
590
+ }
591
+
592
+ // Copy values to threadgroup memory
593
+ STEEL_PRAGMA_UNROLL
594
+ for (short j = 0; j < vec_size; j++) {
595
+ dst[i * kDstStrRow + j * kDstStrCol] = tmp_val[j];
596
+ }
597
+ }
598
+ }
599
+
600
+ /* Iteration helper */
601
+ METAL_FUNC void next() {
602
+ src += tile_stride;
603
+ }
604
+ };
605
+
606
+ // ============ "mlx/backend/metal/kernels/steel/utils/type_traits.h"
607
+
608
+ template <typename... Ts>
609
+ struct make_void {
610
+ typedef void type;
611
+ };
612
+
613
+ template <typename... Ts>
614
+ using void_t = typename make_void<Ts...>::type;
615
+
616
+ template <typename T>
617
+ struct pointer_element {};
618
+
619
+ template <typename T>
620
+ struct pointer_element<thread T*> {
621
+ using type = remove_cv_t<T>;
622
+ };
623
+ template <typename T>
624
+ struct pointer_element<device T*> {
625
+ using type = remove_cv_t<T>;
626
+ };
627
+ template <typename T>
628
+ struct pointer_element<constant T*> {
629
+ using type = remove_cv_t<T>;
630
+ };
631
+ template <typename T>
632
+ struct pointer_element<threadgroup T*> {
633
+ using type = remove_cv_t<T>;
634
+ };
635
+
636
+ template <typename T>
637
+ using pointer_element_t = typename pointer_element<remove_cv_t<T>>::type;
638
+
639
+ // ============ "mlx/backend/metal/kernels/steel/utils/integral_constant.h"
640
+
641
+ ///////////////////////////////////////////////////////////////////////////////
642
+ // Integral constant with casting
643
+ ///////////////////////////////////////////////////////////////////////////////
644
+
645
+ template <int val>
646
+ using Int = integral_constant<int, val>;
647
+
648
+ ///////////////////////////////////////////////////////////////////////////////
649
+ // Binary Operators on Integral constants
650
+ ///////////////////////////////////////////////////////////////////////////////
651
+
652
+ #define integral_const_binop(__op__, __operator__) \
653
+ template <typename T, T tv, typename U, U uv> \
654
+ METAL_FUNC constexpr auto __operator__( \
655
+ integral_constant<T, tv>, integral_constant<U, uv>) { \
656
+ constexpr auto res = tv __op__ uv; \
657
+ return integral_constant<decltype(res), res>{}; \
658
+ }
659
+
660
+ integral_const_binop(+, operator+);
661
+ integral_const_binop(-, operator-);
662
+ integral_const_binop(*, operator*);
663
+ integral_const_binop(/, operator/);
664
+
665
+ integral_const_binop(==, operator==);
666
+ integral_const_binop(!=, operator!=);
667
+ integral_const_binop(<, operator<);
668
+ integral_const_binop(>, operator>);
669
+ integral_const_binop(<=, operator<=);
670
+ integral_const_binop(>=, operator>=);
671
+
672
+ integral_const_binop(&&, operator&&);
673
+ integral_const_binop(||, operator||);
674
+
675
+ #undef integral_const_binop
676
+
677
+ ///////////////////////////////////////////////////////////////////////////////
678
+ // Reduction operators
679
+ ///////////////////////////////////////////////////////////////////////////////
680
+
681
+ template <typename T>
682
+ METAL_FUNC constexpr T sum(T x) {
683
+ return x;
684
+ }
685
+
686
+ template <typename T, typename... Us>
687
+ METAL_FUNC constexpr auto sum(T x, Us... us) {
688
+ return x + sum(us...);
689
+ }
690
+
691
+ // ============ "mlx/backend/metal/kernels/steel/gemm/transforms.h"
692
+
693
+ template <typename OutT, typename InT>
694
+ struct TransformNone {
695
+ static METAL_FUNC OutT apply(InT x) {
696
+ return static_cast<OutT>(x);
697
+ }
698
+
699
+ static METAL_FUNC OutT apply(InT x, OutT) {
700
+ return static_cast<OutT>(x);
701
+ }
702
+ };
703
+
704
+ template <typename OutT, typename InT>
705
+ struct TransformAdd {
706
+ TransformAdd(const float, const float) {}
707
+
708
+ static METAL_FUNC OutT apply(InT x) {
709
+ return static_cast<OutT>(x);
710
+ }
711
+
712
+ static METAL_FUNC OutT apply(InT x, OutT c) {
713
+ return static_cast<OutT>(x) + c;
714
+ }
715
+ };
716
+
717
+ template <typename OutT, typename InT>
718
+ struct TransformAxpby {
719
+ const float alpha;
720
+ const float beta;
721
+
722
+ TransformAxpby(const float alpha_, const float beta_)
723
+ : alpha(alpha_), beta(beta_) {}
724
+
725
+ static METAL_FUNC OutT apply(InT x) {
726
+ return static_cast<OutT>(x);
727
+ }
728
+
729
+ METAL_FUNC OutT apply(InT x, OutT c) const {
730
+ return static_cast<OutT>(x * alpha + (beta * c));
731
+ }
732
+ };
733
+
734
+ template <typename T>
735
+ struct AccumHelper {
736
+ typedef float accum_type;
737
+ };
738
+
739
+ struct BlockSwizzle {
740
+ static METAL_FUNC int2
741
+ swizzle(uint3 tid [[threadgroup_position_in_grid]], const int swizzle_log) {
742
+ const int tid_x = (tid.x) >> swizzle_log;
743
+ const int tid_y =
744
+ ((tid.y) << swizzle_log) + ((tid.x) & ((1 << swizzle_log) - 1));
745
+ return int2(tid_x, tid_y);
746
+ }
747
+ };
748
+
749
+ // ============ "mlx/backend/metal/kernels/steel/attn/mma.h"
750
+
751
+ template <typename RInt, typename CInt>
752
+ struct Shape2D {
753
+ RInt r;
754
+ CInt c;
755
+
756
+ Shape2D(RInt r_, CInt c_) : r(r_), c(c_) {}
757
+ };
758
+
759
+ template <typename Shape, typename Layout>
760
+ struct Layout2D {
761
+ Shape shape;
762
+ Layout layout;
763
+ };
764
+
765
+ template <typename T, int kFragRows_, int kFragCols_>
766
+ struct BaseMMAFrag {
767
+ static_assert(
768
+ kFragRows_ == 8,
769
+ "Only 8 x 8 fragment matrices are currently supported");
770
+ static_assert(
771
+ kFragCols_ == 8,
772
+ "Only 8 x 8 fragment matrices are currently supported");
773
+ };
774
+
775
+ template <typename T>
776
+ struct BaseMMAFrag<T, 8, 8> {
777
+ STEEL_CONST int kFragRows = 8;
778
+ STEEL_CONST int kFragCols = 8;
779
+
780
+ STEEL_CONST int kElemsPerFrag = (kFragRows * kFragCols) / 32;
781
+
782
+ STEEL_CONST int kElemRows = 1;
783
+ STEEL_CONST int kElemCols = 2;
784
+
785
+ static_assert(
786
+ kElemRows * kElemCols == kElemsPerFrag,
787
+ "MMAFrag shape is not consistent with MMAFrag size");
788
+
789
+ typedef metal::simdgroup_matrix<T, kFragRows, kFragCols> mat_type;
790
+ typedef metal::vec<T, kElemsPerFrag> frag_type;
791
+ typedef metal::vec<T, kElemRows> row_frag_type;
792
+ typedef metal::vec<T, kElemCols> col_frag_type;
793
+
794
+ template <typename U>
795
+ using dtype_mat_t = typename metal::simdgroup_matrix<U, kFragRows, kFragCols>;
796
+
797
+ template <typename U>
798
+ using dtype_frag_t = typename metal::vec<U, kElemsPerFrag>;
799
+
800
+ METAL_FUNC static constexpr short2 get_coord(ushort simd_lane_id
801
+ [[thread_index_in_simdgroup]]) {
802
+ const short qid = simd_lane_id / 4;
803
+ const short fm = (qid & 4) + ((simd_lane_id / 2) % 4);
804
+ const short fn = (qid & 2) * 2 + (simd_lane_id % 2) * 2;
805
+ return short2{fn, fm};
806
+ }
807
+
808
+ template <typename SrcPtrType, typename StrX, typename StrY>
809
+ METAL_FUNC static constexpr void
810
+ load(thread frag_type& dst, SrcPtrType src, StrX str_x, StrY str_y) {
811
+ STEEL_PRAGMA_UNROLL
812
+ for (short i = 0; i < kElemRows; i++) {
813
+ STEEL_PRAGMA_UNROLL
814
+ for (short j = 0; j < kElemCols; j++) {
815
+ dst[i * kElemCols + j] = static_cast<T>(src[i * str_x.value + j * str_y.value]);
816
+ }
817
+ }
818
+ }
819
+
820
+ template <
821
+ typename SrcPtrType,
822
+ typename StrX,
823
+ typename StrY,
824
+ typename LimX,
825
+ typename LimY,
826
+ typename OffX,
827
+ typename OffY>
828
+ METAL_FUNC static constexpr void load_safe(
829
+ thread frag_type& dst,
830
+ SrcPtrType src,
831
+ StrX str_x,
832
+ StrY str_y,
833
+ LimX lim_x,
834
+ LimY lim_y,
835
+ OffX off_x = Int<0>{},
836
+ OffY off_y = Int<0>{}) {
837
+ STEEL_PRAGMA_UNROLL
838
+ for (short i = 0; i < kElemRows; i++) {
839
+ STEEL_PRAGMA_UNROLL
840
+ for (short j = 0; j < kElemCols; j++) {
841
+ if ((off_x + i) < lim_x && (off_y + j) < lim_y) {
842
+ dst[i * kElemCols + j] =
843
+ static_cast<T>(src[(off_x + i) * str_x + (off_y + j) * str_y.value]);
844
+ } else {
845
+ dst[i * kElemCols + j] = T(0);
846
+ }
847
+ }
848
+ }
849
+ }
850
+
851
+ template <typename DstPtrType, typename StrX, typename StrY>
852
+ METAL_FUNC static constexpr void
853
+ store(const thread frag_type& src, DstPtrType dst, StrX str_x, StrY str_y) {
854
+ using U = pointer_element_t<DstPtrType>;
855
+
856
+ STEEL_PRAGMA_UNROLL
857
+ for (short i = 0; i < kElemRows; i++) {
858
+ STEEL_PRAGMA_UNROLL
859
+ for (short j = 0; j < kElemCols; j++) {
860
+ dst[i * str_x + j * str_y.value] = static_cast<U>(src[i * kElemCols + j]);
861
+ }
862
+ }
863
+ }
864
+
865
+ template <
866
+ typename DstPtrType,
867
+ typename StrX,
868
+ typename StrY,
869
+ typename LimX,
870
+ typename LimY,
871
+ typename OffX,
872
+ typename OffY>
873
+ METAL_FUNC static constexpr void store_safe(
874
+ const thread frag_type& src,
875
+ DstPtrType dst,
876
+ StrX str_x,
877
+ StrY str_y,
878
+ LimX lim_x,
879
+ LimY lim_y,
880
+ OffX off_x = Int<0>{},
881
+ OffY off_y = Int<0>{}) {
882
+ using U = pointer_element_t<DstPtrType>;
883
+
884
+ STEEL_PRAGMA_UNROLL
885
+ for (short i = 0; i < kElemRows; i++) {
886
+ STEEL_PRAGMA_UNROLL
887
+ for (short j = 0; j < kElemCols; j++) {
888
+ if ((off_x + i) < lim_x && (off_y + j) < lim_y) {
889
+ dst[(off_x + i) * str_x + (off_y + j) * str_y.value] =
890
+ static_cast<U>(src[i * kElemCols + j]);
891
+ }
892
+ }
893
+ }
894
+ }
895
+
896
+ template <typename Atype, typename Btype, typename Ctype>
897
+ METAL_FUNC static constexpr void mma(
898
+ thread frag_type& D,
899
+ thread dtype_frag_t<Atype>& A,
900
+ thread dtype_frag_t<Btype>& B,
901
+ thread dtype_frag_t<Ctype>& C) {
902
+ mat_type D_mat;
903
+ dtype_mat_t<Atype> A_mat;
904
+ dtype_mat_t<Btype> B_mat;
905
+ dtype_mat_t<Ctype> C_mat;
906
+
907
+ reinterpret_cast<thread dtype_frag_t<Atype>&>(A_mat.thread_elements()) = A;
908
+ reinterpret_cast<thread dtype_frag_t<Btype>&>(B_mat.thread_elements()) = B;
909
+ reinterpret_cast<thread dtype_frag_t<Ctype>&>(C_mat.thread_elements()) = C;
910
+
911
+ mma(D_mat, A_mat, B_mat, C_mat);
912
+
913
+ D = reinterpret_cast<thread frag_type&>(D_mat.thread_elements());
914
+ }
915
+
916
+ template <typename Atype, typename Btype, typename Ctype>
917
+ METAL_FUNC static constexpr void mma(
918
+ thread mat_type& D,
919
+ thread dtype_mat_t<Atype>& A,
920
+ thread dtype_mat_t<Btype>& B,
921
+ thread dtype_mat_t<Ctype>& C) {
922
+ simdgroup_multiply_accumulate(D, A, B, C);
923
+ }
924
+
925
+ template <typename Op>
926
+ METAL_FUNC static constexpr void row_reduce(
927
+ thread const frag_type& inp_vals,
928
+ thread T* reduced_vals) {
929
+ T thr_reduce = Op::apply(inp_vals.x, inp_vals.y);
930
+
931
+ T qgr_reduce = simd_shuffle_xor(thr_reduce, ushort(1));
932
+ qgr_reduce = Op::apply(thr_reduce, qgr_reduce);
933
+
934
+ T sgr_reduce = simd_shuffle_xor(qgr_reduce, ushort(8));
935
+ sgr_reduce = Op::apply(qgr_reduce, sgr_reduce);
936
+
937
+ reduced_vals[0] = Op::apply(reduced_vals[0], sgr_reduce);
938
+ }
939
+
940
+ template <typename Op>
941
+ METAL_FUNC static constexpr void row_bin_op(
942
+ thread frag_type& inp_vals,
943
+ thread T* row_vals) {
944
+ STEEL_PRAGMA_UNROLL
945
+ for (short i = 0; i < kElemRows; i++) {
946
+ STEEL_PRAGMA_UNROLL
947
+ for (short j = 0; j < kElemCols; j++) {
948
+ inp_vals[i * kElemCols + j] =
949
+ Op::apply(inp_vals[i * kElemCols + j], row_vals[i]);
950
+ }
951
+ }
952
+ }
953
+ };
954
+
955
+ template <
956
+ typename T,
957
+ int kTileRows_,
958
+ int kTileCols_,
959
+ class MMAFrag_ = BaseMMAFrag<T, 8, 8>>
960
+ struct MMATile {
961
+ using MMAFrag_t = MMAFrag_;
962
+ using elem_type = T;
963
+ STEEL_CONST int kFragRows = MMAFrag_t::kFragRows;
964
+ STEEL_CONST int kFragCols = MMAFrag_t::kFragCols;
965
+ STEEL_CONST int kElemsPerFrag = MMAFrag_t::kElemsPerFrag;
966
+
967
+ STEEL_CONST int kTileRows = kTileRows_;
968
+ STEEL_CONST int kTileCols = kTileCols_;
969
+
970
+ STEEL_CONST int kRows = kTileRows * kFragRows;
971
+ STEEL_CONST int kCols = kTileCols * kFragCols;
972
+
973
+ STEEL_CONST int kNumFrags = kTileRows * kTileCols;
974
+ STEEL_CONST int kElemsPerTile = kNumFrags * kElemsPerFrag;
975
+
976
+ STEEL_CONST int kRowsPerThread = kTileRows * MMAFrag_t::kElemRows;
977
+ STEEL_CONST int kColsPerThread = kTileCols * MMAFrag_t::kElemCols;
978
+
979
+ typedef typename MMAFrag_t::mat_type mat_type;
980
+ typedef typename MMAFrag_t::frag_type frag_type;
981
+
982
+ frag_type val_frags[kNumFrags]; // = {frag_type(0)};
983
+
984
+ METAL_FUNC MMATile() thread {}
985
+
986
+ METAL_FUNC constexpr void clear() {
987
+ STEEL_PRAGMA_UNROLL
988
+ for (short i = 0; i < kNumFrags; ++i) {
989
+ val_frags[i] = frag_type(0);
990
+ }
991
+ }
992
+
993
+ METAL_FUNC constexpr thread frag_type& frag_at(const short i, const short j) {
994
+ return val_frags[i * kTileCols + j];
995
+ }
996
+
997
+ METAL_FUNC constexpr const thread frag_type& frag_at(
998
+ const short i,
999
+ const short j) const {
1000
+ return val_frags[i * kTileCols + j];
1001
+ }
1002
+
1003
+ METAL_FUNC mat_type mat_at(const short i, const short j) {
1004
+ mat_type val_mat;
1005
+ STEEL_PRAGMA_UNROLL
1006
+ for (short ii = 0; ii < kElemsPerFrag; ++ii) {
1007
+ val_mat.thread_elements()[ii] = frag_at(i, j)[ii];
1008
+ }
1009
+ return val_mat;
1010
+ }
1011
+
1012
+ METAL_FUNC thread elem_type* elems() {
1013
+ return reinterpret_cast<thread elem_type*>(val_frags);
1014
+ }
1015
+
1016
+ METAL_FUNC const thread elem_type* elems() const {
1017
+ return reinterpret_cast<const thread elem_type*>(val_frags);
1018
+ }
1019
+
1020
+ template <typename Op>
1021
+ METAL_FUNC void row_reduce(thread T vals[kRowsPerThread]) const {
1022
+ STEEL_PRAGMA_UNROLL
1023
+ for (short i = 0; i < kTileRows; ++i) {
1024
+ STEEL_PRAGMA_UNROLL
1025
+ for (short j = 0; j < kTileCols; ++j) {
1026
+ MMAFrag_t::template row_reduce<Op>(
1027
+ frag_at(i, j), &vals[i * MMAFrag_t::kElemRows]);
1028
+ }
1029
+ }
1030
+ }
1031
+
1032
+ template <typename Op>
1033
+ METAL_FUNC void row_bin_op(thread T vals[kRowsPerThread]) {
1034
+ STEEL_PRAGMA_UNROLL
1035
+ for (short i = 0; i < kTileRows; ++i) {
1036
+ STEEL_PRAGMA_UNROLL
1037
+ for (short j = 0; j < kTileCols; ++j) {
1038
+ MMAFrag_t::template row_bin_op<Op>(
1039
+ frag_at(i, j), &vals[i * MMAFrag_t::kElemRows]);
1040
+ }
1041
+ }
1042
+ }
1043
+
1044
+ template <typename U, int w_x, int w_y, int str_x, int str_y>
1045
+ METAL_FUNC void load(const threadgroup U* src) {
1046
+ STEEL_PRAGMA_UNROLL
1047
+ for (short i = 0; i < kTileRows; ++i) {
1048
+ STEEL_PRAGMA_UNROLL
1049
+ for (short j = 0; j < kTileCols; ++j) {
1050
+ MMAFrag_t::load(
1051
+ frag_at(i, j),
1052
+ &(
1053
+ src[(i * kFragRows) * w_x * str_x +
1054
+ (j * kFragCols) * w_y * str_y]),
1055
+ Int<str_x>{},
1056
+ Int<str_y>{});
1057
+ }
1058
+ }
1059
+ }
1060
+
1061
+ template <typename U, int w_x, int w_y, int str_x, int str_y>
1062
+ METAL_FUNC void store(threadgroup U* dst) const {
1063
+ STEEL_PRAGMA_UNROLL
1064
+ for (short i = 0; i < kTileRows; ++i) {
1065
+ STEEL_PRAGMA_UNROLL
1066
+ for (short j = 0; j < kTileCols; ++j) {
1067
+ MMAFrag_t::store(
1068
+ frag_at(i, j),
1069
+ &(
1070
+ dst[(i * kFragRows) * w_x * str_x +
1071
+ (j * kFragCols) * w_y * str_y]),
1072
+ Int<str_x>{},
1073
+ Int<str_y>{});
1074
+ }
1075
+ }
1076
+ }
1077
+
1078
+ template <typename U, int w_x, int w_y>
1079
+ METAL_FUNC void load(const device U* src, const int ld) {
1080
+ STEEL_PRAGMA_UNROLL
1081
+ for (short i = 0; i < kTileRows; ++i) {
1082
+ STEEL_PRAGMA_UNROLL
1083
+ for (short j = 0; j < kTileCols; ++j) {
1084
+ MMAFrag_t::load(
1085
+ frag_at(i, j),
1086
+ &(src[(i * kFragRows) * w_x * ld + (j * kFragCols) * w_y]),
1087
+ ld,
1088
+ Int<1>{});
1089
+ }
1090
+ }
1091
+ }
1092
+
1093
+ template <typename U, int w_x, int w_y>
1094
+ METAL_FUNC void store(device U* dst, const int ld) const {
1095
+ STEEL_PRAGMA_UNROLL
1096
+ for (short i = 0; i < kTileRows; ++i) {
1097
+ STEEL_PRAGMA_UNROLL
1098
+ for (short j = 0; j < kTileCols; ++j) {
1099
+ MMAFrag_t::store(
1100
+ frag_at(i, j),
1101
+ &(dst[(i * kFragRows) * w_x * ld + (j * kFragCols) * w_y]),
1102
+ ld,
1103
+ Int<1>{});
1104
+ }
1105
+ }
1106
+ }
1107
+
1108
+ template <typename U, int w_x, int w_y>
1109
+ METAL_FUNC void
1110
+ load_safe(const device U* src, const int ld, const short2 src_tile_dims) {
1111
+ STEEL_PRAGMA_UNROLL
1112
+ for (int i = 0; i < kTileRows; ++i) {
1113
+ STEEL_PRAGMA_UNROLL
1114
+ for (int j = 0; j < kTileCols; ++j) {
1115
+ MMAFrag_t::load_safe(
1116
+ frag_at(i, j),
1117
+ src,
1118
+ ld,
1119
+ Int<1>{},
1120
+ src_tile_dims.y,
1121
+ src_tile_dims.x,
1122
+ (i * kFragRows) * w_x,
1123
+ (j * kFragCols) * w_y);
1124
+ }
1125
+ }
1126
+ }
1127
+
1128
+ template <typename U, int w_x, int w_y>
1129
+ METAL_FUNC void
1130
+ store_safe(device U* dst, const int ld, const short2 dst_tile_dims) const {
1131
+ STEEL_PRAGMA_UNROLL
1132
+ for (int i = 0; i < kTileRows; ++i) {
1133
+ STEEL_PRAGMA_UNROLL
1134
+ for (int j = 0; j < kTileCols; ++j) {
1135
+ MMAFrag_t::store_safe(
1136
+ frag_at(i, j),
1137
+ dst,
1138
+ ld,
1139
+ Int<1>{},
1140
+ dst_tile_dims.y,
1141
+ dst_tile_dims.x,
1142
+ (i * kFragRows) * w_x,
1143
+ (j * kFragCols) * w_y);
1144
+ }
1145
+ }
1146
+ }
1147
+ };
1148
+
1149
+ template <
1150
+ typename Dtype,
1151
+ typename Atype,
1152
+ typename Btype,
1153
+ typename Ctype,
1154
+ int M,
1155
+ int N,
1156
+ int K,
1157
+ class MMAFragD,
1158
+ class MMAFragA,
1159
+ class MMAFragB,
1160
+ class MMAFragC>
1161
+ METAL_FUNC void tile_matmad(
1162
+ thread MMATile<Dtype, M, N, MMAFragD>& D,
1163
+ thread MMATile<Atype, M, K, MMAFragA>& A,
1164
+ thread MMATile<Btype, K, N, MMAFragB>& B,
1165
+ thread MMATile<Ctype, M, N, MMAFragC>& C) {
1166
+ STEEL_PRAGMA_UNROLL
1167
+ for (short m = 0; m < M; ++m) {
1168
+ STEEL_PRAGMA_UNROLL
1169
+ for (short n = 0; n < N; ++n) {
1170
+ short m_serp = m; //(n % 2) ? (M - 1 - m) : m;
1171
+ short n_serp = (m % 2) ? (N - 1 - n) : n;
1172
+
1173
+ STEEL_PRAGMA_UNROLL
1174
+ for (short k = 0; k < K; ++k) {
1175
+ MMAFragD::mma(
1176
+ D.frag_at(m_serp, n_serp),
1177
+ A.frag_at(m_serp, k),
1178
+ B.frag_at(k, n_serp),
1179
+ C.frag_at(m_serp, n_serp));
1180
+ }
1181
+ }
1182
+ }
1183
+ }
1184
+
1185
+ template <
1186
+ typename T,
1187
+ typename U,
1188
+ int BM,
1189
+ int BN,
1190
+ int BK,
1191
+ int WM,
1192
+ int WN,
1193
+ bool transpose_a,
1194
+ bool transpose_b,
1195
+ short lda_tgp,
1196
+ short ldb_tgp,
1197
+ typename AccumType = float,
1198
+ typename Epilogue = TransformNone<U, AccumType>>
1199
+ struct BlockMMA {
1200
+ // MMAFrag size
1201
+ STEEL_CONST short kFragSize = 8;
1202
+ using MMAFrag_acc_t = BaseMMAFrag<AccumType, kFragSize, kFragSize>;
1203
+
1204
+ // Warp tile simdgroup matrix strides along M
1205
+ STEEL_CONST short TM_stride = kFragSize * WM;
1206
+ // Warp tile simdgroup matrix strides along M
1207
+ STEEL_CONST short TN_stride = kFragSize * WN;
1208
+
1209
+ // Warp tile size along M
1210
+ STEEL_CONST short TM = BM / TM_stride;
1211
+ // Warp tile size along N
1212
+ STEEL_CONST short TN = BN / TN_stride;
1213
+
1214
+ // Threadgroup A strides
1215
+ STEEL_CONST short A_str_m = transpose_a ? 1 : lda_tgp; // M
1216
+ STEEL_CONST short A_str_k = transpose_a ? lda_tgp : 1; // K
1217
+
1218
+ // Threadgroup B strides
1219
+ STEEL_CONST short B_str_k = transpose_b ? 1 : ldb_tgp; // K
1220
+ STEEL_CONST short B_str_n = transpose_b ? ldb_tgp : 1; // N
1221
+
1222
+ // Threadgroup strides along K
1223
+ STEEL_CONST short tile_stride_a = kFragSize * A_str_k;
1224
+ STEEL_CONST short tile_stride_b = kFragSize * B_str_k;
1225
+
1226
+ // Simdgroup matrices
1227
+ MMATile<AccumType, TM, 1, MMAFrag_acc_t> Atile;
1228
+ MMATile<AccumType, 1, TN, MMAFrag_acc_t> Btile;
1229
+ MMATile<AccumType, TM, TN, MMAFrag_acc_t> Ctile;
1230
+
1231
+ // Offsets within threadgroup
1232
+ short sm;
1233
+ short sn;
1234
+
1235
+ short As_offset;
1236
+ short Bs_offset;
1237
+
1238
+ /* Constructor */
1239
+ METAL_FUNC BlockMMA(
1240
+ ushort simd_group_id [[simdgroup_index_in_threadgroup]],
1241
+ ushort simd_lane_id [[thread_index_in_simdgroup]]) {
1242
+ // Determine thread position in simdgroup matrix
1243
+ short tm = kFragSize * (simd_group_id / WN);
1244
+ short tn = kFragSize * (simd_group_id % WN);
1245
+
1246
+ short2 simd_coord = MMAFrag_acc_t::get_coord(simd_lane_id);
1247
+ sm = simd_coord.y;
1248
+ sn = simd_coord.x;
1249
+
1250
+ // Determine thread and simdgroup offset
1251
+ As_offset = (tm + sm) * A_str_m + (sn)*A_str_k; // M, K
1252
+ Bs_offset = (sm)*B_str_k + (tn + sn) * B_str_n; // K, N
1253
+
1254
+ sm += tm;
1255
+ sn += tn;
1256
+ }
1257
+
1258
+ /* (BM, BK) X (BK, BN) multiply accumulate function */
1259
+ METAL_FUNC void mma(const threadgroup T* As, const threadgroup T* Bs) {
1260
+ // Adjust for simdgroup and thread location
1261
+ As += As_offset;
1262
+ Bs += Bs_offset;
1263
+
1264
+ // Iterate over BK in blocks of kFragSize
1265
+ STEEL_PRAGMA_UNROLL
1266
+ for (short kk = 0; kk < BK; kk += kFragSize) {
1267
+ simdgroup_barrier(mem_flags::mem_none);
1268
+
1269
+ Atile.template load<T, WM, 1, A_str_m, A_str_k>(As);
1270
+
1271
+ simdgroup_barrier(mem_flags::mem_none);
1272
+
1273
+ Btile.template load<T, 1, WN, B_str_k, B_str_n>(Bs);
1274
+
1275
+ simdgroup_barrier(mem_flags::mem_none);
1276
+
1277
+ tile_matmad(Ctile, Atile, Btile, Ctile);
1278
+
1279
+ // Progress to next simdgroup tile
1280
+ As += tile_stride_a;
1281
+ Bs += tile_stride_b;
1282
+ }
1283
+ }
1284
+
1285
+ /* Store results from simdgroup_matrix results into device memory */
1286
+ METAL_FUNC void store_result(device U* D, const int ldd) {
1287
+ // Apply epilogue
1288
+ STEEL_PRAGMA_UNROLL
1289
+ for (short i = 0; i < decltype(Ctile)::kElemsPerTile; i++) {
1290
+ Ctile.elems()[i] = Epilogue::apply(Ctile.elems()[i]);
1291
+ }
1292
+
1293
+ // Adjust for simdgroup and thread location
1294
+ D += sm * ldd + sn;
1295
+
1296
+ Ctile.template store<U, WM, WN>(D, ldd);
1297
+ }
1298
+
1299
+ METAL_FUNC void
1300
+ store_result_safe(device U* D, const int ldd, short2 dst_tile_dims) {
1301
+ // Apply epilogue
1302
+ STEEL_PRAGMA_UNROLL
1303
+ for (short i = 0; i < decltype(Ctile)::kElemsPerTile; i++) {
1304
+ Ctile.elems()[i] = Epilogue::apply(Ctile.elems()[i]);
1305
+ }
1306
+
1307
+ // Adjust for simdgroup and thread location
1308
+ D += sm * ldd + sn;
1309
+ dst_tile_dims -= short2(sn, sm);
1310
+
1311
+ if (dst_tile_dims.x <= 0 || dst_tile_dims.y <= 0)
1312
+ return;
1313
+
1314
+ Ctile.template store_safe<U, WM, WN>(D, ldd, dst_tile_dims);
1315
+ }
1316
+
1317
+ /* Apply epilogue */
1318
+ template <typename UnaryEpilogue>
1319
+ METAL_FUNC void apply_epilogue(thread const UnaryEpilogue& epilogue_op) {
1320
+ // Loop over all simdgroup tiles
1321
+ STEEL_PRAGMA_UNROLL
1322
+ for (short i = 0; i < decltype(Ctile)::kElemsPerTile; i++) {
1323
+ Ctile.elems()[i] = epilogue_op.apply(Ctile.elems()[i]);
1324
+ }
1325
+ }
1326
+
1327
+ /* Apply epilogue */
1328
+ template <typename BinaryEpilogue>
1329
+ METAL_FUNC void apply_epilogue(
1330
+ const device U* C,
1331
+ const int ldc,
1332
+ const int fdc,
1333
+ thread const BinaryEpilogue& epilogue_op) {
1334
+ // Adjust for simdgroup and thread location
1335
+ C += (sm)*ldc + (sn)*fdc;
1336
+
1337
+ // Loop over all simdgroup tiles
1338
+ STEEL_PRAGMA_UNROLL
1339
+ for (short i = 0; i < TM; i++) {
1340
+ STEEL_PRAGMA_UNROLL
1341
+ for (short j = 0; j < TN; j++) {
1342
+ // Get accumulated result and associated offset in C
1343
+ thread auto& accum = Ctile.frag_at(i, j);
1344
+ int offset_c = (i * TM_stride) * ldc + (j * TN_stride) * fdc;
1345
+
1346
+ // Apply epilogue
1347
+ STEEL_PRAGMA_UNROLL
1348
+ for (short k = 0; k < decltype(Ctile)::kElemsPerFrag; k++) {
1349
+ accum[k] = epilogue_op.apply(accum[k], C[offset_c + k * fdc]);
1350
+ }
1351
+ }
1352
+ }
1353
+ }
1354
+
1355
+ /* Apply epilogue */
1356
+ template <typename BinaryEpilogue>
1357
+ METAL_FUNC void apply_epilogue_safe(
1358
+ const device U* C,
1359
+ const int ldc,
1360
+ const int fdc,
1361
+ short2 dst_tile_dims,
1362
+ thread const BinaryEpilogue& epilogue_op) {
1363
+ // Adjust for simdgroup and thread location
1364
+ C += (sm)*ldc + (sn)*fdc;
1365
+ dst_tile_dims -= short2(sn, sm);
1366
+
1367
+ if (dst_tile_dims.x <= 0 || dst_tile_dims.y <= 0)
1368
+ return;
1369
+
1370
+ // Loop over all simdgroup tiles
1371
+ STEEL_PRAGMA_UNROLL
1372
+ for (short i = 0; i < TM; i++) {
1373
+ STEEL_PRAGMA_UNROLL
1374
+ for (short j = 0; j < TN; j++) {
1375
+ // Get accumulated result and associated offset in C
1376
+ thread auto& accum = Ctile.frag_at(i, j);
1377
+ int offset_c = (i * TM_stride) * ldc + (j * TN_stride) * fdc;
1378
+
1379
+ constexpr short kelems = decltype(Ctile)::kElemsPerFrag;
1380
+
1381
+ // Read C
1382
+ U c_elems[kelems] = {0};
1383
+
1384
+ STEEL_PRAGMA_UNROLL
1385
+ for (short k = 0; k < kelems; k++) {
1386
+ if ((j * TN_stride + k) < dst_tile_dims.x) {
1387
+ c_elems[k] = C[offset_c + k * fdc];
1388
+ }
1389
+ }
1390
+
1391
+ // Apply epilogue
1392
+ STEEL_PRAGMA_UNROLL
1393
+ for (short k = 0; k < kelems; k++) {
1394
+ accum[k] = epilogue_op.apply(accum[k], c_elems[k]);
1395
+ }
1396
+ }
1397
+ }
1398
+ }
1399
+
1400
+ /* Store results from simdgroup_matrix results into device memory */
1401
+ METAL_FUNC void store_result(
1402
+ device U* D,
1403
+ const int ldd,
1404
+ const device U* C,
1405
+ const int ldc,
1406
+ const int fdc,
1407
+ thread const Epilogue& epilogue_op) const {
1408
+ // Adjust for simdgroup and thread location
1409
+ C += (sm)*ldc + (sn)*fdc;
1410
+ D += (sm)*ldd + sn;
1411
+
1412
+ constexpr short kelems = decltype(Ctile)::kElemsPerFrag;
1413
+
1414
+ // Loop over all simdgroup tiles
1415
+ STEEL_PRAGMA_UNROLL
1416
+ for (short i = 0; i < TM; i++) {
1417
+ STEEL_PRAGMA_UNROLL
1418
+ for (short j = 0; j < TN; j++) {
1419
+ // Get accumulated result and associated offset in C
1420
+ thread const auto& accum = Ctile.frag_at(i, j);
1421
+ int offset_c = (i * TM_stride) * ldc + (j * TN_stride) * fdc;
1422
+ int offset_d = (i * TM_stride) * ldd + (j * TN_stride);
1423
+
1424
+ // Apply epilogue
1425
+ STEEL_PRAGMA_UNROLL
1426
+ for (short k = 0; k < kelems; k++) {
1427
+ D[offset_d + k] = epilogue_op.apply(accum[k], C[offset_c + k * fdc]);
1428
+ }
1429
+ }
1430
+ }
1431
+ }
1432
+
1433
+ METAL_FUNC void store_result_safe(
1434
+ device U* D,
1435
+ const int ldd,
1436
+ const device U* C,
1437
+ const int ldc,
1438
+ const int fdc,
1439
+ short2 dst_tile_dims,
1440
+ thread const Epilogue& epilogue_op) const {
1441
+ // Adjust for simdgroup and thread location
1442
+ C += (sm)*ldc + (sn)*fdc;
1443
+ D += (sm)*ldd + sn;
1444
+ dst_tile_dims -= short2(sn, sm);
1445
+
1446
+ if (dst_tile_dims.x <= 0 || dst_tile_dims.y <= 0)
1447
+ return;
1448
+
1449
+ constexpr short kelems = decltype(Ctile)::kElemsPerFrag;
1450
+
1451
+ STEEL_PRAGMA_UNROLL
1452
+ for (int i = 0; i < TM; i++) {
1453
+ if (i * TM_stride < dst_tile_dims.y) {
1454
+ STEEL_PRAGMA_UNROLL
1455
+ for (int j = 0; j < TN; j++) {
1456
+ // Get accumulated result and associated offset in C
1457
+ thread const auto& accum = Ctile.frag_at(i, j);
1458
+ int offset_c = (i * TM_stride) * ldc + (j * TN_stride) * fdc;
1459
+ int offset_d = (i * TM_stride) * ldd + (j * TN_stride);
1460
+
1461
+ // Apply epilogue
1462
+ STEEL_PRAGMA_UNROLL
1463
+ for (short k = 0; k < kelems; k++) {
1464
+ if ((j * TN_stride + k) < dst_tile_dims.x) {
1465
+ D[offset_d + k] =
1466
+ epilogue_op.apply(accum[k], C[offset_c + k * fdc]);
1467
+ }
1468
+ }
1469
+ }
1470
+ }
1471
+ }
1472
+ }
1473
+ };
1474
+
1475
+ // ============ "mlx/backend/metal/kernels/steel/attn/kernels/steel_attention.h"
1476
+
1477
+ struct AttnParams {
1478
+ int B; ///< Batch Size
1479
+ int H; ///< Heads
1480
+ int D; ///< Head Dim
1481
+
1482
+ int qL; ///< Query Sequence Length
1483
+ int kL; ///< Key Sequence Length
1484
+
1485
+ int gqa_factor; ///< Group Query factor
1486
+ float scale; ///< Attention scale
1487
+ float softcapping; ///< Softcapping value (1.0 for no softcapping)
1488
+
1489
+ int NQ; ///< Number of query blocks
1490
+ int NK; ///< Number of key/value blocks
1491
+
1492
+ int NQ_aligned; ///< Number of full query blocks
1493
+ int NK_aligned; ///< Number of full key/value blocks
1494
+
1495
+ int qL_rem; ///< Remainder in last query block
1496
+ int kL_rem; ///< Remainder in last key/value block
1497
+ int qL_off; ///< Offset in query sequence start
1498
+
1499
+ int64_t Q_strides[3]; ///< Query strides (B, H, L, D = 1)
1500
+ int64_t K_strides[3]; ///< Key strides (B, H, L, D = 1)
1501
+ int64_t V_strides[3]; ///< Value strides (B, H, L, D = 1)
1502
+ int64_t O_strides[3]; ///< Output strides (B, H, L, D = 1)
1503
+
1504
+ // Flash Attention variable-length support
1505
+ int total_q_tokens; ///< Total number of query tokens (sum of all sequence lengths)
1506
+ int total_k_tokens; ///< Total number of key/value tokens
1507
+ int max_seqlen_q; ///< Maximum query sequence length
1508
+ int max_seqlen_k; ///< Maximum key/value sequence length
1509
+ };
1510
+
1511
+ struct AttnMaskParams {
1512
+ int64_t M_strides[3]; ///< Mask strides (B, H, qL, kL = 1)
1513
+ };
1514
+
1515
+ ///////////////////////////////////////////////////////////////////////////////
1516
+ // GEMM kernels
1517
+ ///////////////////////////////////////////////////////////////////////////////
1518
+
1519
+ constant bool align_Q [[function_constant(200)]];
1520
+ constant bool align_K [[function_constant(201)]];
1521
+
1522
+ constant bool has_mask [[function_constant(300)]];
1523
+ constant bool do_causal [[function_constant(301)]];
1524
+
1525
+ template <typename T>
1526
+ struct TransformScale {
1527
+ T scale;
1528
+ METAL_FUNC TransformScale(T scale_) : scale(scale_) {}
1529
+
1530
+ METAL_FUNC T apply(T x) const {
1531
+ return scale * x;
1532
+ }
1533
+ };
1534
+
1535
+ struct MaxOp {
1536
+ template <typename T>
1537
+ METAL_FUNC static constexpr T apply(T x, T y) {
1538
+ return metal::max(x, y);
1539
+ }
1540
+ };
1541
+
1542
+ struct SumOp {
1543
+ template <typename T>
1544
+ METAL_FUNC static constexpr T apply(T x, T y) {
1545
+ return x + y;
1546
+ }
1547
+ };
1548
+
1549
+ struct MulOp {
1550
+ template <typename T>
1551
+ METAL_FUNC static constexpr T apply(T x, T y) {
1552
+ return x * y;
1553
+ }
1554
+ };
1555
+
1556
+ struct SubOp {
1557
+ template <typename T>
1558
+ METAL_FUNC static constexpr T apply(T x, T y) {
1559
+ return x - y;
1560
+ }
1561
+ };
1562
+
1563
+ struct ExpSubOp {
1564
+ template <typename T>
1565
+ METAL_FUNC static constexpr T apply(T x, T y) {
1566
+ return fast::exp2(x - y);
1567
+ }
1568
+ };
1569
+
1570
+ struct DivOp {
1571
+ template <typename T>
1572
+ METAL_FUNC static constexpr T apply(T x, T y) {
1573
+ return x / y;
1574
+ }
1575
+ };
1576
+
1577
+ // clang-format off
1578
+ template <
1579
+ typename T,
1580
+ int BQ,
1581
+ int BK,
1582
+ int BD,
1583
+ int WM,
1584
+ int WN,
1585
+ typename MaskType = float,
1586
+ typename AccumType = float>
1587
+ [[kernel, max_total_threads_per_threadgroup(WM * WN * 32)]] void attention(
1588
+ const device T* Q [[buffer(0)]],
1589
+ const device T* K [[buffer(1)]],
1590
+ const device T* V [[buffer(2)]],
1591
+ device T* O [[buffer(3)]],
1592
+ const constant AttnParams* params [[buffer(4)]],
1593
+ const constant AttnMaskParams* mask_params [[buffer(5), function_constant(has_mask)]],
1594
+ const device MaskType* mask [[buffer(6), function_constant(has_mask)]],
1595
+ const device int* cu_seqlens_q [[buffer(7)]], // Cumulative query sequence lengths
1596
+ const device int* cu_seqlens_k [[buffer(8)]], // Cumulative key sequence lengths
1597
+ uint simd_lane_id [[thread_index_in_simdgroup]],
1598
+ uint simd_group_id [[simdgroup_index_in_threadgroup]],
1599
+ uint3 tid [[threadgroup_position_in_grid]],
1600
+ uint3 lid [[thread_position_in_threadgroup]]) { // clang-format on
1601
+
1602
+ // Pacifying compiler
1603
+ (void)lid;
1604
+
1605
+ // Flash Attention variable-length indexing
1606
+ // tid.z is now the sequence index within the batch
1607
+ int batch_idx = tid.z;
1608
+ int head_idx = tid.y;
1609
+ int block_idx = tid.x;
1610
+
1611
+ // Get sequence boundaries from cumulative lengths
1612
+ int q_seq_start = cu_seqlens_q[batch_idx];
1613
+ int q_seq_end = cu_seqlens_q[batch_idx + 1];
1614
+ int k_seq_start = cu_seqlens_k[batch_idx];
1615
+ int k_seq_end = cu_seqlens_k[batch_idx + 1];
1616
+
1617
+ int q_seq_len = q_seq_end - q_seq_start;
1618
+ int k_seq_len = k_seq_end - k_seq_start;
1619
+
1620
+ // Check if this block is within the sequence
1621
+ if (block_idx * BQ >= q_seq_len) {
1622
+ return;
1623
+ }
1624
+
1625
+ // Calculate offsets in the packed tensor format
1626
+ // Q/O shape: [total_tokens, num_heads, head_dim]
1627
+ // K/V shape: [total_tokens, num_heads_kv, head_dim]
1628
+ int q_offset = q_seq_start + block_idx * BQ;
1629
+ int k_offset = k_seq_start;
1630
+
1631
+ ulong kv_head_idx = head_idx / params->gqa_factor;
1632
+
1633
+ // Move pointers to the correct position in packed format
1634
+ Q += q_offset * params->H * params->D + head_idx * params->D;
1635
+ K += k_offset * (params->H / params->gqa_factor) * params->D + kv_head_idx * params->D;
1636
+ V += k_offset * (params->H / params->gqa_factor) * params->D + kv_head_idx * params->D;
1637
+ O += q_offset * params->H * params->D + head_idx * params->D;
1638
+
1639
+ if (has_mask) {
1640
+ // Mask indexing would need to be updated based on the mask format
1641
+ mask += batch_idx * mask_params->M_strides[0] +
1642
+ head_idx * mask_params->M_strides[1];
1643
+ }
1644
+
1645
+ // Prepare threadgroup memory
1646
+ constexpr short padQ = 16 / sizeof(T);
1647
+ constexpr short padK = 16 / sizeof(T);
1648
+ constexpr short padV = 16 / sizeof(T);
1649
+
1650
+ constexpr short LDQ_tgp = BD + padQ;
1651
+ constexpr short LDK_tgp = BK + padK;
1652
+ constexpr short LDV_tgp = BD + padV;
1653
+
1654
+ constexpr short tgp_mem_0 = (BK + padK) * (BD);
1655
+ constexpr short tgp_mem_1 = BK * (BD + padV);
1656
+ constexpr short tgp_mem_s = tgp_mem_0 > tgp_mem_1 ? tgp_mem_0 : tgp_mem_1;
1657
+
1658
+ threadgroup T Q_smem[BQ * (BD + padQ)];
1659
+ threadgroup T KV_smem[tgp_mem_s];
1660
+
1661
+ threadgroup T* Qs = Q_smem;
1662
+ threadgroup T* Ks = KV_smem;
1663
+ threadgroup T* Vs = KV_smem;
1664
+
1665
+ // Prepare block loaders
1666
+ using QBlockLoader = BlockLoaderT<
1667
+ /* typename T = */ T,
1668
+ /* short BROWS = */ BQ,
1669
+ /* short BCOLS = */ BD,
1670
+ /* short kDstStrRow = */ LDQ_tgp,
1671
+ /* short kDstStrCol = */ 1,
1672
+ /* short reduction_dim = */ 1,
1673
+ /* short tgp_size = */ WM * WN * 32>;
1674
+
1675
+ // K is loaded in transposed
1676
+ using KBlockLoader = BlockLoaderT<
1677
+ /* typename T = */ T,
1678
+ /* short BROWS = */ BK,
1679
+ /* short BCOLS = */ BD,
1680
+ /* short kDstStrRow = */ 1,
1681
+ /* short kDstStrCol = */ LDK_tgp,
1682
+ /* short reduction_dim = */ 0,
1683
+ /* short tgp_size = */ WM * WN * 32>;
1684
+
1685
+ using VBlockLoader = BlockLoaderT<
1686
+ /* typename T = */ T,
1687
+ /* short BROWS = */ BK,
1688
+ /* short BCOLS = */ BD,
1689
+ /* short kDstStrRow = */ LDV_tgp,
1690
+ /* short kDstStrCol = */ 1,
1691
+ /* short reduction_dim = */ 0,
1692
+ /* short tgp_size = */ WM * WN * 32>;
1693
+
1694
+ // For packed tensors, stride between tokens is H * D
1695
+ int q_stride = params->H * params->D;
1696
+ int kv_stride = (params->H / params->gqa_factor) * params->D;
1697
+
1698
+ QBlockLoader loader_q(
1699
+ Q, q_stride, Qs, simd_group_id, simd_lane_id);
1700
+ KBlockLoader loader_k(
1701
+ K, kv_stride, Ks, simd_group_id, simd_lane_id);
1702
+ VBlockLoader loader_v(
1703
+ V, kv_stride, Vs, simd_group_id, simd_lane_id);
1704
+
1705
+ // Apply softcapping adjustment to scale if needed
1706
+ float adjusted_scale = params->scale;
1707
+ if (params->softcapping != 1.0f) {
1708
+ adjusted_scale = params->scale / params->softcapping;
1709
+ }
1710
+ TransformScale<T> ts(static_cast<T>(adjusted_scale * 1.44269504089));
1711
+
1712
+ // Prepare MMA tiles
1713
+ constexpr short kFragSize = 8; // MMAFrag size
1714
+ using MMAFrag_acc_t = BaseMMAFrag<AccumType, kFragSize, kFragSize>;
1715
+
1716
+ constexpr int kNWarps = WM * WN;
1717
+ static_assert(
1718
+ BQ >= (kNWarps * kFragSize) && BQ % (kNWarps * kFragSize) == 0,
1719
+ "Each simdgroup must host atleast 1 simdgroup matrix along Q sequence.");
1720
+
1721
+ // Q seq frags per warp
1722
+ constexpr int TQ = BQ / (kNWarps * kFragSize);
1723
+ // KV sequence frags (all warps load the same frags)
1724
+ constexpr int TK = BK / kFragSize;
1725
+ // HeadDim frags (all warps load the same frags)
1726
+ constexpr int TD = BD / kFragSize;
1727
+
1728
+ static_assert(TQ == 1, "Check TQ");
1729
+
1730
+ MMATile<AccumType, TQ, 1, MMAFrag_acc_t> Qtile;
1731
+ MMATile<AccumType, 1, TK, MMAFrag_acc_t> Ktile;
1732
+ MMATile<AccumType, TQ, TK, MMAFrag_acc_t> Stile;
1733
+ MMATile<AccumType, 1, 1, MMAFrag_acc_t> Vtile;
1734
+ MMATile<AccumType, TQ, TD, MMAFrag_acc_t> Otile;
1735
+
1736
+ Otile.clear();
1737
+
1738
+ // Prepare mma tile offsets
1739
+ const short2 simd_coord = MMAFrag_acc_t::get_coord(simd_lane_id);
1740
+ const short sm = simd_coord.y;
1741
+ const short sn = simd_coord.x;
1742
+ const short tm = kFragSize * TQ * simd_group_id;
1743
+
1744
+ const short Qs_offset = (tm + sm) * LDQ_tgp + sn;
1745
+ const short Ks_offset = sm * LDK_tgp + sn;
1746
+ const short Vs_offset = sm * LDV_tgp + sn;
1747
+
1748
+ constexpr short Qs_tile_stride = kFragSize;
1749
+ constexpr short Ks_tile_stride = kFragSize * LDK_tgp;
1750
+
1751
+ threadgroup_barrier(mem_flags::mem_threadgroup);
1752
+
1753
+ // Load Q blocks apply scale
1754
+ int q_block_end = min(block_idx * BQ + BQ, q_seq_len);
1755
+ int q_block_size = q_block_end - block_idx * BQ;
1756
+
1757
+ if (q_block_size < BQ) {
1758
+ loader_q.load_safe(short2(BD, q_block_size));
1759
+ } else {
1760
+ loader_q.load_unsafe();
1761
+ }
1762
+ loader_q.apply_inplace_op(ts);
1763
+
1764
+ // Init row reduction variables
1765
+ constexpr short kRowsPT = decltype(Stile)::kRowsPerThread;
1766
+
1767
+ AccumType max_score[kRowsPT];
1768
+ AccumType sum_score[kRowsPT] = {0};
1769
+
1770
+ // Init to -Inf
1771
+ STEEL_PRAGMA_UNROLL
1772
+ for (short i = 0; i < kRowsPT; ++i) {
1773
+ max_score[i] = Limits<AccumType>::min;
1774
+ }
1775
+
1776
+ // Calculate number of K blocks for this sequence.
1777
+ // In general, we want to iterate over all key blocks. However,
1778
+ // when causal masking is enabled we only need to process up to the
1779
+ // last key that influences this query block. In decode mode
1780
+ // (q_seq_len < k_seq_len), the single query token logically sits
1781
+ // at the end of the key sequence. Without adjusting for this the
1782
+ // causal computation would incorrectly restrict processing to only
1783
+ // the first key block, because the query position would appear to
1784
+ // be at index 0. To handle this we compute a causal_offset that
1785
+ // shifts the query indices so they align with the end of the key
1786
+ // sequence when q_seq_len < k_seq_len.
1787
+ int kb_lim = (k_seq_len + BK - 1) / BK;
1788
+
1789
+ if (do_causal) {
1790
+ // Offset the row indices for causal masking when the query length
1791
+ // is smaller than the key length (decode mode). This ensures
1792
+ // that the computed row positions correspond to the correct
1793
+ // positions within the key sequence.
1794
+ int causal_offset = 0;
1795
+ if (q_seq_len < k_seq_len) {
1796
+ causal_offset = k_seq_len - q_seq_len;
1797
+ }
1798
+
1799
+ // Determine the start/end of the current query block in the
1800
+ // (possibly offset) sequence. The block index operates on
1801
+ // query positions but causal_offset places it relative to the
1802
+ // key positions when in decode mode.
1803
+ int q_block_start_in_seq = block_idx * BQ + causal_offset;
1804
+ int q_block_end_in_seq = q_block_start_in_seq + q_block_size;
1805
+
1806
+ // Limit the number of key blocks so that blocks that are strictly
1807
+ // beyond the last valid key (for this row) are not processed.
1808
+ // When causal_offset > 0 this prevents prematurely exiting after
1809
+ // the first block in decode mode.
1810
+ kb_lim = min(kb_lim, (q_block_end_in_seq + BK - 1) / BK);
1811
+ }
1812
+
1813
+ // Loop over KV seq length
1814
+ for (int kb = 0; kb < kb_lim; kb++) {
1815
+ // Load K block and apply scale
1816
+ threadgroup_barrier(mem_flags::mem_threadgroup);
1817
+
1818
+ int k_block_end = min(kb * BK + BK, k_seq_len);
1819
+ int k_block_size = k_block_end - kb * BK;
1820
+
1821
+ if (k_block_size < BK) {
1822
+ loader_k.load_safe(short2(BD, k_block_size));
1823
+ } else {
1824
+ loader_k.load_unsafe();
1825
+ }
1826
+
1827
+ // Do S = Q @ K.T
1828
+ Stile.clear();
1829
+
1830
+ threadgroup_barrier(mem_flags::mem_threadgroup);
1831
+
1832
+ STEEL_PRAGMA_UNROLL
1833
+ for (short dd = 0; dd < TD; dd++) {
1834
+ simdgroup_barrier(mem_flags::mem_none);
1835
+
1836
+ Qtile.template load<T, 1, 1, LDQ_tgp, 1>(
1837
+ &Qs[Qs_offset + dd * Qs_tile_stride]);
1838
+ Ktile.template load<T, 1, 1, LDK_tgp, 1>(
1839
+ &Ks[Ks_offset + dd * Ks_tile_stride]);
1840
+
1841
+ simdgroup_barrier(mem_flags::mem_none);
1842
+
1843
+ tile_matmad(Stile, Qtile, Ktile, Stile);
1844
+ }
1845
+
1846
+ // Mask out length sequence
1847
+ if (k_block_size < BK) {
1848
+ using stile_t = decltype(Stile);
1849
+ using selem_t = typename stile_t::elem_type;
1850
+ constexpr auto neg_inf = -metal::numeric_limits<selem_t>::infinity();
1851
+
1852
+ STEEL_PRAGMA_UNROLL
1853
+ for (short i = 0; i < stile_t::kTileRows; i++) {
1854
+ STEEL_PRAGMA_UNROLL
1855
+ for (short j = 0; j < stile_t::kTileCols; j++) {
1856
+ short col_pos = sn + (j * stile_t::kFragCols);
1857
+ STEEL_PRAGMA_UNROLL
1858
+ for (short jj = 0; jj < stile_t::MMAFrag_t::kElemCols; jj++) {
1859
+ if ((col_pos + jj) >= k_block_size) {
1860
+ Stile.frag_at(i, j)[jj] = neg_inf;
1861
+ }
1862
+ }
1863
+ }
1864
+ }
1865
+ }
1866
+
1867
+ // Mask out if causal
1868
+ if (do_causal) {
1869
+ using stile_t = decltype(Stile);
1870
+ using selem_t = typename stile_t::elem_type;
1871
+ constexpr auto neg_inf = -metal::numeric_limits<selem_t>::infinity();
1872
+
1873
+ STEEL_PRAGMA_UNROLL
1874
+ for (short i = 0; i < stile_t::kTileRows; i++) {
1875
+ // Compute row position for causal mask. In decode mode
1876
+ // (q_seq_len < k_seq_len) the single query row should be
1877
+ // aligned with the end of the key sequence. Without this
1878
+ // offset the row index would be zero and all but the first
1879
+ // key block would be erroneously masked out.
1880
+ int row_pos_causal = block_idx * BQ + tm + sm + (i * stile_t::kFragRows);
1881
+ if (q_seq_len < k_seq_len) {
1882
+ row_pos_causal += (k_seq_len - q_seq_len);
1883
+ }
1884
+ STEEL_PRAGMA_UNROLL
1885
+ for (short j = 0; j < stile_t::kTileCols; j++) {
1886
+ const int col_pos_in_seq = kb * BK + sn + (j * stile_t::kFragCols);
1887
+ STEEL_PRAGMA_UNROLL
1888
+ for (short jj = 0; jj < stile_t::MMAFrag_t::kElemCols; jj++) {
1889
+ if (row_pos_causal < (col_pos_in_seq + jj)) {
1890
+ Stile.frag_at(i, j)[jj] = neg_inf;
1891
+ }
1892
+ }
1893
+ }
1894
+ }
1895
+ }
1896
+
1897
+ // Other masking as needed
1898
+ if (has_mask) {
1899
+ using stile_t = decltype(Stile);
1900
+ using selem_t = typename stile_t::elem_type;
1901
+ constexpr auto neg_inf = -metal::numeric_limits<selem_t>::infinity();
1902
+
1903
+ constexpr bool is_bool = is_same_v<MaskType, bool>;
1904
+ using melem_t = typename metal::conditional_t<is_bool, bool, selem_t>;
1905
+
1906
+ using MMAFrag_mask_t = BaseMMAFrag<melem_t, kFragSize, kFragSize>;
1907
+ using frag_t = typename MMAFrag_mask_t::frag_type;
1908
+
1909
+ STEEL_PRAGMA_UNROLL
1910
+ for (short i = 0; i < stile_t::kTileRows; i++) {
1911
+ // Use sequence-local positions
1912
+ const int row_pos_in_seq = block_idx * BQ + tm + sm + (i * stile_t::kFragRows);
1913
+ STEEL_PRAGMA_UNROLL
1914
+ for (short j = 0; j < stile_t::kTileCols; j++) {
1915
+ const int col_pos_in_seq = kb * BK + sn + (j * stile_t::kFragCols);
1916
+
1917
+ frag_t mfrag;
1918
+
1919
+ MMAFrag_mask_t::load_safe(
1920
+ mfrag,
1921
+ mask,
1922
+ int(mask_params->M_strides[2]),
1923
+ Int<1>{},
1924
+ q_seq_len,
1925
+ k_seq_len,
1926
+ row_pos_in_seq, // Already sequence-local
1927
+ col_pos_in_seq); // Already sequence-local
1928
+
1929
+ STEEL_PRAGMA_UNROLL
1930
+ for (short jj = 0; jj < stile_t::MMAFrag_t::kElemsPerFrag; jj++) {
1931
+ if constexpr (is_bool) {
1932
+ Stile.frag_at(i, j)[jj] =
1933
+ mfrag[jj] ? Stile.frag_at(i, j)[jj] : neg_inf;
1934
+ } else {
1935
+ Stile.frag_at(i, j)[jj] += selem_t(mfrag[jj]);
1936
+ }
1937
+ }
1938
+ }
1939
+ }
1940
+ }
1941
+
1942
+ // Apply softcapping if needed (tanh(score) * softcapping)
1943
+ if (params->softcapping != 1.0f) {
1944
+ using stile_t = decltype(Stile);
1945
+ using selem_t = typename stile_t::elem_type;
1946
+ const selem_t softcapping_val = static_cast<selem_t>(params->softcapping);
1947
+
1948
+ STEEL_PRAGMA_UNROLL
1949
+ for (short i = 0; i < stile_t::kTileRows; i++) {
1950
+ STEEL_PRAGMA_UNROLL
1951
+ for (short j = 0; j < stile_t::kTileCols; j++) {
1952
+ STEEL_PRAGMA_UNROLL
1953
+ for (short jj = 0; jj < stile_t::MMAFrag_t::kElemsPerFrag; jj++) {
1954
+ Stile.frag_at(i, j)[jj] = metal::tanh(Stile.frag_at(i, j)[jj]) * softcapping_val;
1955
+ }
1956
+ }
1957
+ }
1958
+ }
1959
+
1960
+ threadgroup_barrier(mem_flags::mem_threadgroup);
1961
+
1962
+ // Load V blocks
1963
+ if (k_block_size < BK) {
1964
+ loader_v.load_safe(short2(BD, k_block_size));
1965
+ } else {
1966
+ loader_v.load_unsafe();
1967
+ }
1968
+
1969
+ // Do softmax
1970
+
1971
+ // Temp variables
1972
+ AccumType new_max[kRowsPT];
1973
+ AccumType factor[kRowsPT];
1974
+ STEEL_PRAGMA_UNROLL
1975
+ for (short i = 0; i < kRowsPT; ++i) {
1976
+ new_max[i] = max_score[i];
1977
+ }
1978
+
1979
+ // Row max
1980
+ Stile.template row_reduce<MaxOp>(new_max);
1981
+
1982
+ // exp(Si - rowmax(Si))
1983
+ Stile.template row_bin_op<ExpSubOp>(new_max);
1984
+
1985
+ // Factor exp(rowmax(Si) - rowmax(Si-1))
1986
+ STEEL_PRAGMA_UNROLL
1987
+ for (short i = 0; i < kRowsPT; ++i) {
1988
+ factor[i] = fast::exp2(max_score[i] - new_max[i]);
1989
+ }
1990
+
1991
+ // Save max for next iteration
1992
+ STEEL_PRAGMA_UNROLL
1993
+ for (short i = 0; i < kRowsPT; ++i) {
1994
+ max_score[i] = new_max[i];
1995
+ }
1996
+
1997
+ // Row Sum
1998
+ AccumType sum_score_tmp[kRowsPT] = {0};
1999
+ Stile.template row_reduce<SumOp>(sum_score_tmp);
2000
+
2001
+ // Update norm
2002
+ STEEL_PRAGMA_UNROLL
2003
+ for (short i = 0; i < kRowsPT; ++i) {
2004
+ sum_score[i] = sum_score[i] * factor[i] + sum_score_tmp[i];
2005
+ }
2006
+
2007
+ // Update O
2008
+ Otile.template row_bin_op<MulOp>(factor);
2009
+
2010
+ // Load V into registers
2011
+ threadgroup_barrier(mem_flags::mem_threadgroup);
2012
+
2013
+ STEEL_PRAGMA_UNROLL
2014
+ for (short iq = 0; iq < TQ; iq++) {
2015
+ STEEL_PRAGMA_UNROLL
2016
+ for (short id = 0; id < TD; id++) {
2017
+ STEEL_PRAGMA_UNROLL
2018
+ for (short ik = 0; ik < TK; ik++) {
2019
+ if constexpr (BD == 128) {
2020
+ simdgroup_barrier(mem_flags::mem_none);
2021
+ }
2022
+
2023
+ const short kk = ik * kFragSize;
2024
+ const short dd = id * kFragSize;
2025
+
2026
+ Vtile.template load<T, 1, 1, LDV_tgp, 1>(
2027
+ &Vs[Vs_offset + kk * LDV_tgp + dd]);
2028
+
2029
+ if constexpr (BD == 128) {
2030
+ simdgroup_barrier(mem_flags::mem_none);
2031
+ }
2032
+
2033
+ MMAFrag_acc_t::mma(
2034
+ Otile.frag_at(iq, id),
2035
+ Stile.frag_at(iq, ik),
2036
+ Vtile.frag_at(0, 0),
2037
+ Otile.frag_at(iq, id));
2038
+ }
2039
+ }
2040
+ }
2041
+
2042
+ // Prepare for next iteration
2043
+ loader_k.next();
2044
+ loader_v.next();
2045
+ }
2046
+
2047
+ // Normalize output
2048
+ Otile.template row_bin_op<DivOp>(sum_score);
2049
+ threadgroup_barrier(mem_flags::mem_none);
2050
+
2051
+ // Store results
2052
+ // O is already pointing to the correct block position from earlier adjustment
2053
+ // Just need to offset within the block for this thread's tile
2054
+ device T* O_tile = O + (tm + sm) * params->H * params->D + sn;
2055
+
2056
+ if (q_block_size < BQ) {
2057
+ // Only store if this thread's tile is within the valid range
2058
+ if ((tm + sm) < q_block_size && sn < BD) {
2059
+ auto dst_tile_dims = short2(BD - sn, q_block_size - (tm + sm));
2060
+ Otile.template store_safe<T, 1, 1>(O_tile, params->H * params->D, dst_tile_dims);
2061
+ }
2062
+ } else {
2063
+ Otile.template store<T, 1, 1>(O_tile, params->H * params->D);
2064
+ }
2065
+ }
2066
+
2067
+ // clang-format off
2068
+
2069
+ // SDPA full instantiations
2070
+
2071
+ // Instantiate a templated kernel.
2072
+ // Extra args are used as template parameters:
2073
+ // e.g. instantiate_kernel(binary_int, binary, a, b) ->
2074
+ // [[host_name(binary_int)]] [kernel] binary<a, b>
2075
+ #define instantiate_kernel(name, func, ...) \
2076
+ template [[host_name( \
2077
+ name)]] [[kernel]] decltype(func<__VA_ARGS__>) func<__VA_ARGS__>;
2078
+
2079
+ #define instantiate_attn(tname, dtype, bq, bk, bd, wm, wn, mname, mtype) \
2080
+ instantiate_kernel( \
2081
+ "steel_attention_" #tname "_bq" #bq "_bk" #bk "_bd" #bd \
2082
+ "_wm" #wm "_wn" #wn "_mask" #mname, \
2083
+ attention, dtype, bq, bk, bd, wm, wn, mtype, float)
2084
+
2085
+ #define instantiate_attn_shapes_helper(iname, itype, mname, mtype) \
2086
+ instantiate_attn(iname, itype, 16, 8, 256, 2, 1, mname, mtype) \
2087
+ instantiate_attn(iname, itype, 32, 16, 128, 4, 1, mname, mtype) \
2088
+ instantiate_attn(iname, itype, 32, 32, 96, 4, 1, mname, mtype) \
2089
+ instantiate_attn(iname, itype, 32, 32, 80, 4, 1, mname, mtype) \
2090
+ instantiate_attn(iname, itype, 32, 32, 72, 4, 1, mname, mtype) \
2091
+ instantiate_attn(iname, itype, 32, 32, 64, 4, 1, mname, mtype) \
2092
+ instantiate_attn(iname, itype, 32, 32, 32, 4, 1, mname, mtype)
2093
+
2094
+ #define instantiate_attn_mask_helper(iname, itype) \
2095
+ instantiate_attn_shapes_helper(iname, itype, iname, itype) \
2096
+ instantiate_attn_shapes_helper(iname, itype, bool_, bool)
2097
+
2098
+ instantiate_attn_mask_helper(float16, half);
2099
+ instantiate_attn_mask_helper(bfloat16, bfloat16_t);
2100
+ instantiate_attn_mask_helper(float32, float);
2101
+
sdpa-metal/scaled_dot_product_attention.mm ADDED
@@ -0,0 +1,316 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #include <ATen/mps/MPSDevice.h>
2
+ #include <ATen/mps/MPSStream.h>
3
+ #include <torch/torch.h>
4
+
5
+ // Include the auto-generated header with embedded metallib
6
+ #ifdef EMBEDDED_METALLIB_HEADER
7
+ #include EMBEDDED_METALLIB_HEADER
8
+ #else
9
+ #error "EMBEDDED_METALLIB_HEADER not defined"
10
+ #endif
11
+
12
+ static inline id<MTLBuffer> getMTLBufferStorage(const torch::Tensor &tensor) {
13
+ return __builtin_bit_cast(id<MTLBuffer>, tensor.storage().data());
14
+ }
15
+
16
+ // Helper function to get dtype string
17
+ static std::string getDtypeString(torch::ScalarType dtype) {
18
+ switch (dtype) {
19
+ case torch::kFloat:
20
+ return "float32";
21
+ case torch::kHalf:
22
+ return "float16";
23
+ case torch::kBFloat16:
24
+ return "bfloat16";
25
+ default:
26
+ TORCH_CHECK(false, "Unsupported dtype for SDPA: ", dtype);
27
+ }
28
+ }
29
+
30
+ // Helper function to get dtype string for kernel names
31
+ static std::string getKernelDtypeString(torch::ScalarType dtype) {
32
+ switch (dtype) {
33
+ case torch::kFloat:
34
+ return "float32"; // Match the instantiation names
35
+ case torch::kHalf:
36
+ return "float16";
37
+ case torch::kBFloat16:
38
+ return "bfloat16";
39
+ default:
40
+ TORCH_CHECK(false, "Unsupported dtype for SDPA: ", dtype);
41
+ }
42
+ }
43
+
44
+
45
+ // Parameters structure matching Flash Attention's AttnParams
46
+ struct AttnParams {
47
+ int32_t B; // batch size
48
+ int32_t H; // number of heads
49
+ int32_t D; // head dimension
50
+ int32_t qL; // query sequence length (per sequence)
51
+ int32_t kL; // key sequence length (per sequence)
52
+ int32_t gqa_factor; // grouped query attention factor
53
+ float scale; // attention scale
54
+ float softcapping; // softcapping value (1.0 for no softcapping)
55
+ int32_t NQ; // number of query blocks
56
+ int32_t NK; // number of key blocks
57
+ int32_t NQ_aligned; // aligned query blocks
58
+ int32_t NK_aligned; // aligned key blocks
59
+ int32_t qL_rem; // remainder query length
60
+ int32_t kL_rem; // remainder key length
61
+ int32_t qL_off; // query offset
62
+ int64_t Q_strides[3]; // query tensor strides
63
+ int64_t K_strides[3]; // key tensor strides
64
+ int64_t V_strides[3]; // value tensor strides
65
+ int64_t O_strides[3]; // output tensor strides
66
+
67
+ // Flash Attention variable-length support
68
+ int32_t total_q_tokens; // Total number of query tokens
69
+ int32_t total_k_tokens; // Total number of key/value tokens
70
+ int32_t max_seqlen_q; // Maximum query sequence length
71
+ int32_t max_seqlen_k; // Maximum key/value sequence length
72
+ };
73
+
74
+ // Forward declarations for kernel implementations
75
+ void call_flash_attention_varlen(
76
+ id<MTLDevice> device,
77
+ id<MTLCommandBuffer> cmdBuf,
78
+ id<MTLLibrary> lib,
79
+ torch::Tensor &out,
80
+ torch::Tensor &query,
81
+ torch::Tensor &key,
82
+ torch::Tensor &value,
83
+ torch::Tensor &cu_seqlens_q,
84
+ torch::Tensor &cu_seqlens_k,
85
+ int64_t max_seqlen_q,
86
+ int64_t max_seqlen_k,
87
+ bool do_causal,
88
+ double scale,
89
+ double softcapping);
90
+
91
+
92
+ void flash_attention_varlen(
93
+ torch::Tensor &out, // [total_q_tokens, num_heads, head_size]
94
+ torch::Tensor &query, // [total_q_tokens, num_heads, head_size]
95
+ torch::Tensor &key, // [total_k_tokens, num_heads_kv, head_size]
96
+ torch::Tensor &value, // [total_k_tokens, num_heads_kv, head_size]
97
+ torch::Tensor &cu_seqlens_q, // [batch_size + 1]
98
+ torch::Tensor &cu_seqlens_k, // [batch_size + 1]
99
+ int64_t max_seqlen_q, // Maximum query sequence length
100
+ int64_t max_seqlen_k, // Maximum key sequence length
101
+ bool do_causal, // Whether to use causal mask
102
+ double scale, // Attention scale
103
+ double softcapping) { // Softcapping value
104
+
105
+ try {
106
+ // Get device and stream
107
+ id<MTLDevice> device = at::mps::MPSDevice::getInstance()->device();
108
+ at::mps::MPSStream *stream = at::mps::getCurrentMPSStream();
109
+ TORCH_CHECK(stream, "Failed to get current MPS stream");
110
+
111
+ // Get dimensions from Flash Attention format
112
+ int64_t total_q_tokens = query.size(0);
113
+ int64_t num_heads = query.size(1);
114
+ int64_t head_dim = query.size(2);
115
+ int64_t num_heads_kv = key.size(1);
116
+ int64_t batch_size = cu_seqlens_q.size(0) - 1; // cu_seqlens has batch_size + 1 elements
117
+
118
+ // Check if we support this head dimension
119
+ std::vector<int> supported_head_dims = {32, 64, 72, 80, 96, 128, 256};
120
+ bool supported_head_dim = std::find(supported_head_dims.begin(),
121
+ supported_head_dims.end(),
122
+ head_dim) != supported_head_dims.end();
123
+
124
+ TORCH_CHECK(supported_head_dim, "Head dimension ", head_dim, " is not supported");
125
+ TORCH_CHECK(cu_seqlens_q.size(0) == cu_seqlens_k.size(0),
126
+ "cu_seqlens_q and cu_seqlens_k must have the same size");
127
+
128
+ // Load Metal library
129
+ static id<MTLLibrary> lib = nil;
130
+ if (!lib) {
131
+ NSError *error = nil;
132
+ lib = EMBEDDED_METALLIB_NAMESPACE::createLibrary(device, &error);
133
+ TORCH_CHECK(lib,
134
+ "Failed to create Metal library from embedded data: ",
135
+ error.localizedDescription.UTF8String);
136
+ }
137
+
138
+ // Get command buffer
139
+ id<MTLCommandBuffer> cmdBuf = stream->commandBuffer();
140
+ TORCH_CHECK(cmdBuf, "Failed to get MPS command buffer");
141
+
142
+ // For variable-length Flash Attention, always use the full attention kernel
143
+
144
+ // Call the Flash Attention kernel
145
+ call_flash_attention_varlen(device, cmdBuf, lib, out, query, key, value,
146
+ cu_seqlens_q, cu_seqlens_k, max_seqlen_q, max_seqlen_k,
147
+ do_causal, scale, softcapping);
148
+ } catch (const std::exception& e) {
149
+ throw;
150
+ } catch (...) {
151
+ throw;
152
+ }
153
+ }
154
+
155
+ // Implementation of Flash Attention variable-length kernel
156
+ void call_flash_attention_varlen(
157
+ id<MTLDevice> device,
158
+ id<MTLCommandBuffer> cmdBuf,
159
+ id<MTLLibrary> lib,
160
+ torch::Tensor &out,
161
+ torch::Tensor &query,
162
+ torch::Tensor &key,
163
+ torch::Tensor &value,
164
+ torch::Tensor &cu_seqlens_q,
165
+ torch::Tensor &cu_seqlens_k,
166
+ int64_t max_seqlen_q,
167
+ int64_t max_seqlen_k,
168
+ bool do_causal,
169
+ double scale,
170
+ double softcapping) {
171
+
172
+ // Get dimensions
173
+ int64_t total_q_tokens = query.size(0);
174
+ int64_t num_heads = query.size(1);
175
+ int64_t head_dim = query.size(2);
176
+ int64_t num_heads_kv = key.size(1);
177
+ int64_t batch_size = cu_seqlens_q.size(0) - 1;
178
+
179
+ // Grouped Query Attention factor
180
+ int32_t gqa_factor = num_heads / num_heads_kv;
181
+
182
+ // Block sizes based on head dimension
183
+ const int BQ = (head_dim == 256) ? 16 : 32; // Use BQ=16 for head_dim=256
184
+ const int bk = (head_dim == 256) ? 8 : ((head_dim >= 128) ? 16 : 32); // Use bk=8 for head_dim=256
185
+ const int WM = (head_dim == 256) ? 2 : 4; // Use WM=2 for head_dim=256
186
+ const int WN = 1;
187
+
188
+ // Setup parameters
189
+ AttnParams params = {}; // Zero-initialize all fields
190
+ params.B = batch_size;
191
+ params.H = num_heads;
192
+ params.D = head_dim;
193
+ params.gqa_factor = gqa_factor;
194
+ params.scale = static_cast<float>(scale);
195
+ params.softcapping = static_cast<float>(softcapping);
196
+ params.total_q_tokens = total_q_tokens;
197
+ params.total_k_tokens = key.size(0);
198
+ params.max_seqlen_q = max_seqlen_q;
199
+ params.max_seqlen_k = max_seqlen_k;
200
+
201
+ // Initialize fields that might be checked but aren't used in Flash Attention
202
+ params.qL = 0; // Not used in variable-length attention
203
+ params.kL = 0; // Not used in variable-length attention
204
+ params.NQ = 0; // Not used
205
+ params.NK = 0; // Not used
206
+ params.NQ_aligned = 0;
207
+ params.NK_aligned = 0;
208
+ params.qL_rem = 0;
209
+ params.kL_rem = 0;
210
+ params.qL_off = 0;
211
+
212
+ // Strides are not used for packed tensors (contiguous)
213
+ params.Q_strides[0] = 0;
214
+ params.Q_strides[1] = 0;
215
+ params.Q_strides[2] = 0;
216
+ params.K_strides[0] = 0;
217
+ params.K_strides[1] = 0;
218
+ params.K_strides[2] = 0;
219
+ params.V_strides[0] = 0;
220
+ params.V_strides[1] = 0;
221
+ params.V_strides[2] = 0;
222
+ params.O_strides[0] = 0;
223
+ params.O_strides[1] = 0;
224
+ params.O_strides[2] = 0;
225
+
226
+ // For variable-length attention, we'll process each sequence separately
227
+ // The kernel will handle the cu_seqlens internally
228
+
229
+ bool has_mask = false; // Masks are not supported in Flash Attention
230
+
231
+ // Setup function constants
232
+ MTLFunctionConstantValues *constants = [MTLFunctionConstantValues new];
233
+ [constants setConstantValue:&has_mask type:MTLDataTypeBool atIndex:300];
234
+ [constants setConstantValue:&do_causal type:MTLDataTypeBool atIndex:301];
235
+
236
+ // Construct kernel name based on data type and head dimension
237
+ std::string kernel_name = "steel_attention_";
238
+ kernel_name += getKernelDtypeString(query.scalar_type());
239
+ kernel_name += "_bq" + std::to_string(BQ);
240
+ kernel_name += "_bk" + std::to_string(bk);
241
+ kernel_name += "_bd" + std::to_string(head_dim);
242
+ kernel_name += "_wm" + std::to_string(WM) + "_wn" + std::to_string(WN);
243
+ kernel_name += "_maskbool_"; // Always use bool for mask type (no masks supported)
244
+
245
+ // Get kernel function
246
+ NSError *error = nil;
247
+ id<MTLFunction> function = [lib newFunctionWithName:[NSString stringWithUTF8String:kernel_name.c_str()]
248
+ constantValues:constants
249
+ error:&error];
250
+ TORCH_CHECK(function, "Failed to get Metal function: ", kernel_name,
251
+ " Error: ", error ? error.localizedDescription.UTF8String : "unknown");
252
+
253
+ // Create compute pipeline
254
+ id<MTLComputePipelineState> pipeline = [device newComputePipelineStateWithFunction:function error:&error];
255
+ TORCH_CHECK(pipeline, "Failed to create compute pipeline: ",
256
+ error ? error.localizedDescription.UTF8String : "unknown");
257
+
258
+ // Setup command encoder with dispatch sync
259
+ at::mps::MPSStream *stream = at::mps::getCurrentMPSStream();
260
+ dispatch_queue_t q = stream->queue();
261
+ dispatch_sync(q, ^{
262
+ id<MTLComputeCommandEncoder> encoder = [cmdBuf computeCommandEncoder];
263
+ TORCH_CHECK(encoder, "Failed to create compute encoder");
264
+
265
+ [encoder setComputePipelineState:pipeline];
266
+
267
+ // Set buffers
268
+ int buffer_idx = 0;
269
+
270
+ // Query buffer - index 0
271
+ [encoder setBuffer:getMTLBufferStorage(query)
272
+ offset:query.storage_offset() * query.element_size()
273
+ atIndex:buffer_idx++];
274
+
275
+ // Key buffer - index 1
276
+ [encoder setBuffer:getMTLBufferStorage(key)
277
+ offset:key.storage_offset() * key.element_size()
278
+ atIndex:buffer_idx++];
279
+
280
+ // Value buffer - index 2
281
+ [encoder setBuffer:getMTLBufferStorage(value)
282
+ offset:value.storage_offset() * value.element_size()
283
+ atIndex:buffer_idx++];
284
+
285
+ // Output buffer - index 3
286
+ [encoder setBuffer:getMTLBufferStorage(out)
287
+ offset:out.storage_offset() * out.element_size()
288
+ atIndex:buffer_idx++];
289
+
290
+ // Parameters - index 4
291
+ [encoder setBytes:&params length:sizeof(AttnParams) atIndex:buffer_idx++];
292
+
293
+ // Skip mask parameters - indices 5 and 6 (masks not supported)
294
+ buffer_idx += 2;
295
+
296
+ // Set cu_seqlens buffers - indices 7 and 8
297
+ [encoder setBuffer:getMTLBufferStorage(cu_seqlens_q)
298
+ offset:cu_seqlens_q.storage_offset() * cu_seqlens_q.element_size()
299
+ atIndex:7];
300
+ [encoder setBuffer:getMTLBufferStorage(cu_seqlens_k)
301
+ offset:cu_seqlens_k.storage_offset() * cu_seqlens_k.element_size()
302
+ atIndex:8];
303
+
304
+ // Calculate grid dimensions
305
+ // We need to process each sequence independently
306
+ int64_t max_blocks_q = (max_seqlen_q + BQ - 1) / BQ;
307
+
308
+ MTLSize gridSize = MTLSizeMake(max_blocks_q, num_heads, batch_size);
309
+ MTLSize threadgroupSize = MTLSizeMake(32, WM, WN);
310
+
311
+ [encoder dispatchThreadgroups:gridSize threadsPerThreadgroup:threadgroupSize];
312
+ [encoder endEncoding];
313
+
314
+ stream->synchronize(at::mps::SyncType::COMMIT);
315
+ });
316
+ }
tests/__init__.py ADDED
File without changes
tests/test_flash_attention.py ADDED
@@ -0,0 +1,480 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import pytest
3
+ import metal_flash_sdpa
4
+
5
+
6
+ def create_cu_seqlens(seq_lengths):
7
+ """Create cumulative sequence lengths tensor."""
8
+ cu_seqlens = [0]
9
+ for length in seq_lengths:
10
+ cu_seqlens.append(cu_seqlens[-1] + length)
11
+ return torch.tensor(cu_seqlens, dtype=torch.int32, device="mps")
12
+
13
+
14
+ def compute_attention_reference(query, key, value, scale, causal=False, softcapping=1.0, gqa_ratio=1):
15
+ """Compute reference attention output for validation."""
16
+ num_heads = query.shape[1]
17
+ expected = torch.zeros_like(query)
18
+
19
+ for h in range(num_heads):
20
+ kv_h = h // gqa_ratio if gqa_ratio > 1 else h
21
+ q_h = query[:, h, :]
22
+ k_h = key[:, kv_h, :]
23
+ v_h = value[:, kv_h, :]
24
+
25
+ scores = torch.matmul(q_h, k_h.transpose(-1, -2)) * scale
26
+
27
+ # Apply softcapping if not 1.0
28
+ if softcapping != 1.0:
29
+ scores = scores / softcapping
30
+ scores = torch.tanh(scores) * softcapping
31
+
32
+ # Apply causal mask if needed
33
+ if causal:
34
+ seq_len = query.shape[0]
35
+ causal_mask = torch.triu(torch.ones(seq_len, seq_len, device="mps"), diagonal=1).bool()
36
+ scores.masked_fill_(causal_mask, float("-inf"))
37
+
38
+ attn_weights = torch.softmax(scores, dim=-1)
39
+ expected[:, h, :] = torch.matmul(attn_weights, v_h)
40
+
41
+ return expected
42
+
43
+
44
+ def get_tolerance(dtype, head_dim):
45
+ """Get appropriate tolerance based on dtype and head dimension."""
46
+ if dtype == torch.bfloat16:
47
+ return (2e-2, 2e-2) if head_dim >= 96 else (1.6e-2, 1.6e-2)
48
+ elif dtype == torch.float16:
49
+ return (2e-3, 2e-3)
50
+ else:
51
+ return (1e-3, 1e-3)
52
+
53
+
54
+ @pytest.mark.parametrize("dtype", [torch.float32, torch.float16, torch.bfloat16])
55
+ @pytest.mark.parametrize("head_dim", [32, 64, 72, 80, 96, 128, 256])
56
+ @pytest.mark.parametrize("seq_config", [
57
+ # (seq_lengths_q, seq_lengths_k, description)
58
+ ([32], [32], "single_sequence"),
59
+ ([8, 16, 12], [10, 20, 15], "variable_lengths"),
60
+ ([16, 24], [16, 24], "multiple_sequences"),
61
+ ([2], [2], "small_sequence_2"),
62
+ ([4], [4], "small_sequence_4"),
63
+ ([8], [8], "small_sequence_8"),
64
+ ([16], [32], "cross_attention_q_lt_k"),
65
+ ([32], [16], "cross_attention_q_gt_k"),
66
+ ([8], [128], "cross_attention_large_diff"),
67
+ ([1], [64], "single_query_token"),
68
+ ])
69
+ @pytest.mark.parametrize("causal", [False, True])
70
+ def test_flash_attention_comprehensive(dtype, head_dim, seq_config, causal):
71
+ """Comprehensive test for Flash Attention with various configurations."""
72
+ torch.manual_seed(42)
73
+
74
+ seq_lengths_q, seq_lengths_k, _ = seq_config
75
+
76
+ # Skip causal tests for cross-attention cases
77
+ if causal and seq_lengths_q != seq_lengths_k:
78
+ pytest.skip("Causal attention only valid when q_seq == k_seq")
79
+
80
+ # Test parameters
81
+ num_heads = 4
82
+
83
+ # Create cumulative sequence lengths
84
+ cu_seqlens_q = create_cu_seqlens(seq_lengths_q)
85
+ cu_seqlens_k = create_cu_seqlens(seq_lengths_k)
86
+
87
+ total_q = sum(seq_lengths_q)
88
+ total_k = sum(seq_lengths_k)
89
+ max_seqlen_q = max(seq_lengths_q)
90
+ max_seqlen_k = max(seq_lengths_k)
91
+
92
+ # Create input tensors
93
+ query = torch.randn(total_q, num_heads, head_dim, dtype=dtype, device="mps")
94
+ key = torch.randn(total_k, num_heads, head_dim, dtype=dtype, device="mps")
95
+ value = torch.randn(total_k, num_heads, head_dim, dtype=dtype, device="mps")
96
+
97
+ # Scale factor
98
+ scale = 1.0 / (head_dim ** 0.5)
99
+
100
+ # Call Flash Attention
101
+ out = torch.empty_like(query)
102
+ metal_flash_sdpa.flash_attention_varlen(
103
+ out=out,
104
+ query=query,
105
+ key=key,
106
+ value=value,
107
+ cu_seqlens_q=cu_seqlens_q,
108
+ cu_seqlens_k=cu_seqlens_k,
109
+ max_seqlen_q=max_seqlen_q,
110
+ max_seqlen_k=max_seqlen_k,
111
+ do_causal=causal,
112
+ scale=scale,
113
+ softcapping=1.0,
114
+ )
115
+
116
+ # Compute ground truth for each sequence
117
+ expected = torch.zeros_like(out)
118
+ batch_size = len(seq_lengths_q)
119
+
120
+ for i in range(batch_size):
121
+ q_start, q_end = cu_seqlens_q[i].item(), cu_seqlens_q[i+1].item()
122
+ k_start, k_end = cu_seqlens_k[i].item(), cu_seqlens_k[i+1].item()
123
+
124
+ if q_end > q_start and k_end > k_start: # Skip empty sequences
125
+ q_i = query[q_start:q_end]
126
+ k_i = key[k_start:k_end]
127
+ v_i = value[k_start:k_end]
128
+
129
+ expected_i = compute_attention_reference(q_i, k_i, v_i, scale, causal=causal)
130
+ expected[q_start:q_end] = expected_i
131
+
132
+ # Check results
133
+ rtol, atol = get_tolerance(dtype, head_dim)
134
+ torch.testing.assert_close(out, expected, rtol=rtol, atol=atol)
135
+
136
+
137
+ @pytest.mark.parametrize("dtype", [torch.float32, torch.float16, torch.bfloat16])
138
+ @pytest.mark.parametrize("head_dim", [32, 64, 72, 80, 96, 128, 256])
139
+ @pytest.mark.parametrize("gqa_config", [
140
+ # (num_heads, num_kv_heads, seq_len)
141
+ (8, 2, 32), # 4:1 ratio
142
+ (16, 4, 32), # 4:1 ratio
143
+ (16, 8, 32), # 2:1 ratio
144
+ (16, 2, 32), # 8:1 ratio
145
+ (16, 4, 128), # 4:1 ratio with larger sequence
146
+ ])
147
+ def test_flash_attention_gqa(dtype, head_dim, gqa_config):
148
+ """Test Flash Attention with Grouped Query Attention configurations."""
149
+ torch.manual_seed(42)
150
+
151
+ num_heads, num_kv_heads, seq_len = gqa_config
152
+ gqa_ratio = num_heads // num_kv_heads
153
+
154
+ # Create cumulative sequence lengths
155
+ cu_seqlens = create_cu_seqlens([seq_len])
156
+
157
+ # Create input tensors
158
+ query = torch.randn(seq_len, num_heads, head_dim, dtype=dtype, device="mps")
159
+ key = torch.randn(seq_len, num_kv_heads, head_dim, dtype=dtype, device="mps")
160
+ value = torch.randn(seq_len, num_kv_heads, head_dim, dtype=dtype, device="mps")
161
+
162
+ # Scale factor
163
+ scale = 1.0 / (head_dim ** 0.5)
164
+
165
+ # Call Flash Attention
166
+ out = torch.empty_like(query)
167
+ metal_flash_sdpa.flash_attention_varlen(
168
+ out=out,
169
+ query=query,
170
+ key=key,
171
+ value=value,
172
+ cu_seqlens_q=cu_seqlens,
173
+ cu_seqlens_k=cu_seqlens,
174
+ max_seqlen_q=seq_len,
175
+ max_seqlen_k=seq_len,
176
+ do_causal=False,
177
+ scale=scale,
178
+ softcapping=1.0,
179
+ )
180
+
181
+ # Compute ground truth with GQA
182
+ expected = compute_attention_reference(query, key, value, scale, gqa_ratio=gqa_ratio)
183
+
184
+ # Check results
185
+ rtol, atol = get_tolerance(dtype, head_dim)
186
+ torch.testing.assert_close(out, expected, rtol=rtol, atol=atol)
187
+
188
+
189
+ @pytest.mark.parametrize("dtype", [torch.float32, torch.float16, torch.bfloat16])
190
+ @pytest.mark.parametrize("softcapping_config", [
191
+ # (softcapping_value, seq_lengths, head_dim)
192
+ (1.0, [32], 64), # No softcapping
193
+ (50.0, [32, 24], 64), # Regular softcapping
194
+ (10.0, [16], 128), # Strong softcapping
195
+ (1000.0, [16], 64), # Very weak softcapping
196
+ (30.0, [48], 96), # Medium softcapping
197
+ ])
198
+ def test_flash_attention_softcapping(dtype, softcapping_config):
199
+ """Test Flash Attention with various softcapping values."""
200
+ torch.manual_seed(42)
201
+
202
+ softcapping, seq_lengths, head_dim = softcapping_config
203
+ num_heads = 4
204
+
205
+ # Create cumulative sequence lengths
206
+ cu_seqlens = create_cu_seqlens(seq_lengths)
207
+ total_tokens = sum(seq_lengths)
208
+ max_seqlen = max(seq_lengths)
209
+
210
+ # Create input tensors
211
+ query = torch.randn(total_tokens, num_heads, head_dim, dtype=dtype, device="mps")
212
+ key = torch.randn(total_tokens, num_heads, head_dim, dtype=dtype, device="mps")
213
+ value = torch.randn(total_tokens, num_heads, head_dim, dtype=dtype, device="mps")
214
+
215
+ # Scale factor
216
+ scale = 1.0 / (head_dim ** 0.5)
217
+
218
+ # Call Flash Attention with softcapping
219
+ out = torch.empty_like(query)
220
+ metal_flash_sdpa.flash_attention_varlen(
221
+ out=out,
222
+ query=query,
223
+ key=key,
224
+ value=value,
225
+ cu_seqlens_q=cu_seqlens,
226
+ cu_seqlens_k=cu_seqlens,
227
+ max_seqlen_q=max_seqlen,
228
+ max_seqlen_k=max_seqlen,
229
+ do_causal=False,
230
+ scale=scale,
231
+ softcapping=softcapping,
232
+ )
233
+
234
+ # Compute ground truth with softcapping
235
+ expected = torch.zeros_like(query)
236
+
237
+ for start, end in zip(cu_seqlens[:-1], cu_seqlens[1:]):
238
+ if end > start:
239
+ q_seq = query[start:end]
240
+ k_seq = key[start:end]
241
+ v_seq = value[start:end]
242
+
243
+ expected_seq = compute_attention_reference(
244
+ q_seq, k_seq, v_seq, scale, softcapping=softcapping
245
+ )
246
+ expected[start:end] = expected_seq
247
+
248
+ # Check results (higher tolerance for softcapping)
249
+ # Note: Softcapping with strong values (< 50) has higher error due to
250
+ # the interaction between tanh transformation and exp2-based softmax
251
+ if dtype == torch.bfloat16:
252
+ if softcapping < 50:
253
+ rtol, atol = 1.5e-1, 1.5e-1 # Higher tolerance for strong softcapping
254
+ else:
255
+ rtol, atol = 3e-2, 3e-2
256
+ elif dtype == torch.float16:
257
+ if softcapping < 50:
258
+ rtol, atol = 1e-1, 1e-1
259
+ else:
260
+ rtol, atol = 2e-2, 2e-2
261
+ else:
262
+ if softcapping < 50:
263
+ rtol, atol = 1.5e-1, 1.5e-1 # Higher tolerance for strong softcapping with float32
264
+ else:
265
+ rtol, atol = 1e-2, 1e-2
266
+ torch.testing.assert_close(out, expected, rtol=rtol, atol=atol)
267
+
268
+
269
+ @pytest.mark.parametrize("large_seq_config", [
270
+ # (q_seq, k_seq, head_dim, dtype)
271
+ (32, 2048, 64, torch.float32),
272
+ (16, 1024, 96, torch.float16),
273
+ (64, 1536, 64, torch.bfloat16),
274
+ ])
275
+ def test_flash_attention_large_sequences(large_seq_config):
276
+ """Test Flash Attention with large k sequences (>= 1024)."""
277
+ torch.manual_seed(42)
278
+
279
+ q_seq, k_seq, head_dim, dtype = large_seq_config
280
+ num_heads = 4
281
+
282
+ # Create cumulative sequence lengths
283
+ cu_seqlens_q = create_cu_seqlens([q_seq])
284
+ cu_seqlens_k = create_cu_seqlens([k_seq])
285
+
286
+ # Create input tensors
287
+ query = torch.randn(q_seq, num_heads, head_dim, dtype=dtype, device="mps")
288
+ key = torch.randn(k_seq, num_heads, head_dim, dtype=dtype, device="mps")
289
+ value = torch.randn(k_seq, num_heads, head_dim, dtype=dtype, device="mps")
290
+
291
+ # Scale factor
292
+ scale = 1.0 / (head_dim ** 0.5)
293
+
294
+ # Call Flash Attention
295
+ out = torch.empty_like(query)
296
+ metal_flash_sdpa.flash_attention_varlen(
297
+ out=out,
298
+ query=query,
299
+ key=key,
300
+ value=value,
301
+ cu_seqlens_q=cu_seqlens_q,
302
+ cu_seqlens_k=cu_seqlens_k,
303
+ max_seqlen_q=q_seq,
304
+ max_seqlen_k=k_seq,
305
+ do_causal=False,
306
+ scale=scale,
307
+ softcapping=1.0,
308
+ )
309
+
310
+ # Compute ground truth
311
+ expected = compute_attention_reference(query, key, value, scale)
312
+
313
+ # Check results (higher tolerance for large sequences)
314
+ if dtype == torch.bfloat16:
315
+ rtol, atol = 3e-2, 3e-2
316
+ elif dtype == torch.float16:
317
+ rtol, atol = 5e-3, 5e-3
318
+ else:
319
+ rtol, atol = 2e-3, 2e-3
320
+ torch.testing.assert_close(out, expected, rtol=rtol, atol=atol)
321
+
322
+
323
+ def test_flash_attention_edge_cases():
324
+ """Test Flash Attention edge cases."""
325
+ torch.manual_seed(42)
326
+
327
+ # Test 1: Single token sequence
328
+ query = torch.randn(1, 1, 64, device="mps")
329
+ key = torch.randn(1, 1, 64, device="mps")
330
+ value = torch.randn(1, 1, 64, device="mps")
331
+ cu_seqlens = create_cu_seqlens([1])
332
+ out = torch.empty_like(query)
333
+
334
+ metal_flash_sdpa.flash_attention_varlen(
335
+ out=out,
336
+ query=query,
337
+ key=key,
338
+ value=value,
339
+ cu_seqlens_q=cu_seqlens,
340
+ cu_seqlens_k=cu_seqlens,
341
+ max_seqlen_q=1,
342
+ max_seqlen_k=1,
343
+ do_causal=False,
344
+ scale=0.125,
345
+ softcapping=1.0,
346
+ )
347
+
348
+ # With single token, output should equal value
349
+ torch.testing.assert_close(out, value, rtol=1e-5, atol=1e-5)
350
+
351
+ # Test 2: Empty sequence in batch
352
+ seq_lengths = [8, 0, 12] # Middle sequence is empty
353
+ cu_seqlens = create_cu_seqlens(seq_lengths)
354
+ total_tokens = sum(seq_lengths)
355
+
356
+ query = torch.randn(total_tokens, 4, 64, device="mps")
357
+ key = torch.randn(total_tokens, 4, 64, device="mps")
358
+ value = torch.randn(total_tokens, 4, 64, device="mps")
359
+ out = torch.empty_like(query)
360
+
361
+ # This should handle empty sequences gracefully
362
+ metal_flash_sdpa.flash_attention_varlen(
363
+ out=out,
364
+ query=query,
365
+ key=key,
366
+ value=value,
367
+ cu_seqlens_q=cu_seqlens,
368
+ cu_seqlens_k=cu_seqlens,
369
+ max_seqlen_q=max(seq_lengths) if seq_lengths else 0,
370
+ max_seqlen_k=max(seq_lengths) if seq_lengths else 0,
371
+ do_causal=False,
372
+ scale=0.125,
373
+ softcapping=1.0,
374
+ )
375
+
376
+
377
+ def test_flash_attention_unsupported_cases():
378
+ """Test that unsupported cases raise appropriate errors."""
379
+
380
+ # Test 1: Unsupported head dimension
381
+ query = torch.randn(16, 4, 48, device="mps") # head_dim = 48 (not supported)
382
+ key = torch.randn(16, 4, 48, device="mps")
383
+ value = torch.randn(16, 4, 48, device="mps")
384
+ cu_seqlens = create_cu_seqlens([16])
385
+ out = torch.empty_like(query)
386
+
387
+ with pytest.raises(RuntimeError, match="Head dimension .* is not supported"):
388
+ metal_flash_sdpa.flash_attention_varlen(
389
+ out=out,
390
+ query=query,
391
+ key=key,
392
+ value=value,
393
+ cu_seqlens_q=cu_seqlens,
394
+ cu_seqlens_k=cu_seqlens,
395
+ max_seqlen_q=16,
396
+ max_seqlen_k=16,
397
+ do_causal=False,
398
+ scale=0.144,
399
+ softcapping=1.0,
400
+ )
401
+
402
+ # Test 2: Wrong dtype for cu_seqlens (should be int32)
403
+ cu_seqlens_wrong = torch.tensor([0, 16], dtype=torch.int64, device="mps")
404
+ query = torch.randn(16, 4, 64, device="mps")
405
+ key = torch.randn(16, 4, 64, device="mps")
406
+ value = torch.randn(16, 4, 64, device="mps")
407
+
408
+ # This will silently fail (output will be unchanged)
409
+ out = torch.full_like(query, -999.0)
410
+ metal_flash_sdpa.flash_attention_varlen(
411
+ out=out,
412
+ query=query,
413
+ key=key,
414
+ value=value,
415
+ cu_seqlens_q=cu_seqlens_wrong,
416
+ cu_seqlens_k=cu_seqlens_wrong,
417
+ max_seqlen_q=16,
418
+ max_seqlen_k=16,
419
+ do_causal=False,
420
+ scale=0.125,
421
+ softcapping=1.0,
422
+ )
423
+
424
+ # Check that output wasn't modified (kernel didn't run)
425
+ assert (out == -999.0).all(), "cu_seqlens with wrong dtype should cause kernel to not run"
426
+
427
+
428
+ def test_flash_attn_varlen_func():
429
+ """Test the flash_attn_varlen_func compatibility function."""
430
+ torch.manual_seed(42)
431
+
432
+ # Test dimensions
433
+ seq_lengths = [8, 12]
434
+ num_heads = 4
435
+ head_dim = 64
436
+
437
+ # Create cumulative sequence lengths
438
+ cu_seqlens = create_cu_seqlens(seq_lengths)
439
+ total_tokens = sum(seq_lengths)
440
+ max_seqlen = max(seq_lengths)
441
+
442
+ # Create input tensors
443
+ q = torch.randn(total_tokens, num_heads, head_dim, device="mps")
444
+ k = torch.randn(total_tokens, num_heads, head_dim, device="mps")
445
+ v = torch.randn(total_tokens, num_heads, head_dim, device="mps")
446
+
447
+ # Call the compatibility function
448
+ out = metal_flash_sdpa.flash_attn_varlen_func(
449
+ q=q,
450
+ k=k,
451
+ v=v,
452
+ cu_seqlens_q=cu_seqlens,
453
+ cu_seqlens_k=cu_seqlens,
454
+ max_seqlen_q=max_seqlen,
455
+ max_seqlen_k=max_seqlen,
456
+ dropout_p=0.0,
457
+ softmax_scale=None, # Will use 1/sqrt(head_dim)
458
+ causal=False,
459
+ )
460
+
461
+ # Check that output has correct shape and is not zeros
462
+ assert out.shape == q.shape
463
+ assert out.abs().max().item() > 0
464
+
465
+ # Test with causal
466
+ out_causal = metal_flash_sdpa.flash_attn_varlen_func(
467
+ q=q,
468
+ k=k,
469
+ v=v,
470
+ cu_seqlens_q=cu_seqlens,
471
+ cu_seqlens_k=cu_seqlens,
472
+ max_seqlen_q=max_seqlen,
473
+ max_seqlen_k=max_seqlen,
474
+ dropout_p=0.0,
475
+ softmax_scale=0.125,
476
+ causal=True,
477
+ )
478
+
479
+ assert out_causal.shape == q.shape
480
+ assert out_causal.abs().max().item() > 0
torch-ext/metal_flash_sdpa/__init__.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from ._custom_ops import (
2
+ flash_attention_varlen,
3
+ flash_attn_varlen_func,
4
+ )
5
+ from ._ops import ops
6
+
7
+ __all__ = [
8
+ "flash_attention_varlen",
9
+ "flash_attn_varlen_func",
10
+ "ops",
11
+ ]
torch-ext/metal_flash_sdpa/_custom_ops.py ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List, Optional
2
+
3
+ import torch
4
+
5
+ from ._ops import ops
6
+
7
+
8
+ def flash_attention_varlen(
9
+ out: torch.Tensor,
10
+ query: torch.Tensor,
11
+ key: torch.Tensor,
12
+ value: torch.Tensor,
13
+ cu_seqlens_q: torch.Tensor,
14
+ cu_seqlens_k: torch.Tensor,
15
+ max_seqlen_q: int,
16
+ max_seqlen_k: int,
17
+ do_causal: bool = False,
18
+ scale: Optional[float] = None,
19
+ softcapping: float = 1.0,
20
+ ) -> None:
21
+ """
22
+ Flash Attention with variable-length sequences.
23
+
24
+ Args:
25
+ out: Output tensor of shape [total_q_tokens, num_heads, head_dim]
26
+ query: Query tensor of shape [total_q_tokens, num_heads, head_dim]
27
+ key: Key tensor of shape [total_k_tokens, num_heads_kv, head_dim]
28
+ value: Value tensor of shape [total_k_tokens, num_heads_kv, head_dim]
29
+ cu_seqlens_q: Cumulative sequence lengths for queries, shape [batch_size + 1], dtype must be torch.int32
30
+ cu_seqlens_k: Cumulative sequence lengths for keys, shape [batch_size + 1], dtype must be torch.int32
31
+ max_seqlen_q: Maximum sequence length in the query batch
32
+ max_seqlen_k: Maximum sequence length in the key batch
33
+ do_causal: Whether to apply causal masking
34
+ scale: Attention scale factor (default: 1/sqrt(head_dim))
35
+ softcapping: Softcapping value (default: 1.0, must be 1.0 for this implementation)
36
+
37
+ Note:
38
+ - cu_seqlens_q and cu_seqlens_k must have dtype torch.int32 for Metal compatibility
39
+ - Supported head dimensions: 32, 64, 72, 80, 96, 128
40
+ - Masks are not supported
41
+ """
42
+ if scale is None:
43
+ scale = query.shape[-1] ** -0.5
44
+
45
+ ops.flash_attention_varlen(
46
+ out,
47
+ query,
48
+ key,
49
+ value,
50
+ cu_seqlens_q,
51
+ cu_seqlens_k,
52
+ max_seqlen_q,
53
+ max_seqlen_k,
54
+ do_causal,
55
+ scale,
56
+ softcapping,
57
+ )
58
+
59
+ def flash_attn_varlen_func(
60
+ q: torch.Tensor,
61
+ k: torch.Tensor,
62
+ v: torch.Tensor,
63
+ cu_seqlens_q: torch.Tensor,
64
+ cu_seqlens_k: torch.Tensor,
65
+ max_seqlen_q: int,
66
+ max_seqlen_k: int,
67
+ dropout_p: float = 0.0,
68
+ softmax_scale: Optional[float] = None,
69
+ causal: bool = False,
70
+ window_size: tuple = (-1, -1),
71
+ alibi_slopes: Optional[torch.Tensor] = None,
72
+ deterministic: bool = False,
73
+ return_attn_probs: bool = False,
74
+ ) -> torch.Tensor:
75
+ """
76
+ Flash Attention function with API compatible with the original Flash Attention.
77
+
78
+ Note: This implementation does not support:
79
+ - dropout
80
+ - window attention
81
+ - alibi slopes
82
+ - returning attention probabilities
83
+ """
84
+ if dropout_p > 0:
85
+ raise NotImplementedError("Dropout is not supported in this implementation")
86
+ if window_size != (-1, -1):
87
+ raise NotImplementedError("Window attention is not supported")
88
+ if alibi_slopes is not None:
89
+ raise NotImplementedError("ALiBi is not supported")
90
+ if return_attn_probs:
91
+ raise NotImplementedError("Returning attention probabilities is not supported")
92
+
93
+ # Create output tensor
94
+ out = torch.empty_like(q)
95
+
96
+ # Call the kernel
97
+ flash_attention_varlen(
98
+ out=out,
99
+ query=q,
100
+ key=k,
101
+ value=v,
102
+ cu_seqlens_q=cu_seqlens_q,
103
+ cu_seqlens_k=cu_seqlens_k,
104
+ max_seqlen_q=max_seqlen_q,
105
+ max_seqlen_k=max_seqlen_k,
106
+ do_causal=causal,
107
+ scale=softmax_scale,
108
+ softcapping=1.0,
109
+ )
110
+
111
+ return out
112
+
113
+
114
+ __all__ = [
115
+ "flash_attention_varlen",
116
+ "flash_attn_varlen_func",
117
+ ]
torch-ext/torch_binding.cpp ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #include <torch/library.h>
2
+
3
+ #include "registration.h"
4
+ #include "torch_binding.h"
5
+
6
+ TORCH_LIBRARY_EXPAND(TORCH_EXTENSION_NAME, ops) {
7
+ ops.def("flash_attention_varlen(Tensor! out, Tensor query, Tensor key, Tensor value, Tensor cu_seqlens_q, Tensor cu_seqlens_k, int max_seqlen_q, int max_seqlen_k, bool do_causal, float scale, float softcapping) -> ()");
8
+ ops.impl("flash_attention_varlen", torch::kMPS, flash_attention_varlen);
9
+ }
10
+
11
+ REGISTER_EXTENSION(TORCH_EXTENSION_NAME)
torch-ext/torch_binding.h ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #pragma once
2
+
3
+ #include <torch/torch.h>
4
+
5
+ void flash_attention_varlen(
6
+ torch::Tensor &out,
7
+ torch::Tensor &query,
8
+ torch::Tensor &key,
9
+ torch::Tensor &value,
10
+ torch::Tensor &cu_seqlens_q,
11
+ torch::Tensor &cu_seqlens_k,
12
+ int64_t max_seqlen_q,
13
+ int64_t max_seqlen_k,
14
+ bool do_causal,
15
+ double scale,
16
+ double softcapping);