Skip to main content

cherenkov/qwen4_exp/
lowbit.rs

1//! Building the low-bit expert stores from the 4-bit one, in process.
2//! `experts2.bin` and `experts3.bin` coexist beside the source store;
3//! choosing or rebuilding one precision leaves the other file intact.
4//!
5//! `experts.bin` holds each expert's three projections as MLX affine
6//! 4-bit codes (`w = q * scale + bias`, groups of 64). A low-bit store
7//! keeps the same scales and biases and drops the bottom bits of the
8//! codes, reconstructing at the midpoint of the range each new code
9//! covers: `q >> 1` with `(2q + 0.5)` at three bits, `q >> 2` with
10//! `(4q + 1.5)` at two.
11//!
12//! The layout is the part that matters. A kernel that peels codes one at
13//! a time costs 2.3x the 4-bit kernel's time even though it reads fewer
14//! bytes, which makes the whole exercise pointless; these layouts put a
15//! word's codes where one masked cast (`& 0x03030303`, `& 0x01010101`)
16//! yields four codes that are CONSECUTIVE in the deinterleaved input
17//! stream, exactly as the 4-bit nibble trick does. See
18//! `fn_q2_rows_h` and `fn_q3_rows_h` in kernels/qwen4_exp/experts.metal,
19//! which must agree with the packing here bit for bit.
20
21use super::ExpertLayout;
22use crate::units::BYTES_PER_GB;
23use anyhow::{Context, Result};
24use std::io::Write as _;
25use std::os::unix::fs::FileExt as _;
26use std::path::{Path, PathBuf};
27use std::sync::atomic::{AtomicUsize, Ordering};
28
29/// Expert record layout shared by the packer, prefill, and decode.
30/// Low-bit records keep the base order: three weight matrices followed
31/// by six scale/bias blocks copied verbatim from the 4-bit record.
32#[derive(Clone, Copy, Debug)]
33pub struct Layout {
34    pub bits: u32,
35    pub stride: usize,
36    pub mat: usize,
37    pub scale_bytes: usize,
38    pub gate_w: usize,
39    pub up_w: usize,
40    pub down_w: usize,
41    pub gate_s: usize,
42    pub gate_b: usize,
43    pub up_s: usize,
44    pub up_b: usize,
45    pub down_s: usize,
46    pub down_b: usize,
47}
48
49const PAGE: usize = 16384;
50
51impl Layout {
52    /// Preserve the base store's offsets verbatim; prefill and decode use
53    /// this same description rather than reconstructing the record layout.
54    pub fn four_bit(e: &ExpertLayout) -> Self {
55        Self {
56            bits: 4,
57            stride: e.record_stride as usize,
58            mat: e.inter * e.hidden / 2,
59            scale_bytes: (e.gate_b - e.gate_s) as usize,
60            gate_w: e.gate_w as usize,
61            up_w: e.up_w as usize,
62            down_w: e.down_w as usize,
63            gate_s: e.gate_s as usize,
64            gate_b: e.gate_b as usize,
65            up_s: e.up_s as usize,
66            up_b: e.up_b as usize,
67            down_s: e.down_s as usize,
68            down_b: e.down_b as usize,
69        }
70    }
71
72    /// Address-table tag shared with the decode shaders.
73    pub fn kind(&self) -> u8 {
74        match self.bits {
75            3 => 1,
76            2 => 2,
77            _ => 0,
78        }
79    }
80
81    pub fn new(e: &ExpertLayout, bits: u32) -> Result<Self> {
82        anyhow::ensure!(
83            bits == 2 || bits == 3,
84            "low-bit store must be 2 or 3 bits, not {bits}"
85        );
86
87        let codes = e.inter * e.hidden;
88
89        anyhow::ensure!(
90            codes.is_multiple_of(32),
91            "matrices must be a whole number of 32-code chunks"
92        );
93
94        let mat = codes * bits as usize / 8;
95        // One scale and one bias per group of `group` codes, bf16.
96        let scale_bytes = (e.gate_b - e.gate_s) as usize;
97        let body = 3 * mat + 6 * scale_bytes;
98        let stride = body.div_ceil(PAGE) * PAGE;
99
100        Ok(Layout {
101            bits,
102            stride,
103            mat,
104            scale_bytes,
105            gate_w: 0,
106            up_w: mat,
107            down_w: 2 * mat,
108            gate_s: 3 * mat,
109            gate_b: 3 * mat + scale_bytes,
110            up_s: 3 * mat + 2 * scale_bytes,
111            up_b: 3 * mat + 3 * scale_bytes,
112            down_s: 3 * mat + 4 * scale_bytes,
113            down_b: 3 * mat + 5 * scale_bytes,
114        })
115    }
116
117    fn manifest_json(&self, records: usize) -> String {
118        format!(
119            "{{\n \"bits\": {},\n \"layout\": \"vectorized\",\n \"stride\": {},\n \"records\": {},\n \
120             \"w\": {{ \"gate\": {}, \"up\": {}, \"down\": {} }},\n \
121             \"s\": {{ \"gate_s\": {}, \"gate_b\": {}, \"up_s\": {}, \"up_b\": {}, \"down_s\": {}, \"down_b\": {} }},\n \
122             \"source\": \"experts.bin, built in process by src/qwen4_exp/lowbit.rs\"\n}}\n",
123            self.bits,
124            self.stride,
125            records,
126            self.gate_w,
127            self.up_w,
128            self.down_w,
129            self.gate_s,
130            self.gate_b,
131            self.up_s,
132            self.up_b,
133            self.down_s,
134            self.down_b
135        )
136    }
137}
138
139/// Bit position of each of a 32-code chunk's codes in the 2-bit layout:
140/// word index (0 or 1) and the offset within it. One `& 0x03030303` at
141/// shift `2p` then yields the four codes of group `p`, and those four are
142/// consecutive in `xe` (groups 0 and 1) or `xo` (groups 2 and 3).
143fn q2_slots() -> [(usize, u32); 32] {
144    let mut out = [(0usize, 0u32); 32];
145
146    for (c, slot) in out.iter_mut().enumerate() {
147        let (t, r) = (c / 16, c % 16);
148        let (p, b) = match (r % 2 == 0, r < 8) {
149            (true, true) => (0, r / 2),
150            (true, false) => (1, (r - 8) / 2),
151            (false, true) => (2, (r - 1) / 2),
152            (false, false) => (3, (r - 9) / 2),
153        };
154        *slot = (t, (8 * b + 2 * p) as u32);
155    }
156
157    out
158}
159
160/// Three-bit codes split into their upper two bits and lowest bit.
161/// The upper pair uses the 2-bit layout; the lowest bit goes into a third
162/// word at `8 * byte + 4 * word + group`. The shader reconstructs
163/// `code = 2 * upper + lowest` from the corresponding masked casts.
164fn q3_slots() -> [(usize, u32, u32); 32] {
165    let mut out = [(0usize, 0u32, 0u32); 32];
166
167    for (c, slot) in out.iter_mut().enumerate() {
168        let (t, r) = (c / 16, c % 16);
169        let (j, b) = match (r % 2 == 0, r < 8) {
170            (true, true) => (0, r / 2),
171            (true, false) => (1, (r - 8) / 2),
172            (false, true) => (2, (r - 1) / 2),
173            (false, false) => (3, (r - 9) / 2),
174        };
175        *slot = (t, (8 * b + 2 * j) as u32, (8 * b + 4 * t + j) as u32);
176    }
177
178    out
179}
180
181/// Repack one matrix of `codes` 4-bit codes (as u32 words of 8 nibbles)
182/// into `dst`.
183fn pack_matrix(src: &[u8], codes: usize, bits: u32, dst: &mut [u8]) {
184    let read_word = |i: usize| -> u32 {
185        u32::from_le_bytes([src[4 * i], src[4 * i + 1], src[4 * i + 2], src[4 * i + 3]])
186    };
187    let mut q4_codes = [0u8; 32];
188    let chunks = codes / 32;
189
190    if bits == 2 {
191        let slots = q2_slots();
192
193        for chunk in 0..chunks {
194            for w in 0..4 {
195                let word = read_word(chunk * 4 + w);
196
197                for j in 0..8 {
198                    q4_codes[w * 8 + j] = ((word >> (4 * j)) & 0xF) as u8;
199                }
200            }
201
202            let mut packed_words = [0u32; 2];
203
204            for (c, &(t, off)) in slots.iter().enumerate() {
205                packed_words[t] |= ((q4_codes[c] >> 2) as u32) << off;
206            }
207
208            let offset = chunk * 8;
209
210            dst[offset..offset + 4].copy_from_slice(&packed_words[0].to_le_bytes());
211            dst[offset + 4..offset + 8].copy_from_slice(&packed_words[1].to_le_bytes());
212        }
213    } else {
214        let slots = q3_slots();
215
216        for chunk in 0..chunks {
217            for w in 0..4 {
218                let word = read_word(chunk * 4 + w);
219
220                for j in 0..8 {
221                    q4_codes[w * 8 + j] = ((word >> (4 * j)) & 0xF) as u8;
222                }
223            }
224
225            let mut upper_words = [0u32; 2];
226            let mut lowest_bits = 0u32;
227
228            for (c, &(t, upper_offset, low_offset)) in slots.iter().enumerate() {
229                let v = q4_codes[c] >> 1;
230                upper_words[t] |= ((v >> 1) as u32) << upper_offset;
231                lowest_bits |= ((v & 1) as u32) << low_offset;
232            }
233
234            let offset = chunk * 12;
235
236            dst[offset..offset + 4].copy_from_slice(&upper_words[0].to_le_bytes());
237            dst[offset + 4..offset + 8].copy_from_slice(&upper_words[1].to_le_bytes());
238            dst[offset + 8..offset + 12].copy_from_slice(&lowest_bits.to_le_bytes());
239        }
240    }
241}
242
243fn pack_record(e: &ExpertLayout, l: &Layout, src: &[u8], dst: &mut [u8]) {
244    let gate_codes = e.inter * e.hidden;
245    let down_codes = e.hidden * e.inter;
246
247    for (src_off, dst_off, codes) in [
248        (e.gate_w as usize, l.gate_w, gate_codes),
249        (e.up_w as usize, l.up_w, gate_codes),
250        (e.down_w as usize, l.down_w, down_codes),
251    ] {
252        let bytes = codes / 2;
253
254        pack_matrix(
255            &src[src_off..src_off + bytes],
256            codes,
257            l.bits,
258            &mut dst[dst_off..dst_off + l.mat],
259        );
260    }
261
262    for (s, d) in [
263        (e.gate_s, l.gate_s),
264        (e.gate_b, l.gate_b),
265        (e.up_s, l.up_s),
266        (e.up_b, l.up_b),
267        (e.down_s, l.down_s),
268        (e.down_b, l.down_b),
269    ] {
270        dst[d..d + l.scale_bytes].copy_from_slice(&src[s as usize..s as usize + l.scale_bytes]);
271    }
272}
273
274fn paths(dir: &Path, bits: u32) -> (PathBuf, PathBuf) {
275    (
276        dir.join(format!("experts{bits}.bin")),
277        dir.join(format!("manifest{bits}.json")),
278    )
279}
280
281/// Whether a usable store of this layout is already on disk. The
282/// manifest is only a claim, so a few records are re-packed and compared
283/// byte for byte: that is what catches a store written by an older
284/// packing, which would otherwise be read as noise by the kernel.
285fn usable(dir: &Path, e: &ExpertLayout, l: &Layout, records: usize) -> bool {
286    let (bin, man) = paths(dir, l.bits);
287    let Ok(meta) = std::fs::metadata(&bin) else {
288        return false;
289    };
290
291    if meta.len() != (records * l.stride) as u64 {
292        return false;
293    }
294
295    let Ok(bytes) = std::fs::read(&man) else {
296        return false;
297    };
298    let Ok(v) = serde_json::from_slice::<serde_json::Value>(&bytes) else {
299        return false;
300    };
301
302    if v["layout"].as_str() != Some("vectorized")
303        || v["stride"].as_u64() != Some(l.stride as u64)
304        || v["records"].as_u64() != Some(records as u64)
305    {
306        return false;
307    }
308
309    spot_check(dir, e, l, records).unwrap_or(false)
310}
311
312/// Read-only readiness check shared by indexed preparation and the runtime.
313pub(crate) fn is_usable(dir: &Path, e: &ExpertLayout, bits: u32) -> Result<bool> {
314    let layout = Layout::new(e, bits)?;
315
316    Ok(usable(dir, e, &layout, e.layers * e.experts))
317}
318
319/// Re-pack a handful of records spread through the file and compare with
320/// what is stored. Reads about 15 MB, so it costs milliseconds.
321fn spot_check(dir: &Path, e: &ExpertLayout, l: &Layout, records: usize) -> Result<bool> {
322    let (bin, _) = paths(dir, l.bits);
323    let src = std::fs::File::open(dir.join("experts.bin"))?;
324    let dst = std::fs::File::open(&bin)?;
325    let stride4 = e.record_stride as usize;
326    let mut inbuf = vec![0u8; stride4];
327    let mut want = vec![0u8; l.stride];
328    let mut got = vec![0u8; l.stride];
329
330    for r in [0, records / 3, 2 * records / 3, records - 1] {
331        src.read_exact_at(&mut inbuf, (r * stride4) as u64)?;
332        dst.read_exact_at(&mut got, (r * l.stride) as u64)?;
333        want.fill(0);
334        pack_record(e, l, &inbuf, &mut want);
335
336        if want != got {
337            eprintln!(
338                "the {}-bit store does not match the current packing at record {r}",
339                l.bits
340            );
341
342            return Ok(false);
343        }
344    }
345
346    Ok(true)
347}
348
349/// Make sure the `bits`-bit store exists next to `experts.bin`, building
350/// it from the 4-bit records if it is missing, the wrong size, or in the
351/// older code-at-a-time layout. Returns the layout either way.
352///
353/// `force` rebuilds an existing store (--repack).
354pub fn ensure(dir: &Path, e: &ExpertLayout, bits: u32, force: bool) -> Result<Layout> {
355    ensure_with_policy(dir, e, bits, force, true)
356}
357
358pub(crate) fn ensure_with_policy(
359    dir: &Path,
360    e: &ExpertLayout,
361    bits: u32,
362    force: bool,
363    allow_build: bool,
364) -> Result<Layout> {
365    let layouts = ensure_selected(dir, e, &[bits], force, allow_build)?;
366
367    Ok(layouts[0])
368}
369
370/// Build missing targets together, reading each Q4 source record once.
371/// Duplicate precisions are ignored; valid cached stores are left untouched.
372pub fn ensure_many(dir: &Path, e: &ExpertLayout, bits: &[u32]) -> Result<Vec<Layout>> {
373    ensure_selected(dir, e, bits, false, true)
374}
375
376fn ensure_selected(
377    dir: &Path,
378    e: &ExpertLayout,
379    bits: &[u32],
380    force: bool,
381    allow_build: bool,
382) -> Result<Vec<Layout>> {
383    let mut selected = bits.to_vec();
384
385    selected.sort_unstable();
386    selected.dedup();
387
388    let layouts = selected
389        .iter()
390        .map(|&b| Layout::new(e, b))
391        .collect::<Result<Vec<_>>>()?;
392    let records = e.layers * e.experts;
393    let mut pending = Vec::new();
394
395    for l in &layouts {
396        if !force && usable(dir, e, l, records) {
397            continue;
398        }
399
400        anyhow::ensure!(
401            allow_build,
402            "the {}-bit expert store is missing or invalid; server policy forbids building it",
403            l.bits
404        );
405        pending.push(*l);
406    }
407
408    if pending.is_empty() {
409        return Ok(layouts);
410    }
411
412    // Check the combined requirement before invalidating or writing any target.
413    let additional_bytes = pending
414        .iter()
415        .map(|l| announce_build(dir, l, records))
416        .sum();
417
418    crate::storage::require_space(dir, additional_bytes)?;
419
420    for l in &pending {
421        let (_, man) = paths(dir, l.bits);
422
423        // Failed rebuilds must not leave manifests claiming partial files are ready.
424        if man.exists() {
425            std::fs::remove_file(man)?;
426        }
427    }
428
429    let t0 = std::time::Instant::now();
430
431    build(dir, e, &pending, records)?;
432
433    for l in &pending {
434        let (_, man) = paths(dir, l.bits);
435
436        std::fs::write(man, l.manifest_json(records))?;
437    }
438
439    let targets = pending
440        .iter()
441        .map(|l| l.bits.to_string())
442        .collect::<Vec<_>>()
443        .join("+");
444
445    eprintln!(
446        "built the {targets}-bit store in {:.0}s",
447        t0.elapsed().as_secs_f64()
448    );
449
450    Ok(layouts)
451}
452
453fn announce_build(dir: &Path, l: &Layout, records: usize) -> u64 {
454    let bits = l.bits;
455    let (bin, _) = paths(dir, bits);
456    let need = (records * l.stride) as u64;
457
458    eprintln!(
459        "{} the {bits}-bit expert store at {} ({:.1} GB; measured build about 76s, hardware/cache dependent)",
460        if bin.exists() {
461            "rebuilding"
462        } else {
463            "building"
464        },
465        bin.display(),
466        need as f64 / BYTES_PER_GB as f64
467    );
468
469    let have = std::fs::metadata(&bin).map(|m| m.len()).unwrap_or(0);
470
471    need.saturating_sub(have)
472}
473
474fn create_output(dir: &Path, l: &Layout, records: usize) -> Result<std::fs::File> {
475    let (bin, _) = paths(dir, l.bits);
476    let out = std::fs::OpenOptions::new()
477        .create(true)
478        // Preserve existing allocation; set_len sizes the store before every record is rewritten.
479        .truncate(false)
480        .read(true)
481        .write(true)
482        .open(&bin)
483        .with_context(|| format!("creating {}", bin.display()))?;
484
485    out.set_len((records * l.stride) as u64)?;
486
487    Ok(out)
488}
489
490fn build(dir: &Path, e: &ExpertLayout, layouts: &[Layout], records: usize) -> Result<()> {
491    let src_path = dir.join("experts.bin");
492    let src = std::fs::File::open(&src_path)
493        .with_context(|| format!("opening {}", src_path.display()))?;
494    let outputs = layouts
495        .iter()
496        .map(|l| Ok((*l, create_output(dir, l, records)?)))
497        .collect::<Result<Vec<_>>>()?;
498    let stride4 = e.record_stride as usize;
499    // One input and one reusable output buffer per worker, even for two targets.
500    let output_stride = layouts.iter().map(|l| l.stride).max().unwrap_or(0);
501    let next = AtomicUsize::new(0);
502    let done = AtomicUsize::new(0);
503    let workers = std::thread::available_parallelism().map_or(6, |n| n.get().min(8));
504
505    const BATCH: usize = 16;
506
507    std::thread::scope(|s| -> Result<()> {
508        let mut handles = Vec::new();
509
510        for w in 0..workers {
511            let (src, outputs, next, done) = (&src, &outputs, &next, &done);
512
513            handles.push(s.spawn(move || -> Result<()> {
514                let mut inbuf = vec![0u8; stride4];
515                let mut outbuf = vec![0u8; output_stride];
516
517                loop {
518                    let lo = next.fetch_add(BATCH, Ordering::Relaxed);
519
520                    if lo >= records {
521                        return Ok(());
522                    }
523
524                    pack_records(
525                        e,
526                        src,
527                        outputs,
528                        lo..(lo + BATCH).min(records),
529                        &mut inbuf,
530                        &mut outbuf,
531                    )?;
532
533                    let n = done.fetch_add(BATCH, Ordering::Relaxed) + BATCH;
534
535                    if w == 0 && n % 2048 < BATCH {
536                        eprint!("\r  {}/{records} records", n.min(records));
537
538                        let _ = std::io::stderr().flush();
539                    }
540                }
541            }));
542        }
543
544        for h in handles {
545            h.join()
546                .map_err(|_| anyhow::anyhow!("packer thread panicked"))??;
547        }
548
549        Ok(())
550    })?;
551    eprintln!("\r  {records}/{records} records");
552
553    for (_, out) in &outputs {
554        out.sync_all()?;
555    }
556
557    Ok(())
558}
559
560fn pack_records(
561    e: &ExpertLayout,
562    src: &std::fs::File,
563    outputs: &[(Layout, std::fs::File)],
564    records: std::ops::Range<usize>,
565    inbuf: &mut [u8],
566    outbuf: &mut [u8],
567) -> Result<()> {
568    for r in records {
569        src.read_exact_at(inbuf, r as u64 * e.record_stride)
570            .with_context(|| format!("reading record {r}"))?;
571
572        for (l, out) in outputs {
573            let record = &mut outbuf[..l.stride];
574
575            record.fill(0);
576            pack_record(e, l, inbuf, record);
577            out.write_all_at(record, (r * l.stride) as u64)
578                .with_context(|| format!("writing {}-bit record {r}", l.bits))?;
579        }
580    }
581
582    Ok(())
583}
584
585#[cfg(test)]
586#[path = "../../tests/unit/qwen4_exp/lowbit.rs"]
587mod tests;