25 kB 283 lines
e68889be
⋯
persona/audits/opus_phase5_scr_and_persona_v1.md
download raw history
at main Markdown

opus: Phase 5 SCR + disk sessions (#98), contrastive persona / negative slop (#97) — v1 #

short version

  • A (#98):
    • disk sessions and SCR relocation are written as a patch that applies cleanly and passes syntax check. the patch and the probes are in /tmp/opus_scr_probes/.
    • the RoPE/int8 relocation is exact apart from one requantization of k's dims 0..63: attention-score noise +11% over fresh int8.
    • Δ must be a multiple of 4 because of the pooled indexer blocks.
    • the GDN staleness has a closed-form bound for its direct term. on the real gates, ~3/4 of the heads forget B within ~100 tokens, and 104 heads keep it for > 1,000.
    • nothing in A ran on the GPU. the test harness (--scr-test) is in the patch.
  • B (#97):
    • on the real L47 dumps the base model already doesn't produce the rejected slop at these decision points: p(rejected opener) 0.0003, p(closer set) 0.0000. the negative half of the objective is pushing on non-events.
    • the exact head-side solver is built, its gradient is checked, and i ran it four ways. it memorizes the 49 train replies but doesn't transfer the voice: held-out reply CE is flat or worse.
    • every config breaks the 0.01 KL bar on a held-out chat document (0.017–0.075).
    • so the voice belongs in Phase 3's real-forward LoRA, and the rejected side has to be mined from the model's own samples.

A. Phase 5 (#98): disk sessions + SCR on the hybrid stack #

A.0 what the window actually holds (checked against window.cpp / qsa.metal / gdn.metal) #

state layout (source) bytes
GDN recurrence, 36 layers (l % 4 != 3) gst_[gi] f32 S[(i*48 + h)*128 + j], i = key dim, j = value dim (gdn.metal L9; init L556) 3,145,728 each
GDN conv history ghist_[hp_][gi] f32 [10240 ch][3] oldest first; ping-pong by hp_ (L557–558, fold L1675) 122,880 each
PLE history pleHist_ f32 [10240 ch][9] (L541; shifted in commit L1786–1792) 368,640
PLE token window ss.ple_prev[2] (host SessionState, not in checkpoint_save) 8
QSA K, 12 layers kq_[qi] int8 [cell][2 kv][256], ksc_ half [cell][2][4] (one scale per 64 dims) 512 + 16 / cell
QSA V vq_ / vsc_, same shapes, no RoPE 512 + 16 / cell
indexer pooled_ f32 [cell/4][128] (complete blocks only), tail_ f32 [4][128] raw keys (slot cell & 3), dead_ f32 [128] (cell 0 only) 128 / cell + 2,560
MTP layer (if loaded) mkq_ … mpooled_, same QSA layout (L1862–1864) same
  • total fixed = 36·(3 MiB + 120 KiB) = 112.22 MiB of GDN, + PLE + tails/deads = 118,069,248 B (112.60 MiB); per cell 14,208 B (12 · (1,056 + 128)).
  • the existing checkpoint_save (L1688) stores only the running part (GDN + ghist + PLE + tails + deads); its comment says why: "the KV and pooled keys are positional", i.e. --serve assumes they're still in the buffers. a disk session must also carry kq/vq/ksc/vsc[0, n), pooled[0, n/4) and ss.ple_prev, or the restored model has lost its two-token PLE n-gram context.
  • RoPE, exactly (norm_rope L61–81, prep L104–147): position = cell (pos_base is 0 on all five QsaArgs sites), NEOX pairs (i, i+32), i < 32, angle cell · base^(−2i/64), base = 1e7 (read from the GGUF). K is rotated before its int8 quantization (L134 then L136–147), so the rotated dims 0..63 are exactly scale group 0; dims 64..255 and scales 1..3 never see a position. pooled[b] is rope(rms(mean of 4 raw keys) · ik_norm, 4b), also dims 0..63 only, and stays f32. dead is unrotated. q / qidx are rotated at the query's own position at run time, so nothing about them is cached.

A.1 disk session save / load #

/tmp/opus_scr_probes/window_session_scr.patch (applies with patch -p1 -d vendor/strata, dry-run checked; both patched TUs pass clang++ -fsyntax-only -std=gnu++20 -Wall -Wextra with the exact build.ninja include set, only pre-existing warnings):

  • Window::session_save(path, ids, n_cells) / session_load(path, ids, n_cells) + C exports window_session_save/load. file = scr::SessionHeader (magic, version, flags PLE/MTP, n_cells, model shard size + tensor count + rope base, ple_prev, payload size) + token ids + the sections in a fixed order, each 16 KiB-aligned, read/written straight into the MTLBuffer storage (shared memory) with 8 MiB pread/pwrite pieces over 4 threads. no ConversationCheckpoint vectors, no second copy. written to path.partial then renamed. load checks magic/model/ctx/PLE/MTP/exact file size before touching anything and reset()s on a short read.
  • save forces the pending lazy fold first (as checkpoint_save does); load sets pend_fold_ = 0, last_T_ = 0 and writes the committed conv history into ghist_[hp_] (the parity the next window reads), exactly as checkpoint_restore.
  • refuses M-RoPE (vision) sessions: mrope_ comes from the server per request and isn't the window's to save.
  • CLI: --save-session f saves after the prefill of the first n_prompt − 1 tokens; --load-session f replaces that prefill and checks the stored ids equal this prompt's prefix. both print STRATA_SESSION … ms.

exactness: the loaded buffers are bitwise the saved ones and every kernel after that is deterministic, so the continuation is bitwise the continuous run's, with two conditions:

  1. compare against a continuous run that also passed --save-session (the forced fold puts the fold in its own command buffer; same kernel, same inputs, should be bitwise anyway, but it removes the doubt);
  2. STRATA_METAL_MOCE_BONUS=0. with λ > 0 routing depends on which experts are resident, and a fresh process starts cold, so the loaded run routes differently for its first tokens. that's routing drift, not session drift, but it will show up as KLD ≠ 0.

test (parent, GPU, not run by me):

X="--metal-gguf …/K256-q2_0-healed-metal.gguf --ple-gguf … --tokens-file data/m5_val_prompts/prompt_2048tok_ops.txt --n-prompt 1984 --n-gen 64"
STRATA_METAL_MOCE_BONUS=0 strata_window_cli $X --save-session /tmp/s.bin --logits-out /tmp/ref.bin
STRATA_METAL_MOCE_BONUS=0 strata_window_cli $X --load-session /tmp/s.bin --logits-ref /tmp/ref.bin

expect mean_kld=0.000000 top1_agree_pct=100.00 delta_ppl=0.0000, and STRATA_SESSION load_ms vs the first run's prefill_ms.

bytes and time (probe [6]; the read rate is an assumption, measure it):

cells file at 5 GB/s at 3 GB/s
1,024 126.5 MiB 26.5 ms 44.2 ms
4,096 168.1 MiB 35.3 ms 58.8 ms
8,192 223.6 MiB 46.9 ms 78.2 ms
32,768 556.6 MiB 117 ms 195 ms
131,072 1,888.6 MiB 396 ms 660 ms
  • so "< 50 ms" holds to ~8K cells cold if the SSD does ≥ 5 GB/s, and always from the page cache. the floor is the 112.6 MiB of GDN state, which every session pays even at 10 tokens. that's the thing to shrink if many small sessions are parked:
    • an fp16 or bf16 GDN state halves it, but it's no longer bitwise. measure its KLD before using it.
    • for the "many turns of one conversation" case, keep one full session plus per-turn deltas of only the new cells (14 KB/token), with the GDN state of the latest turn only.
  • vs re-prefill: 2K tokens at the M5's prompt rate is seconds; 168 MiB is tens of ms. the session is 2–3 orders of magnitude cheaper.

A.2 SCR relocation: the exact byte/RoPE rules #

scr::relocate_layer (scr_state.hpp, used by Window::scr_relocate, one thread per QSA layer, plus the MTP layer if loaded), for a span of n cells moved by Δ = c_dst − c_src:

  • kq group 0 (dims 0..63): x = q · half(ksc[.][.][0]) → rotate each pair (i, i+32) by Δ·θ_i (cos/sin in double, as the engine's table) → requantize with qsa_prep's rule (sh = half(amax/127), q = clamp(rint(x / float(sh)), ±127)), new ksc[.][.][0]. rotation composes exactly (R(Δθ)R(pθ) = R((p+Δ)θ)), so the only loss is the second rounding.
  • kq dims 64..255, ksc[1..3], vq, vsc: plain byte moves. (probe [1]: 0 mismatches over 20,000 rows.)
  • pooled: complete blocks inside the span, b → b + Δ/4, dims 0..63 rotated by Δ in f32. Δ must be ≡ 0 (mod 4): otherwise every block of the span would mix 4 different cells, and the raw indexer keys aren't stored (only their normed, rotated mean), so it can't be rebuilt. the patch refuses Δ % 4 ≠ 0; the harness pads B' (the test pads with \n, id 198) to make |B'| ≡ |B| (mod 4).
  • the junction block (the block holding c_dst when c_dst % 4 ≠ 0) mixes B' and C cells: it isn't moved, and the splice must recompute at least (4 − c_dst % 4) % 4 of C's first cells (scr_min_junction). the B' prefill writes its first part.
  • tail_ (raw keys of C's last partial block) stays valid because c mod 4 is preserved by Δ ≡ 0 (mod 4). but every prefill writes it, so it has to be restored after the junction recompute. that comes for free with checkpoint_restore(S_ABC). dead_ depends only on cell 0 ∈ A and never changes.
  • walk order: ascending when Δ < 0, descending when Δ > 0, so no source row is overwritten before it's read. (probe [3]: overlapping spans both ways, |Δ| = 1,200 < n = 1,001, plus the Δ % 4 refusal: 0 failures.)

numbers (probe [1]–[2], keys with lognormal per-dim gains, p ≈ 30,000, Δ = −2,048):

  • group-0 RMS error vs the exact float row: fresh prep 1.081e-2, relocated 1.504e-2 (1.39×, the √2 of two independent roundings). group-0 bytes equal a fresh prep at p+Δ for 76.5% of values, the scale for 46.5%.
  • attention score q·k/16 RMS error vs float: fresh int8 1.51e-2, relocated 1.67e-2 (+11%; score RMS 2.90). the relocation's added noise is ~0.5% of a score's size, far below any staleness effect.
  • pooled: max |relocated − fresh| = 1.9e-6 on values up to 13.8: f32 rounding only.
  • the exact alternative (an fp16 shadow of group 0 to re-rotate from) costs +256 B/cell/layer (+22% KV). not worth it at +11% score noise.

A.3 SCR splice order and what "keep the post-edit GDN" means here #

the GDN state is one recurrent state, not a per-position cache, so [A B C] → [A B' C] has only these choices:

variant GDN running state after the splice C's KV cost
fresh (reference) S_{AB'C} exact recomputed prefill |B'| + |C|
keep (CLM / Connectome rule, PLAN §2.5 #4) S_ABC: has read B, never B' relocated; first J cells recomputed after B' prefill |B'| + J
gdnex (diagnostic) S_{AB'C} exact (from the fresh run) relocated (+J) n/a: isolates KV staleness
lossy S_{AB'C[:J]}: never read C past J relocated (+J) prefill |B'| + J

splice procedure (--scr-test, in the patch):

  1. load the [A B C] session (or be there already);
  2. scr_relocate(|A|+|B| → |A|+|B'|, |C|);
  3. checkpoint_restore(S_A), the turn-boundary checkpoint at |A| the harness already has; ple_prev of A;
  4. prefill B' ++ C[:J] at |A| (fresh KV for B' and the junction, GDN goes S_A → S_{AB'C[:J]});
  5. keep: checkpoint_restore(S_ABC) (GDN, ghist, PLE history, tails); ple_prev = C's last two tokens (token ids are never stale);
  6. continue at |A|+|B'|+|C|.

rows printed: orig (no edit, the old context: tells you how much B → B' matters at all), keep / gdnex / lossy × J ∈ {0, 8, 16} (raised to the junction minimum), and lossy J=|C|, which is the fresh prefill and must give KL 0 (built-in sanity row). each row has mean / p90 / max KL vs fresh, top-1, PPL, load / relocate / prefill ms and the speedup vs the fresh B' + C prefill.

strata_window_cli --metal-gguf … --ple-gguf … --ctx 16384 --n-gen 128 \
  --scr-a A.ids --scr-b B.ids --scr-bp Bsummary.ids --scr-c C.ids --scr-d D.ids --scr-j 0,8,16

suggested spans: A = system + 2 turns (~1.5K), B = a long tool output (~3K), B' = its fold summary (~200), C = the last 2–4 turns (~1.5K), D = the real next assistant turn (128). run it with STRATA_METAL_MOCE_BONUS=0, then once at the deployed λ. add the loop check (PLAN §2.7 #13): free-generate 256 tokens from the keep J=0 state and count repeated 4-grams.

A.4 the part of GDN staleness that has a closed form #

from gdn.metal L89–119: S' = g (I − b k kᵀ) S + b k vᵀ with |k| = sqrt(Σcv²/(Σcv²+eps)) ≤ 1, b = sigmoid ∈ (0,1), g = exp(softplus(α + dt_h) · ssm_a_h). two runs over the same C inputs from different start states differ by D' = g (I − b k kᵀ) D, and ‖I − b k kᵀ‖₂ = 1, so per value head

‖S_ABC − S_{AB'C}‖F ≤ exp(Σ{t∈C} log g_t) · ‖S_AB − S_AB'‖_F (direct term)

(probe [4] with gdn.metal's exact update: ratio to the bound ≤ 0.9966 over 64 steps.) the indirect term, C's own k/v/α/β computed under B instead of B', has no closed form. that one is what keep vs gdnex measures.

the real gates (probe [5], ssm_a and ssm_dt.bias from the healed GGUF, at a neutral input α = 0), as half-lives ln 2 / −log g in tokens over the 36 × 48 = 1,728 value heads:

  • min 0.01, p10 1.1, median 18, p90 499, p99 3,746;
  • 414 heads > 100 tokens, 104 > 1,000, three at ~2·10⁶ (L01, L08, L24: no decay at all).
  • so for ~3/4 of heads the direct staleness is gone within ~100 tokens of C. a small set of long-memory heads keeps "having read B" indefinitely. that's the CLM framing ("more faithful to its computational past"), and it's where drift over many folds would accumulate (PLAN §2.9 #1).
  • α is input-dependent, so this is a prior, not a measurement. the per-head Σ log g can be accumulated exactly on the host from alL_ (it's host-visible) if we want a live staleness meter.
  • a possible exact fix for the direct term (not built): "GDN difference transport": S_corr = S_ABC + Φ_C (S_AB' − S_AB). it runs only the homogeneous recurrence D ← g(I − b k kᵀ)D over C, and only for the long-memory heads (Σ log g > −7, i.e. not already decayed by 1e-3), with C's k/b/g kept at prefill time.
    • storage ≈ 256 B/token per k-head kept (fp16 k + b + g), so ~50 heads ≈ 13 KB/token. needs S_AB (a turn-boundary checkpoint) and S_AB' (free from step 4).
    • only worth building if the test shows keep ≫ gdnex.

B. Phase 4/3 (#97): contrastive negative-slop + chuuni/VN persona on the exact head side #

B.1 the exact forward and what an output-side edit can and can't touch #

from the L47 dumps (R_post47 = streams after layer 47's FFN write), per row t: xn_c = R_c ⊙ w_onorm,c · rsqrt(mean R_c² + 1e−6), lo = silu(W_odown · xn / 4), z = ¼ Σ_c xn_c ⊙ σ(W_oup,c · lo), ℓ = W_out z (audit v3 §3.1; output_hc_* through their Q8_0 copies, W_out Q6_K).

  • z doesn't depend on output.weight at all. so with teacher forcing, an edit of a row set S of W_out is exact at every position, and linear: ℓ'_v = ℓ_v + ΔW_v·z for v ∈ S, everything else untouched.
  • the only coupling is the partition function: LSE' = logaddexp(LSE_rest, LSE_{v∈S}(ℓ_v + ΔW_v z)), with LSE_rest the logsumexp over the rows outside S, computed once.
  • one full-vocab pass (LSE_rest, the S logits) makes every later step cost |rows| × |S| × 2560. no 248K-wide product per step.
  • the specials (<|im_end|>, <|im_start|>, <think>, </think>, <|endoftext|>) are kept out of S, so their change is exactly Δlog p_special = −(LSE' − LSE). "no special-token rise" is then the constraint LSE' ≥ LSE − ε at every non-terminal row. that's the v3.1 failure mode (freed mass going to <|im_end|>) turned into one scalar per row.
  • output_hc_down/up are exact too, but they act as per-dim gates in (0, 1) on all of xn, on every token. that's a poor lever for adding new directions, and every edit there reaches the whole vocab. blk.47.ffn_down_shexp (v3.2's tensor) is exact as well, but it shifts R, i.e. all 248K logits through the HC mixer. L40–46 aren't exact (later attention/GDN/routing), as noted in v3.

B.2 the objective (implemented: /tmp/opus_scr_probes/persona_head_solver.cpp) #

over ΔW_S (|S| × 2560, Q6_K-round-tripped for scoring as shipped). p = base, p' = edited:

J = w_nll   · mean_{t ∈ chosen replies, train}  [ LSE'_t − ℓ'_{t,y_t} ]                        voice SFT (exact NLL)
  + w_m     · mean_{t = step0, train}           softplus(m − (ℓ'_{chosen0} − ℓ'_{rej0}))      contrastive margin at the shared prefix
  + w_ul    · mean_{t = step0}                  −log(1 − Σ_{o ∈ O \ y} p'_o)                  unlikelihood, opener slop set O
  + w_close · mean_{t = reply end}              −log(1 − Σ_{c ∈ Cl} p'_c)                     unlikelihood, closer set Cl
  + w_kl    · mean_{t ∈ anchors, train}         [ Σ_{v∈S} p_v(−Δ_v) + LSE'_t − LSE_t ]        = KL(p ‖ p') exactly
  + w_g     · mean_{t: y_t not special}         relu(LSE_t − LSE'_t − ε)²                     specials may rise ≤ e^ε
  + λ ‖ΔW_S‖²,       optional ΔW_S ← ΔW_S (I − Q Qᵀ), Q = top-r of the train anchors' Σ z zᵀ
  • the gradient wrt the S logits is the usual p' − onehot / σ(·) / (p'_v[v∈O] − P_O p'_v)/(1 − P_O) / p' − p / −2h p', then ∂J/∂ΔW = Gᵀ Z. finite differences agree to ~1e−4 relative on 8 entries (fdcheck=1).
  • the margin is taken only at step0. that's the one place where chosen and rejected share their whole prefix, so it's a true same-z preference. later rejected tokens sit in different contexts that weren't dumped. that's also why token-level UL beats sequence DPO here: DPO's Σ log π'/π_ref over a rejected reply would push down every ordinary token inside a slop sentence (v3 §5 item 3 all over again).
  • the sufficient condition for KL: KL(p‖p') ≈ ½ Var_{v∼p}(Δℓ_v), so the null-space projection bounds anchor damage by the anchor energy outside Q.

B.3 measured on the real dumps (p4_contrastive_vn, 3,698 rows + 5 anchor traces; /tmp/opus_scr_probes/persona/run*) #

first, the data: at the 70 step0 positions the base model already gives the rejected opener p = 0.0002–0.0003 (train/held-out mean), and the whole opener-slop set ("Hello", "Certainly", "Great", "Sure", "You", "Here", …) 0.001. at the 63 turn ends, the closer set (" Let", " Please", " Feel", " Hope", " Happy") has p ≈ 0.0000, and <|im_end|> 0.85–0.94.

  • the negative half has nothing to push on: the UL loss starts at 0.0010, and the margin loss (0.49 at iteration 0) is at 0 within 25 iterations. same failure as v3 §3 item 2: rejected_slop was written by another model and isn't this model's behaviour.
  • what the base does get wrong at step0: argmax is a special token in 14/70 (12 × <think>, 2 × <|im_end|>). the trace uses </think>\n (198) before the reply; check whether Phase 3 was trained on </think>\n\n (271), the usual template. a mismatch there would explain it.
run config |S| held-out reply CE (907 tok) held-out p(chosen0) train reply CE KL cal / collat (train anchors) KL ftpo_heldout / blind chat (held-out anchors)
base – – 2.593 0.146 2.825 – –
run1 full objective, 300 it, lr 3e−4, w_kl 20 2,917 2.823 (worse) 0.220 0.041 0.0015 / 0.0017 0.030 / 0.075
run2 + null r=256 (87% of anchor energy), w_kl 100, 150 it 2,917 2.692 0.186 0.240 0.0038 / 0.0053 0.031 / 0.073
run3 + ridge 1e−2, S = targets + top-8 at step0 only 1,129 2.581 0.181 0.256 0.0026 / 0.0026 0.024 / 0.051
run4 as run3, NLL at step0 only (opener steering) 1,654 2.599 0.208 2.718 0.0019 / 0.0012 0.0086 / 0.0168

(no argmax flips to a special token on the plain anchors. on the chat ones it's 0–8 per 1,098 rows. full per-row data in run*/rows.tsv; the shipped Q6_K row patch is run*/output_weight_rows.q6k, format [n][row bytes] {id, 2,100 B}×n.)

  • it memorizes; it doesn't learn a voice. train reply CE 2.83 → 0.04–0.26, held-out reply CE 2.59 → 2.58–2.82 (flat at best). 49 replies are a lookup table for 1–3K rows of W_out given the z's.
  • it also leaks into chat. the plain-text anchors stay ≤ 0.005, but held-out chat documents reach 0.017–0.075. the persona contexts share z directions with any chat turn, and 256 null directions cover only 87% of the anchor energy (chat z's are high-rank).
  • the one effect that transfers is run4's step0 steering: held-out p(chosen0) 0.146 → 0.208, argmax-special 6 → 3 of 21. but blind-chat KL is 0.0168 > 0.01.
  • so against the falsifier: every head-side persona objective i ran exceeds 0.01 KL on a held-out chat document. the 3-domain eval texts (ops/chat_json/ja) aren't dumped, so their KL isn't measured here. ops_tools/ja are closer to the plain anchors (≤ 0.005), but chat_json is chat. don't ship any of these.

B.4 what i'd do instead #

  1. mine the rejected side from this model's own samples (PLAN §7 phase 4). run the 70 prompts at T = 0.8, 8 samples each, and keep the samples whose spans match the slop lexicon / narration patterns; those spans are the rejected tokens, in their real contexts.
    • only then does UL/FTPO have mass to remove. the base already doesn't say "Certainly!" here, so the slop that does exist will be different (narration before tool calls, hedged closers, …).
    • dump those sample traces through the engine (STRATA_METAL_DUMP_MOE) and the same solver scores them exactly.
  2. voice (lowercase, kaomoji, chuuni/VN banter, R-Tuning abstention) belongs in Phase 3's real-forward LoRA. it's content-level and needs the whole network.
    • an exact output-side map can't generalize it from 49 replies.
    • use chosen-NLL plus the mined rejected tokens as token-level UL (not sequence DPO, for the reason in B.2), with the 3-domain KL anchors + a chat-formatted anchor document as a held-out bar.
  3. the step0 problem is a template question first. check </think>\n vs </think>\n\n against the Phase 3 SFT data before training anything on it.
    • if it's real, the exact zero-collateral fix is a position-conditioned sampler bias: at the first assistant token after </think> + newline, add −B to <think>/<|im_end|>.
    • it touches no other position (the same reasoning as the v3 §6.3 prefix penalty).
  4. if a head-side lexical edit is still wanted, reuse this solver with S = only the mined slop tokens + their top-8 natural alternatives, chat anchors in the KL term, and the same bars as v3.2: (a)–(g) plus blind-chat KL ≤ 0.005. keep w_g on: it's what keeps <|im_end|> from collecting the freed mass, by construction.

B.5 a nuance on the Q6_K round trip #

re-quantizing unchanged Q6_K rows isn't idempotent: ggml's scale search lands elsewhere. at ΔW = 0 the 2,917 round-tripped rows alone give KL 0.0002–0.0005 (fdcheck run, iteration 0). so ship original bytes for every row whose edit is below the Q6_K step; only truly edited rows should be re-quantized.

files #

  • /tmp/opus_scr_probes/window_session_scr.patch: patch -p1 -d vendor/strata. new metal/src/engine/scr_state.hpp; window.cpp (session save/load, scr_relocate, exports, rope_base_); strata_window_cli.cpp (--save-session, --load-session, --scr-test). dry-run applies cleanly; both TUs pass clang++ -fsyntax-only -std=gnu++20 -Wall -Wextra with the build.ninja flags (only the pre-existing warnings). not built or run on the GPU.
  • /tmp/opus_scr_probes/scr_state.hpp: header-only layout, file header, relocation, gate math (same file as in the patch).
  • /tmp/opus_scr_probes/scr_probe.cpp (+ binary scr_probe): CPU checks [1]–[6] quoted above (./scr_probe <gguf>, < 1 s, exit 0).
  • /tmp/opus_scr_probes/persona_head_solver.cpp (+ binary): the B.2 solver. persona/spec.txt, persona/vn_roles.txt (row roles of the contrastive trace), persona/vocab.txt, persona/run{1..4}/ (rows.tsv, Q6_K row patch), persona/fd/ (gradient check). each run ~1–2 min CPU (Accelerate), no GPU.

what's still open (needs the GPU, parent's call) #

  1. session round trip: KLD 0 with MOCE_BONUS=0, and the measured load ms at 1K / 4K / 8K cells.
  2. --scr-test on a real fold (A.3): keep vs gdnex vs lossy at J ∈ {0, 8, 16}, the sanity row at 0, speedup, and the 256-token loop check. the decision rule: if keep ≈ gdnex, GDN staleness is negligible and SCR ships with J = 8–16; if keep ≫ gdnex, build the long-memory-head transport (A.4) or re-ground the GDN after N folds.
  3. Phase 4: mined rejected samples (B.4.1), and the template check (B.4.3).