avarok_core/kimi_k3/
cpu_bind.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! Name → [`K3CpuModel`] field bind for unpacked K3 safetensors.
4
5use std::collections::HashMap;
6
7use anyhow::{Context, Result};
8
9use super::cpu_weights::{
10    DenseMlp, K3CpuLayer, K3CpuModel, KdaWeights, MixerW, MlaWeights, MlpW, MoeWeights,
11};
12use super::kda::KdaConfig;
13use super::latent_moe::LatentMoeConfig;
14use super::layer::{K3Graph, K3LayerSpec, MixerKind, MlpKind};
15use super::mla::MlaConfig;
16use crate::config::ModelConfig;
17
18pub fn text_key(prefix: &str, rest: &str) -> String {
19    let p = prefix.trim_end_matches('.');
20    if p.is_empty() {
21        rest.to_string()
22    } else {
23        format!("{p}.{rest}")
24    }
25}
26
27pub(super) fn inventory(
28    c: &ModelConfig,
29    graph: &K3Graph,
30    kda: &KdaConfig,
31    mla: &MlaConfig,
32    moe: &LatentMoeConfig,
33) -> Vec<(String, usize)> {
34    let p = c.weight_prefix.as_str();
35    let h = c.hidden_size;
36    let mut v = vec![
37        (text_key(p, "model.embed_tokens.weight"), c.vocab_size * h),
38        (text_key(p, "model.norm.weight"), h),
39        (text_key(p, "model.output_attn_res_proj.weight"), h),
40        (text_key(p, "model.output_attn_res_norm.weight"), h),
41    ];
42    if !c.tie_word_embeddings {
43        v.push((text_key(p, "lm_head.weight"), c.vocab_size * h));
44    }
45    for spec in &graph.layers {
46        v.extend(layer_inventory(p, spec, c, kda, mla, moe));
47    }
48    v
49}
50
51fn layer_inventory(
52    prefix: &str,
53    spec: &K3LayerSpec,
54    c: &ModelConfig,
55    kda: &KdaConfig,
56    mla: &MlaConfig,
57    moe: &LatentMoeConfig,
58) -> Vec<(String, usize)> {
59    let lp = text_key(prefix, &format!("model.layers.{}", spec.index));
60    let h = c.hidden_size;
61    let mut k = vec![
62        (format!("{lp}.input_layernorm.weight"), h),
63        (format!("{lp}.post_attention_layernorm.weight"), h),
64        (format!("{lp}.mlp_res_norm.weight"), h),
65        (format!("{lp}.mlp_res_proj.weight"), h),
66        (format!("{lp}.self_attention_res_norm.weight"), h),
67        (format!("{lp}.self_attention_res_proj.weight"), h),
68    ];
69    match spec.mixer {
70        MixerKind::Kda => {
71            let q = kda.qkv_dim();
72            k.extend([
73                (format!("{lp}.self_attn.g_proj.weight"), q * h),
74                (format!("{lp}.self_attn.o_proj.weight"), h * q),
75                (format!("{lp}.self_attn.A_log"), kda.heads),
76                (format!("{lp}.self_attn.b_proj.weight"), kda.heads * h),
77                (format!("{lp}.self_attn.dt_bias"), q),
78                (format!("{lp}.self_attn.f_a_proj.weight"), kda.head_dim * h),
79                (format!("{lp}.self_attn.f_b_proj.weight"), q * kda.head_dim),
80                (
81                    format!("{lp}.self_attn.k_conv1d.weight"),
82                    q * kda.conv_kernel,
83                ),
84                (format!("{lp}.self_attn.k_proj.weight"), q * h),
85                (format!("{lp}.self_attn.o_norm.weight"), kda.head_dim),
86                (
87                    format!("{lp}.self_attn.q_conv1d.weight"),
88                    q * kda.conv_kernel,
89                ),
90                (format!("{lp}.self_attn.q_proj.weight"), q * h),
91                (
92                    format!("{lp}.self_attn.v_conv1d.weight"),
93                    q * kda.conv_kernel,
94                ),
95                (format!("{lp}.self_attn.v_proj.weight"), q * h),
96            ]);
97        }
98        MixerKind::Mla => {
99            let qk = mla.heads * mla.qk_head_dim();
100            let dv = mla.heads * mla.v_head_dim;
101            let kv_in = mla.kv_lora_rank + mla.qk_rope_head_dim;
102            let kv_b = mla.heads * (mla.qk_nope_head_dim + mla.v_head_dim);
103            k.extend([
104                (format!("{lp}.self_attn.g_proj.weight"), dv * h),
105                (format!("{lp}.self_attn.o_proj.weight"), h * dv),
106                (
107                    format!("{lp}.self_attn.kv_a_layernorm.weight"),
108                    mla.kv_lora_rank,
109                ),
110                (
111                    format!("{lp}.self_attn.kv_a_proj_with_mqa.weight"),
112                    kv_in * h,
113                ),
114                (
115                    format!("{lp}.self_attn.kv_b_proj.weight"),
116                    kv_b * mla.kv_lora_rank,
117                ),
118                (
119                    format!("{lp}.self_attn.q_a_layernorm.weight"),
120                    mla.q_lora_rank,
121                ),
122                (
123                    format!("{lp}.self_attn.q_a_proj.weight"),
124                    mla.q_lora_rank * h,
125                ),
126                (
127                    format!("{lp}.self_attn.q_b_proj.weight"),
128                    qk * mla.q_lora_rank,
129                ),
130            ]);
131        }
132    }
133    match spec.mlp {
134        MlpKind::Dense => {
135            let inter = c.intermediate_size;
136            k.extend([
137                (format!("{lp}.mlp.down_proj.weight"), h * inter),
138                (format!("{lp}.mlp.gate_proj.weight"), inter * h),
139                (format!("{lp}.mlp.up_proj.weight"), inter * h),
140            ]);
141        }
142        MlpKind::LatentMoe => moe_inventory(&mut k, &lp, c, moe, h),
143    }
144    k
145}
146
147fn moe_inventory(
148    k: &mut Vec<(String, usize)>,
149    lp: &str,
150    c: &ModelConfig,
151    moe: &LatentMoeConfig,
152    h: usize,
153) {
154    let eh = moe.expert_hidden;
155    let lat = moe.latent;
156    let shared = if c.shared_expert_intermediate_size > 0 {
157        c.shared_expert_intermediate_size
158    } else {
159        eh
160    };
161    k.extend([
162        (
163            format!("{lp}.block_sparse_moe.gate.weight"),
164            moe.n_routed * h,
165        ),
166        (
167            format!("{lp}.block_sparse_moe.routed_expert_down_proj.weight"),
168            lat * h,
169        ),
170        (
171            format!("{lp}.block_sparse_moe.routed_expert_norm.weight"),
172            lat,
173        ),
174        (
175            format!("{lp}.block_sparse_moe.routed_expert_up_proj.weight"),
176            h * lat,
177        ),
178    ]);
179    if c.use_routing_bias {
180        k.push((
181            format!("{lp}.block_sparse_moe.gate.e_score_correction_bias"),
182            moe.n_routed,
183        ));
184    }
185    if moe.n_shared > 0 {
186        k.extend([
187            (
188                format!("{lp}.block_sparse_moe.shared_experts.down_proj.weight"),
189                h * shared,
190            ),
191            (
192                format!("{lp}.block_sparse_moe.shared_experts.gate_proj.weight"),
193                shared * h,
194            ),
195            (
196                format!("{lp}.block_sparse_moe.shared_experts.up_proj.weight"),
197                shared * h,
198            ),
199        ]);
200    }
201    for e in 0..moe.n_routed {
202        k.extend([
203            (
204                format!("{lp}.block_sparse_moe.experts.{e}.w1.weight"),
205                eh * lat,
206            ),
207            (
208                format!("{lp}.block_sparse_moe.experts.{e}.w2.weight"),
209                lat * eh,
210            ),
211            (
212                format!("{lp}.block_sparse_moe.experts.{e}.w3.weight"),
213                eh * lat,
214            ),
215        ]);
216    }
217}
218
219fn pull(got: &mut HashMap<String, Vec<f32>>, prefix: &str, rest: &str) -> Result<Vec<f32>> {
220    let k = text_key(prefix, rest);
221    got.remove(&k)
222        .with_context(|| format!("internal: missing {k}"))
223}
224
225pub(super) fn assemble(
226    c: &ModelConfig,
227    graph: K3Graph,
228    kda: KdaConfig,
229    mla: MlaConfig,
230    moe: LatentMoeConfig,
231    got: &mut HashMap<String, Vec<f32>>,
232) -> Result<K3CpuModel> {
233    let p = c.weight_prefix.as_str();
234    let mut layers = Vec::with_capacity(graph.layers.len());
235    for spec in &graph.layers {
236        layers.push(assemble_layer(p, spec, c, &moe, got)?);
237    }
238    let lm_head = match pull(got, p, "lm_head.weight") {
239        Ok(w) => w,
240        Err(_) if c.tie_word_embeddings => pull(got, p, "model.embed_tokens.weight")?,
241        Err(e) => return Err(e),
242    };
243    let embed = if c.tie_word_embeddings {
244        lm_head.clone()
245    } else {
246        pull(got, p, "model.embed_tokens.weight")?
247    };
248    Ok(K3CpuModel {
249        graph,
250        kda,
251        mla,
252        moe,
253        dense_intermediate: c.intermediate_size,
254        eps: c.rms_norm_eps as f32,
255        rope_theta: c.rope_theta as f32,
256        vocab: c.vocab_size,
257        embed,
258        lm_head,
259        final_norm: pull(got, p, "model.norm.weight")?,
260        output_res_proj: pull(got, p, "model.output_attn_res_proj.weight")?,
261        output_res_norm: pull(got, p, "model.output_attn_res_norm.weight")?,
262        layers,
263    })
264}
265
266pub fn assemble_layer(
267    prefix: &str,
268    spec: &K3LayerSpec,
269    c: &ModelConfig,
270    moe: &LatentMoeConfig,
271    got: &mut HashMap<String, Vec<f32>>,
272) -> Result<K3CpuLayer> {
273    let lp = format!("model.layers.{}", spec.index);
274    let mixer = match spec.mixer {
275        MixerKind::Kda => assemble_kda(prefix, &lp, got)?,
276        MixerKind::Mla => assemble_mla(prefix, &lp, got)?,
277    };
278    let mlp = match spec.mlp {
279        MlpKind::Dense => MlpW::Dense(DenseMlp {
280            gate: pull(got, prefix, &format!("{lp}.mlp.gate_proj.weight"))?,
281            up: pull(got, prefix, &format!("{lp}.mlp.up_proj.weight"))?,
282            down: pull(got, prefix, &format!("{lp}.mlp.down_proj.weight"))?,
283        }),
284        MlpKind::LatentMoe => assemble_moe(prefix, &lp, c, moe, got)?,
285    };
286    Ok(K3CpuLayer {
287        spec: *spec,
288        input_norm: pull(got, prefix, &format!("{lp}.input_layernorm.weight"))?,
289        post_norm: pull(
290            got,
291            prefix,
292            &format!("{lp}.post_attention_layernorm.weight"),
293        )?,
294        attn_res_proj: pull(got, prefix, &format!("{lp}.self_attention_res_proj.weight"))?,
295        attn_res_norm: pull(got, prefix, &format!("{lp}.self_attention_res_norm.weight"))?,
296        mlp_res_proj: pull(got, prefix, &format!("{lp}.mlp_res_proj.weight"))?,
297        mlp_res_norm: pull(got, prefix, &format!("{lp}.mlp_res_norm.weight"))?,
298        mixer,
299        mlp,
300    })
301}
302
303fn assemble_kda(prefix: &str, lp: &str, got: &mut HashMap<String, Vec<f32>>) -> Result<MixerW> {
304    let q = pull(got, prefix, &format!("{lp}.self_attn.q_conv1d.weight"))?;
305    let k = pull(got, prefix, &format!("{lp}.self_attn.k_conv1d.weight"))?;
306    let v = pull(got, prefix, &format!("{lp}.self_attn.v_conv1d.weight"))?;
307    let mut conv = q;
308    conv.extend(k);
309    conv.extend(v);
310    Ok(MixerW::Kda(KdaWeights {
311        q_proj: pull(got, prefix, &format!("{lp}.self_attn.q_proj.weight"))?,
312        k_proj: pull(got, prefix, &format!("{lp}.self_attn.k_proj.weight"))?,
313        v_proj: pull(got, prefix, &format!("{lp}.self_attn.v_proj.weight"))?,
314        conv,
315        f_a: pull(got, prefix, &format!("{lp}.self_attn.f_a_proj.weight"))?,
316        f_b: pull(got, prefix, &format!("{lp}.self_attn.f_b_proj.weight"))?,
317        dt_bias: pull(got, prefix, &format!("{lp}.self_attn.dt_bias"))?,
318        a_log: pull(got, prefix, &format!("{lp}.self_attn.A_log"))?,
319        b_proj: pull(got, prefix, &format!("{lp}.self_attn.b_proj.weight"))?,
320        g_proj: pull(got, prefix, &format!("{lp}.self_attn.g_proj.weight"))?,
321        o_norm: pull(got, prefix, &format!("{lp}.self_attn.o_norm.weight"))?,
322        o_proj: pull(got, prefix, &format!("{lp}.self_attn.o_proj.weight"))?,
323    }))
324}
325
326fn assemble_mla(prefix: &str, lp: &str, got: &mut HashMap<String, Vec<f32>>) -> Result<MixerW> {
327    Ok(MixerW::Mla(MlaWeights {
328        q_a: pull(got, prefix, &format!("{lp}.self_attn.q_a_proj.weight"))?,
329        q_a_ln: pull(got, prefix, &format!("{lp}.self_attn.q_a_layernorm.weight"))?,
330        q_b: pull(got, prefix, &format!("{lp}.self_attn.q_b_proj.weight"))?,
331        kv_a: pull(
332            got,
333            prefix,
334            &format!("{lp}.self_attn.kv_a_proj_with_mqa.weight"),
335        )?,
336        kv_a_ln: pull(
337            got,
338            prefix,
339            &format!("{lp}.self_attn.kv_a_layernorm.weight"),
340        )?,
341        kv_b: pull(got, prefix, &format!("{lp}.self_attn.kv_b_proj.weight"))?,
342        g_proj: pull(got, prefix, &format!("{lp}.self_attn.g_proj.weight"))?,
343        o_proj: pull(got, prefix, &format!("{lp}.self_attn.o_proj.weight"))?,
344    }))
345}
346
347fn assemble_moe(
348    prefix: &str,
349    lp: &str,
350    c: &ModelConfig,
351    moe: &LatentMoeConfig,
352    got: &mut HashMap<String, Vec<f32>>,
353) -> Result<MlpW> {
354    let bias = if c.use_routing_bias {
355        pull(
356            got,
357            prefix,
358            &format!("{lp}.block_sparse_moe.gate.e_score_correction_bias"),
359        )?
360    } else {
361        vec![0.0; moe.n_routed]
362    };
363    let shared = if moe.n_shared > 0 {
364        Some(DenseMlp {
365            gate: pull(
366                got,
367                prefix,
368                &format!("{lp}.block_sparse_moe.shared_experts.gate_proj.weight"),
369            )?,
370            up: pull(
371                got,
372                prefix,
373                &format!("{lp}.block_sparse_moe.shared_experts.up_proj.weight"),
374            )?,
375            down: pull(
376                got,
377                prefix,
378                &format!("{lp}.block_sparse_moe.shared_experts.down_proj.weight"),
379            )?,
380        })
381    } else {
382        None
383    };
384    let mut experts = Vec::with_capacity(moe.n_routed);
385    for e in 0..moe.n_routed {
386        let w1k = format!("{lp}.block_sparse_moe.experts.{e}.w1.weight");
387        if !got.contains_key(&text_key(prefix, &w1k)) {
388            // Packed MXFP4: CUDA grouped GEMM owns w1/w2/w3.
389            experts.push((Vec::new(), Vec::new(), Vec::new()));
390            continue;
391        }
392        experts.push((
393            pull(got, prefix, &w1k)?,
394            pull(
395                got,
396                prefix,
397                &format!("{lp}.block_sparse_moe.experts.{e}.w2.weight"),
398            )?,
399            pull(
400                got,
401                prefix,
402                &format!("{lp}.block_sparse_moe.experts.{e}.w3.weight"),
403            )?,
404        ));
405    }
406    Ok(MlpW::Moe(MoeWeights {
407        down: pull(
408            got,
409            prefix,
410            &format!("{lp}.block_sparse_moe.routed_expert_down_proj.weight"),
411        )?,
412        up: pull(
413            got,
414            prefix,
415            &format!("{lp}.block_sparse_moe.routed_expert_up_proj.weight"),
416        )?,
417        norm: pull(
418            got,
419            prefix,
420            &format!("{lp}.block_sparse_moe.routed_expert_norm.weight"),
421        )?,
422        router: pull(got, prefix, &format!("{lp}.block_sparse_moe.gate.weight"))?,
423        bias,
424        experts,
425        shared,
426    }))
427}