1use metal::MTLSize;
18
19use crate::buffer::MlxBuffer;
20use crate::device::MlxDevice;
21use crate::encoder::{as_bytes, CapturedOpKind, CommandEncoder, KernelArg};
22use crate::error::{MlxError, Result};
23use crate::kernel_registry::KernelRegistry;
24use crate::DType;
25
26pub static TREE_ATTENTION_SHADER_SOURCE: &str =
28 include_str!("../shaders/tree_attention.metal");
29
30pub fn register(registry: &mut KernelRegistry) {
40 registry.register_source("tree_attention_dk128", TREE_ATTENTION_SHADER_SOURCE);
41 registry.register_source("tree_attention_dk256", TREE_ATTENTION_SHADER_SOURCE);
42 registry.register_source("tree_attention_dk512", TREE_ATTENTION_SHADER_SOURCE);
43 registry.register_source("tree_attention_f16kv_dk128", TREE_ATTENTION_SHADER_SOURCE);
44 registry.register_source("tree_attention_f16kv_dk256", TREE_ATTENTION_SHADER_SOURCE);
45 registry.register_source("tree_attention_f16kv_dk512", TREE_ATTENTION_SHADER_SOURCE);
46}
47
48pub const TREE_MASK_MASKED: f32 = -65504.0;
53
54pub const TREE_MASK_ATTENDED: f32 = 0.0;
56
57#[derive(Debug, Clone, Copy)]
59pub struct TreeAttentionParams {
60 pub num_heads: u32,
61 pub num_kv_heads: u32,
62 pub head_dim: u32,
63 pub kv_seq_len: u32,
64 pub kv_capacity: u32,
65 pub scale: f32,
66 pub q_seq_len: u32,
68 pub mask_stride: u32,
72}
73
74#[repr(C)]
76#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
77struct TreeAttentionParamsGpu {
78 n_heads: u32,
79 n_kv_heads: u32,
80 head_dim: u32,
81 kv_seq_len: u32,
82 kv_capacity: u32,
83 scale: f32,
84 nwg: u32,
85 q_l: u32,
86 mask_stride: u32,
87}
88
89#[repr(C)]
90#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
91struct FlashAttnVecReduceParamsGpu {
92 nrows: u32,
93}
94
95const NWG: u32 = 32;
98
99#[allow(clippy::too_many_arguments)]
107fn validate_buffers(
108 q: &MlxBuffer,
109 k: &MlxBuffer,
110 v: &MlxBuffer,
111 tree_mask: &MlxBuffer,
112 output: &MlxBuffer,
113 tmp: &MlxBuffer,
114 params: &TreeAttentionParams,
115) -> Result<()> {
116 if q.dtype() != DType::F32 {
118 return Err(MlxError::InvalidArgument(format!(
119 "tree_attention: q dtype must be F32, got {:?}",
120 q.dtype()
121 )));
122 }
123 if k.dtype() != v.dtype() {
124 return Err(MlxError::InvalidArgument(format!(
125 "tree_attention: k.dtype ({:?}) must match v.dtype ({:?})",
126 k.dtype(),
127 v.dtype()
128 )));
129 }
130 if !matches!(k.dtype(), DType::F32 | DType::F16) {
131 return Err(MlxError::InvalidArgument(format!(
132 "tree_attention: k/v dtype must be F32 or F16, got {:?}",
133 k.dtype()
134 )));
135 }
136 if tree_mask.dtype() != DType::F32 {
137 return Err(MlxError::InvalidArgument(format!(
138 "tree_attention: tree_mask dtype must be F32, got {:?}",
139 tree_mask.dtype()
140 )));
141 }
142 if output.dtype() != DType::F32 {
143 return Err(MlxError::InvalidArgument(format!(
144 "tree_attention: output dtype must be F32, got {:?}",
145 output.dtype()
146 )));
147 }
148 if tmp.dtype() != DType::F32 {
149 return Err(MlxError::InvalidArgument(format!(
150 "tree_attention: tmp dtype must be F32, got {:?}",
151 tmp.dtype()
152 )));
153 }
154
155 let head_dim = params.head_dim as usize;
157 let num_heads = params.num_heads as usize;
158 let num_kv_heads = params.num_kv_heads as usize;
159 let q_seq_len = params.q_seq_len as usize;
160 let kv_capacity = params.kv_capacity as usize;
161 let mask_stride = params.mask_stride as usize;
162 let kv_dtype_bytes: usize = match k.dtype() {
163 DType::F32 => 4,
164 DType::F16 => 2,
165 _ => unreachable!("validated above"),
166 };
167
168 let mul = |a: usize, b: usize, ctx: &str| -> Result<usize> {
174 a.checked_mul(b).ok_or_else(|| {
175 MlxError::InvalidArgument(format!(
176 "tree_attention: {} multiplication overflows usize",
177 ctx
178 ))
179 })
180 };
181 let req_q = {
182 let a = mul(num_heads, q_seq_len, "num_heads*q_seq_len")?;
183 let b = mul(a, head_dim, "num_heads*q_seq_len*head_dim")?;
184 mul(b, 4, "req_q")?
185 };
186 let req_kv = {
187 let a = mul(num_kv_heads, kv_capacity, "num_kv_heads*kv_capacity")?;
188 let b = mul(a, head_dim, "num_kv_heads*kv_capacity*head_dim")?;
189 mul(b, kv_dtype_bytes, "req_kv")?
190 };
191 let req_mask = {
192 let a = mul(q_seq_len, mask_stride, "q_seq_len*mask_stride")?;
193 mul(a, 4, "req_mask")?
194 };
195 let req_output = req_q; let req_tmp =
197 tmp_buffer_bytes(params.num_heads, params.head_dim, params.q_seq_len);
198
199 if q.byte_len() < req_q {
200 return Err(MlxError::InvalidArgument(format!(
201 "tree_attention: q buffer too small: have {} bytes, need >= {}",
202 q.byte_len(),
203 req_q
204 )));
205 }
206 if k.byte_len() < req_kv {
207 return Err(MlxError::InvalidArgument(format!(
208 "tree_attention: k buffer too small: have {} bytes, need >= {}",
209 k.byte_len(),
210 req_kv
211 )));
212 }
213 if v.byte_len() < req_kv {
214 return Err(MlxError::InvalidArgument(format!(
215 "tree_attention: v buffer too small: have {} bytes, need >= {}",
216 v.byte_len(),
217 req_kv
218 )));
219 }
220 if tree_mask.byte_len() < req_mask {
221 return Err(MlxError::InvalidArgument(format!(
222 "tree_attention: tree_mask buffer too small: have {} bytes, need >= {}",
223 tree_mask.byte_len(),
224 req_mask
225 )));
226 }
227 if output.byte_len() < req_output {
228 return Err(MlxError::InvalidArgument(format!(
229 "tree_attention: output buffer too small: have {} bytes, need >= {}",
230 output.byte_len(),
231 req_output
232 )));
233 }
234 if tmp.byte_len() < req_tmp {
235 return Err(MlxError::InvalidArgument(format!(
236 "tree_attention: tmp buffer too small: have {} bytes, need >= {}",
237 tmp.byte_len(),
238 req_tmp
239 )));
240 }
241
242 Ok(())
243}
244
245fn validate_params(params: &TreeAttentionParams) -> Result<()> {
246 if params.head_dim != 128 && params.head_dim != 256 && params.head_dim != 512 {
249 return Err(MlxError::InvalidArgument(format!(
250 "tree_attention: head_dim must be 128, 256, or 512, got {}",
251 params.head_dim
252 )));
253 }
254 if params.num_heads == 0 || params.num_kv_heads == 0 {
255 return Err(MlxError::InvalidArgument(
256 "tree_attention: num_heads and num_kv_heads must be > 0".into(),
257 ));
258 }
259 if params.num_heads % params.num_kv_heads != 0 {
260 return Err(MlxError::InvalidArgument(format!(
261 "tree_attention: num_heads ({}) must be divisible by num_kv_heads ({})",
262 params.num_heads, params.num_kv_heads
263 )));
264 }
265 if params.kv_seq_len == 0 {
266 return Err(MlxError::InvalidArgument(
267 "tree_attention: kv_seq_len must be > 0".into(),
268 ));
269 }
270 if params.kv_capacity < params.kv_seq_len {
271 return Err(MlxError::InvalidArgument(format!(
272 "tree_attention: kv_capacity ({}) must be >= kv_seq_len ({})",
273 params.kv_capacity, params.kv_seq_len
274 )));
275 }
276 if params.q_seq_len == 0 {
277 return Err(MlxError::InvalidArgument(
278 "tree_attention: q_seq_len must be > 0".into(),
279 ));
280 }
281 if params.mask_stride < params.kv_seq_len {
282 return Err(MlxError::InvalidArgument(format!(
283 "tree_attention: mask_stride ({}) must be >= kv_seq_len ({})",
284 params.mask_stride, params.kv_seq_len
285 )));
286 }
287 Ok(())
288}
289
290pub fn tree_attention(
313 encoder: &mut CommandEncoder,
314 registry: &mut KernelRegistry,
315 device: &MlxDevice,
316 q: &MlxBuffer,
317 k: &MlxBuffer,
318 v: &MlxBuffer,
319 tree_mask: &MlxBuffer,
320 output: &MlxBuffer,
321 tmp: &MlxBuffer,
322 params: &TreeAttentionParams,
323) -> Result<()> {
324 validate_params(params)?;
325 validate_buffers(q, k, v, tree_mask, output, tmp, params)?;
326
327 let head_dim = params.head_dim;
328 let nwg = NWG;
329
330 let gpu_params = TreeAttentionParamsGpu {
331 n_heads: params.num_heads,
332 n_kv_heads: params.num_kv_heads,
333 head_dim: params.head_dim,
334 kv_seq_len: params.kv_seq_len,
335 kv_capacity: params.kv_capacity,
336 scale: params.scale,
337 nwg,
338 q_l: params.q_seq_len,
339 mask_stride: params.mask_stride,
340 };
341
342 let kv_is_f16 = k.dtype() == DType::F16;
347 let kernel_name = match (head_dim, kv_is_f16) {
348 (128, false) => "tree_attention_dk128",
349 (256, false) => "tree_attention_dk256",
350 (512, false) => "tree_attention_dk512",
351 (128, true) => "tree_attention_f16kv_dk128",
352 (256, true) => "tree_attention_f16kv_dk256",
353 (512, true) => "tree_attention_f16kv_dk512",
354 _ => unreachable!(),
355 };
356 let pipeline = registry.get_pipeline(kernel_name, device.metal_device())?;
357
358 let pk = pad2(head_dim as usize, 128);
360 let pv = pad2(head_dim as usize, 128);
361 let sh = 4 * 32;
362 let shmem_halfs = pk + sh + 2 * pv;
363 let shmem_bytes = shmem_halfs * 2;
364
365 encoder.set_op_kind(CapturedOpKind::Sdpa);
366
367 let threadgroups = MTLSize::new(
368 params.q_seq_len as u64,
369 params.num_heads as u64,
370 nwg as u64,
371 );
372 let threadgroup_size = MTLSize::new(32, 1, 1);
373
374 encoder.encode_threadgroups_with_args_and_shared(
375 pipeline,
376 &[
377 (0, KernelArg::Bytes(as_bytes(&gpu_params))),
378 (1, KernelArg::Buffer(q)),
379 (2, KernelArg::Buffer(k)),
380 (3, KernelArg::Buffer(v)),
381 (4, KernelArg::Buffer(tmp)),
382 (5, KernelArg::Buffer(tree_mask)),
383 ],
384 &[(0, shmem_bytes as u64)],
385 threadgroups,
386 threadgroup_size,
387 );
388
389 if nwg > 1 {
391 encoder.memory_barrier();
392 let total_rows = params.num_heads * params.q_seq_len;
393 let reduce_params = FlashAttnVecReduceParamsGpu { nrows: total_rows };
394
395 let reduce_kernel = match head_dim {
396 128 => "flash_attn_vec_reduce_dk128",
397 256 => "flash_attn_vec_reduce_dk256",
398 512 => "flash_attn_vec_reduce_dk512",
399 _ => unreachable!(),
400 };
401 let reduce_pipeline =
402 registry.get_pipeline(reduce_kernel, device.metal_device())?;
403
404 let reduce_tg = MTLSize::new(total_rows as u64, 1, 1);
405 let reduce_tg_size = MTLSize::new(32 * nwg as u64, 1, 1);
406
407 {
408 let read_ranges = vec![{
409 let s = tmp.contents_ptr() as usize;
410 (s, s + tmp.byte_len())
411 }];
412 let write_ranges = vec![{
413 let s = output.contents_ptr() as usize;
414 (s, s + output.byte_len())
415 }];
416 encoder.set_pending_buffer_ranges(read_ranges, write_ranges);
417 }
418
419 encoder.encode_threadgroups_with_args(
420 reduce_pipeline,
421 &[
422 (0, KernelArg::Bytes(as_bytes(&reduce_params))),
423 (1, KernelArg::Buffer(tmp)),
424 (2, KernelArg::Buffer(output)),
425 (3, KernelArg::Bytes(as_bytes(&nwg))),
426 ],
427 reduce_tg,
428 reduce_tg_size,
429 );
430 }
431
432 Ok(())
433}
434
435#[allow(non_snake_case)]
445pub fn tmp_buffer_bytes(num_heads: u32, head_dim: u32, q_seq_len: u32) -> usize {
446 let nrows = (num_heads as usize).saturating_mul(q_seq_len as usize);
447 let nwg = NWG as usize;
448 let dv = head_dim as usize;
449 let dv_plus_2 = dv.saturating_add(2);
450 nrows
451 .saturating_mul(nwg)
452 .saturating_mul(dv_plus_2)
453 .saturating_mul(std::mem::size_of::<f32>())
454}
455
456fn pad2(x: usize, n: usize) -> usize {
457 (x + n - 1) & !(n - 1)
458}
459
460#[cfg(test)]
461#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
462mod tests {
463 use super::*;
464
465 #[test]
466 fn test_validate_params_ok() {
467 let p = TreeAttentionParams {
468 num_heads: 16,
469 num_kv_heads: 8,
470 head_dim: 256,
471 kv_seq_len: 100,
472 kv_capacity: 1024,
473 scale: 1.0,
474 q_seq_len: 1,
475 mask_stride: 100,
476 };
477 assert!(validate_params(&p).is_ok());
478 }
479
480 #[test]
481 fn test_validate_params_mask_stride_too_small() {
482 let p = TreeAttentionParams {
483 num_heads: 16,
484 num_kv_heads: 8,
485 head_dim: 256,
486 kv_seq_len: 100,
487 kv_capacity: 1024,
488 scale: 1.0,
489 q_seq_len: 1,
490 mask_stride: 50,
491 };
492 let err = validate_params(&p).unwrap_err().to_string();
493 assert!(
494 err.contains("mask_stride"),
495 "expected mask_stride error, got: {err}"
496 );
497 }
498
499 #[test]
500 fn test_validate_params_bad_head_dim() {
501 let p = TreeAttentionParams {
503 num_heads: 16,
504 num_kv_heads: 8,
505 head_dim: 64,
506 kv_seq_len: 100,
507 kv_capacity: 1024,
508 scale: 1.0,
509 q_seq_len: 1,
510 mask_stride: 100,
511 };
512 assert!(validate_params(&p).is_err());
513 }
514
515 #[test]
516 fn adr_037_e4b6_validate_params_accepts_dk128_2026_05_22() {
517 let p = TreeAttentionParams {
519 num_heads: 40,
520 num_kv_heads: 8,
521 head_dim: 128,
522 kv_seq_len: 64,
523 kv_capacity: 256,
524 scale: 1.0,
525 q_seq_len: 1,
526 mask_stride: 64,
527 };
528 validate_params(&p).expect("dk128 should validate");
529 }
530
531 #[test]
532 fn test_validate_params_q_seq_len_zero() {
533 let p = TreeAttentionParams {
534 num_heads: 16,
535 num_kv_heads: 8,
536 head_dim: 256,
537 kv_seq_len: 100,
538 kv_capacity: 1024,
539 scale: 1.0,
540 q_seq_len: 0,
541 mask_stride: 100,
542 };
543 let err = validate_params(&p).unwrap_err().to_string();
544 assert!(err.contains("q_seq_len"), "got: {err}");
545 }
546
547 #[test]
548 fn test_gpu_params_layout() {
549 assert_eq!(std::mem::size_of::<TreeAttentionParamsGpu>(), 36);
551 }
552
553 #[test]
554 fn test_tmp_buffer_bytes() {
555 let bytes = tmp_buffer_bytes(16, 256, 1);
558 assert_eq!(bytes, 16 * 32 * 258 * 4);
559 }
560}