Skip to content

fix(train): gfx1201 ROCm fixes + unsloth bnb-4bit Gemma 4 31B QLoRA#101

Draft
ulises-c wants to merge 88 commits into
mainfrom
feat/gfx1201-rdna4-qlora-fla-training
Draft

fix(train): gfx1201 ROCm fixes + unsloth bnb-4bit Gemma 4 31B QLoRA#101
ulises-c wants to merge 88 commits into
mainfrom
feat/gfx1201-rdna4-qlora-fla-training

Conversation

@ulises-c

@ulises-c ulises-c commented May 29, 2026

Copy link
Copy Markdown
Owner

Closes #100

Summary

  • Patch caching_allocator_warmup on HIP/ROCm to suppress "Found no NVIDIA driver" raised by the CUDA-only allocator warmup in recent transformers
  • Force device_map={"": 0} for the whole QLoRA path so bitsandbytes can place all layers on the single GPU ("auto" can dispatch layers to CPU, which bnb rejects). Covers both live quantization and the pre-quantized dynamic 4-bit unsloth checkpoint (which keeps embeddings/lm_head/gates in 16-bit)
  • Target the community pre-quantized unsloth/gemma-4-31B-it-unsloth-bnb-4bit checkpoint (issue Investigate gfx1201 (RDNA4) training: Flash Linear Attention + Gemma4-31B QLoRA OOM #100): downloads already-NF4 (~19 GB), so it skips 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) and document the prequant vs live-quant launch paths in train-sft-stage2-gemma4-31b.env
  • Drop the 26B-A4B config (wrong target; 31B is the real one)

Test plan

  • --dry-run validates the unsloth 31B config plumbing (config parse, LoRA regex, SFTConfig, data load) on the dev box
  • make test-gpu-stack passes on gfx1201 (confirms bnb 4-bit runtime) — already 13/13 per Investigate gfx1201 (RDNA4) training: Flash Linear Attention + Gemma4-31B QLoRA OOM #100
  • make train-gemma4-31b-stage2-unsloth loads without "Found no NVIDIA driver" on ROCm
  • Loads without bitsandbytes CPU-dispatch rejection (the device_map fix)
  • Gemma 4 31B QLoRA trains end-to-end on R9700 from the unsloth checkpoint

Note: dry-run only validates plumbing. Model load, device_map, and the caching_allocator_warmup patch require the AMD gfx1201 box (this dev machine is NVIDIA).

HuggingFace Model

Gemma4-31B-it-bnb-4bit

🤖 Generated with Claude Code

- 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>
@codecov

codecov Bot commented May 29, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.87879% with 24 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/project/hf_callback.py 88.00% 9 Missing and 6 partials ⚠️
src/project/dataset.py 86.76% 7 Missing and 2 partials ⚠️

📢 Thoughts on this report? Let us know!

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>
@ulises-c ulises-c changed the title fix(train): gfx1201 ROCm fixes + Gemma 4 26B-A4B QLoRA config fix(train): gfx1201 ROCm fixes + unsloth bnb-4bit Gemma 4 31B QLoRA May 29, 2026
@ulises-c

Copy link
Copy Markdown
Owner Author

gfx1201 learnings from PR #99 — relevant to this PR

Picking up Gemma 4 31B QLoRA on the R9700 after the Qwen3.5 classifier work in #99. Key findings that apply here:

1. hipBLASLt may be safe for Gemma 4 — worth testing first

The GPU page faults (rc=134) in #99 were specific to torch_chunk_gated_delta_rule in Qwen3.5's linear attention layers. Gemma 4 uses standard softmax attention — no linear attention, no delta rule op. The crash was not a general hipBLASLt instability, just that one kernel path. Recommend testing with TORCH_USE_HIPBLASLT=1 on a clean GPU before defaulting to the rocBLAS fallback. If it holds for 20+ steps, the 4.1× matmul speedup is available (measured: 5.2ms vs 21.5ms on gfx1201).

2. Add PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True to the launch env

This was what finally stabilized hipBLASLt past the early crash zone in #99 — it prevents VRAM fragmentation during allocation. For a 31B NF4 load (~19 GB weights + optimizer + activations), fragmentation during loading is a real risk. Add it to train-sft-stage2-gemma4-31b.env alongside the other env vars.

3. GPU state corruption cascade — verify clean before each attempt

Every GPU page fault (rc=134) on gfx1201 leaves the KFD in a dirty state. Subsequent runs — even with the correct config — also fault at random steps until you kill -9 <pid> and confirm rocm-smi --showpids returns zero processes. This caused repeated silent failures in #99. The test plan checklist should include a GPU-clean check before each run:

rocm-smi --showpids | grep python && echo "GPU dirty — kill before retrying" || echo "GPU clean"

4. Use setsid instead of nohup for the training wrapper

nohup bash script.sh & doesn't create a new process group — children survive kill -9 -- -$PGID. setsid bash script.sh & puts the entire tree in its own group. Kill the whole tree cleanly with kill -9 -- -$PGID. Prevents the overlapping-process-on-same-GPU issue that caused multiple spurious crashes in this session.

5. test_training_gpu.py from #99

scripts/test_training_gpu.py (added in #99) runs a 7-step GPU smoke test covering fp32/bf16 matmul, sdp-attn, linear-attn proxy, grad-ckpt compat, and VRAM headroom. Useful as a pre-flight before a multi-hour QLoRA run. The linear-attn proxy isn't relevant for Gemma 4, but the matmul/sdp-attn/VRAM steps are.

@ulises-c

Copy link
Copy Markdown
Owner Author

Notes from #94

# From 5090 Run
Critical finding - the SFT model is generatively broken.

The model generated 2048 tokens in 38s (~53 tok/s, fast) but output is textbook repetition collapse:

这样可以帮助他建立更强的数学基础。这样可以帮助他建立更强的数学基础。

这样可以帮助他建立更强的数学基础。[~70+ repetitions, never emits EOS]

Going back to the morning's smoke test, it ALSO showed early degeneration patterns - the trailing `*(此处是观察点....)* *(此处是引导点....)* *(此处是总结点....)*` was the same loop just truncated at 200 tokens. I missed it.

Diagnosis: the model never emits EOS. Without a max_tokens cap, every teacher call runs to context Limit (~150K tokens, taking ~45 min each). The 88x stowdown I saw wasn't cold-start overhead - it was the model never finishing a single response.

What this means for the eval

We sit somewhere in the STATUS_REPORT §2.8 outcome matrix's "fine-tuned Gemma loses both" cell:

> Either undertraining or data-format issue / Document honestly in §Limitations; Locked headline stands at 72.24

Two paths forward

Path A - Run the eval with the cap, report the (likely bad) numbers as evidence of the failure mode. Negative result is still a paper contribution: documents that Pattern-A SFT on this size dataset caused output collapse. Locked headline 72.24 stays as A-grade submission.

Path B - Diagnose root cause first. Most likely candidates:

1. Pattern-A format trained the model to depend on specific structural markers - without them at inference,
2. 05_K_M guantization damaged a small model - test by re-running smoke on the BF16 merged checkpoint (but won't fit on 5090 VRAM; CPU inference is very staw)
3. Chat template mismatch-training used a patched template; serving uses llama.cpp's llama.cpp's Possible structural mismatch.

Which way do you want to play it? My recommendation is Path A first (~45 min to get hard numbers showing the failure) then Path B (post-mortem after we have the data). The negative-result narrative is paper-worthy and matches what §2.8's outcome matrix already anticipates.

@ulises-c

Copy link
Copy Markdown
Owner Author

Session update — schema drift fix + training paused

Root cause (from 5090 run)

The 5090-trained adapter is generatively broken: never emits EOS, runs to context limit (~150K tokens, ~45 min/call). Smoke test and full eval showed the same repetition collapse. Root cause diagnosed via A/B (same model, same GGUF, same server):

  • Training-shape prompt → 20 tokens, EOS, clean Socratic question (0.5 s)
  • Inference-shape prompt → 2048-token cap, 这样可以帮助他建立更强的数学基础 × 70+ reps (38 s)

The failure is a train/serve schema mismatch, not quant damage or chat-template corruption:

Axis Training (old socrat-zh/en) Inference (socrates_teacher())
System prompt 1-line _SOCRAT_ZH_SYSTEM + problem context 7-line rules block, no problem context
Message structure Multi-turn (N user/assistant pairs) Single user message
History Actual HF message turns 历史对话记录:\n学生: ...\n老师: ... blob
Current input label None 当前学生输入: prefix

Fix: new socrat-zh-sft / socrat-en-sft sources

Added to src/project/dataset.py — produce one (system, user, assistant) triple per dialogue turn in the exact format socrates_teacher() constructs at inference:

system: [7-line rules block — identical to socratic_teaching_system.py:404-415]
user:   \n历史对话记录:\n{学生/老师 pairs}\n\n当前学生输入: {s}\n\n苏格拉底教学顾问评估结果: 学生处于 {state} 状态\n苏格拉底教学顾问建议的操作: {action}\n
asst:   {teacher turn}

Render-diff result: MATCH: True (system and user byte-identical to inference for a real dialogue turn).

Backward-compatible: socrat-zh / socrat-en multi-turn loaders are unchanged — build_dpo_pairs.py and tests are unaffected.

Other changes this session

  • quantization_config=None bug: explicit None passed to AutoModelForCausalLM.from_pretrained caused auto_factory.py to overwrite the unsloth checkpoint's config.json quantization config with Nonesupports_quant_method crash. Fixed by only passing quantization_config when it's not None (same for dtype).
  • W&B: wandb>=0.18.0 added as a core dep; _check_wandb() auto-detects login via ~/.netrc, warns and disables tracking if not authenticated. report_to=["wandb"] wired into SFTConfig. Run name defaults to output-dir basename.
  • .gitignore: wandb/ local run artifacts added.

Dry-run

Sources: ['socrat-zh-sft', 'socrat-en-sft']
train: 77,202 records  (per-turn; was 12,244 per-dialogue)
eval:   8,578 records
=== Dry run complete — all checks passed ===

Status

Training paused — running consult before re-launch. Blocking gate: post-checkpoint generation test on inference-shape prompt must confirm clean EOS before trusting the full 55h run.

… 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>
@github-actions github-actions Bot added dependencies Pull requests that update a dependency file ci source labels May 30, 2026
@ulises-c

Copy link
Copy Markdown
Owner Author

Code review patch — dialogue-level split fix (d04bc83)

Finding: the initial socrat-zh-sft / socrat-en-sft loaders called _split on the flat per-turn list. Turn N of a dialogue could land in train while turn N+1 landed in test; test records then contained turn N's teacher response verbatim in their 历史对话记录 blob — contaminating the eval split with training targets.

Fix: split at the dialogue level first, then expand to per-turn records. All turns of a dialogue now stay in the same partition, matching the behaviour of the original multi-turn loaders.

# before — splits flat per-turn list
converted = [turn for r in raw for turn in _socrat_zh_to_sft_records(r)]
return _split(converted, split, seed)          # ← scatters a dialogue's turns

# after — splits dialogues, then expands
split_raw = _split(raw, split, seed)           # ← whole dialogue goes to one split
return [turn for r in split_raw for turn in _socrat_zh_to_sft_records(r)]

Record counts unchanged: 77,202 train / 8,578 eval. All 143 tests pass. Pushed as d04bc83.

Other review findings (accepted / deferred):

  • Evaluation string drift (学生处于 {state} 状态 vs free-form consultant text) — accepted residual; the A/B test confirmed this terse form is sufficient to elicit clean EOS. Filed for future work when a re-annotated dataset is available.
  • _build_inference_user_message divergence risk — logged; mitigation is the render-diff gate before any future change to get_formatted_history().
  • _check_wandb always-on — accepted; W&B is now a core dep and credentials are present. A WANDB_DISABLED=true env var can suppress it if needed.

@ulises-c

Copy link
Copy Markdown
Owner Author

Bilingual (EN/ZH) teacher scaffolding — pros/cons of building it out

Considered making the SFT format language-aware (English scaffolding for socrat-en-sft, Chinese for socrat-zh-sft) rather than the current Chinese-only block applied to both. Full buildout plan tracked in #108. Summary of the trade-offs:

What it concretely requires

It's bigger than the teacher prompt alone, because several pieces are Chinese-only:

  1. English system block (translate the 7-line rules at socratic_teaching_system.py:404-415).
  2. English canonical state→action map (all 35 entries). The EN dataset's action strings mirror the Chinese dataset annotation, not the canonical map (a1 → 生成一个与解题相关的子问题), so this is new authored content.
  3. English user-message labels (历史对话记录/当前学生输入/eval/action/学生/老师).
  4. Language-aware refactor of socrates_teacher / get_formatted_history / get_action_for_state + a language param threaded through serving.
  5. The hidden blocker — the consultant. evaluation/state come from a Chinese-only consultant. Real English parity needs it to classify on English dialogues and emit English evaluation prose; until then English serving still yields a Chinese eval line, so teacher-side bilingual alone doesn't fully close the loop.

Pros

  • A genuinely bilingual product — English students get English Socratic teaching; stronger paper story than English content under Chinese scaffolding.
  • Makes the SocratDataset-EN split coherent (training English content with Chinese scaffolding is itself a mild drift).
  • The shared-source-of-truth refactor permanently kills the drift bug class behind Stage 2b SFT: GGUF pipeline shipped, eval blocked by train/serve schema drift #94.

Cons / risks

  • Scope creep vs. the Stage 2b SFT: GGUF pipeline shipped, eval blocked by train/serve schema drift #94 fix. Stage 2b SFT: GGUF pipeline shipped, eval blocked by train/serve schema drift #94 is "stop the collapse," already achieved by the Chinese-only fix (canonical action map + free-form evaluation field + structural match). Bilingual is a new feature, not a fix.
  • The consultant lift is the expensive ~80% and must be scoped/validated separately.
  • Re-baseline risk on the locked 72.24 / Tables 6/14 — any socrates_teacher edit is sensitive. zh should stay byte-rendered-identical (the Gemma template trims, so boundary whitespace vanishes), but that must be empirically re-verified before trusting it.
  • Translation quality — I'd be authoring the canonical English rules + actions; needs a fluent reviewer.
  • Doubles GPU validation (separate English EOS-gate + eval run) on top of the priority zh 55h run.

Recommendation: phase it

Ship the Chinese-only fix first (it's the real #94 fix), launch the zh run, and treat bilingual as a deliberate follow-up — and when doing it, do the consultant too, or English parity is only half-built. Full detail, design, and acceptance criteria in #108.

Note (infra): uv run pytest fails in this env — pytest isn't a console entry point; use uv run python -m pytest. Worth checking the post-test hook / CI doesn't assume the former.

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>
@github-actions github-actions Bot added the tests label May 31, 2026
…-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>
@github-actions github-actions Bot added the docs label May 31, 2026
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>
@ulises-c

Copy link
Copy Markdown
Owner Author

Phase 0 applied — Stage-2b SFT is now dev-box "100% ready" (eb12dbd)

External-consultant review + patch finishing the #94 train/serve schema fix that closes out this PR. Phase 0 removes the last two drifting lines between the socrat-zh-sft / socrat-en-sft training records and the prompt socrates_teacher() actually sends at inference — the residuals that, left in, would have re-invited the original non-terminating-repetition collapse.

What was drifting, and the fix

Line Was emitting Inference actually sends Fix
苏格拉底教学顾问建议的操作: raw dataset turn["action"] get_action_for_state(state) loader now calls the same canonical map — byte-exact
苏格拉底教学顾问评估结果: templated 学生处于 {state} 状态 live consultant prose loader now uses the dataset's per-turn free-form evaluation field

The action drift was systematica1 (every dialogue's opening teaching turn) never matched (生成一个问题 vs 生成一个与解题相关的子问题), plus several c-states.

Changes

  • socratic_teaching_system.py — hoisted state_to_action to a module-level STATE_TO_ACTION + get_action_for_state() so the loader imports the exact inference source of truth. Behavior-preserving: AST-verified all 35 entries identical to HEAD, and the live socrates_teacher render is byte-for-byte unchanged for a known turn (no eval re-baseline — headline 72.24 / Tables 6/14 untouched).
  • dataset.py_build_inference_user_message takes evaluation; both -sft builders source the action from get_action_for_state(state) and the eval from the dataset evaluation field. Target-turn gating still keys on dataset state+action presence, so the record set is unchanged.
  • test_sft_inference_format.py — un-rigged the action dimension (the parity test previously passed the dataset action straight into inference, so it never tested the action line). The render-diff gate now asserts the action line render-equal too; the eval line is isolated as the sole expected residual.

Verification (this dev box)

  • ✅ Full suite 154 passed / 2 skipped; ruff / pyright / codespell / shellcheck clean (pre-commit gate).
  • ✅ Loaded the real HF sources — socrat-zh-sft and socrat-en-sft, 42,890 records each — and confirmed 0% template-fallback evals: every record carries the dataset's free-form evaluation, so the eval-line fix is not a silent no-op.
  • ✅ Official make train-gemma4-31b-dry-run plumbing passes.
  • zh inference prompt confirmed identical to pre-change HEAD.

⚠️ The EOS gate on the R9700 is still MANDATORY — and must use a live free-form eval

Phase 0 closes the structural and action-line drift, but the eval line still drifts at real serve time — the consultant regenerates fresh prose on every call, so it will not match the dataset's stored annotation token-for-token. test_evaluation_line_is_the_only_residual_drift_under_freeform_consultant pins this as the irreducible residual. Do not read "parity test passes" as "model is collapse-proof." Before trusting the multi-hour run, train a short checkpoint and prove EOS termination on a prompt built with a realistic multi-sentence Chinese evaluation, end-to-end through process_student_input (gold) or via the _capture_inference_prompt path (lighter). A gate built on the 学生处于 {state} template is blind to exactly this residual and will pass while shipping a broken model.

Carry-forward notes for the R9700 launch (§5)

  • Recompute run size — the config comment ("~2298 steps / 11 GB") is stale (per-dialogue era). Per-turn is ~77k train records → ~14k steps at bs1×ga16×3ep (multi-day). With TRAIN_SAVE_STEPS=100 that's 140+ adapter checkpoints (~70 GB)raise TRAIN_SAVE_STEPS and/or set save_total_limit before launch.
  • GPU must be cleanrocm-smi --showpids empty (a prior rc=134 leaves the KFD dirty).
  • hipBLASLt off by default (safe); =1 is an optional ~4× perf bet — test 20+ steps first (feat(train): Qwen3.5-0.8B LoRA classifier — W&B tracking + transformers 5.9 drift fix #99).

Out of scope / known gaps (deferred, not bugs)

  • Bilingual (EN/ZH) Socratic teacher: language-aware train + serve scaffolding #108 (bilingual EN/ZH)socrat-en-sft stays Chinese-scaffolded: its English evaluation prose is dropped into the Chinese teacher prompt as-is. Intended for now.
  • Parity-test blind spot (pre-existing, not introduced here): the action line is now structurally tautological (both train + serve route through get_action_for_state, so map coverage cannot break parity — good). But that means a future edit corrupting a non-a1 map entry (e.g. b2) would corrupt both sides identically and pass every test. Only a1 is pinned by an independent literal assertion; the 35-entry AST-vs-HEAD purity check was point-in-time, not CI. Flagging as a known gap, not fixing in this PR.

@ulises-c

Copy link
Copy Markdown
Owner Author

gfx1201 ROCm training — issues encountered on feat/gfx1201-rdna4-qlora-fla-training

Attempting to reproduce the EOS-gate checkpoint run on the R9700 (gfx1201, 32 GB). Documenting what went wrong and what fixed it.

Issue 1 — make test-vllm auto-installed vLLM and broke the venv

The test-vllm Makefile target detected vLLM was absent and auto-installed vllm==0.22.0, which pulled in torchvision==0.26.0 (CUDA build). That torchvision registers a fake torchvision::nms op at import time that doesn't exist in the ROCm torch, breaking peft and transformers imports:

RuntimeError: operator torchvision::nms does not exist
ModuleNotFoundError: Could not import module 'BloomPreTrainedModel'

Fix: uv pip uninstall vllm torchvision. Both packages are inference-only; the training stack (peft, trl, transformers) doesn't need them.

Recommendation: The test-vllm script should check for vLLM but not auto-install — or install into an isolated env. Auto-installing into the shared training venv is destructive.

Issue 2 — expandable_segments:True unsupported on HIP, caused GPU page fault at step 16

PYTORCH_HIP_ALLOC_CONF=garbage_collection_threshold:0.8,expandable_segments:True generated a warning at startup:

UserWarning: expandable_segments not supported on this platform
(Triggered internally at /pytorch/c10/hip/HIPAllocatorConfig.h:40.)

With this set, the run faulted at step 16 every time:

Memory access fault by GPU node-1 on address 0x7f78cbc00000. Reason: Page not present or supervisor privilege.
GPU core dump failed

Fix: Stripped expandable_segments:True from all Makefile targets. The note in #101 references PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True — on ROCm the variable is PYTORCH_HIP_ALLOC_CONF, and expandable_segments is not a supported key for the HIP allocator.

Issue 3 — GPU KFD dirty state on repeated crashes (per #101 note §3)

After each page fault the KFD is left in a dirty state. Added rocm-smi --showpids check before each retry. Both subsequent retries were preceded by confirming a clean GPU state.

Current state

Running with TORCH_USE_HIPBLASLT=1 and setsid (per §1 and §4 of the #101 notes). Past step 16 is the validation point — will update once we confirm stability past that threshold.

VRAM at step 10 (last clean run): alloc=21.1 GB, reserved=27.8 GB / 31.9 GB total — within headroom, not OOM.

ulises-c and others added 6 commits May 30, 2026 21:54
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>
ulises-c and others added 2 commits June 2, 2026 02:08
…ontinuity

The monitor's WANDB_PROJECT patch (aebcc3f) lives in monitor_stage2.sh, which is
already parsed into the running monitor's memory — so it does NOT apply to the
current crawl's next relaunch (only to a monitor restart = a fresh run). train_sft
re-reads this config on every `make` relaunch, so putting WANDB_PROJECT here means
the NEXT CRASH picks it up without restarting the monitor.

Also pins WANDB_RUN_ID + WANDB_RESUME=allow so every crawl resume appends to ONE
continuous run instead of spawning a new W&B run per crash cycle (the reason the
step axis looked empty — fragmented short runs). Requires W&B auth; if login fails
train_sft._check_wandb still disables reporting regardless of these vars.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The crash PR comments had an empty ``` block because crash_hint grepped for
"page fault|traceback|runtimeerror|out of memory|killed", but the gfx1201 fault
prints "Memory access fault by GPU node-1 ... Page not present" and aborts with
"Aborted (core dumped)" — none of which match (it is "Page not present", not
"page fault"), and a hard HSA abort emits no Python traceback.

Broaden the pattern to the real ROCm/HSA signature (memory access fault, page not
present, hsa_status, aborted, core dumped, hip/cuda error) and lead with the last
tqdm step, so each crash post shows the fault step — the N we are measuring —
directly in the PR instead of forcing a dig into crashlogs/.

NB: crash_hint runs inside the long-lived monitor process, so this takes effect on
the next monitor (re)start, not the current crawl's next relaunch.

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

ulises-c commented Jun 3, 2026

Copy link
Copy Markdown
Owner Author

Issue posted in rocm-libraries #7992

ulises-c and others added 7 commits June 2, 2026 18:18
Adds two scripts to produce the standalone reproducer needed for the
upstream ROCm/rocm-libraries report (#113 comment 1):

- capture-rocblas-bench.sh: wraps a one-shot training resume with
  ROCBLAS_LAYER=2 + ROCBLAS_LOG_BENCH_PATH, verifies the env vars work
  first, then extracts the exact rocblas-bench invocation at
  M=608 N=5376 K=21504 from the live Tensile dispatch
- replay-rocblas-bench.sh: replays the captured line under AMD_LOG_LEVEL=3
  and checks for the GPU page fault + MT64x64x64/ISA1201/DTVB1 ShaderName

Both scripts must be run after the production crawl (monitor_stage2.sh)
releases the GPU — not while it is running.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…t before restart

Crawl reached checkpoint-1230/4826 (~25.5%) before a tokenization deadlock
left a wedged 24 GB KFD context. All 8 retries failed preflight. Documents
GPU reset sequence and tokenization deadlock workaround for next session.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds HFCheckpointCallback to train_sft.py — pushes checkpoint-N to a
HuggingFace model repo in a background thread after every TRAIN_HF_PUSH_EVERY
steps (default 50) and unconditionally on train end.  Commit message encodes
step, epoch, loss, and token accuracy.  Skips gracefully if the previous push
is still in-flight.  Opt-in via TRAIN_HF_REPO env var; wired into the
train-gemma4-31b-stage2-unsloth Makefile target pointing at
ulises-c/SocratesLM-31B-stage2b-QLoRA.

Also bumps save_total_limit 2 → 5 so the local crawl retains enough history
to recover from multi-step crashes without touching HF.

Includes gfx1201 env diagnostics report from 2026-06-05 capture run.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move HFCheckpointCallback out of main() into src/project/hf_callback.py
so it is importable and testable. Removes the nested import threading that
required a ruff --fix workaround on every commit. Adds 7 unit tests covering
step gating, skip-if-in-flight, missing checkpoint dir, commit message format,
forced on_train_end push, and thread join. All 162 tests pass.

Also updates the session 6 handoff.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Minimal Python script that constructs a bnb.nn.Linear4bit layer at the
exact fault shape (in=21504, out=5376, bias=True, nf4, bf16) and runs
200 forward passes under AMD_SERIALIZE_KERNEL=3 to dispatch the buggy
Tensile ISA1201 DTVB1 kernel for upstream ROCm/rocm-libraries filing.

Note in docstring: script does not crash in a small process (wild addr
lands on mapped VA); fault requires full ~19 GB model VA footprint.
Wild addresses are always 2 MB-aligned across all 12+ production crashes.

Also extends codespell skip list to include *.log (runtime output, not
human-edited source — consistent with the existing comment in pyproject.toml).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…dy to file

GPU clean at session start; ROCm torch regression recurred (fixed make install-rocm).
Wild address analysis: all 12 crashes are 2 MB-aligned, ~1.6 GB above B.end.
Standalone reproducer written (scripts/repro_gfx1201_rocblas.py).
Diagnostic SYNC+lvl3+probe run launched from checkpoint-1230 — parse log first next session.
Next: parse diagnostic log, file AMD upstream issue, gpu-preflight, restart monitor.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ulises-c and others added 3 commits June 5, 2026 12:53
replay-rocblas-bench.sh: expand constructed arg list via array
  (read -ra bench_args_arr) instead of unquoted $bench_args — SC2086.
capture-rocblas-bench.sh: cd "$REPO_DIR" || exit 1 — SC2164.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
A prose comment beginning with '# shellcheck' is read by shellcheck as a
malformed directive (SC1072/SC1073), failing the pre-commit hook. Reword it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The crawl monitor was posting one PR comment per crash, flooding PR #101
(code review) with retry spam. Route all events to a dedicated Training
Log issue (#120) instead, and collapse them into ONE pinned, self-updating
comment rather than a comment per event.

- ISSUE_NUMBER=120; new LOG_COMMENT_ID pins the live-log comment (bump it
  per new crawl, same convention as WANDB_RUN_ID). log_row fetches that
  comment, appends a row, and PATCHes it back — the comment body is the
  source of truth, so restarts / outputs/ wipes never lose history or mint
  a second comment.
- Add a progress row every PROGRESS_EVERY=50 steps with an ISO-8601
  timestamp + loss / grad_norm / lr / epoch parsed from the HF logger line
  (ast.literal_eval). Gated on a real metrics line so startup shard/dataset
  bars can't trigger a spurious row.
- crash / STALLED / COMPLETE become table rows; one-line fault signature in
  the Note cell (pipes escaped), full tracebacks stay in crashlogs/.
- HANDOFF_STAGE2_CRAWL.md updated to the single-comment model.

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

ulises-c commented Jun 5, 2026

Copy link
Copy Markdown
Owner Author

Running the crawl monitor (latest) + crash diagnosis

The monitor now posts to a single self-updating comment on the Training Log issue (#120) instead of spamming this PR per crash. Progress rows land every 50 steps (time / loss / grad_norm / lr); crash, STALLED, and COMPLETE are rows in the same table. Landed in e9ae500.

1. Get the latest monitor onto the training host

The host keeps running whatever version was launched — editing/pulling does not change a running monitor. So pull, then relaunch:

cd <repo-on-host>
git pull                                  # must include e9ae500
pgrep -af 'monitor_stage2\.sh'            # is an OLD one still running? kill it first:
# pkill -f 'monitor_stage2\.sh'

2. Launch it

nohup bash scripts/monitor_stage2.sh > outputs/monitor_stage2.log 2>&1 &
tail -f outputs/monitor_stage2.log

It launches training itself (make train-gemma4-31b-stage2-unsloth) with a fresh TRAIN_DATA_SEED, polls every 5 min, and on a fault: archives the log + dmesg, cleans the GPU, quarantines a partial checkpoint, and resumes. A ▶ monitor start row in #120 confirms it's posting.

Every launch is GPU-gated — including the first. Before train_sft.py starts, the monitor (a) waits up to 180s for the GPU/KFD to be verified clean (test_gpu_stack.sh --wait-clean, so it won't fault on stale PTEs), then (b) runs gpu-preflight (test_gpu_stack.sh --preflight), which is a Makefile prereq of the train target. If preflight fails, make aborts and you'll see launch aborted (gpu-preflight failed?) — counts as no forward progress in monitor_stage2.log. You don't run make gpu-preflight yourself — it's automatic.

New crawl (not a resume)? Create a fresh placeholder comment on #120 and paste its numeric id into LOG_COMMENT_ID in monitor_stage2.sh (same "bump per run" convention as WANDB_RUN_ID).

3. If it crashes and nothing posts — diagnose on the host

Nothing posting almost always means the monitor process wasn't running when training faulted (a silent crash), not a bad fault. Run:

cd <repo-on-host>
pgrep -af 'monitor_stage2\.sh'                          # monitor alive?
pgrep -af 'train_sft\.py'                               # training alive?
tail -40 outputs/monitor_stage2.log                     # where the monitor stopped / gh WARNINGs
tail -60 outputs/sft-stage2-gemma4-31b/train.log        # the actual fault
ls -lt outputs/sft-stage2-gemma4-31b/crashlogs/ | head  # did it archive this crash?
gh auth status                                          # token still valid on the host?

Reading it:

Symptom Cause Action
monitor not running + no new crashlogs/ entry monitor was dead when training faulted — silent crash relaunch (step 2)
monitor running + new crashlogs/ entry but no #120 row crash caught but gh failed — look for WARNING: gh ... failed in monitor_stage2.log re-auth gh / check network
train.log tail shows Memory access fault … Page not present the known gfx1201 Tensile GEMM page fault (#113) normal for the crawl — monitor reseeds + resumes
monitor posts ⛔ STALLED 8 consecutive no-progress retries — wedged GPU KFD make diagnose-gfx1201-fault; full recovery in docs/HANDOFF_STAGE2_CRAWL.md

Per-crash full tracebacks + dmesg snapshots are kept in outputs/sft-stage2-gemma4-31b/crashlogs/.

ulises-c and others added 9 commits June 6, 2026 19:12
- scripts/repro_gfx1201_dense.py: improved standalone reproducer that
  pre-allocates ~20 GB GPU filler before placing the Linear4bit layer,
  increasing VA density to match production and making the wild address
  more likely to land in a page-not-present region. Adds backward pass
  (production faults in both forward and backward GEMMs).
- docs/HANDOFF_STAGE2_CRAWL.md: session 8 update — SYNC log parsed
  (fault 0x7efb33000000 2MB-aligned, MT64x64x64_ISA1201_DTVB1 confirmed),
  crawl progressed to checkpoint-1290 (26.7%), all processes killed,
  GPU dirty. Immediate action is gpu-preflight + monitor restart.

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

Both system (/opt/rocm) and torch-bundled rocBLAS libraries dispatch MT128x128x32
identically for all probe variants; tile selection divergence (standalone vs production)
remains unexplained at the binary level. Monitor active at step 1305/4826.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
tqdm uses \r to overwrite its bar in-place; when the process crashes,
"Aborted (core dumped)" appends directly to the last bar state without
a newline, so grep -iE "aborted" matches the whole tqdm line and the
raw bar flooded the GitHub markdown table cell.

Fix: awk -F'\r' '{print $NF}' takes the last \r-separated chunk (the
final bar state + the error suffix), then sed strips any residual
%|...|[...] bar prefix before the pipe-to-fullwidth substitution.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add .hf_last_push persistence file so resumed runs skip already-uploaded
  checkpoints (src/project/hf_callback.py)
- Add on_init launch-push: scans disk for latest checkpoint, pushes
  synchronously if not recorded as uploaded — catches crash mid-push
- Fix numeric-sort bug in _push_latest_on_launch (was sorting alphabetically,
  e.g. checkpoint-90 > checkpoint-100)
- Add step <= last_pushed guard in _maybe_push (force=True bypasses)
- Set TRANSFORMERS_VERBOSITY=error in train_sft.py main() — suppresses
  level-3 INFO noise during full SFT runs
- 9 new unit tests (7 → 16) covering persistence, skip-already-pushed,
  force-push bypass, on_init launch push / skip / numeric ordering
- Update HANDOFF_STAGE2_CRAWL.md with new push behavior docs
After each successful checkpoint push to HF, the callback downloads the
existing SFT_STAGE2B_CHECKPOINT_LOG.md (or starts fresh), adds/updates a
row for the current checkpoint, and re-uploads it.  Rows are keyed by
step number so re-pushes replace rather than duplicate.

- _push() signature extended with epoch, loss, acc parameters
- _build_log_row() formats a table row
- _update_checkpoint_log() downloads, merges, sorts, uploads the log
- hf_hub_download failure = fresh file; upload failure caught and
  printed (non-critical, checkpoint already on HF)
- 5 new tests: row formatting (with/without metrics), fresh-start,
  append, replace
monitor_stage2.sh: last_step() and crash_hint() were picking up the last
N/total pattern in train.log, which was the eval tqdm bar (8578/8578)
rather than the training-step bar — causing every crash row to report
the wrong step. Filter out N==N patterns (100%-complete bars) so only
in-progress training tqdm is matched.

hf_callback.py: _push_latest_on_launch was pushing whatever checkpoint
was latest on disk at resume time (e.g. step 1380, 1670) regardless of
the push_every cadence, polluting the HF repo and the checkpoint log
table with non-multiple-of-50 entries. Add the same push_every guard.
Also fix a double-newline in the log table assembly that broke GFM
rendering (LOG_HEADER trailing \n + join \n = blank line mid-table).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace tqdm/log-scraping progress trigger with checkpoint-based one:
fire when latest_ckpt_step() hits a new multiple of PROGRESS_EVERY (50)
and read epoch/loss/acc/grad_norm/lr from trainer_state.json instead of
parsing train.log. This eliminates the 8578/8578 false-trigger and adds
mean_token_accuracy to the issue #120 log table, matching the detail
level of SFT_STAGE2B_CHECKPOINT_LOG.md on HF.

Removes the now-unused current_step_num / latest_metrics / parse_metrics
/ log_progress helpers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@ulises-c

ulises-c commented Jun 7, 2026

Copy link
Copy Markdown
Owner Author

Next experiments — M-sweep to crack the standalone repro (instructions)

Session 11 (full writeup in #113) found the SYNC-deterministic log records the faulting M as 607, not 608. Every standalone probe so far used M=608 — the even neighbor, from the less-reliable async capture. A second SYNC run faulted at M=439. Both are odd; the tile divergence (standalone → MT128x128x32, production → MT64x64x64) may simply be the off-by-one M crossing a Tensile size-predicate boundary.

Run on the training box (R9700), not the dev box. One GPU — do NOT run while monitor_stage2.sh is crawling. make gpu-preflight first.

1. Sweep M (PRIMARY — minutes)

make gpu-preflight
AMD_SERIALIZE_KERNEL=3 HIP_LAUNCH_BLOCKING=1 AMD_LOG_LEVEL=3 TORCH_USE_HIPBLASLT=0 \
  uv run --no-sync python scripts/probe_gfx1201_tile.py --shape-3d --ckpt \
  --m-sweep 439,512,576,606,607,608,609,640 2>&1 | tee probe_msweep.log

# which tile did each M dispatch?
grep -E 'SWEEP M=|MT[0-9]+x[0-9]+x[0-9]+.*ISA1201' probe_msweep.log \
  | grep -oE 'SWEEP M=[0-9]+|MT[0-9]+x[0-9]+x[0-9]+'

Read the result:

  • Some M -> MT64x64x64 (...DTVB1...): that M is the standalone-repro shape. Go to step 2.
  • All M -> MT128x128x32: M is ruled out — the trigger is the fused ..._Bias_..._UserArgs_... epilogue. Go to step 3.

2. If an M dispatched MT64x64x64 — attempt the standalone crash

Re-run the dense-VA repro (scripts/repro_gfx1201_dense.py) at that exact M with the ~20 GB filler so the wild address lands in a mapped band. A crash here = the standalone reproducer #7992 needs. Capture the ShaderName + fault address and paste both into the upstream draft, then post #7992.

3. If no M dispatched MT64x64x64 — test the epilogue path

... uv run --no-sync python scripts/probe_gfx1201_tile.py --shape-3d --ckpt --m 607 --no-bias 2>&1 | tee probe_nobias.log
... uv run --no-sync python scripts/probe_gfx1201_tile.py --shape-3d --ckpt --m 607          2>&1 | tee probe_bias.log

If MT64x64x64 appears only with bias, the trigger is the fused bias epilogue (internal Tensile dispatch, not reachable from rocblas-bench or a plain Linear4bit) — report that as kernel-confirmation-only in #7992, no standalone crash expected, and fall back to the HIP rocblas_gemm_ex + bias reproducer.

Probe changes (already committed)

scripts/probe_gfx1201_tile.py: default --m is now 607 (SYNC-confirmed), added --m-sweep <csv> (multiple M in one process, banner per M) and --no-bias. Docstring carries the run matrix + detection greps.

Upstream report corrections (#7992)

  • M=608 -> M=607; state M = sample token length, data-order-dependent (439/607 observed).
  • Trigger is column-major B at ldb=K=21504 specifically (same-M K=5376/16384 col-major GEMMs run clean in the same step).
  • Reframe "probabilistic" -> data-order-deterministic; give the fixed data_seed=42 recipe; note seed rotation is a crawl mitigation, not a bug property.

ulises-c and others added 4 commits June 7, 2026 14:32
…p/--no-bias

The SYNC-deterministic log records the faulting GEMM's A operand as
(1, 607, 21504) — M=607, not the 608 every prior standalone probe used
(608 came from the less-reliable async capture; a second SYNC run faulted
at M=439). M is the sample's token length, so the standalone-vs-production
tile divergence (MT128x128x32 vs MT64x64x64) may just be an off-by-one M
crossing a Tensile size predicate.

- default --m -> 607
- --m-sweep <csv>: run multiple M in one process, banner per M, to find
  which M dispatches MT64x64x64
- --no-bias: test the fused …_Bias_…_UserArgs_… epilogue hypothesis

See #113 (Session 11) and #101 for the run plan.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
log() piped through tee -a SELF_LOG while stdout was also redirected to
the same file at launch, writing every line twice. Switch to a direct
append so there is only one writer.

last_step() grepped train.log for N/total tqdm patterns and picked up
dataset-tokenisation bars (8578 per shard) instead of training-step bars
(max_steps=4826) because the training tqdm never appears in train.log.
Replace the tqdm parse in the training-running branch with
latest_ckpt_step(), which is accurate to within save_steps=10 and always
reflects real forward progress.

Co-Authored-By: Claude Sonnet 4.6 <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.

Investigate gfx1201 (RDNA4) training: Flash Linear Attention + Gemma4-31B QLoRA OOM

1 participant