From fb785519400714c1bc2a7dd5c18751f76184e601 Mon Sep 17 00:00:00 2001 From: juliankang4 <200967947+juliankang4@users.noreply.github.com> Date: Sun, 25 Oct 2026 00:16:34 +0900 Subject: [PATCH 01/10] refset: the draft-layer test refuses a set that names two draft layers --- crates/refset/src/mtpref/tests.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/refset/src/mtpref/tests.rs b/crates/refset/src/mtpref/tests.rs index 827a423f3..4a301c620 100644 --- a/crates/refset/src/mtpref/tests.rs +++ b/crates/refset/src/mtpref/tests.rs @@ -399,17 +399,17 @@ fn the_draft_layer_is_the_files() { update" ) }; - let open = |layer: Option| { - let row = layer.map(eh); + let open = |layers: &[usize]| { + let named: Vec = layers.iter().map(|&n| eh(n)).collect(); let mut rows: Vec<&str> = own_rows(); - rows.extend(row.as_deref()); + rows.extend(named.iter().map(String::as_str)); open_own(&dir, &rows).unwrap_or_else(|e| panic!("{e}")) }; - open(Some(48)) + open(&[48]) .check_draft_layer(48) .unwrap_or_else(|e| panic!("{e}")); - for (layer, named) in [(Some(47), "[47]"), (None, "[]")] { - let e = open(layer) + for (layers, named) in [(&[47][..], "[47]"), (&[], "[]"), (&[47, 48], "[47, 48]")] { + let e = open(layers) .check_draft_layer(48) .expect_err("another draft layer"); let want = format!( @@ -417,7 +417,7 @@ fn the_draft_layer_is_the_files() { ); assert!( matches!(&e, RefError::Malformed { what, .. } if *what == want), - "{layer:?}: {e}" + "{layers:?}: {e}" ); } remove(&dir); From 30f577c65a1fcdcbcc0a1278073c972466e06982 Mon Sep 17 00:00:00 2001 From: juliankang4 <200967947+juliankang4@users.noreply.github.com> Date: Sun, 25 Oct 2026 00:16:34 +0900 Subject: [PATCH 02/10] check-arch: the self-test pins rule 2's literal scan on fixed lines --- tools/check-arch.sh | 14 ++++++++++++-- tools/check-recipes.sh | 3 ++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/tools/check-arch.sh b/tools/check-arch.sh index ec9519fae..e0a2d038e 100644 --- a/tools/check-arch.sh +++ b/tools/check-arch.sh @@ -21,7 +21,7 @@ # ①·④는 어느 모드에서도 엄격하다. # Exit: 0 clean, 1 a violation, 2 no verdict: an unknown argument, no architecture directory, or a listing or scan whose # grep or find fails (a file it cannot read never passes as a file with no hit). `--self-test` runs the architecture -# listing and each scan on a missing path and exits. +# listing and each scan on a missing path, rule ②'s scan on fixed lines, and exits. set -euo pipefail cd "$(dirname "$0")/.." # shellcheck source=tools/grep-answer.sh @@ -133,8 +133,10 @@ scan_spawns() { # scan_spawns : rule ⑤'s spawn count per file into sp } # The listing and each scan on a missing path end with no verdict, and each scan on /dev/null (no hit) is clean. +# Rule ②'s awk reads a key's dot as a dot: the bare "blk." beside a name that only starts like a key is clean, and a +# key with its dot beside it is a hit. self_test() { - local fails=0 out rc n scan + local fails=0 out rc n scan d expect() { [ "$2" = "$3" ] || { echo "check-arch: self-test $1: got [$2], want [$3]" >&2; fails=$((fails + 1)); } } @@ -156,6 +158,14 @@ self_test() { rc=0 out=$( ("scan_$scan" /dev/null && printf '%s' "${lits-}${archread-}${archpath-}${spawned-}") 2>&1) || rc=$? expect "$scan-clean" "$rc $out" "0 " done + d=$(mktemp -d) + printf '%s\n' 'let k = ("blk.", "deepseek2x");' > "$d/a.rs" + rc=0 out=$( (scan_lits "$d" && printf '%s' "$lits") 2>&1) || rc=$? + expect lits-dot "$rc $out" "0 " + printf '%s\n' 'let k = ("blk.", "deepseek2.expert_count");' > "$d/a.rs" + rc=0 out=$( (scan_lits "$d" && printf '%s' "$lits") 2>&1) || rc=$? + expect lits-key "$rc $out" "0 $d/a.rs:1:let k = (\"blk.\", \"deepseek2.expert_count\");" + rm -rf "$d" [ "$fails" = 0 ] || { echo "check-arch: self-test: $fails failed" >&2; return 1; } echo "check-arch: self-test ok" } diff --git a/tools/check-recipes.sh b/tools/check-recipes.sh index 412568a9f..c530742ab 100755 --- a/tools/check-recipes.sh +++ b/tools/check-recipes.sh @@ -206,7 +206,8 @@ blk_scancheck() { tool_selftest "check-$1.sh"; } # (a build.rs that is a link, and a broken one), no tree walk. blk_unsafecount() { blk_scancheck unsafe; } # check-arch.sh scans the tree for the architecture split's five rules: a scan whose grep or find cannot read ends the -# check with no verdict. The architecture listing and each scan are tested here on a missing path. +# check with no verdict. The architecture listing and each scan are tested here on a missing path, and rule 2's literal +# scan on fixed lines. blk_archscan() { blk_scancheck arch; } # check-loads.sh scans crates/*/src for a generated module load outside the shared slot: a source file it cannot list # or read ends the check with 70. The family and load scans are tested here on fixed input and a missing file. From aec3b9a88ad77ebe60d4ceb1f3555e82bb5bcc2a Mon Sep 17 00:00:00 2001 From: juliankang4 <200967947+juliankang4@users.noreply.github.com> Date: Sun, 25 Oct 2026 00:16:39 +0900 Subject: [PATCH 03/10] gpu: the link relay tests fill and check every word of a span --- crates/gpu/src/host/link/pump.rs | 44 ++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/crates/gpu/src/host/link/pump.rs b/crates/gpu/src/host/link/pump.rs index f0c0b6813..38bc0ae14 100644 --- a/crates/gpu/src/host/link/pump.rs +++ b/crates/gpu/src/host/link/pump.rs @@ -772,6 +772,7 @@ pub(super) mod tests { const PROG: usize = 128; const IMAGE: usize = 192; const ROWS: usize = 256; + const SPAN: usize = 64; const TAIL: usize = 320; const GO_1: usize = 8; const CNT_1: usize = 72; @@ -789,7 +790,7 @@ pub(super) mod tests { // The row's go, which publishes its image, and its done, which publishes // its rows and counts in the progress word. fn row() -> Vec { - let (image, rows) = (IMAGE..IMAGE + 64, ROWS..ROWS + 64); + let (image, rows) = (IMAGE..IMAGE + SPAN, ROWS..ROWS + SPAN); vec![ Crossing { counter: GO, @@ -868,10 +869,25 @@ pub(super) mod tests { // The model cards run the tier protocol's ops: add one to the counter // each writes, wait for at least one on the counter each reads and take - // one back. Goes `ks` carry image k and dones rows k + ROWS_OF; the bytes - // are checked for every go up to `checked`, the ones before a release. + // one back. Goes `ks` carry image k and dones rows k + ROWS_OF in every + // word of the span; the bytes are checked for every go up to `checked`, + // the ones before a release. const ROWS_OF: u32 = 1 << 20; + fn fill(p: &Mem, at: usize, v: u32) { + for off in (at..at + SPAN).step_by(4) { + p.word(off).store(v, Ordering::Relaxed); + } + } + + /// The first word of the span at `at` that is not `v`, as (byte, word). + fn stray(p: &Mem, at: usize, v: u32) -> Option<(usize, u32)> { + (at..at + SPAN) + .step_by(4) + .map(|off| (off, p.word(off).load(Ordering::Relaxed))) + .find(|&(_, w)| w != v) + } + fn tier_card( tier: Arc, ks: std::ops::RangeInclusive, @@ -883,11 +899,14 @@ pub(super) mod tests { if !wait_take(&tier, GO, until) { return Err(format!("the tier's go {k} never came")); } - let img = tier.word(IMAGE).load(Ordering::Relaxed); - if k <= checked && img != k { - return Err(format!("the tier's go {k} carried the image of go {img}")); + if k <= checked + && let Some((off, img)) = stray(&tier, IMAGE, k) + { + return Err(format!( + "the tier's go {k} carried the image of go {img} at byte {off}" + )); } - tier.word(ROWS).store(k + ROWS_OF, Ordering::Relaxed); + fill(&tier, ROWS, k + ROWS_OF); tier.word(CNT).fetch_add(1, Ordering::Release); tier.word(PROG).fetch_add(1, Ordering::Release); } @@ -902,14 +921,17 @@ pub(super) mod tests { until: Instant, ) -> Result<(), String> { for k in ks { - stage.word(IMAGE).store(k, Ordering::Relaxed); + fill(stage, IMAGE, k); stage.word(GO).fetch_add(1, Ordering::Release); if !wait_take(stage, CNT, until) { return Err(format!("the stage's done {k} never came")); } - let rows = stage.word(ROWS).load(Ordering::Relaxed); - if k <= checked && rows != k + ROWS_OF { - return Err(format!("the stage's done {k} carried rows {rows:#x}")); + if k <= checked + && let Some((off, rows)) = stray(stage, ROWS, k + ROWS_OF) + { + return Err(format!( + "the stage's done {k} carried rows {rows:#x} at byte {off}" + )); } } Ok(()) From ead0bc5a42ac516c9f2d0facc51f73fe4dbc78e9 Mon Sep 17 00:00:00 2001 From: juliankang4 <200967947+juliankang4@users.noreply.github.com> Date: Sun, 25 Oct 2026 00:19:07 +0900 Subject: [PATCH 04/10] tokenizer: gemma4 respells a fragment's spaces before it decodes the fragment, so an overlong space stays a space --- crates/tokenizer/src/encoder.rs | 16 +++++++++------- crates/tokenizer/src/lib.rs | 10 +++++++--- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/crates/tokenizer/src/encoder.rs b/crates/tokenizer/src/encoder.rs index ab093b3f4..9e9abad2d 100644 --- a/crates/tokenizer/src/encoder.rs +++ b/crates/tokenizer/src/encoder.rs @@ -90,6 +90,14 @@ impl Encoder { bpe: encoder, nfc, } => { + // The reference respells a fragment's spaces before it decodes the fragment, so + // an overlong U+0020 stays a space. + let text = if kind.spelling() == Spelling::Metaspace { + escape_whitespace(text, escaped); + escaped.as_slice() + } else { + text + }; if kind.strict_utf8() { unicode::decode_strict(text, cpts); if *nfc { @@ -108,13 +116,7 @@ impl Encoder { unicode::encode_cpt(cpt, word); } at += n; - let text = if kind.spelling() == Spelling::Metaspace { - escape_whitespace(word, escaped); - escaped.as_slice() - } else { - word.as_slice() - }; - encoder.word(text, bpe, out, tokens); + encoder.word(word, bpe, out, tokens); } } Self::Spm { diff --git a/crates/tokenizer/src/lib.rs b/crates/tokenizer/src/lib.rs index a1aa2ddab..1bf7c24ff 100644 --- a/crates/tokenizer/src/lib.rs +++ b/crates/tokenizer/src/lib.rs @@ -1270,13 +1270,16 @@ mod tests { /// `<0xXX>` tokens, USER_DEFINED tokens (and `<|channel|>`, which the /// reference makes one) are cut out without special parsing while /// CONTROL ones are not, and BOS is added though the file does not ask. - /// Only U+0020 becomes U+2581; a literal U+2581 reads as a space, so it - /// does not round-trip; invalid UTF-8 is one U+FFFD per bad byte. A + /// Only U+0020 becomes U+2581, and before the text is decoded, so an + /// overlong one (C0 A0) stays a space and falls back to `<0x20>`; a + /// literal U+2581 reads as a space, so it does not round-trip; invalid + /// UTF-8 is one U+FFFD per bad byte. A /// header that asks for a BOS or an EOS it cannot name is refused, as the /// reference asserts one, and so is a vocabulary missing a byte token, /// where the fallback would drop that byte. /// - /// FAIL-first: without the U+2581 respelling "a b" is `a <0x20> b`; + /// FAIL-first: without the U+2581 respelling "a b" is `a <0x20> b`; with + /// the respelling after decoding, "a\xC0\xA0b" is `a ▁b`; /// without the newline-word lookup "\n\n\n" is three `\n`; with one symbol /// per byte "a b" falls back to the three bytes of U+2581; with the file's /// `add_bos_token` the BOS is missing; without the BOS-id, EOS-id or @@ -1334,6 +1337,7 @@ mod tests { let cases: &[(&[u8], bool, &[&str])] = &[ (b"a b", true, &["a", "\u{2581}b"]), ("a\u{2581}b".as_bytes(), true, &["a", "\u{2581}b"]), + (b"a\xC0\xA0b", true, &["a", "<0x20>", "b"]), (b"a\n\n\nb", true, &["a", "\n\n\n", "b"]), (b"\n\n", true, &["\n", "\n"]), (b"\n\n\n\n", true, &["\n", "\n", "\n", "\n"]), From c1f1baf1aa67eb9b3708cfb1e357c7816d0be48c Mon Sep 17 00:00:00 2001 From: juliankang4 <200967947+juliankang4@users.noreply.github.com> Date: Sun, 25 Oct 2026 00:20:16 +0900 Subject: [PATCH 05/10] placement: n1_is_today holds an NVMe fill to one read on two Sparks --- crates/placement/src/placement/nodes_scale.rs | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/crates/placement/src/placement/nodes_scale.rs b/crates/placement/src/placement/nodes_scale.rs index 67dfcbcb7..94e8830fe 100644 --- a/crates/placement/src/placement/nodes_scale.rs +++ b/crates/placement/src/placement/nodes_scale.rs @@ -862,7 +862,9 @@ fn expected_sets() { /// host in its gathered rows, one on the NVMe tier refused by name, and the Qwen3.8 /// file's row width as given, whatever its head tensors' order (its model /// tensors and first two layers, planned by its program's routed rule). A host -/// reserve on node 0 raises its bytes by itself. +/// reserve on node 0 raises its bytes by itself. A table read from NVMe adds +/// its fill to the step once: twice the read rate takes half the fill off, on +/// one Spark and on two, whose split and node windows stay. #[test] fn n1_is_today() { for case in cases() { @@ -988,6 +990,26 @@ fn n1_is_today() { assert_eq!(nvme.serial_nvme, 4 * row); let (_, faster_step) = step_of(&plan, n, 2_000_000_000); assert_eq!(filled_step - faster_step, 2 * row); + let two_sparks = |nvme_read| { + let mut nodes = spark_nodes(2, shape.layers, fabric(2, LAMBDAS[0])); + let budgets = budgets_on(&model, &mut nodes, &[machine.clone(), machine.clone()]); + let w = Workload { + nvme_read, + ..workload(3_000) + }; + let plan = plan_nodes(&nodes, &nvme, &budgets, w).expect("two Sparks"); + let p = plan.predict().expect("a prediction"); + let windows: Vec = p.per_node.iter().map(|l| l.window_ns).collect(); + (plan.owned().to_vec(), windows, p.step_ns) + }; + let (owned, windows, filled_pair) = two_sparks(1_000_000_000); + assert!( + owned.iter().all(|&o| o > 0), + "both Sparks hold ids: {owned:?}" + ); + let (faster_owned, faster_windows, faster_pair) = two_sparks(2_000_000_000); + assert_eq!((faster_owned, faster_windows), (owned, windows)); + assert_eq!(filled_pair - faster_pair, 2 * row); row_table_tier(&mut plan, u64::MAX, 0, 1, ROW_PAGE, RowTableSource::Split) .expect("the table's tier"); assert_eq!(plan.row_tier().expect("one tier"), Some(Device::Host)); From 48b0f4fe6d0a375438a2673da5de8f2019e8fc0f Mon Sep 17 00:00:00 2001 From: juliankang4 <200967947+juliankang4@users.noreply.github.com> Date: Sun, 25 Oct 2026 00:22:19 +0900 Subject: [PATCH 06/10] gpu-gemma-embedding2: the readback test poisons the reused rows, so a missed copy fails --- crates/gpu-gemma-embedding2/src/pass.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/gpu-gemma-embedding2/src/pass.rs b/crates/gpu-gemma-embedding2/src/pass.rs index e328c2fc3..3d77692f2 100644 --- a/crates/gpu-gemma-embedding2/src/pass.rs +++ b/crates/gpu-gemma-embedding2/src/pass.rs @@ -674,6 +674,7 @@ mod tests { enc.pass(&[0, 1], true)?; assert_eq!(enc.readback.len(), 2 * hp.n_out); let at = enc.readback.as_ptr(); + enc.readback.fill(f32::NAN); let pass = enc.pass(&[1], true)?; let rows = pass.taps.unwrap().result_embd; assert_eq!(rows.len(), hp.n_out); From 3bb5a68ede618d5398f84bbc91bd36b779c3aaf3 Mon Sep 17 00:00:00 2001 From: juliankang4 <200967947+juliankang4@users.noreply.github.com> Date: Sun, 25 Oct 2026 00:26:14 +0900 Subject: [PATCH 07/10] gpu-gates: probe_nvread's eviction retry has a test with a kept edge folio --- crates/gpu-gates/src/bin/probe_nvread.rs | 123 +++++++++++++++++------ 1 file changed, 94 insertions(+), 29 deletions(-) diff --git a/crates/gpu-gates/src/bin/probe_nvread.rs b/crates/gpu-gates/src/bin/probe_nvread.rs index 4c0dd503e..d91bee38a 100644 --- a/crates/gpu-gates/src/bin/probe_nvread.rs +++ b/crates/gpu-gates/src/bin/probe_nvread.rs @@ -54,7 +54,7 @@ use std::collections::HashMap; use std::ops::Range; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::sync::Arc; use std::sync::mpsc::{Receiver, Sender, channel}; use std::thread::JoinHandle; @@ -62,9 +62,9 @@ use std::time::Instant; use bloomery_gpu_gates::record::{self, NVREAD, PROBE_NVREAD, Record}; use bloomery_gpu_gates::{GateError, exit_with, ref_model_path}; -use engram::RowTable; use engram::direct::{DIRECT_ALIGN, DirectFile, aligned_span}; -use engram::rows::{PageAdvice, SplitTable}; +use engram::rows::{PageAdvice, PageCount, SplitTable}; +use engram::{EngramError, RowTable}; use gguf::Split; /// The `general.architecture` the probe reads. @@ -420,6 +420,56 @@ struct Part { id: u32, } +/// A stack's eviction as [`drop_parts`] takes it: [`SplitTable`]'s. +trait Evict { + fn path(&self) -> &Path; + fn evict_rows(&self, ids: &[u32]) -> Result<(), EngramError>; + fn resident_pages(&self, ids: &[u32]) -> Result; + fn evict_shard(&self) -> Result<(), EngramError>; +} + +impl Evict for SplitTable { + fn path(&self) -> &Path { + SplitTable::path(self) + } + + fn evict_rows(&self, ids: &[u32]) -> Result<(), EngramError> { + SplitTable::evict_rows(self, ids) + } + + fn resident_pages(&self, ids: &[u32]) -> Result { + SplitTable::resident_pages(self, ids) + } + + fn evict_shard(&self) -> Result<(), EngramError> { + SplitTable::evict_shard(self) + } +} + +/// Drop the parts' pages from the process and the page cache: each part's +/// rows, then the whole shard of a part that kept a page (an edge folio can +/// cross a row or tensor boundary). +fn drop_parts(tables: &[impl Evict], parts: &[Part]) -> Result<(), GateError> { + for p in parts { + tables[p.t].evict_rows(&[p.id])?; + } + for p in parts { + let table = &tables[p.t]; + let pages = table.resident_pages(&[p.id])?; + if pages.resident != 0 { + eprintln!( + "probe_nvread: retrying eviction over whole shard {}: expert {} kept {} of {} pages", + table.path().display(), + p.id, + pages.resident, + pages.total + ); + table.evict_shard()?; + } + } + Ok(()) +} + /// A case's batches. #[derive(Clone, Copy)] enum Shape { @@ -542,28 +592,6 @@ impl Rig { }) } - /// Drop the parts' pages from the process and the page cache. - fn drop_parts(&self, parts: &[Part]) -> Result<(), GateError> { - for p in parts { - self.tables[p.t].evict_rows(&[p.id])?; - } - for p in parts { - let table = &self.tables[p.t]; - let pages = table.resident_pages(&[p.id])?; - if pages.resident != 0 { - eprintln!( - "probe_nvread: retrying eviction over whole shard {}: expert {} kept {} of {} pages", - table.path().display(), - p.id, - pages.resident, - pages.total - ); - table.evict_shard()?; - } - } - Ok(()) - } - /// (resident pages, spanned pages) over the parts. fn pages(&self, parts: &[Part]) -> Result<(u64, u64), GateError> { let (mut resident, mut total) = (0, 0); @@ -587,7 +615,7 @@ impl Rig { t: self.layers / 2 * STACKS.len(), id: id as u32, }]; - self.drop_parts(&part)?; + drop_parts(&self.tables, &part)?; let (resident, total) = self.pages(&part)?; if resident != 0 { return Err(format!( @@ -607,7 +635,7 @@ impl Rig { ) .into()); } - self.drop_parts(&part) + drop_parts(&self.tables, &part) } /// The drawn batch of `shape`, its picks as labels, its parts dropped and @@ -652,7 +680,7 @@ impl Rig { (parts, vec![layer.to_string()]) } }; - self.drop_parts(&parts)?; + drop_parts(&self.tables, &parts)?; let (resident, _) = self.pages(&parts)?; if resident == 0 { return Ok((parts, labels)); @@ -779,7 +807,7 @@ impl Rig { .into()); } s.asked[0] = total * self.tables[parts[0].t].page_bytes(); - self.drop_parts(parts)?; + drop_parts(&self.tables, parts)?; let reads = self.reads(parts)?; s.asked[1] = reads.iter().map(|r| r.span as u64).sum(); s.asked[2] = s.asked[1]; @@ -863,8 +891,45 @@ impl Rig { #[cfg(test)] mod tests { + use std::cell::Cell; + use super::*; + /// A stack whose row eviction keeps one page of the row, as an edge folio + /// that crosses into a neighbour does; only its whole shard's eviction + /// drops it. + struct EdgeFolio(Cell); + + impl Evict for EdgeFolio { + fn path(&self) -> &Path { + Path::new("edge.gguf") + } + + fn evict_rows(&self, _: &[u32]) -> Result<(), EngramError> { + self.0.set(self.0.get().min(1)); + Ok(()) + } + + fn resident_pages(&self, _: &[u32]) -> Result { + Ok(PageCount { + resident: self.0.get(), + total: 4, + }) + } + + fn evict_shard(&self) -> Result<(), EngramError> { + self.0.set(0); + Ok(()) + } + } + + #[test] + fn a_retained_edge_folio_leaves_with_its_shard() { + let tables = [EdgeFolio(Cell::new(4))]; + drop_parts(&tables, &[Part { t: 0, id: 3 }]).unwrap(); + assert_eq!(tables[0].resident_pages(&[3]).unwrap().resident, 0); + } + #[test] fn partition_covers_every_item_once_in_even_ranges() { for (len, n) in [(9, 8), (3, 8), (768, 8), (5, 1), (0, 4), (8, 8)] { From fedae5f2ae04d2ed0a2974b55d5c9e7ebc2c675d Mon Sep 17 00:00:00 2001 From: juliankang4 <200967947+juliankang4@users.noreply.github.com> Date: Sun, 25 Oct 2026 00:28:53 +0900 Subject: [PATCH 08/10] hf: keys reads the vocabulary and the special ids in a stage each --- crates/hf/src/vocab.rs | 158 +++++++++++++++++++++++++++-------------- 1 file changed, 104 insertions(+), 54 deletions(-) diff --git a/crates/hf/src/vocab.rs b/crates/hf/src/vocab.rs index 275ce7b9e..c2a9b0e45 100644 --- a/crates/hf/src/vocab.rs +++ b/crates/hf/src/vocab.rs @@ -323,40 +323,24 @@ pub struct Keys { pub stop: Vec, } -/// The `tokenizer.*` keys of the repo at `dir`. -/// -/// # Errors -/// -/// A file that cannot be read or parsed; in `tokenizer.json`, its `model`, an added token, -/// `tokenizer_config.json` or `generation_config.json`, a key the tables of this module do not -/// name or a value they do not allow; a `tokenizer.json` without its vocabulary or merges, or with -/// a merge that is not two tokens without a space whose operands and joined token are all in its -/// vocabulary; an added token whose id or content conflicts, or whose proper suffix -/// starts another added token; a token spelled as an unused id's `[PAD]`; a vocabulary size -/// `config.json` does not give; a special token in `tokenizer_config.json` that is neither a -/// string nor an added token's object, or that no added token spells; a special token id that is -/// not an id or a list of them, lies outside the vocabulary, or is a BOS, pad, unknown or -/// separator id other than the one exported; an `add_bos_token` or `add_eos_token` that is not -/// boolean, and a `chat_template` that is not a string, each by name. -pub fn keys(dir: &Path, config: &Config) -> Result { +/// A tokenizer's vocabulary as the GGUF keys spell it: every id's token and type, the merges in +/// HF's rank order, and the added tokens by id (content, special). +struct Vocabulary<'a> { + tokens: Vec, + types: Vec, + merges: Vec, + added: HashMap, +} + +/// The vocabulary of `tokenizer.json` (`model` its `model`), its added tokens checked against +/// `tokenizer_config.json` (`tc`), and its merges. +fn vocabulary<'a>( + model: &'a Json, + tok: &'a Json, + tc: &Json, + config: &Config, +) -> Result, Error> { let bad = |what: String| Error::Vocab(what); - let read = |file: &str| crate::read_json(dir, file, Error::Io, Error::Json); - let tok = read("tokenizer.json")?.ok_or_else(|| bad("no tokenizer.json".into()))?; - let tc = read("tokenizer_config.json")?.unwrap_or_else(|| serde_json::json!({})); - let generation = read("generation_config.json")?.unwrap_or_else(|| serde_json::json!({})); - for (file, value, table) in [ - ("tokenizer.json", &tok, TOKENIZER), - ("tokenizer_config.json", &tc, TOKENIZER_CONFIG), - ("generation_config.json", &generation, GENERATION), - ] { - schema::check(value, file, &[table]).map_err(bad)?; - } - let model = tok - .get("model") - .ok_or_else(|| bad("tokenizer.json has no model".into()))?; - schema::check(model, "model", &[MODEL]).map_err(bad)?; - let size = config.size; - let nfc = tok["normalizer"] == serde_json::json!({"type":"NFC"}); let vocab = model["vocab"] .as_object() .ok_or_else(|| bad("tokenizer.json has no model.vocab".into()))?; @@ -449,7 +433,7 @@ pub fn keys(dir: &Path, config: &Config) -> Result { } } let (mut tokens, mut types) = (Vec::new(), Vec::new()); - for id in 0..size { + for id in 0..config.size { let (t, ty) = match (by_id.get(&id), added.get(&id)) { (None, _) => { let pad = format!("[PAD{id}]"); @@ -501,25 +485,28 @@ pub fn keys(dir: &Path, config: &Config) -> Result { .filter(|(i, p)| last[p] == *i) .map(|(_, (a, b))| Value::String(format!("{a} {b}"))) .collect(); - let mut out = vec![ - ( - "tokenizer.ggml.model".to_string(), - Value::String("gpt2".into()), - ), - ( - "tokenizer.ggml.pre".to_string(), - Value::String("qwen35".into()), - ), - ("tokenizer.ggml.tokens".to_string(), Value::Array(tokens)), - ("tokenizer.ggml.token_type".to_string(), Value::Array(types)), - ("tokenizer.ggml.merges".to_string(), Value::Array(merges)), - ]; - if nfc { - out.push(( - "tokenizer.ggml.normalizer".into(), - Value::String("NFC".into()), - )); - } + Ok(Vocabulary { + tokens, + types, + merges, + added, + }) +} + +/// [`special_ids`]: the `tokenizer.ggml._token_id` keys and the generation stop ids. +type SpecialIds = (Vec<(String, Value)>, Option>); + +/// The special ids the keys export, as `tokenizer.ggml._token_id` keys, and the +/// generation stop ids: each kind's token named in `tokenizer_config.json` (`tc`), else +/// `config.json`'s id, else `generation_config.json`'s. +fn special_ids( + tc: &Json, + generation: &Json, + config: &Config, + added: &HashMap, +) -> Result { + let bad = |what: String| Error::Vocab(what); + let mut ids = Vec::new(); let mut stop = None; for (i, (kind, key)) in SPECIAL.iter().enumerate() { let named = match &tc[format!("{kind}_token")] { @@ -567,9 +554,72 @@ pub fn keys(dir: &Path, config: &Config) -> Result { ))); } if let Some(id) = id { - out.push((format!("tokenizer.ggml.{key}"), Value::U32(id))); + ids.push((format!("tokenizer.ggml.{key}"), Value::U32(id))); } } + Ok((ids, stop)) +} + +/// The `tokenizer.*` keys of the repo at `dir`. +/// +/// # Errors +/// +/// A file that cannot be read or parsed; in `tokenizer.json`, its `model`, an added token, +/// `tokenizer_config.json` or `generation_config.json`, a key the tables of this module do not +/// name or a value they do not allow; a `tokenizer.json` without its vocabulary or merges, or with +/// a merge that is not two tokens without a space whose operands and joined token are all in its +/// vocabulary; an added token whose id or content conflicts, or whose proper suffix +/// starts another added token; a token spelled as an unused id's `[PAD]`; a vocabulary size +/// `config.json` does not give; a special token in `tokenizer_config.json` that is neither a +/// string nor an added token's object, or that no added token spells; a special token id that is +/// not an id or a list of them, lies outside the vocabulary, or is a BOS, pad, unknown or +/// separator id other than the one exported; an `add_bos_token` or `add_eos_token` that is not +/// boolean, and a `chat_template` that is not a string, each by name. +pub fn keys(dir: &Path, config: &Config) -> Result { + let bad = |what: String| Error::Vocab(what); + let read = |file: &str| crate::read_json(dir, file, Error::Io, Error::Json); + let tok = read("tokenizer.json")?.ok_or_else(|| bad("no tokenizer.json".into()))?; + let tc = read("tokenizer_config.json")?.unwrap_or_else(|| serde_json::json!({})); + let generation = read("generation_config.json")?.unwrap_or_else(|| serde_json::json!({})); + for (file, value, table) in [ + ("tokenizer.json", &tok, TOKENIZER), + ("tokenizer_config.json", &tc, TOKENIZER_CONFIG), + ("generation_config.json", &generation, GENERATION), + ] { + schema::check(value, file, &[table]).map_err(bad)?; + } + let model = tok + .get("model") + .ok_or_else(|| bad("tokenizer.json has no model".into()))?; + schema::check(model, "model", &[MODEL]).map_err(bad)?; + let nfc = tok["normalizer"] == serde_json::json!({"type":"NFC"}); + let Vocabulary { + tokens, + types, + merges, + added, + } = vocabulary(model, &tok, &tc, config)?; + let mut out = vec![ + ( + "tokenizer.ggml.model".to_string(), + Value::String("gpt2".into()), + ), + ( + "tokenizer.ggml.pre".to_string(), + Value::String("qwen35".into()), + ), + ("tokenizer.ggml.tokens".to_string(), Value::Array(tokens)), + ("tokenizer.ggml.token_type".to_string(), Value::Array(types)), + ("tokenizer.ggml.merges".to_string(), Value::Array(merges)), + ]; + if nfc { + out.push(( + "tokenizer.ggml.normalizer".into(), + Value::String("NFC".into()), + )); + } + let (ids, stop) = special_ids(&tc, &generation, config, &added)?; + out.extend(ids); for kind in ["bos", "eos"] { let key = format!("add_{kind}_token"); let b = match tc.get(&key).filter(|v| !v.is_null()) { From b9ac58654fd5e541c03d6dcc8d0e04c3e53a7298 Mon Sep 17 00:00:00 2001 From: juliankang4 <200967947+juliankang4@users.noreply.github.com> Date: Sun, 25 Oct 2026 00:30:08 +0900 Subject: [PATCH 09/10] gpu-gates: h2d_probe's readback arm times a fresh and a host-written destination --- crates/gpu-gates/src/bin/h2d_probe.rs | 58 ++++++++++++++++++++++++++- crates/gpu-gates/src/record.rs | 21 ++++++++++ crates/gpu/src/tensor.rs | 3 +- 3 files changed, 79 insertions(+), 3 deletions(-) diff --git a/crates/gpu-gates/src/bin/h2d_probe.rs b/crates/gpu-gates/src/bin/h2d_probe.rs index 9d74d3742..40a9fee54 100644 --- a/crates/gpu-gates/src/bin/h2d_probe.rs +++ b/crates/gpu-gates/src/bin/h2d_probe.rs @@ -45,7 +45,17 @@ //! interval_ms= bytes= GB/s=`, bytes counted when their copy is //! known complete; then one `h2d loop arm= summary …` line. //! -//! Every copy arm samples each of its cards' +//! - `readback`: the other direction, card to host, into host memory each +//! call allocates, as a request path's destination: `--reps` (8) calls of +//! `--bytes` (32 MiB) into memory nothing has written +//! (`DeviceBuffer::to_host_vec`), then as many into memory the host writes +//! first (`bloomery_gpu::to_host`), each timed on the host clock with its +//! fill: `h2d arm=readback … dest=fresh|filled bytes= calls= first_ms= +//! median_ms=`. A card that reaches pageable memory through the host's page +//! tables (GB10's unified memory) pays each fresh page's first touch inside +//! the copy. +//! +//! Every copy arm but `readback` samples each of its cards' //! `current_link_speed`/`current_link_width` from sysfs every `SAMPLE_EVERY` while //! its copies are in flight (an idle link trains down) and prints `h2d link //! arm= card= bdf= samples= [ x: , …]`. @@ -56,6 +66,8 @@ #[cfg(feature = "gpu")] fn main() -> std::process::ExitCode { + use bloomery_gpu_gates::record; + record::at_main("h2d_probe", record::H2D_PROBE); bloomery_gpu_gates::exit_with("h2d_probe", probe::run()) } @@ -80,6 +92,8 @@ mod probe { const PATH_BYTES: usize = 1 << 30; /// Timed copies of a one-shot path unless `--reps` says otherwise. const PATH_REPS: usize = 8; + /// Bytes of a readback unless `--bytes` says otherwise: 32 MiB. + const READBACK_BYTES: usize = 32 << 20; /// Bytes of a sustained copy and of a loop's source window: one Qwen3.8 /// layer-batch's routed experts (a whole number of pages). const LAYER_BYTES: usize = 1_600_000_000; @@ -107,10 +121,11 @@ mod probe { Attach, PageableLoop, StagedLoop, + Readback, } impl Arm { - const ALL: [(&'static str, Arm); 8] = [ + const ALL: [(&'static str, Arm); 9] = [ ("pinned", Arm::Pinned), ("pageable", Arm::Pageable), ("registered", Arm::Registered), @@ -119,6 +134,7 @@ mod probe { ("attach", Arm::Attach), ("pageable-loop", Arm::PageableLoop), ("staged-loop", Arm::StagedLoop), + ("readback", Arm::Readback), ]; fn parse(name: &str) -> Result { @@ -147,6 +163,7 @@ mod probe { fn default_bytes(self) -> usize { match self { Arm::Pinned | Arm::Pageable | Arm::Registered => PATH_BYTES, + Arm::Readback => READBACK_BYTES, _ => LAYER_BYTES, } } @@ -552,6 +569,41 @@ mod probe { Ok(()) } + /// The `readback` arm: each destination's calls, the first and the median. + fn readback(stream: &CudaStream, card: &Card, a: &Args) -> Result<(), GateError> { + use bloomery_gpu_gates::record::{H2D_READBACK, Record}; + + let bytes = a.bytes.unwrap_or(Arm::Readback.default_bytes()); + let reps = a.reps.unwrap_or(Arm::Readback.default_reps()); + let dev = DeviceBuffer::::zeroed(stream, bytes / size_of::())?; + stream.synchronize()?; + for dest in ["fresh", "filled"] { + let mut ms = Vec::with_capacity(reps); + for _ in 0..reps { + let t = Instant::now(); + let host = if dest == "fresh" { + dev.to_host_vec(stream)? + } else { + bloomery_gpu::to_host(stream, &dev)? + }; + ms.push(t.elapsed().as_secs_f64() * 1e3); + drop(host); + } + let first = ms[0]; + ms.sort_by(f64::total_cmp); + Record::new(&H2D_READBACK) + .w("card", format_args!("{:?}", card.name)) + .w("bdf", &card.bdf) + .w("dest", dest) + .u("bytes", bytes) + .u("calls", reps) + .f("first_ms", first) + .f("median_ms", ms[reps / 2]) + .print(); + } + Ok(()) + } + fn registered( ctx: &Arc, stream: &CudaStream, @@ -977,6 +1029,8 @@ mod probe { card.print()?; if arm.is_loop() { copy_loop(&ctx, &first, &card, arm, &a)?; + } else if arm == Arm::Readback { + readback(&first, &card, &a)?; } else { one_shot(&ctx, &first, &card, arm, &a)?; } diff --git a/crates/gpu-gates/src/record.rs b/crates/gpu-gates/src/record.rs index facd69fe3..a05bcd127 100644 --- a/crates/gpu-gates/src/record.rs +++ b/crates/gpu-gates/src/record.rs @@ -998,6 +998,22 @@ pub static NVREAD: Kind = Kind { ], }; +/// One destination kind's readback timings, including allocation and fill. +pub static H2D_READBACK: Kind = Kind { + name: "h2d_readback", + head: "h2d arm=readback", + parts: &[ + key("card", Text, ""), + key("bdf", Word, ""), + key("dest", Word, ""), + key("bytes", U64, "B"), + key("calls", U64, ""), + key("first_ms", F64(4), "ms"), + key("median_ms", F64(4), "ms"), + ], + doc: "Readback into a fresh or host-filled allocation, including allocation and fill time.", +}; + /// One rank's line of `link_probe --gather`: its gather latency and checks. pub static LINK_GATHER: Kind = Kind { name: "link_gather", @@ -3157,6 +3173,9 @@ pub static GATE_DEEPSEEK41_PREFILL: &[&Kind] = &[&STAT_PREFILL_SPLIT]; /// The records `probe_nvread` prints: one row an arm and case. pub static PROBE_NVREAD: &[&Kind] = &[&NVREAD]; +/// The readback result that `h2d_probe` prints. +pub static H2D_PROBE: &[&Kind] = &[&H2D_READBACK]; + /// The record `link_probe` prints under `--gather`: one line a rank. pub static LINK_PROBE: &[&Kind] = &[&LINK_GATHER]; @@ -4378,6 +4397,7 @@ mod tests { GENERATE_QWEN3MOE, GATE_DEEPSEEK41_PREFILL, PROBE_NVREAD, + H2D_PROBE, LINK_PROBE, GATE_NVTIER, groups::serve_client::SPAWN, @@ -4813,6 +4833,7 @@ mod tests { CLEF_HIDDEN_BIN, GATE_DEEPSEEK41_PREFILL, PROBE_NVREAD, + H2D_PROBE, LINK_PROBE, GATE_NVTIER, groups::serve_client::SPAWN, diff --git a/crates/gpu/src/tensor.rs b/crates/gpu/src/tensor.rs index b7c78b678..cdd5ea659 100644 --- a/crates/gpu/src/tensor.rs +++ b/crates/gpu/src/tensor.rs @@ -337,7 +337,8 @@ impl HostFill for u16 { /// reaches pageable memory through the host's page tables (GB10's unified /// memory) faults on each page a copy is the first to write, one page at a /// time; a new allocation nothing has written is such pages. A large one is -/// advised onto huge pages first ([`advise_huge`]). +/// advised onto huge pages first ([`advise_huge`]). `h2d_probe --arm readback` +/// (`bloomery-gpu-gates`) times a readback into each kind of destination. #[must_use] pub fn host_filled(n: usize) -> Vec { let mut host = Vec::with_capacity(n); From 93f0cd59b286f16d0a83a0407cf1ce3113f33138 Mon Sep 17 00:00:00 2001 From: juliankang4 <200967947+juliankang4@users.noreply.github.com> Date: Sun, 25 Oct 2026 00:39:04 +0900 Subject: [PATCH 10/10] gpu-gates: a dense qwen35 header plans through qwen35_on, its arena the whole load's --- crates/gpu-gates/src/bin/generate_qwen3moe.rs | 6 + .../src/bin/shared/qwen3moe_place.rs | 165 ++++++++++++++++++ 2 files changed, 171 insertions(+) diff --git a/crates/gpu-gates/src/bin/generate_qwen3moe.rs b/crates/gpu-gates/src/bin/generate_qwen3moe.rs index a9b556279..37e43f735 100644 --- a/crates/gpu-gates/src/bin/generate_qwen3moe.rs +++ b/crates/gpu-gates/src/bin/generate_qwen3moe.rs @@ -391,6 +391,12 @@ fn dense_shortage_halves_the_default_context() { q3place::tests::dense_shortage_halves_the_default_context(); } +#[cfg(all(test, feature = "gpu"))] +#[test] +fn a_dense_header_plans_through_qwen35_on() { + q3place::tests::a_dense_header_plans_through_qwen35_on(); +} + #[cfg(feature = "gpu")] #[path = "shared/qwen38_place.rs"] mod q38place; diff --git a/crates/gpu-gates/src/bin/shared/qwen3moe_place.rs b/crates/gpu-gates/src/bin/shared/qwen3moe_place.rs index 0cda2ad15..57f07dd52 100644 --- a/crates/gpu-gates/src/bin/shared/qwen3moe_place.rs +++ b/crates/gpu-gates/src/bin/shared/qwen3moe_place.rs @@ -1474,4 +1474,169 @@ pub(super) mod tests { let refused = Whole::Refused("unsupported tensor".into()).dense_refusal(TARGET as usize); assert!(short_or(refused).is_err()); } + + /// A dense qwen35 header of the body's smallest shapes, written to `path` over a sparse file + /// of its full length: hidden 256, three gated-delta layers (16 key and 32 value heads of + /// 128) and a gated attention layer (16 query and 2 key heads of 256), a SwiGLU FFN of 256 in + /// each and no expert key; every projection Q4_K, the head Q6_K, the norms, β, α and the + /// conv F32. + fn dense_header(path: &std::path::Path) -> Split { + use gguf::Value as V; + const H: u64 = 256; + const FF: u64 = 256; + const VOCAB: u64 = 256; + const CHANNELS: u64 = (2 * 16 + 32) * 128; + const ATTN: u64 = 32 * 128; + let u = |k: &str, v: u32| (format!("qwen35.{k}"), V::U32(v)); + let kvs = vec![ + ( + gguf::GENERAL_ARCHITECTURE.to_owned(), + V::String("qwen35".to_owned()), + ), + u("block_count", 4), + u("embedding_length", H as u32), + u("context_length", 4096), + u("feed_forward_length", FF as u32), + u("attention.head_count", 16), + u("attention.head_count_kv", 2), + u("attention.key_length", 256), + u("attention.value_length", 256), + ( + "qwen35.attention.layer_norm_rms_epsilon".to_owned(), + V::F32(1e-6), + ), + u("rope.dimension_count", 64), + ( + "qwen35.rope.dimension_sections".to_owned(), + V::Array([11, 11, 10, 0].map(V::I32).to_vec()), + ), + ("qwen35.rope.freq_base".to_owned(), V::F32(1e7)), + u("ssm.conv_kernel", 4), + u("ssm.state_size", 128), + u("ssm.group_count", 16), + u("ssm.time_step_rank", 32), + u("ssm.inner_size", ATTN as u32), + u("full_attention_interval", 4), + ( + "tokenizer.ggml.model".to_owned(), + V::String("gpt2".to_owned()), + ), + ( + "tokenizer.ggml.pre".to_owned(), + V::String("qwen35".to_owned()), + ), + ( + "tokenizer.ggml.tokens".to_owned(), + V::Array((0..VOCAB).map(|i| V::String(i.to_string())).collect()), + ), + ]; + let (q4, q6, f32) = ( + gguf::GgmlType::Q4_K, + gguf::GgmlType::Q6_K, + gguf::GgmlType::F32, + ); + let mut tensors = vec![ + ("token_embd.weight".to_owned(), vec![H, VOCAB], q4), + ("output_norm.weight".to_owned(), vec![H], f32), + ("output.weight".to_owned(), vec![H, VOCAB], q6), + ]; + for l in 0..4 { + let mixer: &[(&str, &[u64], gguf::GgmlType)] = if l < 3 { + &[ + ("attn_qkv.weight", &[H, CHANNELS], q4), + ("attn_gate.weight", &[H, ATTN], q4), + ("ssm_conv1d.weight", &[4, CHANNELS], f32), + ("ssm_dt.bias", &[32], f32), + ("ssm_a", &[32], f32), + ("ssm_alpha.weight", &[H, 32], f32), + ("ssm_beta.weight", &[H, 32], f32), + ("ssm_norm.weight", &[128], f32), + ("ssm_out.weight", &[ATTN, H], q4), + ] + } else { + &[ + ("attn_q.weight", &[H, 2 * ATTN], q4), + ("attn_k.weight", &[H, 512], q4), + ("attn_v.weight", &[H, 512], q4), + ("attn_q_norm.weight", &[256], f32), + ("attn_k_norm.weight", &[256], f32), + ("attn_output.weight", &[ATTN, H], q4), + ] + }; + let block: [(&str, &[u64], gguf::GgmlType); 5] = [ + ("attn_norm.weight", &[H], f32), + ("post_attention_norm.weight", &[H], f32), + ("ffn_gate.weight", &[H, FF], q4), + ("ffn_up.weight", &[H, FF], q4), + ("ffn_down.weight", &[FF, H], q4), + ]; + for &(stem, dims, ty) in mixer.iter().chain(&block) { + tensors.push((format!("blk.{l}.{stem}"), dims.to_vec(), ty)); + } + } + let decls = tensors + .into_iter() + .map(|(name, dims, ty)| { + let (Some(blck), Some(tsz)) = (ty.blck_size(), ty.type_size()) else { + panic!("{name}: {ty:?} has no block info"); + }; + let nbytes = gguf::tensor_bytes(blck, tsz, &dims).expect("small tensors"); + gguf::write::TensorDecl { + name, + dims, + type_id: ty.as_u32(), + nbytes, + } + }) + .collect(); + let layout = gguf::write::Layout::new(&kvs, decls).expect("the dense layout"); + let len = layout.file_len(); + let mut file = std::fs::File::create(path).expect("the header file"); + drop(gguf::write::Writer::new(&mut file, layout).expect("the header")); + file.set_len(len).expect("the file's length"); + Split::open(path).expect("the dense header opens") + } + + /// A dense qwen35 file (no router) plans through [`PlaceQ3::qwen35_on`]: its machine counts + /// the whole load's ubatch arena, as the load sizes it, not a placed arena, which a file with + /// no router refuses; and the plan answers at the load's context. + #[allow( + dead_code, + reason = "`generate_qwen3moe`'s test calls it; every other bin that includes this file leaves it unused" + )] + pub(crate) fn a_dense_header_plans_through_qwen35_on() { + let path = + std::env::temp_dir().join(format!("bloomery-q35-dense-{}.gguf", std::process::id())); + let split = dense_header(&path); + let o = Open35 { + ctx: 4096, + mma: false, + ubatch: 512, + kv: KvQ8::F16, + media: false, + }; + let census = [DeviceInfo { + ordinal: 0, + name: "NVIDIA RTX A6000".to_owned(), + total_bytes: 50_952_536_064, + free_bytes: 50_000_000_000, + uuid: [1; 16], + pci_bus: "0000:41:00.0".to_owned(), + held_by: None, + integrated: false, + }]; + let place = Place::parse("cuda0").expect("a card word"); + let q = PlaceQ3::qwen35_on(&split, place, &census, o, Seqs::ONE, Beside::default()) + .unwrap_or_else(|e| panic!("a dense file plans: {e}")); + let WholeLoad::Checked { arena, .. } = + Qwen35moeModel::whole_load(&split, o).expect("the whole load reads the file") + else { + panic!("a qwen35 whole load checks its arena"); + }; + assert_eq!(q.arch, "qwen35"); + assert_eq!(workstation::counted_arena_bytes(&q.machine.cards[0]), arena); + q.plan(o.ctx, &PlanLevers::default()) + .unwrap_or_else(|e| panic!("the dense plan at {}: {e}", o.ctx)); + std::fs::remove_file(&path).expect("the header file"); + } }