1use anyhow::{Context, Result, bail, ensure};
19use metrale_gpu_runtime::gpu::{DevicePtr, GpuBackend, KernelHandle};
20use metrale_gpu_runtime::kernel_args::{KernelLaunch, div_ceil};
21use metrale_model_weights::kimi_k3_host::{KDA_L2_EPS, KdaConfig, KdaState};
22
23pub const MODULE: &str = "kda_decode";
25pub const CONV_ENTRY: &str = "k3_kda_conv_update_f32";
26pub const RECURRENT_ENTRY: &str = "k3_kda_recurrent_step_f32";
27const CONV_BLOCK: u32 = 128;
28
29#[derive(Clone, Copy, Debug)]
30pub struct K3KdaDecodeKernels {
31 pub conv: KernelHandle,
32 pub recurrent: KernelHandle,
33}
34
35impl K3KdaDecodeKernels {
36 pub fn resolve(gpu: &dyn GpuBackend) -> Result<Self> {
37 Ok(Self {
38 conv: gpu.kernel(MODULE, CONV_ENTRY)?,
39 recurrent: gpu.kernel(MODULE, RECURRENT_ENTRY)?,
40 })
41 }
42}
43
44fn f32_bytes(v: &[f32]) -> Vec<u8> {
45 v.iter().flat_map(|x| x.to_le_bytes()).collect()
46}
47
48fn bytes_f32(b: &[u8]) -> Vec<f32> {
49 b.chunks_exact(4)
50 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
51 .collect()
52}
53
54fn up(gpu: &dyn GpuBackend, v: &[f32], hold: &mut Vec<DevicePtr>) -> Result<DevicePtr> {
55 let b = f32_bytes(v);
56 let p = gpu.alloc(b.len().max(1))?;
57 hold.push(p);
58 gpu.copy_h2d(&b, p)?;
59 Ok(p)
60}
61
62pub struct KdaDeviceState {
70 pub conv: DevicePtr,
71 pub recurrent: DevicePtr,
72 conv_elems: usize,
73 recurrent_elems: usize,
74}
75
76impl KdaDeviceState {
77 pub fn alloc_and_upload(gpu: &dyn GpuBackend, host: &KdaState) -> Result<Self> {
78 let conv_b = f32_bytes(&host.conv);
79 let rec_b = f32_bytes(&host.recurrent);
80 let conv = gpu.alloc(conv_b.len().max(1))?;
81 let recurrent = match gpu.alloc(rec_b.len().max(1)) {
82 Ok(ptr) => ptr,
83 Err(error) => {
84 let _ = gpu.free(conv);
85 return Err(error);
86 }
87 };
88 let upload = gpu
89 .copy_h2d(&conv_b, conv)
90 .and_then(|()| gpu.copy_h2d(&rec_b, recurrent));
91 if let Err(error) = upload {
92 let _ = gpu.free(conv);
93 let _ = gpu.free(recurrent);
94 return Err(error);
95 }
96 Ok(Self {
97 conv,
98 recurrent,
99 conv_elems: host.conv.len(),
100 recurrent_elems: host.recurrent.len(),
101 })
102 }
103
104 pub fn download(&self, gpu: &dyn GpuBackend, host: &mut KdaState) -> Result<()> {
105 self.validate_geometry(host.conv.len(), host.recurrent.len())?;
106 let mut conv_b = vec![0u8; host.conv.len() * 4];
107 let mut rec_b = vec![0u8; host.recurrent.len() * 4];
108 gpu.copy_d2h(self.conv, &mut conv_b)?;
109 gpu.copy_d2h(self.recurrent, &mut rec_b)?;
110 host.conv = bytes_f32(&conv_b);
111 host.recurrent = bytes_f32(&rec_b);
112 Ok(())
113 }
114
115 fn validate_geometry(&self, conv_elems: usize, recurrent_elems: usize) -> Result<()> {
116 ensure!(
117 self.conv_elems == conv_elems && self.recurrent_elems == recurrent_elems,
118 "k3 kda: resident state rank"
119 );
120 Ok(())
121 }
122
123 pub fn free(self, gpu: &dyn GpuBackend) -> Result<()> {
124 let conv = gpu.free(self.conv);
125 let recurrent = gpu.free(self.recurrent);
126 conv.and(recurrent)
127 }
128}
129
130pub fn launch_k3_kda_decode_token(
133 gpu: &dyn GpuBackend,
134 kernels: &K3KdaDecodeKernels,
135 x_qkv: &[f32],
136 conv_w: &[f32],
137 gate: &[f32],
138 beta: &[f32],
139 cfg: &KdaConfig,
140 state: &mut KdaState,
141 stream: u64,
142) -> Result<Vec<f32>> {
143 launch_k3_kda_decode_inner(
144 gpu,
145 kernels,
146 x_qkv,
147 conv_w,
148 gate,
149 beta,
150 cfg,
151 Some(state),
152 None,
153 stream,
154 )
155}
156
157pub fn launch_k3_kda_decode_token_on_device(
160 gpu: &dyn GpuBackend,
161 kernels: &K3KdaDecodeKernels,
162 x_qkv: &[f32],
163 conv_w: &[f32],
164 gate: &[f32],
165 beta: &[f32],
166 cfg: &KdaConfig,
167 device: &KdaDeviceState,
168 stream: u64,
169) -> Result<Vec<f32>> {
170 launch_k3_kda_decode_inner(
171 gpu,
172 kernels,
173 x_qkv,
174 conv_w,
175 gate,
176 beta,
177 cfg,
178 None,
179 Some(device),
180 stream,
181 )
182}
183
184#[allow(clippy::too_many_arguments)]
185fn launch_k3_kda_decode_inner(
186 gpu: &dyn GpuBackend,
187 kernels: &K3KdaDecodeKernels,
188 x_qkv: &[f32],
189 conv_w: &[f32],
190 gate: &[f32],
191 beta: &[f32],
192 cfg: &KdaConfig,
193 mut host: Option<&mut KdaState>,
194 device: Option<&KdaDeviceState>,
195 stream: u64,
196) -> Result<Vec<f32>> {
197 let (h, d, k) = (cfg.heads, cfg.head_dim, cfg.conv_kernel);
198 let c = cfg.conv_dim();
199 if x_qkv.len() != c {
200 bail!("k3 kda: x_qkv {} != conv_dim {c}", x_qkv.len());
201 }
202 if conv_w.len() != cfg.conv_elems() {
203 bail!("k3 kda: conv_w {} != {}", conv_w.len(), cfg.conv_elems());
204 }
205 if gate.len() != cfg.qkv_dim() || beta.len() != h {
206 bail!("k3 kda: gate/beta rank");
207 }
208 if let Some(state) = host.as_ref()
209 && (state.conv.len() != cfg.conv_elems() || state.recurrent.len() != cfg.recurrent_elems())
210 {
211 bail!("k3 kda: state rank");
212 }
213 if k == 0 || d == 0 {
214 bail!("k3 kda: D and conv_kernel must be > 0");
215 }
216 if let Some(state) = device {
217 state.validate_geometry(cfg.conv_elems(), cfg.recurrent_elems())?;
218 }
219
220 let mut hold = Vec::new();
221 let run = (|| {
222 let dx = up(gpu, x_qkv, &mut hold)?;
223 let dw = up(gpu, conv_w, &mut hold)?;
224 let dgate = up(gpu, gate, &mut hold)?;
225 let dbeta = up(gpu, beta, &mut hold)?;
226 let (dconv, drec, pull_state) = if let Some(d) = device {
227 (d.conv, d.recurrent, false)
228 } else {
229 let s = host.as_ref().context("k3 kda: host or device state")?;
230 (
231 up(gpu, &s.conv, &mut hold)?,
232 up(gpu, &s.recurrent, &mut hold)?,
233 true,
234 )
235 };
236 let dy = gpu.alloc((c * 4).max(1))?;
237 hold.push(dy);
238 let dout = gpu.alloc((cfg.qkv_dim() * 4).max(1))?;
239 hold.push(dout);
240
241 KernelLaunch::new(gpu, kernels.conv)
242 .grid([div_ceil(c as u32, CONV_BLOCK), 1, 1])
243 .block([CONV_BLOCK, 1, 1])
244 .arg_ptr(dx)
245 .arg_ptr(dw)
246 .arg_ptr(dconv)
247 .arg_ptr(dy)
248 .arg_u32(c as u32)
249 .arg_u32(k as u32)
250 .launch(stream)
251 .context("k3_kda_conv_update_f32")?;
252
253 let rec_block = (d as u32).min(128);
254 KernelLaunch::new(gpu, kernels.recurrent)
255 .grid([h as u32, 1, 1])
256 .block([rec_block, 1, 1])
257 .shared_mem((3 * d * 4) as u32)
258 .arg_ptr(dy)
259 .arg_ptr(dgate)
260 .arg_ptr(dbeta)
261 .arg_ptr(drec)
262 .arg_ptr(dout)
263 .arg_u32(h as u32)
264 .arg_u32(d as u32)
265 .arg_f32(KDA_L2_EPS)
266 .launch(stream)
267 .context("k3_kda_recurrent_step_f32")?;
268 gpu.synchronize(stream)?;
269
270 let mut out_b = vec![0u8; cfg.qkv_dim() * 4];
271 gpu.copy_d2h(dout, &mut out_b)?;
272 if pull_state {
273 let state = host.as_mut().context("k3 kda: host state for D2H")?;
274 let mut conv_b = vec![0u8; cfg.conv_elems() * 4];
275 let mut rec_b = vec![0u8; cfg.recurrent_elems() * 4];
276 gpu.copy_d2h(dconv, &mut conv_b)?;
277 gpu.copy_d2h(drec, &mut rec_b)?;
278 state.conv = bytes_f32(&conv_b);
279 state.recurrent = bytes_f32(&rec_b);
280 }
281 Ok(bytes_f32(&out_b))
282 })();
283 for p in hold {
284 let _ = gpu.free(p);
285 }
286 run
287}
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292 use metrale_gpu_runtime::gpu::mock::{MockArg, MockGpuBackend};
293 use metrale_model_weights::kimi_k3_host::kda_decode_token;
294
295 #[test]
296 fn resolve_looks_up_k3_entries() {
297 let gpu = MockGpuBackend::new();
298 let _ = K3KdaDecodeKernels::resolve(&gpu).unwrap();
299 assert_eq!(
300 gpu.kernel_lookups_snapshot(),
301 vec![
302 (MODULE.to_string(), CONV_ENTRY.to_string()),
303 (MODULE.to_string(), RECURRENT_ENTRY.to_string()),
304 ]
305 );
306 }
307
308 #[test]
309 fn mock_launch_contract_twin_geometry() {
310 let gpu = MockGpuBackend::new();
311 let k = K3KdaDecodeKernels::resolve(&gpu).unwrap();
312 let cfg = KdaConfig::twin_0_40b();
313 let mut state = KdaState::new(&cfg);
314 let x = vec![0.1f32; cfg.conv_dim()];
315 let w = vec![0.2f32; cfg.conv_elems()];
316 let gate = vec![-0.5f32; cfg.qkv_dim()];
317 let beta = vec![0.25f32; cfg.heads];
318 let _ = launch_k3_kda_decode_token(&gpu, &k, &x, &w, &gate, &beta, &cfg, &mut state, 3)
319 .unwrap();
320 let launches = gpu.launches_snapshot();
321 assert_eq!(launches.len(), 2, "conv then recurrent");
322 let c = cfg.conv_dim() as u32;
323 assert_eq!(launches[0].grid, [div_ceil(c, CONV_BLOCK), 1, 1]);
324 assert_eq!(launches[0].block, [CONV_BLOCK, 1, 1]);
325 assert_eq!(launches[0].shared_mem, 0);
326 assert_eq!(launches[0].stream, 3);
327 assert_eq!(launches[0].args.len(), 6);
328 assert_eq!(
329 launches[0].args[4],
330 MockArg::Bytes(c.to_le_bytes().to_vec())
331 );
332 assert_eq!(
333 launches[0].args[5],
334 MockArg::Bytes((cfg.conv_kernel as u32).to_le_bytes().to_vec())
335 );
336
337 let rec = &launches[1];
338 assert_eq!(rec.grid, [cfg.heads as u32, 1, 1]);
339 assert_eq!(rec.block, [cfg.head_dim as u32, 1, 1]);
340 assert_eq!(rec.shared_mem, (3 * cfg.head_dim * 4) as u32);
341 assert_eq!(rec.args.len(), 8);
342 assert_eq!(
343 rec.args[7],
344 MockArg::Bytes(KDA_L2_EPS.to_le_bytes().to_vec())
345 );
346 }
347
348 #[test]
349 fn cpu_oracle_is_the_compare_target() {
350 let cfg = KdaConfig::twin_0_40b();
351 let mut state = KdaState::new(&cfg);
352 let x = vec![0.1f32; cfg.conv_dim()];
353 let w = vec![0.2f32; cfg.conv_elems()];
354 let gate = vec![-0.5f32; cfg.qkv_dim()];
355 let beta = vec![0.25f32; cfg.heads];
356 let y = kda_decode_token(&x, &w, &gate, &beta, &cfg, &mut state);
357 assert_eq!(y.len(), cfg.qkv_dim());
358 assert!(y.iter().any(|v| v.abs() > 1e-8));
359 }
360
361 #[test]
362 fn device_resident_state_skips_conv_recurrent_d2h() {
363 let gpu = MockGpuBackend::new();
364 let k = K3KdaDecodeKernels::resolve(&gpu).unwrap();
365 let cfg = KdaConfig::twin_0_40b();
366 let host = KdaState::new(&cfg);
367 let x = vec![0.1f32; cfg.conv_dim()];
368 let w = vec![0.2f32; cfg.conv_elems()];
369 let gate = vec![-0.5f32; cfg.qkv_dim()];
370 let beta = vec![0.25f32; cfg.heads];
371 let device = KdaDeviceState::alloc_and_upload(&gpu, &host).unwrap();
372 let before = gpu.d2h_blocking_count();
373 let _ =
374 launch_k3_kda_decode_token_on_device(&gpu, &k, &x, &w, &gate, &beta, &cfg, &device, 0)
375 .unwrap();
376 let pulled = gpu.d2h_blocking_count() - before;
377 assert_eq!(
378 pulled, 1,
379 "resident decode must D2H the output only, not conv/recurrent (got {pulled})"
380 );
381 device.free(&gpu).unwrap();
382 }
383}