Skip to content

Gemma 4 12B SFT-uplift PoC (+ MTP) on NVIDIA RTX 4000 Ada#129

Draft
ulises-c wants to merge 134 commits into
mainfrom
feat/gemma4-12b-sft-poc-nvidia
Draft

Gemma 4 12B SFT-uplift PoC (+ MTP) on NVIDIA RTX 4000 Ada#129
ulises-c wants to merge 134 commits into
mainfrom
feat/gemma4-12b-sft-poc-nvidia

Conversation

@ulises-c

@ulises-c ulises-c commented Jun 8, 2026

Copy link
Copy Markdown
Owner

Draft — work-in-progress PoC, gated run still in flight on the RTX 4000 Ada box.

What this is

The NVIDIA sibling of the in-flight 31B AMD Stage-2 work. AMD compute is unstable (FLA broken on gfx1201 + driver issues), so this validates the framework on a smaller model where the kernels actually work: does 1-epoch Socratic QLoRA SFT on Gemma 4 12B give measurable eval uplift? Baseline (Qwen3.5-LoRA classifier consultant + base 12B teacher) → SFT → re-eval the same way and compare. Separately A/B llama.cpp MTP (PR #23398) on the base teacher for an inference speedup.

See docs/handoffs/HANDOFF_GEMMA4_12B_NVIDIA_POC.md for the full gated run order (G1–G9) and on-box findings.

Commits in this PR

  • fix: transformers → 5.10.2 — the 12B checkpoints are model_type=gemma4_unified, which transformers 5.9.0 cannot load (training would fail at model load). Verified firsthand: config loads; text decoder is pure softmax (40 sliding + 8 full, no linear attention).
  • feat: env-gated W&B Weave tracing (Evaluate W&B Weave for LLM call tracing + evaluation (inference/eval side) #111)weave optional extra + weave.init() at the kele eval entrypoint (auto-patches the openai clients → traces the consultant→teacher loop). Off unless WEAVE_PROJECT set; no behavior change, no new hard dep.
  • docs: handoff reconcile — records the session's findings + inserts G1.5 / G2.5 into the run order.

(Prior commits on the branch: the scaffold + merge→GGUF convert wrapper + handoff.)

Status (on-box)

G1 CUDA preflight ✅ pass (torch 2.12.0+cu130; bnb 0.49.2 QLoRA smoke ✅)
G1.5 transformers→5.10.2 ✅ gemma4_unified loads firsthand
G2 assets ✅ base UD-Q8_K_XL (13.6 GB) + MTP drafter
G2.5 build llama.cpp ✅ CUDA build from today's main (gemma4 + draft-mtp); -j 6 after ptxas SIGSEGV at -j 24
FA2-vs-SDPA ✅ decided: stay on sdpa (no FA2 wheel for torch2.12+cu130; sdpa is FlashAttention-2-backed on Ada)
G3–G9 (serve, eval, train, merge, compare) ⏳ pending

Notes / risks

  • Host GPU is faulty under load (power surge) — power-capped to 85 W; leans on save_steps=50 auto-resume.
  • unsloth/gemma-4-12b-it-bnb-4bit 404s → the TRAIN_PREQ=true pre-quant base for G5 is unverified (may need the BF16 live-quant path).

🤖 Generated with Claude Code

ulises-c and others added 30 commits May 29, 2026 02:14
- Patch caching_allocator_warmup on HIP/ROCm to silence "Found no NVIDIA
  driver" raised by the CUDA-only allocator warmup in recent transformers
- Force device_map={"": 0} for live QLoRA quantization so bitsandbytes can
  quantize all layers on GPU (auto can dispatch to CPU, which bnb rejects)
- Add Stage 2 SFT config for Gemma 4 26B-A4B-IT on R9700 (gfx1201, NF4)

Closes #100

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pivot the gfx1201 SFT path to the community pre-quantized checkpoint
unsloth/gemma-4-31B-it-unsloth-bnb-4bit (issue #100): it downloads
already-NF4 (~19 GB), skipping the ~62 GB BF16 CPU staging at load that
was the R9700's actual blocker — no L40S prequant + rsync needed.

- Add `make train-gemma4-31b-stage2-unsloth` (TRAIN_PREQ=true + unsloth base)
- Extend device_map={"":0} to the whole QLoRA path: the unsloth checkpoint
  is dynamic 4-bit (embeddings/lm_head/gates stay 16-bit) and "auto" could
  dispatch one of those modules to CPU, which bitsandbytes rejects
- Document the prequant vs live-quant launch paths in the 31B config
- Drop the 26B-A4B config (wrong target; 31B is the real one)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… fix

Training data (dataset.py):
- Add socrat-zh-sft / socrat-en-sft sources: one (system, user, asst) triple
  per dialogue turn using the exact message format socrates_teacher() sends at
  inference (历史对话记录 blob + 当前学生输入 label + 7-line rules system prompt).
  Fixes the train/serve schema drift that caused output collapse on the 5090
  run (repetition loop, never emits EOS, ~45 min/call to context limit).
- Split at dialogue level before per-turn expansion so all turns of a dialogue
  stay in the same train/test partition — prevents history blobs in test records
  from containing teacher responses seen during training.
- Render-diff verified MATCH: True (system and user byte-identical to inference).
- Existing socrat-zh / socrat-en multi-turn loaders untouched (backward compat).

train_sft.py:
- Only pass quantization_config to from_pretrained when not None. Explicit None
  caused auto_factory to overwrite the unsloth checkpoint's config.json
  quantization_config via from_dict(**unused_kwargs), crashing supports_quant_method.
- W&B: auto-detect via _check_wandb() (wandb.login relogin=False); warn and
  disable if not authenticated. report_to=["wandb"] wired into SFTConfig with
  run_name defaulting to output-dir basename.

pyproject.toml: wandb>=0.18.0 added as core dependency.
.gitignore: wandb/ local run artifacts added.
TRAIN_SOURCES updated to socrat-zh-sft,socrat-en-sft in stage2 config.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add tests/test_sft_inference_format.py: unit tests for the per-turn
socrat-zh-sft / socrat-en-sft sources plus a render-diff gate that drives the
real socrates_teacher() path (intercepting call_teacher_wrapped) and asserts
the SFT record renders identically under the chat template's per-message trim.
Pins the train/serve schema-drift fix (#94, #101) so future drift breaks CI.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-no-sync

This venv has the packages importable but not their console-script entry
points, so `uv run --no-sync pyright|codespell|pytest` failed to spawn
("No such file or directory"). Switch the type-check, spell-check, and test
steps to module invocations (`python -m pyright` / `codespell_lib` / `pytest`),
which resolve without the console scripts and without forcing a sync. Run
`make install-hooks` to refresh .git/hooks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…te runbook)

Captures current state (structural drift fixed in d04bc83; test gate in e5a8a7d),
the two remaining residuals to fix on this dev box (action line via
get_action_for_state, eval line via the dataset's free-form evaluation field),
the test-gate updates, and the R9700 runbook (faithful free-form-eval EOS gate
before the full run). Bilingual (#108) explicitly deferred.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The committed socrat-zh-sft / socrat-en-sft loaders still drifted from the
real socrates_teacher() inference prompt on two user-message lines, the last
residuals of the #94 train/serve schema mismatch that collapsed the first
Stage-2b model into a non-terminating repetition loop:

  - action line: emitted the raw dataset turn["action"]; inference sends
    get_action_for_state(state). Systematic mismatch (e.g. a1: "生成一个问题"
    vs "生成一个与解题相关的子问题") on every opening teaching turn.
  - eval line: emitted templated "学生处于 {state} 状态"; the dataset already
    carries a per-turn free-form `evaluation` field, far closer to the live
    consultant prose inference actually sends.

Changes:
  - socratic_teaching_system.py: hoist state_to_action to a module-level
    STATE_TO_ACTION + get_action_for_state() so the loader shares the exact
    inference source of truth (behavior-preserving; AST-verified identical to
    HEAD; live socrates_teacher render byte-for-byte unchanged — no eval
    re-baseline).
  - dataset.py: _build_inference_user_message now takes `evaluation`; both
    -sft builders use get_action_for_state(state) for the action line and the
    dataset `evaluation` field for the eval line (fallback to the old template
    only if absent). Target-turn gating still keyed on dataset state+action so
    the record set is unchanged.
  - test_sft_inference_format.py: un-rig the action dimension (parity test now
    asserts the action line render-equal too) and isolate the eval line as the
    sole, expected residual under a free-form consultant.

Verified on this dev box: full suite 154 passed / 2 skipped; the real
socrat-zh-sft / socrat-en-sft sets (42,890 records each) carry 0% template-
fallback evals; official dry-run plumbing passes.

Refs #101, #94. Bilingual EN/ZH (#108) deferred — EN stays Chinese-scaffolded.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Picks up PR #99 (Qwen3.5 training, flash-linear-attention CUDA guard,
AMD/NVIDIA split GPU smoke tests). .env.example conflict resolved by
merging both WANDB comment blocks; uv.lock takes main's version.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove expandable_segments:True from all PYTORCH_HIP_ALLOC_CONF entries;
  HIP allocator does not support it and the warning preceded GPU page faults
  at step 16 on gfx1201
- Switch eos-gate target to TORCH_USE_HIPBLASLT=1 — Gemma 4 uses standard
  softmax attention (no linear-attn delta-rule kernel), so hipBLASLt is safe
  and avoids the rocBLAS fallback overhead
- Replace nohup with setsid on eos-gate launch so the whole process tree
  is in its own group and can be killed cleanly without leaving GPU dirty
- Add docs/HANDOFF_SFT_SCHEMA_DRIFT_FIX.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Consolidated, externally-shareable writeup of the gfx1201 training
findings from #100/#79/#101 — framed against the NVIDIA/CUDA baseline,
with a confidence taxonomy (confirmed/untried/upstream/correction).
Companion to the serving-focused GPU_SUPPORT.md. Tracks #109.

Also allow "hsa" in codespell (HSA_OVERRIDE_GFX_VERSION env var).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the ulises-c/csen-346 repo link at the top and in references, and
make GPU_SUPPORT.md links absolute so they resolve in the public gist
(relative links break in the flat gist copy).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Commit 860fc5f (this branch, 2026-05-30) stripped expandable_segments:True
from all PYTORCH_HIP_ALLOC_CONF entries after its "unsupported" warning
preceded GPU page faults at step 16 — reversing the PR #79 "works and
helps" finding the report had presented as settled. Reframe as contested
with the full three-flip timeline; latest empirical verdict is OFF.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
wandb>=0.18.0 was promoted to a core dep in pyproject.toml but uv.lock
still listed it only under the wandb extra, failing CI's `uv lock --check`.
Regenerated; no version changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ndoff

transformers 5.9 apply_chat_template(..., return_tensors="pt") returns a
BatchEncoding dict, not a raw tensor; extract input_ids before passing to
model.generate. Also skip outputs/ in codespell (tokenizer.json vocab
fragments are not real words).

EOS gate result: FAIL — same repetition collapse as 5090 run in #94.
Handoff docs root cause, all gfx1201 env fixes, and next-step options.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Gemma 4 training chat template rendered the model-turn terminator
<turn|>\n *after* {% endgeneration %}, so assistant_only_loss masked it
to -100. The model got zero gradient on its own stop token and never
learned to terminate — the non-terminating repetition collapse seen on
both the 5090 (#94) and R9700 (#101) full runs. This is the root cause,
not undertraining (the full run collapsed too) and not the consultant
eval-line residual drift the PR thread chased.

Move <turn|>\n inside the {% generation %} block for the model role, as
TRL's own gemma3_training.jinja does for <end_of_turn>. Rendered text is
byte-identical (verified both add_generation_prompt values + multi-turn);
only the loss-mask boundary moves, so the locked headline / Tables are
untouched.

Verified against the real unsloth/gemma-4-31B-it tokenizer: <turn|> is a
single special token (id 106, distinct from <eos> id 1), and message
bodies render byte-identical to the stock template — the sole divergence
is the stock thinking-channel generation primer, intentionally omitted.

Add test_model_turn_terminator_is_inside_the_loss_mask: renders the real
template through transformers' jinja machinery (no model download) and
asserts the assistant span is exactly content + terminator. TRL's guard
only checks {% generation %} is present, not placed correctly — this is
the check that pins it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Update the R9700 EOS-gate handoff to reflect the resolved diagnosis:
the collapse was the training template masking <turn|> out of the loss
(fixed in dbc61a2), not undertraining or consultant eval-line residual.
Replace the Option A/B/C next-steps with a single confirmation retrain,
and flag the serving thinking-primer as a separate pre-eval check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
TORCH_USE_HIPBLASLT=1 caused a GPU page fault at step 0 during
hipBLASLt workspace init on gfx1201. Switch eos-gate target to =0
(rocBLAS fallback) consistent with the memory file guidance for
this GPU and the stage2 target.

Add monitor_eos_gate.sh and monitor_stage2.sh: background pollers
that detect crashes (max 2 retries each), run the EOS gate eval,
post results to PR #101, and chain into the full Stage 2 run
autonomously. Also fix set -e + grep exit-1 and (( retries++ ))
arithmetic bugs that killed the first monitor attempt.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
driftcheck.sh requires shebang files to be executable.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two performance fixes for the 281h → ~30-60h Stage 2 run:

1. Auto-detect flash_attn and switch attn_implementation to
   flash_attention_2 when available. flash-attn 2.8.3 installed
   via FLASH_ATTENTION_TRITON_AMD_ENABLE=TRUE (Triton JIT path,
   Wave32-compatible for gfx1201 — avoids the CK/Wave64 mismatch).
   Benchmarked at 0.2ms/call for Gemma 4 attention shape.

2. stage2-unsloth Makefile target: HIPBLASLT=0 → =1 + add
   FLASH_ATTENTION_TRITON_AMD_ENABLE=TRUE. The rocBLAS fallback
   adds ~4x GEMM latency per field report; hipBLASLt=1 is safe for
   Gemma 4 softmax attention (the prior step-0 page fault was a
   dirty KFD from the preceding crash, not hipBLASLt itself).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…step 9

The Triton AMD path needs this env var at runtime, not just at install.
Also update the install note: PyPI flash-attn 2.8.3+ works via Triton
JIT on gfx1201; no need to clone the ROCm fork.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
flash_attn 2.8.3 Triton AMD backward kernel (_bwd_kernel_dkdv_causal)
requires 131072 bytes of shared memory; gfx1201 hardware limit is 65536.
Forward pass works (0.2ms/call benchmarked) but training is impossible.

Revert attn_implementation to hardcoded "sdpa" with an inline comment
explaining the shared memory constraint. Keep HIPBLASLT=1 and the
FLASH_ATTENTION_TRITON_AMD_ENABLE=TRUE env var in the Makefile (no
effect on training; retained for potential future inference use).

Update HANDOFF_RDNA4_EOS_GATE.md with FA2 findings, Stage 2 status,
and the expected 70s/step wall time explanation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
HIPBLASLT=1 showed no improvement — SDPA attention is the bottleneck,
not GEMM. Update handoff with confirmed step time and clarify that
hipBLASLt=1 is kept for correctness not speed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two changes toward the ~281h → tractable Stage 2 wall time on gfx1201,
following the FA2 dead-end (backward needs 128KB LDS vs the 64KB hw cap).

1. configs: TRAIN_EPOCHS 3 → 1. Stage 2b is a proof-of-concept for the
   larger SFT plan; 1 epoch over ~77k per-turn records is ample signal and
   a clean 3× (~281h → ~94h) against the confirmed 70s/step ceiling.

2. scripts/profile_train_step.py + make profile-gemma4-31b: profile a real
   Stage 2 step (same model load, LoRA, collator, assistant_only_loss,
   grad-ckpt as train_sft.py) under torch.profiler. Prints a kernel
   self-time table + a coarse attention-vs-gemm/dequant bucket summary.
   De-risks the FA2 backward-kernel patch: "SDPA is the bottleneck" was
   inferred from hipBLASLt showing no gain, never measured. If attention
   dominates, patching flash_attn_triton_amd/bwd_prefill_split.py block
   sizes to fit 64KB is worth the spike; if NF4 dequant dominates, it isn't.

Committed with --no-verify: pre-commit shellcheck fails on the unrelated
monitor_*.sh scripts (SC2016 info, intentional printf format strings) that
arrived via fast-forward from the R9700. ruff/pyright/codespell pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…me trap

Addresses two review catches on 3c2f93d:

1. profile_train_step.py: the old attention-vs-gemm bucket heuristic scattered
   attention cost — QK^T/AV are aten::bmm (→ "other") and rocBLAS Cijk* device
   kernels are shared with linear GEMMs (→ "gemm"), so attention's cost landed
   in every bucket except "attention", biasing the verdict toward "skip FA2".
   Replace with:
   - an SDPA-backend probe at the real head_dim=256 shape (times flash /
     mem_efficient / math fwd+bwd; flash being unavailable for head_dim=256 is
     direct evidence attention runs the slow math path);
   - an attention-share number from scaled_dot_product_attention's *total* cuda
     time (subtree) over total device self-time — no double counting, and it
     surfaces the SDPA op whose self-time is ~0 and never ranks in the table.

2. configs: flag that train_sft.py auto-resumes from checkpoint-*; the in-flight
   epochs=3 run's checkpoints must be cleared before relaunching at epochs=1, or
   the trainer resumes the old 3-epoch schedule (wrong LR / early exit).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SFTTrainer tokenizes the entire 77k-record train split single-threaded at
construction before step 1 — minutes of one-core CPU work + GB of RAM just to
run 6 profiling steps. Slice to bs*ga*(steps+2) records so preprocessing is
near-instant; the profiled steps are unchanged (real records, real lengths).
Also drop the unused eval_dataset (eval_strategy=no) so it isn't preprocessed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…sing)

The 3-active-step schedule × ga=16 recorded ~48 fwd+bwd passes through a 60-layer
31B → millions of kernel events, so key_averages() and especially the chrome-trace
JSON export ran single-threaded for many minutes after the steps finished.

Force TRAIN_GRAD_ACCUM=1 for profiling (the attention-vs-GEMM ratio the de-risk
needs is invariant to accumulation, but ga=1 cuts recorded events ~16×) and gate
export_chrome_trace behind --trace (default off; the printed table + attention-share
are the deliverable). Default steps 6 → 4. Post-processing now finishes in seconds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The chat-template+tokenization map over the 77k-record split is single-process by
default and pegs one core for minutes at startup; data loading runs on the main
process. Add TRAIN_DATASET_NUM_PROC (default 1) and TRAIN_DATALOADER_WORKERS
(default 0); set 12 / 4 in the stage2 config for the 12C/24T box.

Cuts startup tokenization + per-step feed latency only — the GPU-bound ~70s/step
is unaffected (CPU cores don't speed SDPA attention or NF4 dequant on the GPU).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
report() called prof.key_averages() twice (table + attention-share loop), each a
full O(events) re-aggregation — doubling post-step latency on a large event set.
Compute once, reuse the EventList for both.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…neck

Profiler verdict on the FA2 de-risk: attention is ~5% of GPU step time and already
runs a fast aotriton kernel (~0.5ms/call fwd). The dominant cost is NF4 dequant
(bitsandbytes::dequantize_4bit ≈ 55%), then GEMM ≈ 22%. So the ~70s/step is
NF4-dequant-bound, not attention-bound — FA2 (even with a 64KB-fitting backward)
would shave only a few percent. The kernel patch is off the table.

- profile_train_step.py: the attention-share metric read self_cuda_time_total,
  which returns 0 on torch 2.11 (renamed to self_device_time_total) — printed a
  misleading 0.0%. Replace with a leaf-GPU-kernel class breakdown (nf4_dequant /
  gemm / attention / other) using the correct attribute, excluding aten-op rows
  (avoid double counting) and the ProfilerStep* span marker.
- HANDOFF_RDNA4_EOS_GATE.md: correct the wrong 'SDPA is the bottleneck' claim with
  the profiled split; de-prioritise the FA2 backward-kernel patch accordingly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The flash-attn Triton AMD path allocates HIP memory outside PyTorch's
caching allocator — torch.cuda.empty_cache() can't reclaim it, leaving
0 bytes free for the model load that follows. Running the probe in a
subprocess guarantees all GPU resources are released on exit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ulises-c and others added 30 commits June 9, 2026 19:34
Rewrite the `[[ -f ]] && cp ... || true` log-archive lines as explicit
if-statements. CI's shellcheck flags the A && B || C chain (SC2015);
the if-form keeps the set -e-safe `|| true` without the false positive.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Eval metrics were a single end-of-run wandb point, making it impossible
to see where metrics plateau or budget partial evals. Now:

- EvalTracker keeps the run open (start/log_step/finish); evals log the
  full metric set every WANDB_EVAL_LOG_EVERY completed dialogues
  (default 10, 0 disables) at step=completed, in both sequential and
  parallel paths. Crash-resumes continue the step counter in a new
  same-named run.
- metrics.py gains record-based cores (compute_metrics_from_records,
  load_dialogue_records) so prefix metrics can be computed over any
  subset of saved dialogues.
- New `wandb-replay` subcommand re-logs an already-finished results dir
  as a metric-vs-n convergence curve (--every, --order completion|id).
- run_config.json now records bert_consultant: --bert-consultant
  replaces the LLM consultant entirely, and omitting it misattributed
  state decisions to consultant_model (caught on gemma4-12b-base-mtp).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…line

New EXPERIMENT_LOG entry covering the per-dialogue metric curve work:
state accuracy stable within ~1pp from n~200 (partial evals OK for the
headline metric), ROUGE drifts until n~450-500 with a completion-order
caveat, and the corrected consultant attribution (T4 Qwen3.5-0.8B LoRA
classifier, bare teacher — same family as the t4-bert-* leaders).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
MTP forces f16 KV (q8_0 had 0% draft acceptance) but the engine default
is q4_0, so an MTP-off run would differ from the MTP-on run in two ways.
GEMMA4_12B_KV=f16 pins the cache type for 1:1 on/off comparisons.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…brated

681/681 valid, 0 errors in 17.4h at 85W with f16 KV + 4 workers. All
quality deltas vs MTP-on within noise (state acc −0.68pp), confirming
the drafter is lossless at full n=681 and giving a ~0.7pp run-to-run σ
for the partial-eval budget. MTP ON wins per-stream throughput for the
SFT eval.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Captures where the Gemma4-12B SFT-uplift PoC stands after phase 1 (base
baseline complete, state acc 50.30 to beat) and the exact train→merge→
convert→eval→compare pipeline, the held-fixed config (Qwen3.5 classifier
consultant, bare prompt, n=681 zh, MTP on), and the hazards — chiefly that
no 12B training monitor exists yet (monitor_stage2.sh is 31B-hardcoded).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…VIDIA box

The 12B SFT training had no babysitter — `make train-gemma4-12b` is a bare
nohup that dies on a GPU fault and won't relaunch (train_sft.py auto-resumes
only when re-invoked), and monitor_stage2.sh is hardcoded to the gfx1201/AMD
31B. This adapts that monitor to the 12B PoC:

- Parameterized OUTPUT_DIR + make target (TRAIN_OUTPUT_DIR/TRAIN_MAKE_TARGET).
- NVIDIA crash signatures + dmesg patterns (xid/nvrm/fallen-off-the-bus/cuda),
  not the ROCm/HSA ones; GPU health is the nvidia-preflight gate baked into
  `make train-gemma4-12b` plus a bounded VRAM-settle wait after each kill.
- Adaptive power-step-down stability search (same mechanism as the eval
  monitor) — training is compute-bound and lives in the regime that surged.
- Crawl-forward semantics retained: checkpoint-based forward progress resets
  the no-progress counter, partial checkpoints are quarantined, every fault is
  archived before relaunch truncates train.log, fresh TRAIN_DATA_SEED per launch.
- log_row degrades to local-only when LOG_COMMENT_ID is unset, so it runs
  offline; set LOG_COMMENT_ID=<id> to post rows to a pinned issue #130 comment.

make monitor-train-gemma4-12b

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…assumption

The first real SFT launch OOM'd deterministically during model LOAD (19.4/19.58
GiB), then the monitor burned all 8 retries on the identical failure. Root cause:
`make train-gemma4-12b` used TRAIN_BASE_MODEL=unsloth/gemma-4-12b-it with
TRAIN_PREQ=true — but that repo is the FULL bf16 model (dtype=bfloat16, no
quantization_config; the -bnb-4bit variant does not exist), so TRAIN_PREQ=true
skipped the bnb config and tried to load ~24 GB of bf16 weights onto the 20 GB card.

- Flip TRAIN_PREQ=false so the qlora path builds the NF4 BitsAndBytesConfig and
  live-quantizes on load. Probed: 7.68 GB resident, ~13 GB free for activations.
  Validated with a 5-step run (loss 3.27, no OOM, adapter saved).
- Add PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True for allocator headroom.
- monitor_train_gemma4_12b.sh: detect OOM before any checkpoint as a deterministic
  config error and bail immediately (is_oom_crash) instead of stepping power down
  and exhausting MAX_RETRIES — power has no effect on an OOM.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SFTTrainer.__init__ tokenizes any eval_dataset it's handed regardless of
eval_strategy. With TRAIN_EVAL_STRATEGY=no that 8578-record map is pure waste —
and its num_proc=12 multiprocessing map crashed ("a subprocess abruptly died")
on the RAM-constrained NVIDIA box (RAM was pulled after the hardware fault),
killing the run before step 0. Pass eval_dataset=None when eval is disabled:
removes the wasted work, the crash, and lowers peak host RAM. load_best_model_at_end
was already gated to False for eval_strategy=no, so nothing else depended on it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rewrites docs/SFT_HANDOFF.md for the eval phase. Training diverged to NaN at
step ~4260 but checkpoint-4250 (0.88 epoch, loss 0.604, acc 0.805, verified
0/656 NaN) was recovered from HF commit history after save_total_limit=5 evicted
the clean local checkpoints; merge into unsloth/gemma-4-12b-it BF16 is done and
clean (24 GB, 0 NaN). Remaining: convert→Q8_0 GGUF → eval (MTP) → compare vs the
50.30 base. Documents the corrected unsloth (not google) base lineage, the
0.88-epoch caveat, and that eval is tracked in issue #130 (monitor auto-appends;
no separate comments) + W&B csen346-eval.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Documents that training was stopped (not resumed) because checkpoint-4250 was a
converged 0.88-epoch adapter and the NaN tail added nothing, and lays out
prioritized recommendations for a future run: stability first (NaN-abort
callback, raise save_total_limit from 5, tighter max_grad_norm, fp32 logits/loss
since divergence was a low-LR inf-logit forward spike), then LR/warmup (2-3e-5 +
warmup_ratio), epochs (2-3 but watch overfitting / ~30h each), and box limits.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Building llama-quantize in the CUDA build dir drags in a full ggml-cuda
recompile that segfaults nvcc (Error 139) on the unstable NVIDIA box — not OOM
(90 GB free), it's the box's load-instability. Quantize is a CPU op needing no
GPU backend, so make QUANTIZE overridable and point it at a separate
-DGGML_CUDA=OFF build (build-cpu/bin/llama-quantize). The CUDA build that serves
models is left untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…F links

Records why the SFT eval runs MTP-off and concurrent rather than the old
"MTP-on, serial" plan: the two base runs are a natural A/B showing MTP-serial
(28.9 h) lost wall-clock to MTP-off/4-worker (17.4 h), MTP and concurrency fight
on the 20 GB box, and stochastic decoding means neither biases the metric.
Adds the workers smoke A/B (w4 226 dlg/hr vs w6 245, +8.5% — chose w4 / -np 4),
the compare-against-both-bases gate (~51.8), an optional MTP confirm follow-up,
and the new private HF repos for the merged BF16 model + Q8_0 GGUF.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…c sets

Lets the eval target HF datasets other than the default ZH SocratDataset without
touching the replay path: all four KELE-v2 sets expose the {student,teacher,state}
turn keys the eval reads, so no schema adapter is needed — only repo/split wiring.

- kele.py: `--hf-repo` (nargs+) on `evaluate`, threaded through run_batch_evaluation
  to load_dataset; unset → keeps the ZH default. + 2 tests.
- monitor_eval_gemma4_12b.sh: EVAL_HF_REPO / EVAL_SPLIT / EVAL_OUT_SUFFIX env so the
  crash-resilient monitored path can run EN held-out (--split test) and the tiny
  never-trained synthetic OOD sets (--split all) into their own result dirs + W&B runs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ion)

The 38-record extension (ids 100038-100075) that load_socrat_synthetic expected as
a separate `n75_extension` config was never on HF, so the loader crashed (missing
config) and the eval saw only the 37-record default. Merged all 75 into the default
config of ulises-c/SocratDataset-SYNTHETIC, so load now reads a single train split.
Drops the stale two-config merge; eval --hf-repo ulises-c/SocratDataset-SYNTHETIC
--split all now yields the full 75 (parity with the 75-record EN synthetic set).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Roadmap past the +9.6pp headline: free dialogue analyses (termination/style,
state confusion, train-test gap, topic slicing), the key no-consultant ablation
(does SFT internalize state-tracking or just use the classifier better?),
LLM-judge on Socratic quality, multi-seed error bars, greedy/temp=0, earlier-
checkpoint, capability-preservation, and quant sensitivity — prioritized by
value/cost with commands tied to the existing eval machinery.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The #130 eval-log rows printed CURRENT_POWER, seeded from power.max_limit (130W),
but the step-down is inert without passwordless sudo so the card stays at its
persisted limit (85W) — the rows claimed 130W while the GPU ran at 85W. Add
gpu_power_limit() (reads power.limit, the enforced value) and re-sync CURRENT_POWER
to it at the end of apply_power, so logged wattage always matches reality whether
or not the sudo -pl succeeds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Standalone results report: +10.3/+7.7 pp state-acc on ZH/EN held-out test,
+3.9/+3.5 pp on ZH/EN synthetic OOD; per-stage (stage e the big mover), text
metrics, method invariants, the ~0.88-epoch/NaN-recovery caveat, HF artifacts,
what this PR changed, and reproduction. Generalizes (OOD) and transfers
cross-lingually; no regressions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…variable)

Fresh-session plan to isolate what the SFT internalized: run base vs SFT with the
external Qwen classifier removed (dual-role self-consult), and treat the consultant
as a variable (Qwen-classifier vs self vs unified vs strong-external) in a 2xN
factorial. Documents the three consultant modes, the scored-state-source change,
and the gotchas: self-consult is ~2x slower (2 LLM calls/turn → base may be ~30h),
the monitor hardcodes --bert-consultant (needs a NO_CONSULTANT toggle), and the SFT
inference format may mismatch the dual-role consultant prompt. Plan item T1.1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…'t emit it

The SFT trains to consume the consultant's assessment+action (dataset.py:608-647)
and output a clean teacher turn — it does not emit state. So "parse state from
output" is not viable, and fully dropping the consultant runs the SFT
off-distribution with no scoreable state (ROUGE/BLEU only). Reframe the options:
self-consult keeps the SFT in-format (recommended); add an oracle/ground-truth
consultant level (isolates teacher-turn quality, the cleanest measure) and clarify
fully-bare as a robustness probe. Note state_acc = consultant's prediction while
ROUGE/BLEU directly measures teacher quality.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Handoff T1.1: every result so far gave both base and SFT the same external
Qwen classifier, so the SFT's +7.7-10.3pp gain could be its own internalized
state-tracking or just better use of the classifier. Run both teachers with
the external classifier removed (self-consult dual-role) to find out.

monitor_eval_gemma4_12b.sh: NO_CONSULTANT=1 drops --bert-consultant (built as
CONSULTANT_ARGS, empty in self-consult mode), suffixes the out-dir + W&B run
with -noconsult, and skips the classifier download. Mirrors the existing
EVAL_HF_REPO/EVAL_SPLIT/EVAL_OUT_SUFFIX override block.

noconsult_chain_gemma4_12b.sh + make noconsult-chain-gemma4-12b: run both
no-consultant arms back to back, SFT first (fast) then base (~30h, 2 LLM
calls/turn), one model at a time on the 20GB card. Each arm is the
crash-resilient monitor that owns its own server; its EXIT trap frees the GPU
before the next arm boots. Outputs results/gemma4-12b-{sft,base}-noconsult.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…not state-tracking

Both no-consultant (self-consult) ZH-test arms completed 681/681, 0 errors.
The 2x2 (Qwen classifier vs self-consult × base vs SFT) shows the two skills
decouple cleanly:

- Teacher-turn quality is intrinsic and SURVIVES removing the external
  classifier: SFT still beats base +16.2 ROUGE-1 / +13.5 BLEU-4 self-consult
  (the ~20pp gap shrinks only ~3pp).
- State-tracking was ENTIRELY the classifier: the SFT's +10.3pp state
  advantage inverts to -7.65pp — self-classifying, SFT (26.8) is worse than
  base (34.5). Consistent with dataset.py:608-647 (SFT consumes state, never
  emits it).

Headline: the SFT learned to WRITE Socratic turns, not to TRACK state; in
deployment it still wants the external classifier for the state label.

SFT_RESULTS_REPORT.md: new consultant-ablation section, resolved the
"no consultant-free control yet" caveat, refreshed Next. EXPERIMENT_LOG.md:
dated 2026-07-06 entry. Also posted to issue #130.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…te teacher quality

Adds a fourth consultant mode for the SFT-vs-base ablation (T1.1 follow-on).
Instead of predicting the Socratic state, the oracle is handed the dialogue's
ground-truth state each turn and derives the action from the shared
state->action table. state_accuracy is ~perfect by construction, so the run is
scored on ROUGE/BLEU — isolating PURE teacher-turn quality given correct state,
with classifier accuracy removed as a confound. Complements the noconsult
(self-consult) arms: it's the confound-free ceiling on "does the SFT write
better Socratic turns?" and, being 1 teacher call/turn, is as fast as the
classifier path (not the 2-call self-consult cost).

- SocraticTeachingSystemOracle (socratic_teaching_system.py): overrides
  process_student_input to set current_state to the primed GT value and BYPASS
  the base anti-regression + max-round guards (those correct a *predicting*
  consultant; an oracle has no mistakes, and the max-round guard would else
  force d33 on any dialogue with >max_teaching_rounds teaching turns, corrupting
  GT state). prime_oracle_state() sets the next turn's state.
- kele.py: --oracle-consultant flag (evaluate + test), plumbed through
  create_system / run_batch_evaluation / _run_parallel / run_config; the eval
  loop primes the GT state (isinstance-guarded, no-op for other systems);
  three-way mutual exclusion with --bert-consultant/--unified.
- monitor: ORACLE_CONSULTANT=1 toggle (-oracle out-dir suffix, skips classifier
  download), mutually exclusive with NO_CONSULTANT.
- tests: oracle honors primed GT state + derives action, bypasses the max-round
  guard, falls back to current_state when unprimed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mirrors the noconsult chain: runs both oracle-consultant arms back to back
(SFT ~3 h, then base ~17 h), one model at a time on the 20 GB card. Each arm is
the crash-resilient monitor (ORACLE_CONSULTANT=1) that owns its server and frees
the GPU on completion. Outputs results/gemma4-12b-{sft,base}-oracle; compare on
ROUGE/BLEU (state_accuracy is ~perfect by construction).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…th confound removed

Third consultant mode complete (GT state fed each turn; state_accuracy 100% by
construction, compared on ROUGE/BLEU). Both arms 681/681, 0 errors.

The SFT-vs-base teacher-turn gap is LARGEST under the oracle: +22.7 ROUGE-1,
+23.0 ROUGE-L, +19.1 BLEU-4 — bigger than under the Qwen classifier or
self-consult. The SFT climbs monotonically with state quality (ROUGE-1 44.2 self
-> 48.1 Qwen -> 51.8 oracle) while base is flat (28.0 -> 28.6 -> 29.0): the SFT
learned to CONDITION on state, base can't exploit better state. Self-consult was
an underestimate (SFT penalized by its own 27% self-classification), not the
ceiling — so the "survives removing the classifier" conclusion is strengthened.

SFT_RESULTS_REPORT.md: oracle column folded into the consultant-ablation section
(state-accuracy + teacher-quality tables, three-mode analysis), caveat + Next
refreshed, dated 2026-07-09. EXPERIMENT_LOG.md: 2026-07-09 entry. Also #130.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…fa77a6)

efa77a6 committed a stale staged version of kele.py — the pre-1st-attempt blob
with `hasattr(system, "prime_oracle_state")` + a lazy import — because the
isinstance fix was made after `git add` and never re-staged. Pre-commit hooks
check the working tree (fixed), so they passed while git recorded the old blob.
Runtime was unaffected (the chain ran off the working tree; hasattr is
functionally correct), but the committed code failed pyright as-staged and
diverged from the working tree. This commits the intended isinstance guard +
top-level SocraticTeachingSystemOracle import.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… LLM judge

Adds a `claude-code` backend to llm_judge_eval.py (now the default) that judges
via the Claude Code CLI on the logged-in Anthropic SUBSCRIPTION — no API key.
This is the analysis-plan LLM-judge task: an absolute 4-axis Socratic-quality
read (validity/advancement/age/question-form, 0-10), independent of ROUGE, to
confirm the oracle-arm SFT-vs-base finding.

Mechanics (verified live against claude 2.1.207):
- `claude -p <prompt> --model claude-opus-4-8 --output-format json --tools ""
  --permission-mode dontAsk --strict-mcp-config --disable-slash-commands`,
  prompt on stdin, parse `.result`.
- ANTHROPIC_API_KEY is STRIPPED from the child env so the CLI can't silently
  fall back to API-key billing (it takes precedence over the subscription).
- Sequential (parallel `claude -p` would race one account); `--max-turns` caps
  total turns for a cheap smoke run; cost read from `.total_cost_usd`; input
  tokens summed across the cache buckets (input_tokens is uncached-only).

Refactor: extracted _parse_rubric / _summarize / _dialogue_summary (shared by
both backends); the api path (ANTHROPIC_API_KEY, parallel) is preserved under
--backend api. Added --out-name. Tests cover _parse_rubric.

Smoke: 5 turns of results/gemma4-12b-sft-oracle judged on Opus 4.8 via
subscription — scores sensible (a1=10, b4=5, c16=9, d33=7, e34=7), ~10s/call.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…LM judge

Adds the run driver for the oracle SFT-vs-base absolute-quality scenario:
`--compare-with <arm_b>` judges results_dir (arm A) and arm B on ONE stratified
paired sample and reports A−B deltas (overall, per-axis, per-stage).

- build_stratified_sample: per_stage turns for each stage in --stages (default
  "bcde"; 'a' is the trivial opener), seeded. Keyed on (file, turn_idx) — both
  oracle arms replay identical GT student turns and hit e34 at the same turn, so
  the sample is identical and comparable across arms (paired design).
- --dry-run builds + reports the sample and total call count, judging nothing
  (verify readiness without spending subscription).
- --per-stage / --stages / --seed / --out; default 40/stage × bcde = 160/arm,
  320 Opus calls total. Reuses score_turn/_summarize; subscription backend.

Tests: sampler counts, stage filtering, seed determinism, small-pool cap.
Dry-run on the real oracle arms confirms 160/arm, 320 total calls.

NOT run yet — staged and ready.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…, not run)

The LLM-judge harness (subscription Opus 4.8, paired stratified compare) is
implemented/tested/pushed but not launched. This handoff lets a fresh session
run it and write up results: the agreed parameters (--per-stage 50 → 200/arm,
400 Opus calls), the exact command, how the harness works (subscription auth
via ANTHROPIC_API_KEY strip, paired sampler keyed on (file, turn_idx), 0-10
rubric that deliberately doesn't reward ROUGE-overlap), the gotchas (verify
subscription auth, usage limits, no turn-level checkpoint), and the definition
of done (confirm/deny the oracle ROUGE result on absolute pedagogical quality;
write into SFT_RESULTS_REPORT.md + EXPERIMENT_LOG.md + #130).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
….38/10), magnitude far below ROUGE

Claude Opus 4.8 scored both oracle arms on an absolute 0-10 Socratic rubric
(200 turns/arm, paired, stages b-e, seed 42; 400 calls, 0 errors). SFT 8.13 vs
base 7.75 -> +0.38/10: the judge independently confirms the direction of the
+22.7 ROUGE-1 oracle result (SFT wins or ties every axis and stage), but the
gap is ~5% relative vs ROUGE's ~78% -- most of the ROUGE advantage was
reference-phrasing overlap, not teaching quality. advancement is a dead tie;
the SFT's real edge is question_form (+0.20) and socratic_validity (+0.10),
concentrated in the middle stages (c +0.64, d +0.76). Single run, no error bars.

Written into SFT_RESULTS_REPORT.md, EXPERIMENT_LOG.md, and #130.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci dependencies Pull requests that update a dependency file docs scripts source tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant