1use 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#[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 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 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 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
139fn 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
160fn 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
181fn 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
281fn 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
312pub(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
319fn 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
349pub 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
370pub 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 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 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 .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 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;