ferrotensor_graph/flash_attention.rs
1//! Flash Attention - Memory-Efficient Attention Mechanism
2//!
3//! Implements FlashAttention-2 algorithm for O(N) memory instead of O(N²).
4//! Uses tiled matrix multiplication with online softmax normalization.
5
6use ferrotensor_backend::{AttentionOptions, ComputeEngine, UnifiedTensor, WgpuContext};
7use std::sync::Arc;
8
9/// Flash Attention layer with tiled computation and online softmax.
10///
11/// Memory efficient: O(N) instead of O(N²) for standard attention.
12/// Speed: 2-4x faster than naive implementation.
13///
14/// # Example
15/// ```no_run
16/// # use ferrotensor_backend::{ComputeEngine, UnifiedTensor, WgpuContext};
17/// # use ferrotensor_graph::flash_attention::{FlashAttention};
18/// let ctx = pollster::block_on(WgpuContext::new());
19/// let engine = ComputeEngine::new(&ctx);
20/// // [batch, seq_len, hidden]
21/// let q = UnifiedTensor::zeros(&ctx, &[1, 128, 768]);
22/// let k = UnifiedTensor::zeros(&ctx, &[1, 128, 768]);
23/// let v = UnifiedTensor::zeros(&ctx, &[1, 128, 768]);
24/// let flash_attn = FlashAttention::new(768, 12, true);
25/// let output = flash_attn.forward(&q, &k, &v, &ctx, &engine);
26/// ```
27pub struct FlashAttention {
28 dim: usize,
29 num_heads: usize,
30 head_dim: usize,
31 causal: bool,
32 #[allow(dead_code)]
33 scale: f32,
34
35 // Optional: Use optimized multi-head kernel
36 #[allow(dead_code)]
37 use_multi_head_kernel: bool,
38}
39
40/// KV-Cache for efficient autoregressive generation
41pub struct KVCache {
42 /// Cached keys for each layer
43 pub k_cache: Vec<Arc<UnifiedTensor>>,
44 /// Cached values for each layer
45 pub v_cache: Vec<Arc<UnifiedTensor>>,
46 /// Maximum sequence length
47 pub max_seq_len: usize,
48 /// Current position in cache
49 pub current_pos: usize,
50}
51
52impl FlashAttention {
53 /// Create a new FlashAttention layer.
54 ///
55 /// # Arguments
56 /// * `dim` - Total dimension (will be split across heads)
57 /// * `num_heads` - Number of attention heads
58 /// * `causal` - Whether to apply causal masking (for autoregressive models)
59 pub fn new(dim: usize, num_heads: usize, causal: bool) -> Self {
60 let head_dim = dim / num_heads;
61 let scale = 1.0 / (head_dim as f32).sqrt();
62 Self {
63 dim,
64 num_heads,
65 head_dim,
66 causal,
67 scale,
68 use_multi_head_kernel: num_heads > 1,
69 }
70 }
71
72 /// `[seq, dim]`, `[batch, seq, dim]` or `[batch, seq, heads, head_dim]`
73 /// viewed as `[batch, seq, num_heads, head_dim]`.
74 fn heads_view(&self, t: &UnifiedTensor) -> UnifiedTensor {
75 let (batch, seq) = match t.shape.len() {
76 2 => (1, t.shape[0]),
77 3 | 4 => (t.shape[0], t.shape[1]),
78 n => panic!("FlashAttention expects a 2-D, 3-D or 4-D tensor, got {n}-D"),
79 };
80 assert_eq!(
81 t.shape.iter().product::<usize>(),
82 batch * seq * self.dim,
83 "tensor {:?} does not have dim {} per token",
84 t.shape,
85 self.dim
86 );
87 t.reshaped(&[batch, seq, self.num_heads, self.head_dim])
88 }
89
90 /// Forward pass using Flash Attention algorithm.
91 ///
92 /// # Arguments
93 /// * `q` - Query tensor `[batch, seq_q, dim]`, `[seq_q, dim]` or
94 /// `[batch, seq_q, num_heads, head_dim]`
95 /// * `k` - Key tensor with the same layout and `seq_k` tokens
96 /// * `v` - Value tensor with the same layout as `k`
97 ///
98 /// # Returns
99 /// Output tensor with the same shape as `q`.
100 pub fn forward(
101 &self,
102 q: &UnifiedTensor,
103 k: &UnifiedTensor,
104 v: &UnifiedTensor,
105 ctx: &WgpuContext,
106 engine: &ComputeEngine,
107 ) -> UnifiedTensor {
108 let output = UnifiedTensor::empty(ctx, &q.shape);
109 engine.flash_attention_v2(
110 ctx,
111 &self.heads_view(q),
112 &self.heads_view(k),
113 &self.heads_view(v),
114 &self.heads_view(&output),
115 self.causal,
116 0.0,
117 0,
118 None,
119 );
120 output
121 }
122
123 /// Memory for one head's attention scores: the full `seq x seq` matrix
124 /// that standard attention stores versus the `seq x head_dim` accumulator
125 /// Flash Attention keeps instead (f32, per head, scores only).
126 pub fn memory_savings(&self, seq_len: usize) -> String {
127 let standard_memory = seq_len * seq_len * 4;
128 let flash_memory = seq_len * self.head_dim * 4;
129 let savings = standard_memory as f32 / flash_memory as f32;
130 format!(
131 "{:.1}x less memory ({} vs {} per head for seq={})",
132 savings,
133 format_bytes(flash_memory),
134 format_bytes(standard_memory),
135 seq_len
136 )
137 }
138
139 /// Backward pass for Flash Attention.
140 ///
141 /// Computes gradients dQ, dK, dV using recomputation to avoid
142 /// storing the full attention matrix.
143 ///
144 /// # Arguments
145 /// * `q` - Query tensor from forward pass
146 /// * `k` - Key tensor from forward pass
147 /// * `v` - Value tensor from forward pass
148 /// * `output` - Output from forward pass
149 /// * `grad_output` - Gradient of the output
150 ///
151 /// # Returns
152 /// FlashAttentionGrads containing dQ, dK, dV
153 #[allow(clippy::too_many_arguments)]
154 pub fn backward(
155 &self,
156 q: &UnifiedTensor,
157 k: &UnifiedTensor,
158 v: &UnifiedTensor,
159 output: &UnifiedTensor,
160 grad_output: &UnifiedTensor,
161 ctx: &WgpuContext,
162 engine: &ComputeEngine,
163 ) -> FlashAttentionGrads {
164 let grad_q = UnifiedTensor::empty(ctx, &q.shape);
165 let grad_k = UnifiedTensor::empty(ctx, &k.shape);
166 let grad_v = UnifiedTensor::empty(ctx, &v.shape);
167
168 engine.flash_attention_v2_backward(
169 ctx,
170 &self.heads_view(q),
171 &self.heads_view(k),
172 &self.heads_view(v),
173 &self.heads_view(output),
174 &self.heads_view(grad_output),
175 &self.heads_view(&grad_q),
176 &self.heads_view(&grad_k),
177 &self.heads_view(&grad_v),
178 self.causal,
179 );
180
181 FlashAttentionGrads {
182 grad_q,
183 grad_k,
184 grad_v,
185 }
186 }
187}
188
189/// Gradients from Flash Attention backward pass.
190#[derive(Debug)]
191pub struct FlashAttentionGrads {
192 /// Gradient with respect to queries
193 pub grad_q: UnifiedTensor,
194 /// Gradient with respect to keys
195 pub grad_k: UnifiedTensor,
196 /// Gradient with respect to values
197 pub grad_v: UnifiedTensor,
198}
199
200impl std::fmt::Debug for FlashAttention {
201 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
202 f.debug_struct("FlashAttention")
203 .field("dim", &self.dim)
204 .field("num_heads", &self.num_heads)
205 .field("head_dim", &self.head_dim)
206 .field("causal", &self.causal)
207 .finish()
208 }
209}
210
211/// Format a byte count with a binary unit (B, KiB, MiB, GiB).
212fn format_bytes(bytes: usize) -> String {
213 const UNITS: [&str; 4] = ["B", "KiB", "MiB", "GiB"];
214 let mut value = bytes as f64;
215 let mut unit = 0;
216 while value >= 1024.0 && unit < UNITS.len() - 1 {
217 value /= 1024.0;
218 unit += 1;
219 }
220 if unit == 0 {
221 format!("{bytes} B")
222 } else {
223 format!("{value:.1} {}", UNITS[unit])
224 }
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230
231 #[test]
232 fn test_memory_savings() {
233 let attn = FlashAttention::new(512, 8, true);
234 let savings = attn.memory_savings(2048);
235 assert!(savings.contains("x less memory"));
236 // head_dim 64: 512*64*4 = 128 KiB vs 512*512*4 = 1 MiB.
237 let attn = FlashAttention::new(512, 8, true);
238 assert_eq!(
239 attn.memory_savings(512),
240 "8.0x less memory (128.0 KiB vs 1.0 MiB per head for seq=512)"
241 );
242 }
243
244 #[test]
245 fn test_format_bytes() {
246 assert_eq!(format_bytes(0), "0 B");
247 assert_eq!(format_bytes(1023), "1023 B");
248 assert_eq!(format_bytes(1536), "1.5 KiB");
249 assert_eq!(format_bytes(3 * 1024 * 1024 * 1024), "3.0 GiB");
250 }
251
252 #[test]
253 fn test_creation() {
254 let attn = FlashAttention::new(768, 12, true);
255 assert_eq!(attn.dim, 768);
256 assert_eq!(attn.num_heads, 12);
257 assert_eq!(attn.head_dim, 64);
258 assert!(attn.causal);
259 }
260}
261
262// ============================================================================
263// GROUPED QUERY ATTENTION (GQA) - Used by Llama 3, Mistral, etc.
264// ============================================================================
265
266/// Grouped Query Attention (GQA) - Efficient attention with fewer KV heads
267///
268/// GQA uses fewer key-value heads than query heads, reducing memory and
269/// computation while maintaining quality. This is the attention mechanism
270/// used by Llama 3, Mistral, and many modern LLMs.
271///
272/// # Memory Savings
273/// - Standard MHA: `num_heads` K and V projections
274/// - GQA: `num_kv_heads` K and V projections (typically 2-8x fewer)
275///
276/// # Example
277/// ```no_run
278/// # use ferrotensor_backend::{ComputeEngine, UnifiedTensor, WgpuContext};
279/// # use ferrotensor_graph::flash_attention::{GroupedQueryAttention};
280/// let ctx = pollster::block_on(WgpuContext::new());
281/// let engine = ComputeEngine::new(&ctx);
282/// // [batch, seq_len, hidden]
283/// let q = UnifiedTensor::zeros(&ctx, &[1, 128, 4096]);
284/// let k = UnifiedTensor::zeros(&ctx, &[1, 128, 1024]);
285/// let v = UnifiedTensor::zeros(&ctx, &[1, 128, 1024]);
286/// // Llama 3 8B: 32 query heads share 8 KV heads (no dropout, no mask)
287/// let gqa = GroupedQueryAttention::new(4096, 32, 8, true, 0.0);
288/// let output = gqa.forward(&q, &k, &v, None, &ctx, &engine);
289/// ```
290pub struct GroupedQueryAttention {
291 /// Total hidden dimension
292 pub dim: usize,
293 /// Number of query heads
294 pub num_heads: usize,
295 /// Number of key-value heads (fewer than query heads)
296 pub num_kv_heads: usize,
297 /// Dimension per head
298 pub head_dim: usize,
299 /// Number of query heads per KV head
300 pub num_groups: usize,
301 /// Whether to use causal masking
302 pub causal: bool,
303 /// Scaling factor for attention scores
304 #[allow(dead_code)]
305 scale: f32,
306 /// Dropout probability (0.0 = no dropout)
307 pub dropout_prob: f32,
308}
309
310impl GroupedQueryAttention {
311 /// Create a new GQA layer.
312 ///
313 /// # Arguments
314 /// * `dim` - Total hidden dimension
315 /// * `num_heads` - Number of query heads
316 /// * `num_kv_heads` - Number of key-value heads (must divide num_heads evenly)
317 /// * `causal` - Whether to apply causal masking
318 /// * `dropout_prob` - Dropout probability
319 ///
320 /// # Panics
321 /// Panics if `num_heads` is not divisible by `num_kv_heads`
322 pub fn new(
323 dim: usize,
324 num_heads: usize,
325 num_kv_heads: usize,
326 causal: bool,
327 dropout_prob: f32,
328 ) -> Self {
329 assert!(
330 num_heads.is_multiple_of(num_kv_heads),
331 "num_heads ({}) must be divisible by num_kv_heads ({})",
332 num_heads,
333 num_kv_heads
334 );
335
336 let head_dim = dim / num_heads;
337 let num_groups = num_heads / num_kv_heads;
338 let scale = 1.0 / (head_dim as f32).sqrt();
339
340 Self {
341 dim,
342 num_heads,
343 num_kv_heads,
344 head_dim,
345 num_groups,
346 causal,
347 scale,
348 dropout_prob,
349 }
350 }
351
352 /// Forward pass with grouped query attention.
353 ///
354 /// # Arguments
355 /// * `q` - Query tensor [batch, seq_q, num_heads, head_dim]
356 /// * `k` - Key tensor [batch, seq_k, num_kv_heads, head_dim]
357 /// * `v` - Value tensor [batch, seq_k, num_kv_heads, head_dim]
358 ///
359 /// # Returns
360 /// Output tensor [batch, seq_q, num_heads, head_dim]
361 pub fn forward(
362 &self,
363 q: &UnifiedTensor,
364 k: &UnifiedTensor,
365 v: &UnifiedTensor,
366 mask: Option<&UnifiedTensor>,
367 ctx: &WgpuContext,
368 engine: &ComputeEngine,
369 ) -> UnifiedTensor {
370 self.forward_at(q, k, v, mask, 0, ctx, engine)
371 }
372
373 /// Like [`forward`](Self::forward), with the first query at absolute
374 /// position `pos`: query `i` attends to keys `0..=pos + i` when causal.
375 /// Used to decode against a KV cache, where `k`/`v` hold the cache.
376 #[allow(clippy::too_many_arguments)]
377 pub fn forward_at(
378 &self,
379 q: &UnifiedTensor,
380 k: &UnifiedTensor,
381 v: &UnifiedTensor,
382 mask: Option<&UnifiedTensor>,
383 pos: usize,
384 ctx: &WgpuContext,
385 engine: &ComputeEngine,
386 ) -> UnifiedTensor {
387 // Validation: Expect 4D tensors [batch, seq, heads, dim]
388 if q.shape.len() != 4 || k.shape.len() != 4 || v.shape.len() != 4 {
389 panic!("GroupedQueryAttention expects 4D tensors (batch, seq, heads, dim)");
390 }
391
392 let batch_size = q.shape[0];
393 let seq_q = q.shape[1];
394
395 let output = UnifiedTensor::empty(ctx, &[batch_size, seq_q, self.num_heads, self.head_dim]);
396
397 // Use time-based seed for stochastic dropout
398 let seed = std::time::SystemTime::now()
399 .duration_since(std::time::UNIX_EPOCH)
400 .unwrap_or_default()
401 .as_nanos() as u64;
402
403 let opts = AttentionOptions {
404 causal: self.causal,
405 causal_offset: pos as u32,
406 dropout_prob: self.dropout_prob,
407 seed,
408 ..AttentionOptions::default()
409 };
410 engine.flash_attention_v2_with(ctx, q, k, v, &output, &opts, mask);
411
412 output
413 }
414
415 /// Get memory savings compared to standard MHA
416 pub fn memory_savings(&self) -> String {
417 let mha_kv_params = 2 * self.num_heads * self.head_dim;
418 let gqa_kv_params = 2 * self.num_kv_heads * self.head_dim;
419 let savings = mha_kv_params as f32 / gqa_kv_params as f32;
420
421 format!(
422 "{:.1}x KV memory savings ({} vs {} KV heads)",
423 savings, self.num_kv_heads, self.num_heads
424 )
425 }
426}
427
428// ============================================================================
429// MULTI-QUERY ATTENTION (MQA) - Single KV head for maximum efficiency
430// ============================================================================
431
432/// Multi-Query Attention (MQA) - Extreme efficiency with single KV head
433///
434/// MQA uses a single key-value head shared across all query heads.
435/// This provides maximum memory efficiency at some quality cost.
436///
437/// # Use Cases
438/// - Inference-optimized models
439/// - Memory-constrained deployments
440/// - High-throughput serving
441///
442/// # Example
443/// ```no_run
444/// # use ferrotensor_backend::{ComputeEngine, UnifiedTensor, WgpuContext};
445/// # use ferrotensor_graph::flash_attention::{MultiQueryAttention};
446/// let ctx = pollster::block_on(WgpuContext::new());
447/// let engine = ComputeEngine::new(&ctx);
448/// // [batch, seq_len, hidden]
449/// let q = UnifiedTensor::zeros(&ctx, &[1, 128, 768]);
450/// let k = UnifiedTensor::zeros(&ctx, &[1, 128, 64]);
451/// let v = UnifiedTensor::zeros(&ctx, &[1, 128, 64]);
452/// // 12 query heads share one KV head
453/// let mqa = MultiQueryAttention::new(768, 12, true, 0.0);
454/// let output = mqa.forward(&q, &k, &v, None, &ctx, &engine);
455/// ```
456pub struct MultiQueryAttention {
457 /// Total hidden dimension
458 pub dim: usize,
459 /// Number of query heads
460 pub num_heads: usize,
461 /// Dimension per head
462 pub head_dim: usize,
463 /// Whether to use causal masking
464 pub causal: bool,
465 /// Internal GQA with num_kv_heads = 1
466 gqa: GroupedQueryAttention,
467}
468
469impl MultiQueryAttention {
470 /// Create a new MQA layer.
471 ///
472 /// # Arguments
473 /// * `dim` - Total hidden dimension
474 /// * `num_heads` - Number of query heads
475 /// * `causal` - Whether to apply causal masking
476 pub fn new(dim: usize, num_heads: usize, causal: bool, dropout_prob: f32) -> Self {
477 let head_dim = dim / num_heads;
478 let gqa = GroupedQueryAttention::new(dim, num_heads, 1, causal, dropout_prob);
479
480 Self {
481 dim,
482 num_heads,
483 head_dim,
484 causal,
485 gqa,
486 }
487 }
488
489 /// Forward pass with multi-query attention.
490 ///
491 /// # Arguments
492 /// * `q` - Query tensor [batch, seq_q, num_heads, head_dim]
493 /// * `k` - Key tensor [batch, seq_k, 1, head_dim]
494 /// * `v` - Value tensor [batch, seq_k, 1, head_dim]
495 ///
496 /// # Returns
497 /// Output tensor [batch, seq_q, num_heads, head_dim]
498 pub fn forward(
499 &self,
500 q: &UnifiedTensor,
501 k: &UnifiedTensor,
502 v: &UnifiedTensor,
503 mask: Option<&UnifiedTensor>,
504 ctx: &WgpuContext,
505 engine: &ComputeEngine,
506 ) -> UnifiedTensor {
507 // Delegate to GQA with num_kv_heads = 1
508 self.gqa.forward(q, k, v, mask, ctx, engine)
509 }
510
511 /// Get memory savings compared to MHA
512 pub fn memory_savings(&self) -> String {
513 format!(
514 "{}x KV memory savings (1 vs {} KV heads)",
515 self.num_heads, self.num_heads
516 )
517 }
518}
519
520// ============================================================================
521// SLIDING WINDOW ATTENTION - For long sequences
522// ============================================================================
523
524/// Sliding Window Attention for efficient long-context processing
525///
526/// Limits attention to a local window around each position, enabling
527/// linear memory scaling for very long sequences.
528///
529/// # Use Cases
530/// - Long document processing (100K+ tokens)
531/// - Streaming input processing
532/// - Memory-efficient inference
533///
534/// # Example
535/// ```no_run
536/// # use ferrotensor_backend::{ComputeEngine, UnifiedTensor, WgpuContext};
537/// # use ferrotensor_graph::flash_attention::{SlidingWindowAttention};
538/// let ctx = pollster::block_on(WgpuContext::new());
539/// let engine = ComputeEngine::new(&ctx);
540/// // [batch, seq_len, hidden]
541/// let q = UnifiedTensor::zeros(&ctx, &[1, 128, 768]);
542/// let k = UnifiedTensor::zeros(&ctx, &[1, 128, 768]);
543/// let v = UnifiedTensor::zeros(&ctx, &[1, 128, 768]);
544/// let swa = SlidingWindowAttention::new(768, 12, 4096, true);
545/// let output = swa.forward(&q, &k, &v, &ctx, &engine);
546/// ```
547pub struct SlidingWindowAttention {
548 /// Total hidden dimension
549 pub dim: usize,
550 /// Number of attention heads
551 pub num_heads: usize,
552 /// Dimension per head
553 pub head_dim: usize,
554 /// Window size for local attention
555 pub window_size: usize,
556 /// Whether to use causal masking
557 pub causal: bool,
558 #[allow(dead_code)]
559 scale: f32,
560}
561
562impl SlidingWindowAttention {
563 /// Create a new sliding window attention layer.
564 ///
565 /// # Arguments
566 /// * `dim` - Total hidden dimension
567 /// * `num_heads` - Number of attention heads
568 /// * `window_size` - Maximum distance for attention (tokens only attend within this window)
569 /// * `causal` - Whether to apply causal masking
570 pub fn new(dim: usize, num_heads: usize, window_size: usize, causal: bool) -> Self {
571 let head_dim = dim / num_heads;
572 let scale = 1.0 / (head_dim as f32).sqrt();
573
574 Self {
575 dim,
576 num_heads,
577 head_dim,
578 window_size,
579 causal,
580 scale,
581 }
582 }
583
584 /// Forward pass with sliding window attention.
585 ///
586 /// Each position can only attend to positions within `window_size` distance.
587 ///
588 /// # Arguments
589 /// * `q` - Query tensor [batch, seq_q, dim]
590 /// * `k` - Key tensor [batch, seq_k, dim]
591 /// * `v` - Value tensor [batch, seq_k, dim]
592 ///
593 /// # Returns
594 /// Output tensor [batch, seq_q, dim]
595 pub fn forward(
596 &self,
597 q: &UnifiedTensor,
598 k: &UnifiedTensor,
599 v: &UnifiedTensor,
600 ctx: &WgpuContext,
601 engine: &ComputeEngine,
602 ) -> UnifiedTensor {
603 let view = |t: &UnifiedTensor| {
604 t.reshaped(&[t.shape[0], t.shape[1], self.num_heads, self.head_dim])
605 };
606 let output = UnifiedTensor::empty(ctx, &q.shape);
607 let opts = AttentionOptions {
608 causal: self.causal,
609 window: self.window_size as u32,
610 ..AttentionOptions::default()
611 };
612 engine.flash_attention_v2_with(
613 ctx,
614 &view(q),
615 &view(k),
616 &view(v),
617 &view(&output),
618 &opts,
619 None,
620 );
621 output
622 }
623
624 /// Get memory savings compared to full attention
625 pub fn memory_savings(&self, seq_len: usize) -> String {
626 let full_attention = seq_len * seq_len;
627 let window_attention = seq_len * self.window_size.min(seq_len);
628 let savings = full_attention as f32 / window_attention as f32;
629
630 format!(
631 "{:.1}x attention savings (window={}, seq={})",
632 savings, self.window_size, seq_len
633 )
634 }
635}
636
637// ============================================================================
638// ATTENTION MASK - Custom masking patterns
639// ============================================================================
640
641/// Attention mask for custom attention patterns
642#[derive(Debug, Clone)]
643pub enum AttentionMask {
644 /// No mask (full attention)
645 None,
646 /// Causal mask (lower triangular)
647 Causal,
648 /// Padding mask to ignore padded tokens
649 Padding(Vec<usize>), // lengths of non-padded sequences
650 /// Sliding window mask
651 SlidingWindow(usize),
652 /// Custom mask tensor
653 Custom(Arc<UnifiedTensor>),
654}
655
656impl AttentionMask {
657 /// Create a causal mask
658 pub fn causal() -> Self {
659 Self::Causal
660 }
661
662 /// Create a padding mask from sequence lengths
663 pub fn from_lengths(lengths: Vec<usize>) -> Self {
664 Self::Padding(lengths)
665 }
666
667 /// Create a sliding window mask
668 pub fn sliding_window(window_size: usize) -> Self {
669 Self::SlidingWindow(window_size)
670 }
671}
672
673impl std::fmt::Debug for GroupedQueryAttention {
674 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
675 f.debug_struct("GroupedQueryAttention")
676 .field("dim", &self.dim)
677 .field("num_heads", &self.num_heads)
678 .field("num_kv_heads", &self.num_kv_heads)
679 .field("num_groups", &self.num_groups)
680 .field("head_dim", &self.head_dim)
681 .field("causal", &self.causal)
682 .field("dropout", &self.dropout_prob)
683 .finish()
684 }
685}
686
687impl std::fmt::Debug for MultiQueryAttention {
688 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
689 f.debug_struct("MultiQueryAttention")
690 .field("dim", &self.dim)
691 .field("num_heads", &self.num_heads)
692 .field("head_dim", &self.head_dim)
693 .field("causal", &self.causal)
694 .finish()
695 }
696}
697
698impl std::fmt::Debug for SlidingWindowAttention {
699 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
700 f.debug_struct("SlidingWindowAttention")
701 .field("dim", &self.dim)
702 .field("num_heads", &self.num_heads)
703 .field("head_dim", &self.head_dim)
704 .field("window_size", &self.window_size)
705 .field("causal", &self.causal)
706 .finish()
707 }
708}