Skip to main content

poly_kv/
shape.rs

1use serde::{Deserialize, Serialize};
2
3use crate::error::{PolyKvError, Result};
4
5/// The type of attention mechanism in the transformer model.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
7#[serde(rename_all = "lowercase")]
8pub enum AttentionType {
9    /// Multi-Head Attention: num_kv_heads == num_heads
10    MHA,
11    /// Multi-Query Attention: num_kv_heads == 1
12    MQA,
13    /// Grouped-Query Attention: 1 < num_kv_heads < num_heads
14    GQA,
15}
16
17/// Describes the tensor shape of a KV cache for a specific model family.
18///
19/// This is the logical layout, not the physical memory layout.
20#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
21pub struct KvTensorShape {
22    /// The attention mechanism type.
23    pub attention_type: AttentionType,
24    /// Number of transformer layers.
25    pub num_layers: u32,
26    /// Number of attention heads.
27    pub num_heads: u32,
28    /// Number of key/value heads (for GQA: smaller than num_heads).
29    pub num_kv_heads: u32,
30    /// Dimension of each attention head (usually 64 or 128).
31    pub head_dim: usize,
32    /// Hidden size / model dimension (d_model).
33    pub hidden_size: usize,
34}
35
36impl KvTensorShape {
37    /// Validate that the shape is internally consistent.
38    pub fn validate(&self) -> Result<()> {
39        if self.num_layers == 0 {
40            return Err(PolyKvError::InvalidShape("num_layers must be > 0".into()));
41        }
42        if self.num_heads == 0 {
43            return Err(PolyKvError::InvalidShape("num_heads must be > 0".into()));
44        }
45        if self.num_kv_heads == 0 {
46            return Err(PolyKvError::InvalidShape("num_kv_heads must be > 0".into()));
47        }
48        if self.head_dim == 0 {
49            return Err(PolyKvError::InvalidShape("head_dim must be > 0".into()));
50        }
51        if self.hidden_size == 0 {
52            return Err(PolyKvError::InvalidShape("hidden_size must be > 0".into()));
53        }
54
55        match self.attention_type {
56            AttentionType::MHA => {
57                if self.num_kv_heads != self.num_heads {
58                    return Err(PolyKvError::InvalidShape(format!(
59                        "MHA requires num_kv_heads ({}) == num_heads ({})",
60                        self.num_kv_heads, self.num_heads
61                    )));
62                }
63            }
64            AttentionType::MQA => {
65                if self.num_kv_heads != 1 {
66                    return Err(PolyKvError::InvalidShape(format!(
67                        "MQA requires num_kv_heads == 1, got {}",
68                        self.num_kv_heads
69                    )));
70                }
71            }
72            AttentionType::GQA => {
73                if self.num_kv_heads >= self.num_heads {
74                    return Err(PolyKvError::InvalidShape(format!(
75                        "GQA requires num_kv_heads ({}) < num_heads ({})",
76                        self.num_kv_heads, self.num_heads
77                    )));
78                }
79            }
80        }
81
82        // d_model must be a multiple of num_heads
83        if self.hidden_size % self.num_heads as usize != 0 {
84            return Err(PolyKvError::InvalidShape(format!(
85                "hidden_size ({}) must be divisible by num_heads ({})",
86                self.hidden_size, self.num_heads
87            )));
88        }
89
90        Ok(())
91    }
92
93    /// Total KV elements per token (key + value) for one layer.
94    pub fn kv_elements_per_token_per_layer(&self) -> usize {
95        self.num_kv_heads as usize * self.head_dim * 2 // key + value
96    }
97
98    /// Total KV bytes per token (f32) for one layer.
99    pub fn kv_bytes_per_token_per_layer(&self) -> usize {
100        self.kv_elements_per_token_per_layer() * 4
101    }
102
103    /// Total KV elements for N tokens across all layers.
104    pub fn total_kv_elements(&self, num_tokens: usize) -> usize {
105        self.num_layers as usize * num_tokens * self.kv_elements_per_token_per_layer()
106    }
107
108    /// Total raw bytes for N tokens across all layers.
109    pub fn total_kv_bytes(&self, num_tokens: usize) -> usize {
110        self.total_kv_elements(num_tokens) * 4
111    }
112
113    /// Dimension for a single key or value vector for one KV head.
114    pub fn head_vector_dim(&self) -> usize {
115        self.head_dim
116    }
117
118    /// Number of KV head vectors per token per layer.
119    pub fn kv_vectors_per_token_per_layer(&self) -> usize {
120        self.num_kv_heads as usize * 2 // one key + one value per KV head
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    fn valid_mha_shape() -> KvTensorShape {
129        KvTensorShape {
130            attention_type: AttentionType::MHA,
131            num_layers: 32,
132            num_heads: 32,
133            num_kv_heads: 32,
134            head_dim: 128,
135            hidden_size: 4096,
136        }
137    }
138
139    #[test]
140    fn test_mha_shape_validates() {
141        let shape = valid_mha_shape();
142        assert!(shape.validate().is_ok());
143    }
144
145    #[test]
146    fn test_gqa_shape_validates() {
147        let shape = KvTensorShape {
148            attention_type: AttentionType::GQA,
149            num_layers: 40,
150            num_heads: 32,
151            num_kv_heads: 8,
152            head_dim: 128,
153            hidden_size: 4096,
154        };
155        assert!(shape.validate().is_ok());
156    }
157
158    #[test]
159    fn test_mqa_shape_validates() {
160        let shape = KvTensorShape {
161            attention_type: AttentionType::MQA,
162            num_layers: 32,
163            num_heads: 32,
164            num_kv_heads: 1,
165            head_dim: 128,
166            hidden_size: 4096,
167        };
168        assert!(shape.validate().is_ok());
169    }
170
171    #[test]
172    fn test_ragged_shape_rejected() {
173        let mut shape = valid_mha_shape();
174        shape.num_kv_heads = 16; // MHA mismatch
175        assert!(shape.validate().is_err());
176    }
177
178    #[test]
179    fn test_zero_layers_rejected() {
180        let mut shape = valid_mha_shape();
181        shape.num_layers = 0;
182        assert!(shape.validate().is_err());
183    }
184}