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