Skip to main content

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}