1use serde::{Deserialize, Serialize};
2
3use crate::error::{PolyKvError, Result};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
7#[serde(rename_all = "lowercase")]
8pub enum AttentionType {
9 MHA,
11 MQA,
13 GQA,
15}
16
17#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
21pub struct KvTensorShape {
22 pub attention_type: AttentionType,
24 pub num_layers: u32,
26 pub num_heads: u32,
28 pub num_kv_heads: u32,
30 pub head_dim: usize,
32 pub hidden_size: usize,
34}
35
36impl KvTensorShape {
37 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 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 pub fn kv_elements_per_token_per_layer(&self) -> usize {
95 self.num_kv_heads as usize * self.head_dim * 2 }
97
98 pub fn kv_bytes_per_token_per_layer(&self) -> usize {
100 self.kv_elements_per_token_per_layer() * 4
101 }
102
103 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 pub fn total_kv_bytes(&self, num_tokens: usize) -> usize {
110 self.total_kv_elements(num_tokens) * 4
111 }
112
113 pub fn head_vector_dim(&self) -> usize {
115 self.head_dim
116 }
117
118 pub fn kv_vectors_per_token_per_layer(&self) -> usize {
120 self.num_kv_heads as usize * 2 }
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; 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}