1use std::collections::BTreeSet;
27
28use anyhow::{Context, Result, bail};
29use spark_runtime::gpu::{DevicePtr, GpuBackend};
30
31use super::{Glm5NextKdaConfig, Glm5NextKdaWeights};
32use crate::weight_map::DenseWeight;
33
34#[derive(Clone, Copy, PartialEq, Eq, Debug)]
36pub enum KdaDtype {
37 Bf16,
38 F32,
39}
40
41impl KdaDtype {
42 pub fn parse(s: &str) -> Option<Self> {
43 match s {
44 "BF16" => Some(Self::Bf16),
45 "F32" => Some(Self::F32),
46 _ => None,
47 }
48 }
49 pub fn name(self) -> &'static str {
50 match self {
51 Self::Bf16 => "BF16",
52 Self::F32 => "F32",
53 }
54 }
55}
56
57pub struct RawTensor<'a> {
59 pub dtype: KdaDtype,
60 pub shape: Vec<usize>,
61 pub bytes: &'a [u8],
62}
63
64pub trait KdaTensorSource {
67 fn get(&self, name: &str) -> Option<RawTensor<'_>>;
68 fn names(&self) -> Vec<String>;
70}
71
72#[derive(Clone, Copy, Debug)]
77pub struct TensorSpec {
78 pub name: &'static str,
79 pub dtype: KdaDtype,
80 dims: &'static [Dim],
81}
82
83#[derive(Clone, Copy, Debug)]
84enum Dim {
85 Q,
86 X,
87 D,
88 H,
89 K,
90 One,
91}
92
93use Dim::{D as DD, H as DH, K as DK, One as D1, Q as DQ, X as DX};
94
95pub const KDA_TENSORS: &[TensorSpec] = &[
96 TensorSpec {
97 name: "self_attn.q_proj.weight",
98 dtype: KdaDtype::Bf16,
99 dims: &[DQ, DX],
100 },
101 TensorSpec {
102 name: "self_attn.k_proj.weight",
103 dtype: KdaDtype::Bf16,
104 dims: &[DQ, DX],
105 },
106 TensorSpec {
107 name: "self_attn.v_proj.weight",
108 dtype: KdaDtype::Bf16,
109 dims: &[DQ, DX],
110 },
111 TensorSpec {
112 name: "self_attn.q_conv1d.weight",
113 dtype: KdaDtype::Bf16,
114 dims: &[DQ, D1, DK],
115 },
116 TensorSpec {
117 name: "self_attn.k_conv1d.weight",
118 dtype: KdaDtype::Bf16,
119 dims: &[DQ, D1, DK],
120 },
121 TensorSpec {
122 name: "self_attn.v_conv1d.weight",
123 dtype: KdaDtype::Bf16,
124 dims: &[DQ, D1, DK],
125 },
126 TensorSpec {
127 name: "self_attn.f_a_proj.weight",
128 dtype: KdaDtype::Bf16,
129 dims: &[DD, DX],
130 },
131 TensorSpec {
132 name: "self_attn.f_b_proj.weight",
133 dtype: KdaDtype::Bf16,
134 dims: &[DQ, DD],
135 },
136 TensorSpec {
137 name: "self_attn.g_a_proj.weight",
138 dtype: KdaDtype::Bf16,
139 dims: &[DD, DX],
140 },
141 TensorSpec {
142 name: "self_attn.g_b_proj.weight",
143 dtype: KdaDtype::Bf16,
144 dims: &[DQ, DD],
145 },
146 TensorSpec {
147 name: "self_attn.b_proj.weight",
148 dtype: KdaDtype::Bf16,
149 dims: &[DH, DX],
150 },
151 TensorSpec {
153 name: "self_attn.A_log",
154 dtype: KdaDtype::F32,
155 dims: &[DH],
156 },
157 TensorSpec {
158 name: "self_attn.dt_bias",
159 dtype: KdaDtype::F32,
160 dims: &[DQ],
161 },
162 TensorSpec {
163 name: "self_attn.o_norm.weight",
164 dtype: KdaDtype::Bf16,
165 dims: &[DD],
166 },
167 TensorSpec {
168 name: "self_attn.o_proj.weight",
169 dtype: KdaDtype::Bf16,
170 dims: &[DX, DQ],
171 },
172];
173
174pub const DSA_MARKERS: &[&str] = &[
177 "self_attn.kv_a_proj_with_mqa.weight",
178 "self_attn.indexer.wk.weight",
179];
180
181impl TensorSpec {
182 pub fn expected_shape(&self, c: &Glm5NextKdaConfig) -> Vec<usize> {
183 self.dims
184 .iter()
185 .map(|d| match d {
186 Dim::Q => c.qkv_dim(),
187 Dim::X => c.hidden,
188 Dim::D => c.head_dim,
189 Dim::H => c.heads,
190 Dim::K => c.conv_kernel,
191 Dim::One => 1,
192 })
193 .collect()
194 }
195}
196
197#[derive(Clone, Copy, PartialEq, Eq, Debug)]
199pub enum AttnBlockKind {
200 Kda,
201 Dsa,
203 Unknown,
204}
205
206pub fn classify_attn_block(names: &[String]) -> AttnBlockKind {
209 let set: BTreeSet<&str> = names.iter().map(String::as_str).collect();
210 if DSA_MARKERS.iter().all(|m| set.contains(m)) {
211 return AttnBlockKind::Dsa;
212 }
213 if KDA_TENSORS.iter().all(|t| set.contains(t.name)) {
214 return AttnBlockKind::Kda;
215 }
216 AttnBlockKind::Unknown
217}
218
219#[derive(Clone, Debug, Default)]
222pub struct KdaBindReport {
223 pub layer_idx: usize,
224 pub bound: usize,
225 pub self_attn_seen: usize,
226 pub non_attn_seen: usize,
227 pub unknown_self_attn: Vec<String>,
228 pub bytes: usize,
229}
230
231fn upload(gpu: &dyn GpuBackend, bytes: &[u8]) -> Result<DevicePtr> {
232 let p = gpu.alloc(bytes.len().max(1))?;
233 gpu.copy_h2d(bytes, p)?;
234 Ok(p)
235}
236
237pub fn bind_kda_weights(
242 gpu: &dyn GpuBackend,
243 cfg: &Glm5NextKdaConfig,
244 layer_idx: usize,
245 src: &dyn KdaTensorSource,
246) -> Result<(Glm5NextKdaWeights, KdaBindReport)> {
247 cfg.validate()?;
248 let names = src.names();
249 let mut rep = KdaBindReport {
250 layer_idx,
251 ..Default::default()
252 };
253
254 let known: BTreeSet<&str> = KDA_TENSORS.iter().map(|t| t.name).collect();
255 for n in &names {
256 if n.starts_with("self_attn.") {
257 rep.self_attn_seen += 1;
258 if !known.contains(n.as_str()) {
259 rep.unknown_self_attn.push(n.clone());
260 }
261 } else {
262 rep.non_attn_seen += 1;
263 }
264 }
265 if !rep.unknown_self_attn.is_empty() {
266 bail!(
267 "layer {layer_idx}: {} unrecognised self_attn tensor(s): {:?} — a KDA block has \
268 exactly {} and this binder refuses to skip anything",
269 rep.unknown_self_attn.len(),
270 rep.unknown_self_attn,
271 KDA_TENSORS.len()
272 );
273 }
274
275 let mut fetch = |spec: &TensorSpec| -> Result<Vec<u8>> {
276 let t = src
277 .get(spec.name)
278 .with_context(|| format!("layer {layer_idx}: missing {}", spec.name))?;
279 if t.dtype != spec.dtype {
280 bail!(
281 "layer {layer_idx}: {} is {} but a KDA block requires {} — casting it would \
282 change the numerics",
283 spec.name,
284 t.dtype.name(),
285 spec.dtype.name()
286 );
287 }
288 let want = spec.expected_shape(cfg);
289 if t.shape != want {
290 bail!(
291 "layer {layer_idx}: {} has shape {:?}, expected {want:?}",
292 spec.name,
293 t.shape
294 );
295 }
296 let elem = match spec.dtype {
297 KdaDtype::Bf16 => 2,
298 KdaDtype::F32 => 4,
299 };
300 let expect_bytes = want.iter().product::<usize>() * elem;
301 if t.bytes.len() != expect_bytes {
302 bail!(
303 "layer {layer_idx}: {} is {} B, shape {want:?} implies {expect_bytes} B",
304 spec.name,
305 t.bytes.len()
306 );
307 }
308 rep.bound += 1;
309 rep.bytes += t.bytes.len();
310 Ok(t.bytes.to_vec())
311 };
312
313 let by_name = |n: &str| -> &TensorSpec { KDA_TENSORS.iter().find(|t| t.name == n).unwrap() };
314 let mut raw = |n: &str| fetch(by_name(n));
315
316 let q_proj = raw("self_attn.q_proj.weight")?;
317 let k_proj = raw("self_attn.k_proj.weight")?;
318 let v_proj = raw("self_attn.v_proj.weight")?;
319 let mut conv = raw("self_attn.q_conv1d.weight")?;
323 conv.extend_from_slice(&raw("self_attn.k_conv1d.weight")?);
324 conv.extend_from_slice(&raw("self_attn.v_conv1d.weight")?);
325 debug_assert_eq!(conv.len(), cfg.conv_dim() * cfg.conv_kernel * 2);
326 let f_a = raw("self_attn.f_a_proj.weight")?;
327 let f_b = raw("self_attn.f_b_proj.weight")?;
328 let g_a = raw("self_attn.g_a_proj.weight")?;
329 let g_b = raw("self_attn.g_b_proj.weight")?;
330 let b_proj = raw("self_attn.b_proj.weight")?;
331 let a_log = raw("self_attn.A_log")?;
332 let dt_bias = raw("self_attn.dt_bias")?;
333 let o_norm = raw("self_attn.o_norm.weight")?;
334 let o_proj = raw("self_attn.o_proj.weight")?;
335
336 let dw = |b: &[u8]| -> Result<DenseWeight> {
337 Ok(DenseWeight {
338 weight: upload(gpu, b)?,
339 })
340 };
341 let w = Glm5NextKdaWeights {
342 q_proj: dw(&q_proj)?,
343 k_proj: dw(&k_proj)?,
344 v_proj: dw(&v_proj)?,
345 conv: dw(&conv)?,
346 f_a: dw(&f_a)?,
347 f_b: dw(&f_b)?,
348 dt_bias: upload(gpu, &dt_bias)?,
349 a_log: upload(gpu, &a_log)?,
350 b_proj: dw(&b_proj)?,
351 g_a: dw(&g_a)?,
352 g_b: dw(&g_b)?,
353 o_norm: dw(&o_norm)?,
354 o_proj: dw(&o_proj)?,
355 };
356 if rep.bound != KDA_TENSORS.len() {
357 bail!(
358 "layer {layer_idx}: bound {} of {} tensors",
359 rep.bound,
360 KDA_TENSORS.len()
361 );
362 }
363 Ok((w, rep))
364}
365
366#[cfg(test)]
367mod tests {
368 use super::*;
369
370 fn cfg() -> Glm5NextKdaConfig {
371 Glm5NextKdaConfig {
372 hidden: 4096,
373 heads: 64,
374 head_dim: 128,
375 conv_kernel: 4,
376 gate_lower_bound: -5.0,
377 rms_norm_eps: 1e-5,
378 l2_eps: 1e-6,
379 chunk: 32,
380 }
381 }
382
383 #[test]
386 fn tensor_spec_matches_the_audited_checkpoint_shapes() {
387 let c = cfg();
388 let want: &[(&str, &str, &[usize])] = &[
389 ("self_attn.q_proj.weight", "BF16", &[8192, 4096]),
390 ("self_attn.k_proj.weight", "BF16", &[8192, 4096]),
391 ("self_attn.v_proj.weight", "BF16", &[8192, 4096]),
392 ("self_attn.q_conv1d.weight", "BF16", &[8192, 1, 4]),
393 ("self_attn.k_conv1d.weight", "BF16", &[8192, 1, 4]),
394 ("self_attn.v_conv1d.weight", "BF16", &[8192, 1, 4]),
395 ("self_attn.f_a_proj.weight", "BF16", &[128, 4096]),
396 ("self_attn.f_b_proj.weight", "BF16", &[8192, 128]),
397 ("self_attn.g_a_proj.weight", "BF16", &[128, 4096]),
398 ("self_attn.g_b_proj.weight", "BF16", &[8192, 128]),
399 ("self_attn.b_proj.weight", "BF16", &[64, 4096]),
400 ("self_attn.A_log", "F32", &[64]),
401 ("self_attn.dt_bias", "F32", &[8192]),
402 ("self_attn.o_norm.weight", "BF16", &[128]),
403 ("self_attn.o_proj.weight", "BF16", &[4096, 8192]),
404 ];
405 assert_eq!(
406 KDA_TENSORS.len(),
407 want.len(),
408 "the KDA block has exactly 15 tensors"
409 );
410 for (n, dt, sh) in want {
411 let s = KDA_TENSORS.iter().find(|t| &t.name == n).expect(n);
412 assert_eq!(s.dtype.name(), *dt, "{n} dtype");
413 assert_eq!(s.expected_shape(&c), sh.to_vec(), "{n} shape");
414 }
415 }
416
417 #[test]
420 fn dsa_and_mtp_blocks_do_not_classify_as_kda() {
421 let dsa: Vec<String> = [
422 "self_attn.kv_a_proj_with_mqa.weight",
423 "self_attn.kv_a_layernorm.weight",
424 "self_attn.kv_b_proj.weight",
425 "self_attn.q_a_proj.weight",
426 "self_attn.q_b_proj.weight",
427 "self_attn.indexer.wk.weight",
428 "self_attn.indexer.wq_b.weight",
429 "self_attn.o_proj.weight",
430 ]
431 .iter()
432 .map(|s| s.to_string())
433 .collect();
434 assert_eq!(classify_attn_block(&dsa), AttnBlockKind::Dsa);
435
436 let kda: Vec<String> = KDA_TENSORS.iter().map(|t| t.name.to_string()).collect();
437 assert_eq!(classify_attn_block(&kda), AttnBlockKind::Kda);
438
439 assert_eq!(classify_attn_block(&kda[1..]), AttnBlockKind::Unknown);
441 }
442
443 #[test]
444 fn chunk_width_is_bounded_by_the_shared_memory_ceiling() {
445 let mut c = cfg();
446 assert!(c.validate().is_ok(), "C=32 must fit");
447 assert!(c.smem_scan() <= SMEM_CEILING);
448 c.chunk = 64;
449 assert!(
450 c.validate().is_err(),
451 "C=64 needs 81920 B and must be rejected, not truncated"
452 );
453 }
454
455 use super::super::SMEM_CEILING;
456}