metrale_model_arch/kimi_k3/
state.rs1use std::any::Any;
14
15use anyhow::{Result, bail, ensure};
16use metrale_gpu_runtime::gpu::GpuBackend;
17use metrale_model_weights::kimi_k3_host::{LayerCache, MlaConfig};
18
19use super::kda_cuda::KdaDeviceState;
20use super::mla_cuda::MlaDeviceKv;
21
22use metrale_model_layers::layer::LayerState;
23
24pub struct K3CpuFallbackState {
27 pub cache: LayerCache,
28 pub device_kda: Option<KdaDeviceState>,
31 pub device_mla: Option<MlaDeviceKv>,
34}
35
36impl K3CpuFallbackState {
37 pub fn new(cache: LayerCache) -> Self {
38 Self {
39 cache,
40 device_kda: None,
41 device_mla: None,
42 }
43 }
44
45 pub fn ensure_device_kda(&mut self, gpu: &dyn GpuBackend) -> Result<()> {
46 if self.device_kda.is_none()
47 && let LayerCache::Kda(host) = &self.cache
48 {
49 self.device_kda = Some(KdaDeviceState::alloc_and_upload(gpu, host)?);
50 }
51 Ok(())
52 }
53
54 pub fn ensure_device_mla(
55 &mut self,
56 gpu: &dyn GpuBackend,
57 cfg: &MlaConfig,
58 cap: usize,
59 ) -> Result<()> {
60 if let Some(device) = &self.device_mla {
61 return device.validate_cfg(cfg);
62 }
63 let LayerCache::Mla(host) = &self.cache else {
64 bail!("K3 CUDA MLA: mixer state is not MLA KV");
65 };
66 let k_row = cfg.heads * cfg.qk_head_dim();
67 let v_row = cfg.heads * cfg.v_head_dim;
68 self.device_mla = Some(MlaDeviceKv::alloc_and_upload(gpu, host, cap, k_row, v_row)?);
69 Ok(())
70 }
71
72 pub fn snapshot(&self, gpu: &dyn GpuBackend, stream: u64) -> Result<Vec<u8>> {
73 let mut cache = self.cache.clone();
74 if let (Some(device), LayerCache::Kda(host)) = (&self.device_kda, &mut cache) {
75 gpu.synchronize(stream)?;
76 device.download(gpu, host)?;
77 }
78 if let (Some(device), LayerCache::Mla(host)) = (&self.device_mla, &mut cache) {
79 gpu.synchronize(stream)?;
80 device.download(gpu, host)?;
81 }
82 Ok(cache.to_bytes())
83 }
84
85 pub fn restore(&mut self, gpu: &dyn GpuBackend, bytes: &[u8], stream: u64) -> Result<()> {
86 let cache = LayerCache::from_bytes(bytes)?;
87 match (&self.cache, &cache) {
88 (LayerCache::Kda(current), LayerCache::Kda(restored)) => ensure!(
89 current.conv.len() == restored.conv.len()
90 && current.recurrent.len() == restored.recurrent.len(),
91 "K3 restore: KDA state geometry mismatch"
92 ),
93 (LayerCache::Mla(current), LayerCache::Mla(restored)) => {
94 validate_mla_restore(current, restored, self.device_mla.as_ref())?;
95 }
96 _ => bail!("K3 restore: mixer variant mismatch"),
97 }
98 gpu.synchronize(stream)?;
99 self.release(gpu)?;
100 self.cache = cache;
101 Ok(())
102 }
103
104 pub fn release(&mut self, gpu: &dyn GpuBackend) -> Result<()> {
105 let kda = if let Some(device) = self.device_kda.take() {
106 device.free(gpu)
107 } else {
108 Ok(())
109 };
110 let mla = if let Some(device) = self.device_mla.take() {
111 device.free(gpu)
112 } else {
113 Ok(())
114 };
115 kda.and(mla)
116 }
117}
118
119fn validate_mla_restore(
120 current: &metrale_model_weights::kimi_k3_host::MlaKv,
121 restored: &metrale_model_weights::kimi_k3_host::MlaKv,
122 device: Option<&MlaDeviceKv>,
123) -> Result<()> {
124 if restored.seq_len == 0 {
125 ensure!(
126 restored.k.is_empty() && restored.v.is_empty(),
127 "K3 restore: empty MLA seq with payload"
128 );
129 return Ok(());
130 }
131 ensure!(
132 restored.k.len()
133 == restored
134 .seq_len
135 .saturating_mul(restored.k.len() / restored.seq_len)
136 && restored.v.len()
137 == restored
138 .seq_len
139 .saturating_mul(restored.v.len() / restored.seq_len)
140 && restored.k.len() / restored.seq_len > 0
141 && restored.v.len() / restored.seq_len > 0,
142 "K3 restore: MLA KV rank"
143 );
144 let k_row = restored.k.len() / restored.seq_len;
145 let v_row = restored.v.len() / restored.seq_len;
146 if current.seq_len > 0 {
147 ensure!(
148 current.k.len() / current.seq_len == k_row
149 && current.v.len() / current.seq_len == v_row,
150 "K3 restore: MLA KV geometry mismatch"
151 );
152 } else if let Some(device) = device {
153 ensure!(
154 device.k_row() == k_row && device.v_row() == v_row,
155 "K3 restore: MLA KV geometry mismatch"
156 );
157 }
158 Ok(())
159}
160
161impl LayerState for K3CpuFallbackState {
162 fn as_any(&self) -> &dyn Any {
163 self
164 }
165 fn as_any_mut(&mut self) -> &mut dyn Any {
166 self
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173 use metrale_gpu_runtime::gpu::GpuBackend;
174 use metrale_gpu_runtime::gpu::mock::MockGpuBackend;
175 use metrale_model_weights::kimi_k3_host::{KdaConfig, KdaState};
176
177 #[test]
178 fn resident_snapshot_restore_and_release_preserve_authoritative_state() {
179 let gpu = MockGpuBackend::new();
180 let mut state =
181 K3CpuFallbackState::new(LayerCache::Kda(KdaState::new(&KdaConfig::twin_0_40b())));
182 state.ensure_device_kda(&gpu).unwrap();
183 state.ensure_device_kda(&gpu).unwrap();
184 assert_eq!(gpu.alloc_count(), 2, "reuse resident state between tokens");
185 assert!(state.restore(&gpu, b"invalid", 0).is_err());
186 assert_eq!(
187 gpu.alloc_count(),
188 2,
189 "invalid snapshot must leave state intact"
190 );
191 let device = state.device_kda.as_ref().unwrap();
192 gpu.copy_h2d(&7.25f32.to_le_bytes(), device.recurrent)
193 .unwrap();
194 let bytes = state.snapshot(&gpu, 0).unwrap();
195 let LayerCache::Kda(saved) = LayerCache::from_bytes(&bytes).unwrap() else {
196 panic!("expected KDA");
197 };
198 assert_eq!(saved.recurrent[0], 7.25);
199 state.restore(&gpu, &bytes, 0).unwrap();
200 assert!(state.device_kda.is_none());
201 assert_eq!(gpu.alloc_count(), 0);
202 state.ensure_device_kda(&gpu).unwrap();
203 assert_eq!(state.snapshot(&gpu, 0).unwrap(), bytes);
204 state.release(&gpu).unwrap();
205 state.release(&gpu).unwrap();
206 assert_eq!(gpu.alloc_count(), 0);
207 }
208
209 #[test]
210 fn failed_resident_allocation_releases_partial_state() {
211 let gpu = MockGpuBackend::new();
212 let cfg = KdaConfig::twin_0_40b();
213 gpu.set_max_allocation_bytes(cfg.conv_elems() * 4);
214 let mut state = K3CpuFallbackState::new(LayerCache::Kda(KdaState::new(&cfg)));
215 assert!(state.ensure_device_kda(&gpu).is_err());
216 assert!(state.device_kda.is_none());
217 assert_eq!(gpu.alloc_count(), 0);
218 }
219
220 #[test]
221 fn resident_mla_snapshot_restore_and_empty_restore() {
222 use metrale_model_weights::kimi_k3_host::{MlaConfig, MlaKv};
223
224 let gpu = MockGpuBackend::new();
225 let cfg = MlaConfig::twin_0_40b();
226 let k_row = cfg.heads * cfg.qk_head_dim();
227 let v_row = cfg.heads * cfg.v_head_dim;
228 let mut seeded = MlaKv::default();
229 seeded.append(&vec![0.5f32; k_row], &vec![0.25f32; v_row]);
230 let mut state = K3CpuFallbackState::new(LayerCache::Mla(seeded));
231 state.ensure_device_mla(&gpu, &cfg, 8).unwrap();
232 state.ensure_device_mla(&gpu, &cfg, 8).unwrap();
233 assert_eq!(gpu.alloc_count(), 2, "reuse resident MLA between tokens");
234 let bytes = state.snapshot(&gpu, 0).unwrap();
235 let LayerCache::Mla(saved) = LayerCache::from_bytes(&bytes).unwrap() else {
236 panic!("expected MLA");
237 };
238 assert_eq!(saved.seq_len, 1);
239 assert_eq!(saved.k[0], 0.5);
240 assert!(state.restore(&gpu, b"invalid", 0).is_err());
241 assert_eq!(
242 gpu.alloc_count(),
243 2,
244 "invalid snapshot must leave state intact"
245 );
246 state.restore(&gpu, &bytes, 0).unwrap();
247 assert!(state.device_mla.is_none());
248 let empty = LayerCache::Mla(MlaKv::default()).to_bytes();
249 state.restore(&gpu, &empty, 0).unwrap();
250 let LayerCache::Mla(cleared) = &state.cache else {
251 panic!("expected MLA");
252 };
253 assert_eq!(cleared.seq_len, 0);
254 state.ensure_device_mla(&gpu, &cfg, 8).unwrap();
255 assert_eq!(state.device_mla.as_ref().unwrap().seq_len, 0);
256 state.release(&gpu).unwrap();
257 assert_eq!(gpu.alloc_count(), 0);
258 }
259}