Commit Graph
80 Commits
Author SHA1 Message Date
wassnameandClaudypoo e613982c2e readme: moral-aliens framing -- map up top, design choices, dev/full modes
Reframes from MFV-only to the multi-instrument eval: leads with the LLM-vs-human-cultures
map and quickstart; documents the design choices (logprobs for sensitivity, sliding think
budget, fwd/rev position debias w/ arXiv:2308.11483, SI answer-flip metric, coherence
canary); dev (N=1 x 2 orderings, 64 think, greedy) vs full (N=4 x 2 orderings, high think,
+SI +sampling variance) modes; instrument zoo (MFV forced-choice working, Likert landed but
wiring in progress); used-in + moral_stories_foundations training labels. Preserves the
mechanism/labels/validation/citation content. Lint clean (humanizer).

Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
2026-06-21 10:27:09 +08:00
wassnameandClaudypoo c173efa7b9 instrument: Option-3 spec (one reader, reducer-only variation) + canonicalize-at-reader
Unifies forced-choice (nominal) and Likert (ordinal, expectation over the integer
distribution) on tinymfv's answer-token reader. Per scientist panel (docs/reviews/
sci_ma_*.md): canonicalize every frame's distribution to one forward orientation before
metrics+reducer (fixes the asymmetric nominal-reader/ordinal-reducer reflection), renormalize
p with pmass kept as canary, add ordinal |E-error| metric, keying applied only in the profile
reducer (proven orthogonal to framing, not a double-flip), cross-scale guard, negative-control
shuffle. 8 pure-function unit tests pass.

Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
2026-06-21 10:12:05 +08:00
wassnameandClaudypoo b726db958b fix: drop dangling mean_pmass_format key in evaluate() return (NameError); gitignore .pi/
Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
2026-06-18 22:03:28 +08:00
wassname fde697130d Merge branch 'main' of https://github.com/wassname/tinymfv 2026-06-18 21:32:07 +08:00
wassnameandClaudypoo 799042e16e readme: clarify informedness bullet
Lead with the plain point, introduce + link Youden's J, spell out the macro
averaging (one-vs-rest per foundation) and point at _informedness for the
formula. Fix stale "two scalars" -> "three". Drop the "flip-informedness"
coinage and "the headline" tell. External-panel comprehension pass: ready
(4.1/5), accuracy and caveats 4-5 across panelists.

Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
2026-06-18 20:07:25 +08:00
wassnameandClaudypoo 6e60bc2c53 forced_choice: print informedness; drop stale nll_prompt refs
nll_prompt was removed from per_row in f585864 but the script still read it,
crashing the smoke test. Remove the dead refs and surface the new
informedness scalar alongside top1_acc.

Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
2026-06-18 19:47:21 +08:00
wassnameandClaudypoo 205e96bf6a eval: add macro informedness (Youden's J) headline metric
Chance-corrected, argmax-only companion to mean_nll: moves when the answer
flips, not when confidence shifts. 0 = base-rate guessing, so it exposes
majority-class models that top1_acc flatters. Same flip-informedness family
as steering-lite's surgical informedness, anchored on the human argmax here.
README also points at the paired training set moral_stories_foundations.

Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
2026-06-18 19:47:20 +08:00
wassname 220208205c fix: package vignette data with tinymfv 2026-06-13 22:50:56 +08:00
wassname ab4fcd2932 guided: case (c) pmass_allowed=0.0 instead of NaN
When the model emits </think> in natural generation but the answer-slot
window detection fails, that's coherence collapse — the model "finished
thinking" without producing JSON. pmass=0.0 is the honest measurement
(no probability mass on allowed tokens at a non-existent slot) and lets
the coherence canary see the failure as a real signal rather than
propagating NaN through np.mean to crash c_scan. nll_json stays NaN
since no JSON was emitted to score.

Triggered by qwen3.6-27b nf4 + LoRA at c=1.0: 1/4 samples hit case (c)
and the NaN aborted c_scan instead of letting it walk down further.
2026-05-23 21:28:06 +00:00
wassname 0b39d2d3f7 guided: nan_to_num natural-path log_softmax inputs
Qwen3.6-27B nf4 + adapter at c=1.0 produced a non-finite raw logit at a
single generated step in 1/4 samples (others used forced-prefill path);
the natural-path F.log_softmax propagated NaN into mean_pmass_allowed,
crashing c_scan. Bound with nan_to_num(±1e4) — leaves argmax-finite rows
unchanged.
2026-05-23 08:42:31 +00:00
wassnameandClaude Opus 4.7 b777c84e22 guided: trim docstrings and rename _rollout_kv_fork → _rollout_natural_or_forced
Drop legacy-cache-bug rationale from module + function docstrings; the design
stands on its own. Rename to match what the function does (no forking).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 06:57:44 +00:00
wassnameandClaude Opus 4.7 ce5d8c349d guided: hybrid natural+forced eval (architecture-independent, no KV slicing)
Phase 1: batched generate with min_new_tokens=max_new_tokens so cache is uniform
length across the batch (no early stop at </think>). Phase 2: single batched
forced-suffix forward over that cache. Per-sample classification picks
gen.scores at the natural answer position (case a), forced logits (case b
interrupted), or NaN (case c emitted </think> but no answer).

Drops _slice_pkv_one + per-sample fork. The slice helper used layer.keys /
layer.values which crashes on Qwen3.5/3.6 LinearAttentionLayer (gated-delta-net
recurrent state has no .keys/.values). Uniform-length batched cache sidesteps
the cache surface entirely.

Bumps transformers>=5.7 for the Qwen3.5/3.6 gated-delta-net cached-forward
bugfix (resolves to 5.9.0).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 06:54:54 +00:00
wassname 49751fbeab refactor: update evaluation metrics to include pmass_allowed and nll_json 2026-05-21 06:05:16 +00:00
wassnameandClaude Opus 4.7 ef20a83504 guided: skip_special_tokens kwarg + token-id emitted_close
Two paired changes the previous commit should have included.

skip_special_tokens kwarg on guided_rollout_forced_choice and evaluate()
threads into tok.decode for gen_text / gen_text_rev. Default False (return
the raw stream with </think>, chat markers, etc.) matches the "return all
the free things" principle. Callers who want stripped output strip
themselves.

emitted_close now uses a token-id match on gen_ids (`(gen_ids ==
think_end_id).any()`) instead of substring on the decoded text. On models
that mark </think> as a special token, the old substring check would
silently always return False when skip_special_tokens=True stripped it.
Qwen3 currently does NOT mark </think> as special so the bug is latent
there, but the fix is strictly more robust and decouples the detection
from the decode flag.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 03:35:46 +00:00
wassnameandClaude Opus 4.7 7d42568f8d guided: add n_samples / temperature / top_p for sampled think traces
Lets callers ask for N sampled think rollouts per direction instead of one
greedy trace. Per direction we Bayesian-model-average the answer logprobs
across the N samples (logsumexp_n lp - log N) before the fwd/rev average.
Raw per-sample [N, K] logprob matrices stay on the result as
lp_fwd_samples / lp_rev_samples so callers can re-aggregate (log-pooling,
majority vote, etc.).

gen_text and gen_text_rev are now always list[str] of length N (even at
N=1). think_tokens, think_tokens_rev, emitted_close, emitted_close_rev are
length-N lists. At N=1 the BMA is the identity and headline numbers match
the prior greedy path bit-for-bit.

Default max_think_tokens lowered 256 -> 64 for faster default eval (was
expensive overhead on small models that rarely emit </think> anyway).
README updated to match.

Phase 1.5 / Phase 2 already operated per-row, so they extend to B*N
expanded rows without change. Added an explicit assert that the HF
num_return_sequences expansion matches len(user_prompts) * n_samples.

Smoke-tested on Qwen3-0.6B: greedy N=1 matches BMA identity; N=4
temperature=0.7 returns [4, 7] sample matrices and finite pmass; guard
raises if n_samples>1 with temperature=0. evaluate() throughput log
extended to sum fwd+rev think tokens over all samples.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 01:17:23 +00:00
wassname d411af3569 default conditions to other_violate only; drop "negation" wording
The "negation" mention in pyproject + __init__ docstring was stale —
the actual second pass is internal fwd+rev enum-order debias inside
guided_rollout (position-bias cancellation), not a negation framing.

self_violate is not in Clifford 2015 classic (other-violation only).
Default `evaluate(..., conditions=...)` to ("other_violate",); callers
who want both can opt in explicitly. Halves walltime per eval.

CONDITIONS in data.py still lists both (other_violate, self_violate)
as available — the change is only the evaluate() default.
2026-05-20 22:03:00 +00:00
wassname 726324e772 misc 2026-05-20 04:31:24 +00:00
wassnameandClaude Opus 4.7 bfd3a572cf api: drop stripped think_text, return full gen_text only
Old API returned both `think_text` (stripped at </think>) and
`gen_text_full` (everything) — confusing dual field where one was a
strict subset of the other. Library should never silently drop info;
callers can split on `_CLOSE_MARKER` themselves (one line) if they
want the pre-close subset.

Rename:
  think_text     -> gen_text        (forward-frame full decoded gen)
  think_text_rev -> gen_text_rev    (reverse-frame full decoded gen)
  gen_text_full  -> dropped         (redundant with new gen_text)

Internal `_rollout_kv_fork` now returns 3-tuples
(gen_text, n_think, emitted_close) instead of 4-tuples; suf_ids_for
closure updated. per_row dict in eval.py exposes gen_text + gen_text_rev.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 02:05:22 +00:00
wassnameandClaude Opus 4.7 5eabe37f8e guided: rewind KV cache to natural EOS per-sample before forcing answer
Drop force_min_new_tokens — banning EOS to force a 2048-token think
generates ~250 tokens of real reasoning + </think>, then ~1800 tokens of
post-EOS sycophancy spew. Measuring pmass at the forced-answer slot with
that spew in the KV cache corrupted the coherence signal.

Replace with per-sample Phase 1.5: find each sample's first </think> in
phase1_ids, slice the batched DynamicCache (B, n_heads_kv, seq, d_head)
down to one sample × end_pos seq via _slice_pkv_one. The Phase 2 suffix
forward then runs per-sample over the rewound cache so the answer slot
sees only the coherent thinking trace.

GQA-safe (slices batch + seq, not heads). Phase 1 stays batched, Phase
1.5/2 loop adds ~5-10% wall-clock for the bs=1 forward.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 00:27:02 +00:00
wassnameandClaude Opus 4.7 f58586408c guided: reuse Phase 1 KV cache for Phase 2 + drop nll_prompt
Refactor _rollout_kv_fork from 3 phases (gen → prefix-forward →
prefix+suffix-forward) to 2 phases (gen → suffix-only-forward with
cached pkv). The function name finally matches what it does again.

Phase 1: generate(..., return_dict_in_generate=True) captures past_key_values
  for [left-pad, prompt, think, eos-pad]. Same as before; generation already
  used cache internally.

Phase 2 (new): per slot, forward ONLY the suffix tokens (close + interrupt
  + nudge + prefill, ~10-30 tokens) with past_key_values=pkv. Logits come
  out at suffix positions only; pick the last real one. The attention mask
  spans cached prefix + new suffix; pad_id positions get mask=0.

Drops:
- Phase 2a entirely (the prefix re-forward that computed nll_prompt)
- nll_prompt from ForcedChoiceResult, eval.py per_row, eval output dict
- All the sp_per_row / sp_ids_per_row retokenisation gymnastics + boundary-
  merge edge cases (lines 107-129 in the old code) — no more text round-trip
- ~115 lines net

Per-row prompt-NLL was a free diagnostic from the prefix forward; with the
forward gone it would cost a dedicated extra forward. pmass_format is the
stronger coherence canary anyway (per AGENTS.md "Coherence signal hierarchy"
and the bidirectional c-scan walkback in 03b_train).

Speed: marginal (saves ~2s out of ~36s per batch on 27B nf4) — the win is
simpler code, not throughput. Module docstring updated to reflect 2-phase
reality.

Smoke (downstream weight-steering-lite repo, on tiny-random) PASS.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 04:12:20 +00:00
wassnameandClaude Opus 4.7 92505d9562 guided: drop use_cache=False noops; eval: surface per-row think_tokens distribution
use_cache=False on Phase 2a/2b single forwards saved zero compute — the
flag was leftover from the multi-slot KV-fork era (commit ada854c)
that was refactored away (d34dbfa). One forward per call, no cache to
reuse. Cleaner without it; behaviour identical.

eval.py: add `think_tokens` + `emitted_close` to per_row dict (data was
already in ForcedChoiceResult, just not captured). Log distribution
after eval: median/p75/p90/p99/max + emitted_close count. Lets us see
the actual think budget used vs max_think_tokens cap, to decide if the
512 bump (from 128) is paying for itself or can revert.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 03:59:03 +00:00
wassname c4797cd732 expose pmass_format as aggregate signal
ForcedChoiceResult now carries pmass_format (sum prob mass on the K
foundation answer tokens at the JSON answer slot, averaged across fwd
and rev framings). eval.py aggregates it as mean_pmass_format in both
the headline return dict and the info subdict, and propagates per-row
for sweep/audit consumers.

Direct coherence canary: drops when steering pushes the model toward
non-foundation tokens (gibberish, refusal, format collapse). Independent
of which foundation is picked — complementary to top1_acc (label-
agreement; intentional target shift) and mean_nll_prompt (teacher-forced
prompt nll; falls under steering even when generations break).

Surfacing this lets downstream callers (weight-steering-lite walkback,
report dashboards) gate on actual coherence rather than misusing top1
as a budget.
2026-05-18 11:26:00 +00:00
wassnameandClaude Opus 4.7 f9a490c71d verbose trace: log at DEBUG so callers can hide it from INFO sinks
The 1-row demo block (prompt + think + nudge + prefill + scored token +
64-token free continuation) used logger.info, which meant any caller
wrapping the function in an INFO-level sink (e.g. an agent harness)
got the full trace in their stdout. Downgrade to logger.debug so it
still lands in the user's verbose log but doesn't leak to agents.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 22:22:29 +00:00
wassnameandClaude Sonnet 4.6 9b4e094724 tqdm: mininterval=60 so progress shows in captured logs (pueue/non-tty)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 02:04:02 +00:00
wassname 1b145d6f34 Merge branch 'main' of https://github.com/wassname/tinymfv 2026-05-14 11:35:06 +00:00
wassname d34dbfa9f8 Refactor guided rollout scoring to use flat prefix+suffix approach and remove full attention assertion 2026-05-14 11:35:03 +00:00
wassname e3b663d7f7 proofread 2026-05-13 10:48:28 +08:00
wassname cb31acff28 readme 2026-05-13 10:46:18 +08:00
wassname (Michael J Clark) 139c023bb0 Update README.md 2026-05-12 11:38:06 +08:00
wassname (Michael J Clark) 0b4381704f Update README.md 2026-05-12 11:30:04 +08:00
wassname (Michael J Clark) 9d13d7c44a Update README.md 2026-05-12 11:29:20 +08:00
wassname (Michael J Clark) 9db4a568d0 Update README.md 2026-05-12 11:11:42 +08:00
wassname (Michael J Clark) 844fe93821 Update README.md 2026-05-12 11:10:33 +08:00
wassname (Michael J Clark) ed90b79935 Update README.md 2026-05-12 11:05:02 +08:00
wassname 9abddaeac5 return pmass 2026-05-08 16:36:36 +08:00
wassname 0e4443398b clean 2026-05-08 16:14:07 +08:00
wassname 845484483d misc 2026-05-08 16:06:41 +08:00
wassname b20ec56a09 forced-choice unification: drop legacy llm_/calibrated_, normalize human_*, justfile->09 2026-05-08 16:02:25 +08:00
wassname d827a1f873 rename 2026-05-08 15:37:31 +08:00
wassname 0442935279 clean 2026-05-08 15:33:29 +08:00
wassname 8dfaf299ca rename 2026-05-08 15:30:06 +08:00
wassname b12770cb78 fixes, naming 2026-05-08 15:23:54 +08:00
wassname c96d02a675 refactor 2026-05-08 15:15:14 +08:00
wassname d796df85c8 improved to have better airisk, better eval that distinguished factors 2026-05-08 14:04:56 +08:00
wassname 0a769d71a8 Merge feat/batched-guided-rollout: KV-fork unified guided rollout
- guided.py turn-boundary close+nudge scoring (chat-template probed)
- unified guided_rollout / _batch / _multibool onto _rollout_kv_fork core
- dropped prompt_nll, answer_text, raw_full_text, rep_ratio_think (no callers)
- verbose=True now logs full convo + 64-tok generate continuation
- file shrinks ~600->~340 lines

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

# Conflicts:
#	src/tinymfv/core.py
2026-05-06 13:26:09 +08:00
wassname 053009b22e core: drop prompt_nll plumbing in analyse()
Follow-up to e8b51b2 which removed prompt_nll from GuidedResult. Nothing
in eval.py or analyse callers reads it anymore.
2026-05-06 13:17:46 +08:00
wassname e8b51b2b48 guided: unify binary path onto multibool KV-fork core
Replace 3 parallel scoring paths (guided_rollout / _batch / _multibool)
with a single internal `_rollout_kv_fork` core: phase-1 batched think,
one cached prefix forward, N forked suffix forwards (one per scoring
slot). Binary case is just N_slots=1.

Drops from GuidedResult (no callers): answer_text, raw_full_text,
rep_ratio_think, prompt_nll. eval.py updated accordingly. _ngram_rep_ratio
and _scoring_text helpers removed -- their logic folded into the core.

Verbose=True now logs the full conversation (prefix + suffix the model
sees) plus a 64-token free-form generate continuation, so format issues
are obvious from one slot's log.

File shrinks from ~600 to ~340 lines. smoke_batch_parity passes (bf16
max Δp_true=0.098 within 0.20 tol; pre-existing batched-greedy drift).
2026-05-06 13:16:02 +08:00
wassnameandClaude Opus 4.7 a59003d2d0 guided: turn-boundary scoring text via chat-template probe
Replace mid-turn splice (`I should answer now.</think>{prefill}`) with a
clean turn close + user nudge + fresh assistant prefill. Mirrors what a
chat UI emits when a human interrupts a partial assistant turn, which is
on-policy in chat-tuned training data. Empirically: pmass_format ~0.987
on smoke set vs the OOD splice path.

Close marker is probed from the tokenizer's chat template (sentinel
diff), so it works on Qwen/ChatML, Llama3 (`<|eot_id|>`), etc -- no
hardcoded `<|im_end|>`.

Drops:
- emitted_prefill field (no callers)
- try/except TypeError around apply_chat_template (defensive)
- enable_thinking=False kwarg (some templates reject it; complete
  assistant messages auto-strip the think block anyway)
- `\nI should answer now.` fallback in multibool

Adds:
- verbose=True flag on guided_rollout to log scoring_text for debugging
- _assistant_close(tok) sentinel-probe helper

Note: prompt_nll magnitudes shift since scoring_text now includes the
user-nudge tokens. Not comparable to pre-refactor saved results.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-06 12:39:10 +08:00
wassnameandClaude Sonnet 4.6 573f815361 journal: Qwen3-4B multibool results (task 288)
pmass=1.000, 0/132 low-pmass, inter-foundation |r|=0.154 (down from 0.51).
4B fully resolves the foundation-conflation problem seen at 0.6B.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 11:57:08 +08:00
wassnameandClaude Sonnet 4.6 2ca6aa9c6c multibool: interrupt-msg fork, generate trace on low-pmass, journal update
- suf_ids_for() now uses interrupt-msg format: close assistant turn, inject
  per-foundation user question + {"Answer": prefill -- fixes authority pmass
  (0.344->0.898) by avoiding JSON string-priming from key names
- Add _FOUNDATION_DESCS and _DEFAULT_MULTIBOOL_HINT rubric for discrimination
- Low-pmass diagnostic: first occurrence now runs .generate(max_new_tokens=32)
  to show what model actually produces; subsequent cases log top-5 tokens
- suf_ids_per stored so diagnostic generate can reconstruct full input
- Journal: add inter-foundation correlation results (mean |r|=0.51); note
  Spearman vs human raters is not a valid metric (exclusive vs independent labels)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 11:29:33 +08:00