Skip to main content

cherenkov/qwen4_exp/pack/
source.rs

1//! Lazy tensor views let both checkpoint formats use the same packed writer.
2//! BF16 conversion holds one quantization group at a time, including when a
3//! fused expert tensor spans an entire layer. No converted checkpoint is staged.
4
5use super::affine;
6use crate::model::{
7    CheckpointConventions, Preparation, TensorRole, describe_raw, qwen::tensor_role,
8};
9use crate::qwen4_exp::Qwen4ExpConfig;
10use crate::tensors::{Dtype, ModelWeights, TensorInfo};
11use anyhow::{Context, Result, ensure};
12use half::bf16;
13use memmap2::Advice;
14use std::collections::HashMap;
15use std::io::Write;
16use std::ops::Range;
17use std::path::Path;
18
19#[derive(Clone, Copy)]
20enum Part {
21    Weight,
22    Scale,
23    Bias,
24}
25
26#[derive(Clone, Copy)]
27struct Quantized {
28    group: usize,
29    part: Part,
30    /// Contiguous elements selected from each expert in a fused gate/up tensor.
31    segment: usize,
32    stride: usize,
33    start: usize,
34}
35
36#[derive(Clone, Copy)]
37enum Conversion {
38    Copy,
39    AddOne,
40    Quantize(Quantized),
41}
42
43pub(super) struct Tensor {
44    pub dtype: Dtype,
45    pub shape: Vec<usize>,
46    pub nbytes: usize,
47    source: TensorInfo,
48    conversion: Conversion,
49}
50
51pub(super) struct Source {
52    raw: ModelWeights,
53    pub tensors: HashMap<String, Tensor>,
54    /// Only BF16 import adjusts metadata; prequantized inputs remain unchanged.
55    pub config: Option<serde_json::Value>,
56}
57
58impl Source {
59    pub fn load(dir: &Path) -> Result<Self> {
60        let raw = ModelWeights::load_raw(dir)?;
61        let config = serde_json::from_slice(&std::fs::read(dir.join("config.json"))?)?;
62        let description = describe_raw(&raw.checkpoint, &config)?;
63
64        if let Preparation::Unsupported { reasons } = description.preparation() {
65            anyhow::bail!(
66                "unsupported checkpoint: {}",
67                serde_json::to_string(&reasons)?
68            );
69        }
70
71        let native = matches!(
72            description.format.conventions,
73            CheckpointConventions::Qwen4ExpHuggingFace
74        );
75        let mut source = Self {
76            raw,
77            tensors: HashMap::new(),
78            config: None,
79        };
80
81        if !native {
82            for (name, info) in &source.raw.tensors {
83                source.tensors.insert(name.clone(), Tensor::copy(info));
84            }
85
86            return Ok(source);
87        }
88
89        let cfg = Qwen4ExpConfig::load(dir)?;
90        let tensors = source.raw.tensors.clone();
91
92        for (name, info) in tensors {
93            source
94                .import(&name, &info)
95                .with_context(|| format!("importing {name}"))?;
96        }
97
98        source.configure(dir, &cfg)?;
99        eprintln!("BF16 input: quantizing to affine Q4 while packing");
100
101        Ok(source)
102    }
103
104    pub fn tensor(&self, name: &str) -> Result<&Tensor> {
105        self.tensors
106            .get(name)
107            .with_context(|| format!("tensor {name:?} not found"))
108    }
109
110    pub fn estimated_bytes(&self) -> u64 {
111        self.tensors.values().map(|t| t.nbytes as u64).sum()
112    }
113
114    pub fn prefetch(&self, tensor: &Tensor) {
115        // Do not fault in a whole BF16 layer just to convert its first group.
116        if self.config.is_some() {
117            return;
118        }
119
120        let t = &tensor.source;
121        let _ = self
122            .raw
123            .checkpoint
124            .objects
125            .mapping(crate::model::ObjectId(t.shard))
126            .expect("validated source object")
127            .advise_range(Advice::WillNeed, t.offset, t.nbytes);
128    }
129
130    pub fn bytes(&self, tensor: &Tensor) -> Result<&[u8]> {
131        ensure!(
132            matches!(tensor.conversion, Conversion::Copy),
133            "tensor requires conversion"
134        );
135
136        Ok(self.raw.tensor_bytes(&tensor.source))
137    }
138
139    pub fn write(&self, tensor: &Tensor, range: Range<usize>, out: &mut impl Write) -> Result<()> {
140        ensure!(
141            range.start <= range.end && range.end <= tensor.nbytes,
142            "tensor write out of bounds"
143        );
144
145        let bytes = self.raw.tensor_bytes(&tensor.source);
146
147        match tensor.conversion {
148            Conversion::Copy => out.write_all(&bytes[range])?,
149            Conversion::AddOne => write_norm(bytes, range, out)?,
150            Conversion::Quantize(q) => q.write(bytes, range, out)?,
151        }
152
153        Ok(())
154    }
155
156    fn insert(&mut self, name: String, tensor: Tensor) -> Result<()> {
157        ensure!(
158            !self.tensors.contains_key(&name),
159            "duplicate normalized tensor {name}"
160        );
161        self.tensors.insert(name, tensor);
162
163        Ok(())
164    }
165
166    fn import(&mut self, name: &str, info: &TensorInfo) -> Result<()> {
167        let Some(name) = normalized_name(name) else {
168            return Ok(());
169        };
170
171        ensure!(
172            matches!(info.dtype, Dtype::BF16 | Dtype::I64),
173            "native import expects BF16 weights or I64 metadata"
174        );
175        ensure!(info.shape.iter().all(|&n| n > 0), "empty tensor");
176
177        if let Some(prefix) = name.strip_suffix(".experts.gate_up_proj") {
178            return self.fused_experts(prefix, info);
179        }
180
181        if let Some(prefix) = name.strip_suffix(".experts.down_proj") {
182            ensure!(
183                info.shape.len() == 3,
184                "expected [experts, hidden, intermediate]"
185            );
186
187            return self.quantized(
188                &format!("{prefix}.switch_mlp.down_proj"),
189                info,
190                &info.shape,
191                0,
192                1,
193            );
194        }
195
196        if quantized_weight(&name, info) {
197            return self.quantized(name.trim_end_matches(".weight"), info, &info.shape, 0, 1);
198        }
199
200        let mut tensor = Tensor::copy(info);
201
202        if folded_norm(&name) {
203            ensure!(
204                info.dtype == Dtype::BF16 && info.shape.len() == 1,
205                "expected BF16 norm vector"
206            );
207
208            tensor.conversion = Conversion::AddOne;
209        }
210
211        self.insert(name, tensor)
212    }
213
214    fn fused_experts(&mut self, prefix: &str, info: &TensorInfo) -> Result<()> {
215        ensure!(
216            info.shape.len() == 3 && info.shape[1].is_multiple_of(2),
217            "expected [experts, 2 * intermediate, hidden]"
218        );
219
220        let shape = [info.shape[0], info.shape[1] / 2, info.shape[2]];
221
222        for (half, projection) in ["gate_proj", "up_proj"].iter().enumerate() {
223            self.quantized(
224                &format!("{prefix}.switch_mlp.{projection}"),
225                info,
226                &shape,
227                half,
228                2,
229            )?;
230        }
231
232        Ok(())
233    }
234
235    fn quantized(
236        &mut self,
237        prefix: &str,
238        info: &TensorInfo,
239        shape: &[usize],
240        half: usize,
241        halves: usize,
242    ) -> Result<()> {
243        ensure!(info.dtype == Dtype::BF16, "expected BF16 matrix");
244
245        let width = *shape.last().context("matrix shape missing")?;
246        let group = if prefix.contains(".ngram_embedding.") {
247            [64, 32, 16, 8]
248                .into_iter()
249                .find(|g| width.is_multiple_of(*g))
250                .context("n-gram width must be divisible by 8")?
251        } else {
252            ensure!(
253                width.is_multiple_of(64),
254                "matrix width {width} must be divisible by 64"
255            );
256
257            64
258        };
259        let segment = shape[shape.len() - 2] * width;
260
261        for (suffix, part, dtype, divisor) in [
262            ("weight", Part::Weight, Dtype::U32, 8),
263            ("scales", Part::Scale, Dtype::BF16, group),
264            ("biases", Part::Bias, Dtype::BF16, group),
265        ] {
266            let mut output_shape = shape.to_vec();
267            *output_shape.last_mut().unwrap() /= divisor;
268            let nbytes = output_shape.iter().product::<usize>() * dtype.size();
269            let tensor = Tensor {
270                dtype,
271                shape: output_shape,
272                nbytes,
273                source: info.clone(),
274                conversion: Conversion::Quantize(Quantized {
275                    group,
276                    part,
277                    segment,
278                    stride: segment * halves,
279                    start: segment * half,
280                }),
281            };
282
283            self.insert(format!("{prefix}.{suffix}"), tensor)?;
284        }
285
286        Ok(())
287    }
288
289    fn configure(&mut self, dir: &Path, cfg: &Qwen4ExpConfig) -> Result<()> {
290        let mut config: serde_json::Value =
291            serde_json::from_slice(&std::fs::read(dir.join("config.json"))?)?;
292        let has_mtp = self.tensors.keys().any(|name| name.starts_with("mtp."));
293
294        if has_mtp {
295            ensure!(
296                cfg.mtp_num_hidden_layers > 0,
297                "MTP weights present but config has no MTP layers"
298            );
299            self.tensor("mtp.fc_embedding.weight")?;
300            self.tensor("mtp.fc_hidden.weight")?;
301        } else {
302            let text = if config.get("text_config").is_some() {
303                &mut config["text_config"]
304            } else {
305                &mut config
306            };
307            text["mtp_num_hidden_layers"] = 0.into();
308
309            if let Some(mtp) = text.get_mut("mtp") {
310                mtp["num_hidden_layers"] = 0.into();
311            }
312
313            eprintln!("no MTP weights found; the packed model will use ordinary decode");
314        }
315
316        self.config = Some(config);
317
318        Ok(())
319    }
320}
321
322impl Tensor {
323    fn copy(source: &TensorInfo) -> Self {
324        Self {
325            dtype: source.dtype,
326            shape: source.shape.clone(),
327            nbytes: source.nbytes,
328            source: source.clone(),
329            conversion: Conversion::Copy,
330        }
331    }
332}
333
334impl Quantized {
335    fn write(self, source: &[u8], range: Range<usize>, out: &mut impl Write) -> Result<()> {
336        let size = match self.part {
337            Part::Weight => self.group / 2,
338            _ => 2,
339        };
340
341        ensure!(
342            range.start.is_multiple_of(size) && range.end.is_multiple_of(size),
343            "unaligned quantization group"
344        );
345
346        for index in range.start / size..range.end / size {
347            let element = index * self.group;
348            let offset =
349                (self.start + element / self.segment * self.stride + element % self.segment) * 2;
350            let bytes = source
351                .get(offset..offset + self.group * 2)
352                .context("fused tensor range out of bounds")?;
353            let group = affine::quantize(bytes)?;
354            let data = match self.part {
355                Part::Weight => &group.weight[..size],
356                Part::Scale => &group.scale,
357                Part::Bias => &group.bias,
358            };
359
360            out.write_all(data)?;
361        }
362
363        Ok(())
364    }
365}
366
367fn normalized_name(name: &str) -> Option<String> {
368    if let Some(suffix) = name.strip_prefix("model.language_model.") {
369        return Some(format!("language_model.model.{suffix}"));
370    }
371
372    if name == "lm_head.weight" {
373        return Some("language_model.lm_head.weight".into());
374    }
375
376    if name.starts_with("mtp.") {
377        return Some(name.into());
378    }
379
380    if name.starts_with("model.visual.") || name.starts_with("vision_tower.") {
381        return None;
382    }
383
384    // HF text-only checkpoints omit the outer multimodal language_model.
385    name.strip_prefix("model.")
386        .map(|suffix| format!("language_model.model.{suffix}"))
387}
388
389fn quantized_weight(name: &str, info: &TensorInfo) -> bool {
390    matches!(
391        tensor_role(name, info.shape.len()),
392        TensorRole::Projection | TensorRole::Embedding | TensorRole::NgramEmbedding
393    )
394}
395
396fn folded_norm(name: &str) -> bool {
397    [
398        "hc_norm",
399        "q_norm",
400        "k_norm",
401        "q_layernorm",
402        "k_layernorm",
403        "norm_key",
404        "norm_query",
405        "norm_conv",
406    ]
407    .iter()
408    .any(|norm| name.ends_with(&format!(".{norm}.weight")))
409}
410
411fn write_norm(bytes: &[u8], range: Range<usize>, out: &mut impl Write) -> Result<()> {
412    ensure!(
413        range.start.is_multiple_of(2) && range.end.is_multiple_of(2),
414        "unaligned BF16 norm"
415    );
416
417    for pair in bytes[range].as_chunks::<2>().0 {
418        let value = bf16::from_bits(u16::from_le_bytes(*pair)).to_f32();
419
420        ensure!(value.is_finite(), "non-finite norm weight");
421        out.write_all(&bf16::from_f32(1.0 + value).to_le_bytes())?;
422    }
423
424    Ok(())
425}
426
427#[cfg(test)]
428#[path = "../../../tests/unit/qwen4_exp/pack/source.rs"]
429mod tests;