phase 1 methodology audit: calibration corpus, REAP/trace hook, expert selection #
read-only audit. scope: scripts/assemble_corpus.py, scripts/patch_imatrix.py, scripts/select_and_eval_prunes.py, PLAN.md §2.3 / §2.5 / §6.1 / §6.4.
the C++ was checked against current llama.cpp master (tools/imatrix/imatrix.cpp, src/llama-graph.cpp::build_moe_ffn, src/models/qwen4exp.cpp, common/common.h, common/arg.cpp), fetched for this audit. the Colab clone may be a slightly different commit; the version-sensitive points are marked.
the datasets were checked via the HF API / datasets-server (file listings, first 100–300 rows) and the real calibration_ja.txt was downloaded.
the run audited is the one launched in the parent session:
llama-imatrix -m Swift-...-GSQ-RCO-IQ3_S-00001-of-00002.gguf -f calibration_mix_400k.txt -c 512 -b 512 -ngl 99 --chunks 128 --output-frequency 10 --no-ppl (no --parse-special, no --moe-cache-mib).
verdict #
| # | finding | severity | affects the run already done? |
|---|---|---|---|
| A1 | --chunks 128 -c 512 = 65,536 tokens, read sequentially from the file start. the corpus is ~0.6–0.7M tokens. ukisai and hotchpotch contribute 0 tokens; ops = ~6–8 Terminal-Bench "low-effort failure rerun" transcripts; JA = only the first ~150–200 ChiTako paragraphs |
blocker | yes |
| A2 | no --parse-special: parse_special defaults to false in imatrix, so every <|im_start|>, <|im_end|>, <think>, </think>, <tool_call> is tokenized as literal text pieces, never as the special tokens the model routes on in real use |
major | yes |
| A3 | domains are balanced by characters, not tokens. JA is ~2–3× more tokens per char, so the 65K-token window is ~50–60% JA, ~20–25% ops, ~20% chat (PLAN: ~30/25/25+) | major | yes |
| A4 | Terminal-Bench split is by sorted path: calib = low/traces/* [0:20], eval = [20:25]. 2 of the 5 eval transcripts (medium/.../adaptive-rejection-sampler, medium/.../chess-best-move) are the same tasks as calib transcripts #1 and #2, and both of those are inside the 65K tokens actually used |
major (leak) | yes |
| A5 | ChiTako is split on "\n\n" paragraphs, not on its 916 chunks. 196 / 1,039 kept blocks (18.9%) are exact duplicates; Monte-Carlo over 5 random splits: 3–8 of the 40 eval blocks (~15%) share >50% of their 20-char shingles with the 250 calib blocks |
major (leak) | yes (the duplicated ones sit in the window) |
| A6 | ukisai split is by row, but task carries _run2…_run5 and __dsv4-trial-2 suffixes: several rollouts of the same base task can land on both sides. also every row's first user message starts with the same 2,834-char Terminus-2 preamble, so after [:6000] ≈47% of every ukisai doc (calib and eval) is identical boilerplate |
major (leak + waste) | no for calib (0 tokens used), yes for eval_ops_tools.txt |
| A7 | Whittle prompt is already chat-templated (<|im_start|>system ... <|im_start|>assistant\n<think>\n); the script wraps it again → nested <|im_start|>user\n<|im_start|>system…, two <think> openers. prompt[:4000] cuts the 38K-char agent prompts inside the identical tool schema; response/tool_calls are ignored, so 10% of rows have an empty answer and "sandbox-verified tool use" is mostly a repeated schema header. no kind filter despite the comment (first 100 rows: 45% math/gsm) |
major | yes |
| A8 | hotchpotch file picked with next(rglob("*.parquet")): filesystem order decides train or test split, non-deterministic across machines. some source ids repeat (1/100 in test) → same passage, different question, possible on both sides |
minor | no (0 calib tokens) |
| B1 | on_logits, on_weighted_out stride/shape math, trace record layout, header offset, save_all() call sites: correct for this build and this command (details §2) |
ok | — |
| B2 | version hazard: with --moe-cache-mib > 0, the down MUL_MAT_ID's src[2] is cache slot ids, not expert ids → REAP stats silently attributed to the wrong experts. with *_exps_s scale tensors or a down bias, ffn_moe_weighted->src[0] is not MUL_MAT_ID → REAP stats silently all zero |
major latent (not triggered by this command) | no |
| B3 | trace header ntok is only written at clean exit; trace has no token ids / positions / chunk ids, so §6.1.2's "token ids, turn boundaries, session ids" are not met; the 8,192-token trace is ~2 interleave rounds of teacher-forced prefill, not agent sessions |
minor/scope | yes |
| B4 | intercepting ffn_moe_logits-* with an early return true drops the normal imatrix stats for blk.*.ffn_gate_inp.weight from full_imatrix.gguf |
minor (only if that imatrix is reused to quantize) | yes |
| C1 | ranking uses reap_l2_sum = Σ g·‖f‖ = count × REAP. published REAP is the mean over routed tokens. the sum is frequency-weighted, so it inherits A3's domain-share skew and is biased against rare-token specialists — the exact "reliability knee" risk §2.3 names |
major (method) | yes |
| C2 | "retained REAP mass" in the report is maximized by construction when ranking by the same metric; it ignores substitution (norm_topk renormalization) and is the kind of router statistic §2.3 already says can predict the opposite of behavior | minor (reporting) | — |
| C3 | calibration has no image tokens → vision experts will look dead and get pruned (PLAN's own ISTA lesson); current keep maps are not exportable as-is | major (known, re-flagged) | yes |
falsifier status: hit. train/eval leakage confirmed (A4, A5, A6), and the calibration actually used is not the corpus that was designed (A1–A3, A7). no arithmetic bug in the C++ hook for this build/command; two latent silent-failure paths (B2).
1. calibration corpus and split hygiene (assemble_corpus.py) #
1.1 what the imatrix run actually sees (A1, A3) #
compute_imatrix takes n_chunk = min(--chunks, tokens/n_ctx) and walks the token stream from the start, in order (imatrix.cpp ~L1464–1525). nothing is shuffled. with -c 512 --chunks 128 that is the first 65,536 tokens.
the file is built as ops / chat / ja 4,000-char blocks interleaved round-robin, where each stream is a concatenation in source order:
- ops =
tb_calib_texts(20 × 8,000 chars = 160K chars) thenuki_calib_texts - chat = whittle docs (each ≤ ~10K chars)
- ja =
ja_calib_blocks[:250](avg 330 chars → ~83K chars) thenhp_calib_texts
per round (12K chars) at plausible Qwen ratios (TB markdown ~3.2 ch/tok, English ~3.5–4, JA ~1.3–2): ~1,250 + ~1,100 + ~2,000–3,000 ≈ 4.4–5.4K tokens. 65,536 tokens ≈ 12–15 rounds ≈ 48–60K chars per stream. the conclusion is robust to the exact ratios:
- ukisai only starts after 160K ops chars (~40 rounds, >180K tokens). 0 ukisai tokens.
- hotchpotch only starts after ~83K JA chars (~20 rounds, >90K tokens). 0 hotchpotch tokens.
- ops = TB transcripts #1–#7ish, sorted by path:
low/traces/adaptive-rejection-sampler, chess-best-move, dna-assembly, dna-insert, extract-moves-from-video, filter-js-from-html, gcode-to-text…. thelow/directory is the dataset's failure reruns at low effort (session paths readtb21-low-failures-…); the 89-taskxhighset is never touched. - TB
transcript.mdis a markdown rendering (## Tool activity / Tool: bash / Outcome: …), not the Qwen chat template. it is off-distribution for routing compared to real agent turns. - token shares in the window are roughly JA 50–60%, ops 20–25%, chat ~20%, versus PLAN §2.3's ~30% ops / ~25% chat+JSON / ~25% JA. the comment "~1.5 MB (~420K tokens)" assumes ~3.6 chars/token across the board, which JA breaks.
the trace (max_trace_tokens = 8192) is the first 16 chunks ≈ 2 rounds: about one TB transcript, 1–2 whittle docs and a handful of ChiTako paragraphs.
fix: run with --chunks -1 (or ≥ corpus/512), and build the file so any prefix is representative: interleave at document granularity across all sources (round-robin per source within each domain), budget each domain by tokens (tokenize with the model's vocab when assembling), and shuffle document order with a fixed seed. at ~3 min per 128 chunks on the A100 (parent's estimate), the full ~1,200 chunks is ~30 min.
1.2 special tokens are not special (A2) #
common_params::parse_special = false (common.h) and compute_imatrix tokenizes with common_tokenize(ctx, params.prompt, true, params.parse_special). the run has no --parse-special. so the ChiTako text, the whittle templates and the hand-built <|im_start|>{role} wrappers all become runs of ordinary BPE pieces (<, |, im, _start, …). the router never sees the real role / think / tool-call tokens in this calibration, and those are exactly the positions where routing for "clean termination" and tool syntax happens. ChiTako's own README tells you to pass --parse-special.
the KL eval should tokenize the same way it will be served (special tokens parsed), otherwise it under-measures damage on template tokens.
1.3 leakage, per source #
Terminal-Bench (A4), confirmed from the repo listing. 160 transcript.md files: xhigh 89, medium 27, low 22, off 20, 2 reruns. sorted(rglob) → [0:20] are 20 of the 22 low/ tasks; [20:25] are low/tune-mjcf, low/winning-avg-corewars, medium/adaptive-rejection-sampler, medium/build-pov-ray, medium/chess-best-move. two eval transcripts are the same task (same container image, same files, same opening tool outputs) as calib transcripts #1 and #2, which are inside the 65K-token window. difflib ratio of the first 8,000 chars of low/ vs medium/ chess-best-move = 0.27: task-level leakage, not verbatim copies.
fix: split by task name (the path component after traces/), and prefer xhigh/ for both sides.
ChiTako (A5), confirmed on the real file. calibration_ja.txt is 916 chunks joined by "\n\n", but chunks contain internal "\n\n" paragraph breaks (wikipedia title/body, code blocks, multi-turn chats): splitting on "\n\n" gives 2,244 pieces, 1,039 of them >100 chars. 196 of those 1,039 are exact duplicates of another block (the builder reuses templates: e.g. one system prompt appears 21×, several Python helper snippets 7–8×). shuffling paragraphs then cutting 88/12 puts paragraphs of the same chunk, and literal duplicates, on both sides. over 5 random seeds, 3–8 of the 40 eval blocks had >50% shingle overlap with the 250 calib blocks (mean overlap 0.08–0.21). the exact rng state of the script couldn't be reproduced offline (it depends on the ukisai/whittle table sizes), so this is an estimate of the expected rate, not the exact count.
fix: dedupe first (exact + near-dup), and split on the builder's chunk boundaries or, simpler, take the eval JA from a disjoint source (e.g. hotchpotch test split, which is already sampled from fineweb2-edu-ja's test portion).
ukisai (A6), confirmed from rows 0–100, 5000–5100, 10000–10100. columns are conversations, agent, model, model_provider, date, task, episode, run_id, trial_name. task values include nl2bash-1908-0000, nl2bash-1908-0000_run2 … _run5, inferredbugs-0001 and inferredbugs-0001__dsv4-trial-2. each row is one episode (user message = full history so far + assistant reply); the first user message is 4.5K–133K chars and always begins with the same 2,834-char Terminus-2 instruction before Task Description. consequences:
- a row-level shuffle can put
_run2of a task in calib and_run4in eval: same task text, same initial terminal state. - with
[:6000], ~47% of every ukisai document is that identical preamble, and the rest is mostly the task statement + first terminal output. 220 copies of the preamble is wasted calibration budget, and ineval_ops_tools.txtit gives the pruned model an easy, memorizable stretch that flatters KL. uki_other = [i for i in uki_indices if i not in set(uki_nl2bash)]rebuilds the set every iteration (O(N²), ~15K × 15K); slow, not wrong."term" in taskalso matches any task id containing "term" — check what it actually selects. fix: group-split on the base task id (re.sub(r'(_run\d+|__dsv4-trial-\d+)$', '', task)), keep one episode per trial (the last, which contains the whole trajectory), strip the shared preamble after its first occurrence, and render with the real chat template (tokenizer.apply_chat_template) instead of a hand-rolled one.
whittle (A7), confirmed from rows 0–100 of shard 0. no duplicate prompts in the sample, so no prompt-level leak seen. but the formatting is broken: prompt already contains the full template up to <|im_start|>assistant\n<think>\n (100/100 rows), and the script wraps it in another user turn. agent1 prompts are 38K chars, so [:4000] keeps only the start of the identical # Tools schema. the dataset's response (which carries <tool_call> blocks) and tool_calls columns are unused; 10/100 rows have an empty answer. no kind filter is applied, contrary to the comment (sample: math 31, longctx 20, agent1 15, json 15, gsm 14, chat 5).
fix: use prompt + response as-is (it is already the exact serving format), filter/stratify by kind, truncate from the left of long prompts (keep the end, where the task and the latest tool output are), and parse special tokens.
hotchpotch (A8). next(rglob("*.parquet")) returns train or test depending on directory order. test has 1 repeated id per 100 rows (several questions per passage); with 240 sampled rows the chance of one passage on both sides is small but nonzero. fix: pick the file explicitly, group by id/url.
1.4 context-length caveat #
-c 512 means every chunk starts cold (no prior context, recurrent GDN state cleared per chunk by llama_memory_clear), and documents are cut every ~4,000 chars with a domain switch in the middle of many windows. routing at positions <512 in a spliced window isn't the routing of a 50K-token agent session. it's what imatrix normally does, so it's not a bug, but for a prune that has to survive long tool loops, -c 2048–4096 with the same token budget is closer to the workload.
2. C++ hook (patch_imatrix.py) #
2.1 patch mechanics #
replace("bool IMatrixCollector::collect_imatrix(", hook + ..., 1): inserts the struct before the definition (the in-class declaration has noIMatrixCollector::prefix). ok.target_askmatches master verbatim. ok.target_lock, replaced once: the firststd::lock_guard<std::mutex> lock(m_mutex);in the file is insidecollect_imatrix(char 20,833 > definition at 19,589). ok."g_collector.save_imatrix();"is replaced without a count: it occurs twice in master, at L1899 (combine-only mode, no prompt) and L2041 (end of main). the periodic--output-frequency 10saves callsave_imatrix()from insidecollect_imatrixwith nog_collector.prefix, so they are not patched. so in a normal runsave_all()runs exactly once, at the end. good.- footgun: running
llama-imatrix --in-file a.gguf --in-file b.gguf(combine mode) withREAP_OUTunset overwrites/content/reap_saliency.jsonwith all zeros. low risk because the run usedREAP_OUT=/content/reap_saliency_full.json, but worth a guard (if (!any_data) return;). - if the run dies (timeout / OOM),
save_all()never runs: no JSON at all, and the trace header keepsntok = 0. readers should deriventok = (filesize - 16) / 12288and ignore the header when it's 0.
- footgun: running
2.2 on_logits (router softmax mass + top-64 trace) #
- graph facts (qwen4exp.cpp L1142–1159, build_moe_ffn): every one of the 48 trunk layers is MoE (
GGML_ASSERT(ffn_gate_inp != nullptr)); gating is softmax,exp_probs_b = nullptr,norm_w = true,w_scale = expert_weights_scale;ffn_moe_logits = mul_mat(gate_inp, cur)→[n_expert, n_tokens], freshly allocated, contiguous F32. sodata + row * n_expertis the right stride,prob_sumis the correct router probability, and the top-10 of the trace's top-64 logits is exactly the real selection (no selection bias, no group masking). - MTP block has
il = 48→ rejected byil >= N_LAYERS. ok. il == 0sizes the per-ubatch buffer andil == 47flushes it;cur_ubatch_tokenspersists across layers 1..46 and is reset after the flush. with-b 512 -c 512there is one sequence per batch and one ubatch per chunk, so trace rows are in text order and every layer sees the samen_tokens.trace_tokens_writtenis only advanced at layer 47, after all 48 layers were filled. the clampmin(n_tokens, max − written)handles the last partial ubatch. correct.- caveat: with
-b > -cimatrix packsn_seq = n_batch / n_ctxchunks into one batch, and the hybrid (GDN) memory splits ubatches per sequence, so trace rows would no longer be in text order and nothing in the record says which chunk/position a row is. the command used-b 512, so not triggered; keep it that way or log(chunk, pos)per row.
- caveat: with
- record layout: per token, per layer, 64 × u16 ids then 64 × f16 logits = 256 B; ×48 = 12,288 B/token. header =
Q4RT(4) + ver(2) + n_layer(2) + n_expert(2) + top_m(2) + ntok(4) = 16 B, andfseek(fp, 12)hits the ntok field exactly.fseek+fwriteon a"wb"stream is legal. expected file size for the full run: 16 + 8,192 × 12,288 = 100,663,312 B. - nits: the softmax
expis computed twice per element;inv_exp_safe(inv_sum)can't be non-finite sincesum_exp ≥ 1; a NaN logit would breakpartial_sort's strict weak ordering (UB). none of these change results on finite input.
2.3 on_weighted_out (REAP numerator) #
ffn_moe_weighted = ggml_mul(experts, weights)withexperts = mul_mat_id(down_exps, …, selected_experts)→[n_embd, n_expert_used, n_tokens], andweights→[1, n_expert_used, n_tokens], broadcast on dim 0. theggml_mulresult is a new contiguous tensor;idx * nb[1] + row * nb[2]addresses slotidxof tokenrow. correct.ids = down_t->src[2]=selected_experts, an[n_expert_used, n_tokens]I32 that may be a non-contiguous view of the argsort;ggml_backend_tensor_get(ids, buf, 0, ggml_nbytes(ids))copies the view's strided extent from its data pointer, androw * nb[1] + idx * nb[0]is exactly upstream imatrix's own indexing. slotidxof the weighted tensor andids[idx, row]refer to the same expert, becausemul_mat_idwrites output slotidxforids[idx, row]. correct.‖w·f‖₂ = w·‖f‖₂because the softmax-derived weight is > 0, soreap_l2_sum[e] / count[e]is exactly REAP's S_j with the renormalized top-k gate (which is also Cerebras' current default,renormalize_router_weights).w_scaleis a per-layer constant and doesn't change within-layer ranking.- latent silent failures (B2), not triggered by this command but present in master:
build_lora_mm_id(..., slots): when the MoE cache is on (--moe-cache-mib N), the downMUL_MAT_IDgetssrc[2] = slots(cache slot indices). the hook would read slots as expert ids; they pass theex < 512check and silently credit the wrong experts.- if the GGUF has
ffn_down_exps_s(per-expert scales) or a down bias,ffn_moe_weighted->src[0]is aMUL/ADD_ID, notMUL_MAT_ID; theiffalls through and returnstruewith no stats → all REAP sums 0, and the selector silently degrades to ranking byprob_sum(the tie-break). the parent's smoke run reported non-zero REAP sums, so (2) didn't fire in that build, but nothing guards against it. fix: captureffn_moe_topk-{il}in the callback and use it as the id source, andGGML_ABORTifdown_tisn't aMUL_MAT_IDover the real expert ids.
if (std::isfinite(sq))silently drops non-finite rows and their count; upstream imatrix aborts on non-finite instead. it should at least count drops.
2.4 side effect on the imatrix itself (B4) #
returning early for ffn_moe_logits-* means the MUL_MAT on blk.N.ffn_gate_inp.weight never reaches the dense-collection branch, so full_imatrix.gguf has no entries for the router weights. harmless for pruning; it matters only if that imatrix gets reused for quantization (routers are usually kept at high precision anyway). fix: fall through to the normal path after collecting instead of return true.
2.5 invariants to check on the existing artifacts (cheap, do these first) #
on reap_saliency_full.json (65,536 tokens, top-10):
- per layer:
Σ_e count == 655,360for all 48 layers (catches B2-type skips and NaN drops) - per layer:
Σ_e prob_sum ≈ 65,536(±1e-3 relative) trace_tokens_written == 8192, trace file = 100,663,312 B, and for each trace row the top-10 ids appear incount(spot check)- recompute
countfrom the trace's top-10 for the first 8,192 tokens and compare against a separate run of the hook limited to 16 chunks: they must match exactly
3. saliency metric and selection (select_and_eval_prunes.py) #
3.1 what each metric is #
| field | formula | what it measures |
|---|---|---|
count |
|X_j| | routing frequency ("frequency" baseline in REAP; weakest) |
prob_sum |
Σ_x softmax(z(x))_j over all tokens | soft frequency; ignores output magnitude; includes mass on tokens where j wasn't selected |
reap_l2_sum (default) |
Σ_{x∈X_j} g_j(x)·‖f_j(x)‖ | count × REAP: expected gated output magnitude per calibration token |
reap_l2_sq_sum |
Σ_{x∈X_j} (g_j‖f_j‖)² | gated output energy, outlier-heavy; first-order "delete without replacement" error |
| (missing) REAP | reap_l2_sum / count |
the published criterion (Lasby et al., arXiv 2510.13999): mean over routed tokens |
3.2 problems with the default (C1) #
- it isn't REAP. the paper deliberately averages over X_j so a rarely-routed expert that matters a lot when it fires isn't pruned just for being rare. the sum multiplies that back by frequency. that's a legitimate criterion ("expected contribution per token"), but it's a different method and should be named and ablated as such.
- it inherits the corpus mix linearly. with A3 (JA over-weighted by tokens) and A1 (no ukisai/hotchpotch), the sum protects JA/wiki experts and under-protects terminal/tool-loop experts. the mean is less sensitive to mix (still not immune).
- rare-token specialists are what the sum prunes first. §2.3's known risk ("reliability knee ~K=288–320: rare tokens, clean termination") is a rare-event failure. at K=224 (56% of experts removed) a frequency-weighted score is the wrong default. and with A2, the "clean termination" tokens (
<|im_end|>,</think>) never even appeared as special tokens in calibration. - the mean has its own trap: with |X_j| small,
sum/countis high-variance (one large-norm hit → top rank). use a shrinkage estimator, e.g.(Σ + m·μ_layer) / (count + m)with m≈20–50, or requirecount ≥ c_minbefore trusting the mean.
recommended ranking: compute per-domain REAP (separate imatrix passes per domain file, or log a domain id), normalize within each domain/layer, and keep an expert if it is in the top set for any domain (max over domains) or under a weighted mixture with explicit weights (e.g. ops 0.35 / chat+json 0.25 / JA 0.25 / reasoning 0.15). that makes the domain budget an explicit knob instead of an accident of chars-per-token. ablate {REAP mean, REAP-sum, per-domain-max REAP} at each K on the tier-1 KL screen; PLAN §2.3 already says to pick by behavior, not router stats.
3.3 pruning without router retraining (C2) #
- with
norm_w = trueand softmax gating, the gate weights areexp(z_i) / Σ_{i∈top10} exp(z_i): the global softmax denominator cancels. dropping router rows therefore doesn't inflate probabilities; it only changes which 10 experts are picked. a token whose top-10 contained a pruned expert gets the next survivor (rank 11, 12, …) and all 10 weights renormalize. so the per-token error is a substitution error,Σ w'_i f_i − Σ w_i f_i, not the deleted expert's mass. neitherreap_l2_sumnor "retained REAP mass" measures that. - you can measure it offline from the Q4RT trace (top-64 logits are enough: at K=224 the expected number of survivors in the top-64 is ~28): for each keep map, compute per token/layer the fraction of top-10 slots replaced, the old-weight mass that moved, and the rank of the substitutes. that's a better router-side screen than retained mass and costs nothing. (needs a trace drawn from the full, representative corpus; see A1.)
- "retained REAP mass" when selecting by the same metric is maximal by construction; report it, but don't use it to compare K or metrics.
- per-layer constant K is forced by GGUF; saliency concentration differs per layer, so the min-layer numbers the script already reports are the right thing to watch. stats come from the unpruned model: pruning layer l shifts layer l+1's inputs, which a one-shot score ignores. RCO (§2.3) handles this properly; one-shot REAP doesn't.
- the shared expert (
ffn_*_shexp, sigmoid-gated) is untouched by slicing and buffers some substitution error. good, but it also means layer-level KL can look fine while specific rare-token behaviors break, which is why §6.4 tier 2 has to gate the decision.
3.4 smaller issues in the selector #
kept = sorted(ranked[:k]): ascending order, which is what GGUF slicing needs (router rows and expert slabs stay aligned). ok.- tie-break
(score, prob_sum, count): fine; zero-hit experts fall back toprob_sum. - no validation:
k ≤ n_exp, every layer present,Σcountinvariant (§2.5). add asserts so a silently-zero REAP file (B2) can't produce a keep map. - the expert ids in the existing trace are pre-prune ids; any cache replay on a pruned model needs the keep-map remap plus re-selection of the top-10 among survivors from the stored top-64.
- vision (C3): no image tokens in calibration, so vision-path experts get ~0 score and go first. PLAN §2.3 says images are added before final export; the current keep maps must not be exported or used to judge vision.
4. recommended order of fixes #
- rebuild the corpus: document-level round-robin across all five sources, token-budgeted per domain, group-split by task/chunk/id, dedupe, whittle
prompt+responseas-is, ukisai last-episode-per-trial with the preamble kept once, TB split by task usingxhigh/, hotchpotch eval from itstestsplit, ChiTako split on chunks (or eval JA from a different source). - rerun imatrix with
--parse-special,--chunks -1(all ~0.6–0.7M tokens),-b 512,-c 2048if time allows; keep--moe-cache-miboff. - harden the hook: ids from
ffn_moe_topk-{il}, abort on unexpecteddown_t, count non-finite drops, fall through to normal imatrix collection for the router, write the trace header on every flush (so a killed run is readable), and log(chunk, pos, token_id)per trace row. - emit REAP mean (with shrinkage) and per-domain stats; build keep maps for {mean, sum, per-domain-max} × {320, 256, 224}.
- screen with tier-1 KL on leak-free eval files (tokenized with special tokens), plus the trace-based substitution metrics from §3.3; finalists to §6.4 tier 2.
- check the §2.5 invariants on the existing JSON/trace before deleting them: they tell you whether the hook itself was sound even though the corpus wasn't.