opus audit: phase 3 prep (slop-miner v4 fixes, speech-act surgery, Q2_0 + transplant) #
read-only · auditor: opus-review-phase3-prep · scope: v4 §4 fix list (mine_llm_isms_from_transcripts.py, results/banned_llm_isms.json, PLAN.md §2.4/§2.6/§2.7.A #26–#27/§5.2), scripts/prepare_phase3_sft_data.py + results/phase3_speech_act_surgery_report.json + data/phase3_sft_seed_examples.jsonl, scripts/phase3_lora_and_transplant.py. upstream reference: llama.cpp master a0e6dc7 (2026-10-14); Q2_0 was added in bec4772 (#24448).
verdict: BLOCKED for phase 3. the miner fixes mostly landed. the two new phase 3 scripts both have correctness bugs that would quietly corrupt training:
- the
Q2_0codec doesn't match ggml. the zero point, the scale and the bit layout are all wrong. decoding real ggml bytes with the script gives values with Pearson r = 0.064 against the true dequant. the self-test only round-trips the script against itself, so it can't catch this. - speech-act surgery flattens every reply onto one line.
" ".join(units)removes newlines, lists and tables, and puts code fences inline. 79% of the gemini reference turns have newlines and 21% have fences. - the emoji stripper eats the kaomoji the persona is supposed to have. of the 95 characters it removed from the gemini reference, 93 are
✧ ♡ ♥ ✿ ☆ ♨. - the transplant guard is a blacklist. fused
ffn_gate_up_exps(the experts) classify asother_frozen, and the transplant will overwriteother_frozen. several real qwen4exp tensor names also don't match the regex.
self-tests: python3 -B mine_llm_isms_from_transcripts.py --self-test, python3 -B scripts/prepare_phase3_sft_data.py --self-test and python3 -B scripts/phase3_lora_and_transplant.py --self-test all PASS (exit 0). no __pycache__ anywhere in the repo afterward (the empty dir from v4 is gone too). my probe scripts live in /tmp (q2check.py, clscheck.py, surgcheck.py), and the sqlite DB was opened mode=ro.
1. v4 §4 fix list #
| # | fix | status | evidence |
|---|---|---|---|
| 1 | let me check vs abstention conflict |
done (option b) | PLAN §2.4 L108: target = "i don't know / i'm not sure / i haven't checked … or a direct search tool call (never officecore 'let me check')". PROTECTED_STOP_NGRAMS L171–174 now also covers haven't checked, didn't run, haven't run, don't think. every let me … row goes to phase3_sentence_surgery, and none are in the FTPO subset. none of the 70 phrases (as words) appears in either abstention target string. nit: the search-abstention target ends in "— pulling it up first.", which is itself a pre-tool announcement, the thing §2.6 says to remove. |
| 2 | per-order p_indep + 3-gram self-test |
code done, barely changes the ranking | the prior is now 0.45·pooled + 0.45·per-order unigram + 0.10·per-order uniform, and all 70 δ/z recompute exactly from stored alpha_w (0 mismatches). but on real data the pooled term still dominates α_w for c_hum=0 rows, so δ hits the ceiling log(1+N/(0.45α₀)) + log((N_h+α₀)/(N_l+α₀)) = 2.256. 27/50 c_hum=0 rows sit within 0.06 of it (14× order 3, 9× order 4, 4× order 2). z/√c_llm = 0.277–0.312 for all 50 c_hum=0 rows, and Spearman(z, c_llm) = 0.996. so c_hum=0 rows are still ordered by raw frequency. the self-test 5b comment says "realistic alpha_0=5000.0" but the code passes alpha_0=200.0 with toy counts, and it only asserts δ₁ ≠ δ₂. PLAN #26 L262 says 0.5/0.5 and "c_llm never cancels … for any order"; neither matches the code or the data. impact is now low, because selection is driven by the hand lists (see 3). fix the #26 text, or drop the pooled term from the prior (leave-one-out / external background). |
| 3 | genre confound | done, but by whitelisting | 0/70 rows are first-person action reports and generic English is gone. but 63/70 rows come straight from the hand-written RHETORICAL_ALLOWLIST (28) or SIGNPOST_PREFIXES (35). only 7 are "organic": you want, your call, think that's, check what, don't want, i'd want, want to know, and those 7 are the questionable ones. the miner is now a confirmation filter over a curated list, not a discovery tool. that's fine, but PLAN/memory should say so. FIRST_PERSON_WORK_REPORT_WORDS is misnamed (it holds one, same, this, fine, real, cheap, things). |
| 4 | family filter | stronger, still below the v4 ask | now requires ≥ 2 families with ≥ 2 hits and non-top families ≥ max(4, 3% of c_llm). v4 asked for each family ≥ max(5, 5%). 14/70 rows still have non-top share < 10%. 11 rows have a non-Claude per-turn rate < 0.2× Claude's (Claude = 70.6% of turns): so i'll, so i'm (0.08), so i'd, i'll check, right away, your call, want me to do, i'll look at, think that's, i'll check the, the catch is. three of these are FTPO rows. |
| 5 | §5.2 range + sha256 | range done, hash open | L422 now reads 17.4–19.9%. calibration_mix_v2.txt sha256 is still recorded nowhere (assemble_corpus.py L270 prints it, but there's been no Colab re-run since). |
| 6 | self-tests | done | added a CC isSidechain row, an albedo root-vs-child user row, a numeric δ/z recompute, and a 3-gram case. the 3-gram case is weak (see 2). |
FTPO subset (20 phrases, 360 tuples) worth a decision from dawn: think that's (organic, 4 non-Claude hits; FTPO here suppresses "i think that's …", which is exactly the hedging the abstention work wants), so far and as expected (honest progress qualifiers), rather than (very common English construction). I'd drop think that's outright. tuple bookkeeping is consistent: the per-row union equals the global set (1332), and the phase4 union equals ftpo_qwen_token_id_tuples (360). I did not re-tokenize: tokenizers isn't installed locally (v4 already checked the mapping procedure).
2. scripts/prepare_phase3_sft_data.py #
blockers #
- B2. the output loses all formatting.
apply_speech_act_surgeryrebuilds the reply with" ".join(units)(L171), andSENTENCE_SPLIT_REsplits on\n+. this runs on every non-override target, even when nothing is stripped. probe:"Root cause…\n\n- set …\n- clamp MSS\n\n| k | kld |…"→"root cause … - set … - clamp MSS | k | kld | |---|---| …". a fence becomes"here is the fix: ```python\nx = 1\n``` that should do it.", which is invalid markdown because the fence isn't at the start of a line. 79% (520/656) of the gemini reference text turns contain newlines and 21% (140) contain fences. SFT on this teaches the model to emit wall-of-text replies with broken fences. fix: find the sentence spans to drop in the original string and delete only those spans, keeping the separators around them. - B3. kaomoji get stripped.
UNICODE_EMOJI_REincludes U+2600–27BF. in the read-only gemini audit, the 95 stripped characters were U+2727 ✧ ×50, U+2661 ♡ ×32, ♥ ×4, ✿ ×3, ♨ ×2, ☆ ×2, 🐾 ×1, ✌ ×1. so ≥ 93/95 are kaomoji decoration in the target voice. probe:(◕‿◕✿)→(◕‿◕),♡ ☆ ♪deleted,❤️leaves an orphan U+FE0F, and 🇯🇵 flags survive (regional indicators aren't covered). fix: strip onlyEmoji_Presentation/pictographs ≥ U+1F000 plus FE0F/200D/skin tones/regional indicators, and keep U+2600–27BF.
major #
- M1. technical last sentences get dropped as "closing solicitations".
if you want,if you'd rather,whenever you wantandhappy tomatch at the start of the last sentence, whatever comes after. probes, all stripped: "If you want lower prefill latency, set--ubatch 4096." · "If you'd rather trade JA quality for speed, K256 is 9.5 GB." · "Whenever you want a manual run,systemctl start backup." · "Happy to report all 9 keep-maps load." fix: only strip when the sentence is an offer (second-person question, or a "(i can|i'll) … for you" clause), and never strip one that carries code, numbers or an imperative instruction. - M2. pre-tool stripping deletes reasoning and misses the obvious cases. it drops the whole sentence: "Let me check whether
MemoryHighis set, since if it's unset the slice can balloon to 14 GB." loses the hypothesis. it only looks at trailing sentences, so "Let me check the unit file. The OOM is …" is kept.and i'll,let me actually,your call,want to know,you want me to …?, and the synonyms §2.6 itself names (shall i …,would you prefer …,should i go ahead …) aren't matched at all. JA isn't handled either: there's no。split, and "了解です。Let me check the logs." is kept. JA is ~25% of the mix. - M3. stripping an opener can flip the meaning. "You're right, but the race is in the writer" → "but the race is in the writer…", and "Makes sense, except …" → "except …". those openers introduce disagreement. don't strip when the rest starts with but/except/though/however.
- M4. lowercasing breaks identifiers outside backticks.
README.md→readme.md,Makefile→makefile,HOME/CUDA_VISIBLE_DEVICES→ lowercase,K=320→k=320,Qwen3.8-Flash-Next→qwen3.8-Flash-Next(the--?[\w\-]+flag pattern protects-Flash-Nextbut not the head). protect ALL_CAPS_UNDERSCORE,name.ext, CamelCase, and tokens containing digits or=. - M5. KD targets won't line up with teacher logits.
known_keep_teachersetsuse_teacher_topk_logits=Trueon a rewritten target (lowercased, opener/closing stripped). PLAN §2.4 L116 has top-k logits generated offline. if they come from the teacher's original text, the token positions won't line up. order has to be: surgery first, then teacher-forced top-k on the surgered target. write that down in §2.4. - M6. the R-Tuning matcher is a raw substring
all()."42"matches"1042"and"rope"matches"europe"(both →known_keep_teacher, ratio 1.0). the RFC seed passes two mutually exclusive keywords (["not an assigned", "does not exist"]) toall(), so four students who all agree with the (correct) teacher still come outlost_abstain_honest(verified). there are also just two canned abstention strings for every lost fact, which invites mode collapse onto one phrase. fix: normalized answer match with any-of alias sets and word boundaries (or a judge), a template pool for abstentions, and evaluate over-refusal per §2.4 L118. - M7. the phase3 partition in
banned_llm_isms.jsonisn't wired to anything.prepare_phase3_sft_data.pynever reads it. add a coverage test that fails when aphase3_speech_act_ngramsphrase has no matching rule and no explicit waiver.
fine #
- the gemini audit uses a read-only URI and exports counts only. 655 text turns: 544
gemini-3.8-flash, 111gemini-4-argon. I checked the tool linkage against the DB: text items are followed by auserrow (409) or the end of the session (243), never by a separate tool item, and only 4 items carry text plustool_calls. sopre_tool_narrations_stripped = 1is real: these models basically don't narrate tool calls. that's good evidence for the "silent tool call" target. - voice-override passthrough is correct, the seed JSONL matches the report preview, and there's no personal text in either.
3. scripts/phase3_lora_and_transplant.py #
B1. Q2_0 codec ≠ ggml (blocker) #
upstream ggml-quants.c (quantize_row_q2_0_ref L74, dequantize_row_q2_0 L439; struct ggml-common.h L187):
| ggml | script | |
|---|---|---|
| value | (q − 1)·d → {−d, 0, +d, +2d} |
(q − 2)·d → {−2d, −d, 0, +d} |
| scale | d = amax |
d = amax/2 |
| layout | element j → qs[j/4] >> 2·(j%4) (4 consecutive per byte) |
element j+16k → qs[j] >> 2k (quarter-strided, Q4_0-style) |
measured on 16,384 N(0,1) weights (pure-Python path; numpy isn't installed for python3 either): script bytes ≠ ggml bytes. decoding real ggml bytes with the script gives max |err| 10.97 against ggml's own dequant, rel-err 2.99, and Pearson 0.064. the self-test's "exact grid" block decodes under ggml to [-0.25,-0.25,-0.25,-0.25,0,0,0,0] instead of [-0.5,-0.25,0,0.25,…]. training against these "deployment experts" would train against noise. fix: dequant ((qs[j>>2] >> ((j&3)*2)) & 3) - 1) * d. add a golden test built with a real ggml quantizer (gguf-py has no Q2_0 class, so use llama-quantize on a 1-tensor F32 GGUF or a tiny C harness linked against ggml). aside: the upstream ref quantizer never emits code 3 (|w/amax| ≤ 1, so it's effectively ternary). a GSQ-produced file may use all four codes, which is why the dequant formula is the part that has to be exact.
also: the numpy path returns .tolist(), which is billions of Python floats for ~16 GB of experts. it needs to return an ndarray/torch tensor. there's no torch path despite the docstring, and no LoRA or merge code yet, despite the filename.
B4. tensor classification and the transplant guard (blocker) #
against llama.cpp src/models/qwen4exp.cpp load_arch_tensors (L171–302) + llama-arch.cpp names:
transplant_gguf_tensorsrefuses onlyfrozen_routed_experts/frozen_ple_and_embd.other_frozengets written.blk.N.ffn_gate_up_exps.weight(the fused formcreate_tensor_gate_up_expstries first) classifies asother_frozen, so the expert guard can be bypassed. fix: whitelist (cat == "trainable_dense_router_shared"or refuse), and add the fused name (plus_expsbias/scale tensors) toFROZEN_EXPERT_RE.- real names the regex misses (→
other_frozen, so not trainable):blk.N.indexer.{q_proj,k_proj,q_norm,k_norm}.weight(the code assumesindexer_*with underscores;indexer_weightsdoesn't exist),attn_q_norm/attn_k_norm,ple_norm_key,output_hc_{norm,down,up}.weight,ssm_a(no suffix; frozen is probably fine, but it should be deliberate). the regex lists names that don't exist for this arch (ffn_norm,post_attention_norm,attn_norm,output_norm("there is no output_norm: the final hyper-connection mixer carries it")). - the scope goes past PLAN §2.2 L59 ("attention/QSA, GDN, shared expert, router, norms"):
hc_*andple_{key,value,conv1d,norm_*}are also trainable. the Swift precedent touched onlyssm_out/attn_output/ffn_down_shexp(§2.1). decide this and write it into §2.2. the MTP layer (blk.48.*, hardcoded) stays frozen while the trunk dense weights move, so expect MTP acceptance to drop. measure it, or tune it too. - §2.2 L63 "quantize only those tensors at the types the Swift GGUF already uses" isn't implemented. the transplant only checks byte length, so the caller has to produce bytes in the tensor's on-disk
ggml_type. there's no codec for those types here, and a same-size wrong-type buffer would pass. at minimum, take(ggml_type, bytes)and assert the type matches. - minor: a
.tmpis left behind on exceptions, there's no free-space check before copying a ~40 GB shard, and no directory fsync. the byte-identity self-test is otherwise sound (header + expert bytes unchanged, router replaced).
4. fix list (ordered) #
- Q2_0: fix dequant/quant to the ggml layout, add a golden vector from real ggml, return arrays instead of lists. (B1)
- transplant: whitelist-only writes, fused
gate_up_expsin the frozen set, real qwen4exp names (indexer dotted, q/k norms, ple_norm_key, output_hc_*), a type check on replacements, and record the trainable set in §2.2. (B4) - surgery: delete spans in place instead of re-joining. narrow the emoji class so ✧♡♥✿☆♪ survive. (B2, B3)
- surgery false positives: closing-solicitation offer test, clause-level pre-tool removal, no opener strip before but/except, identifier-safe lowercasing, JA sentence split + patterns, §2.6 synonyms, coverage test against the JSON phase3 list. (M1–M4, M7)
- R-Tuning: surgery → teacher-forced top-k ordering in §2.4, word-boundary any-of matching, abstention template pool, and drop "pulling it up first". (M5, M6)
- miner/PLAN: drop
think that'sfrom FTPO (and decide onso far/as expected). correct PLAN #26 (0.45/0.45/0.10, pooled ceiling still binds), fix the self-test 5b α₀ comment, and say in PLAN/memory that selection is allowlist-driven. paste the sha256 after the next Colab run.
nothing in the repo was modified; this report is the only file written.