Qwen3.8-9B Hermes FC — Todo
A QLoRA continue-tune of the v26 balanced model
(qwen3.8-9b-hermes-fc-balanced), trained with a focused dose of real
todo-tool trajectories plus a condensation experiment. This is the model
that falsified the "sub-10B models cannot learn interactive state tools via
SFT" claim (Gemini Deep Research §5): pure QLoRA SFT with 393 todo-call
demos taught autonomous todo reach-for on novel multi-step tasks.
- Base model: Empero/Qwen3.8-9B (Apache-2.0)
- Warm start: v26 merged weights (
qwen3.8-9b-hermes-fc-balanced), QLoRA adapter on top (envelope + tool tokens already baked in) - Training data: 1,706 rows in ShareGPT format, 393 todo calls:
- 1,543 v26 rows (real Hermes trajectories + SWE-rebench + APIGen + When2Call)
- 72 real todo-loop rows (DJLougen agent traces, native Hermes XML
envelopes, validate→mutate→verify feedback loops;
todo-realband) - 72 condensed system-prompt rows (same todo loops, ~90-token system stub
instead of the full 6.3K–25.9K-char prompt;
todo-real-condensedband) - 19 todo gap-fill rows (
gapfill-todoband: todo + session_search / lcm_recall / lcm_grep variants)
- Method: QLoRA 4-bit, rank 16 / alpha 16, dropout 0, batch 1 x grad-accum 8 (effective 8), lr 2e-4, warmup 0.1, MAX_SEQ 6144, 1 epoch = 214 steps (final loss 0.1822)
- Vocabulary: 248077 → 248079 (2 added tool tokens:
<|tool_call|>,<|tool_response|>) - Weights: BF16 full merge (12 shards, 18.4 GB, 9.20B params) plus a ROCmFPX quantized GGUF (see companion repo)
- Architecture note: merge preserves the 15-key MTP (multi-token
prediction) head from the base —
Qwen3_5ForConditionalGenerationwithtext_config.vocab_size = 248079. Visual tower dropped (text-only).
Addendum 2026-09-03 — evaluation-methodology correction
Read this before you use the numbers below.
After this checkpoint was published, we tested the base model
(Empero/Qwen3.8-9B-Distill, no fine-tuning) through the live Hermes
interface: the OpenAI-native tools parameter path that Hermes actually
uses. The base model calls tools correctly on its own.
The harness that certified this checkpoint did not use that interface.
It baked tool schemas into the system text, never passed tools, and
required raw <tool_call> XML in the response. That prompt shape matches
neither the base model's own chat template nor the live Hermes request
path. Every gate score in the v26-to-v29 lineage measured this synthetic
interface.
The statement in this README that "the base distill contributes zero tool-calling" is incorrect. It measured the synthetic interface.
Corrected comparison (native tools path, same 4-tool battery, greedy):
| model | tier-1 fired | tier-1 name=todo | tier-2 todo-first | tier-3 fired (want 0) | tier-4 fired (want 0) |
|---|---|---|---|---|---|
| base (no fine-tune) | 18/20 | 17/20 | 3/10 | 5/10 | 0/10 |
| v28 ckpt-175 (this model) | 17/20 | 10/20 | 8/10 | 9/10 | 4/10 |
| armB2-175 (clean retrain) | 10/20 | 8/20 | 10/10 | 4/10 | 0/10 |
What the fine-tune lineage actually changed: todo-first planning on multi-step tasks (tier-2 name=todo 3/10 base to 8/10 v28 to 10/10 armB2). That is real, and it is the one capability the lineage added. The costs: restraint regressions (tier-3 over-trigger 5/10 to 9/10; tier-4 tool-drift on trivia 0/10 to 4/10) and, in armB2, over-suppression (tier-1 10/20).
Status: the fine-tune program is frozen as of 2026-09-03. The base model is the author's runtime default. This checkpoint stays published as the strongest measured demonstration of SFT-taught todo-first planning in the lineage, and as the artifact that exposed the harness error. The Evaluation section below describes behavior on the synthetic interface; do not read it as native-interface competence.
Reproduction: scripts/native_battery.py + scripts/todo_probes.jsonl
scripts/no_drift_probes.jsonlproduce the corrected table (any GGUF,CTX=65536to match the serve config).
- Base model: Empero/Qwen3.8-9B (Apache-2.0)
- Warm start: v27 merged weights (
qwen3.8-9b-hermes-fc-todo), QLoRA adapter on top (envelope + tool tokens already baked in) - Training data: 218 trajectories (ShareGPT format, 551 todo calls),
generated by GLM-5.3-Flash via the HuggingFace router on 112 synthetic
tasks mined from the author's agent corpus — not the author's own traces:
- 190 teacher trajectories (85% verified by sandbox execution; failures with recovery injected for 15.8% of rows)
- 61 rows carry distilled short-chain-of-thought; 159 envelope-only
- 12 synthetic web-search tasks (first web signal)
- Full dataset published separately (see
hermes-fc-v28-data); probes were kept virgin (zero overlap with generation or training)
- Method: QLoRA 4-bit, rank 16 / alpha 16, dropout 0, batch 1 x grad-accum 8 (effective 8), lr 2e-4, warmup 0.1, MAX_SEQ 6144, 8 epochs = 214 steps (final loss 0.02085, avg train loss 0.09405)
- Vocabulary: 248077 → 248079 (2 added tool tokens:
<|tool_call|>,<|tool_response|>) - Weights: BF16 full merge (13 shards, 18.9 GB, 9.20B params) plus a ROCmFPX quantized GGUF (see companion repo)
- Architecture note: merge preserves the 15-key MTP (multi-token
prediction) head from the base —
Qwen3_5ForConditionalGenerationwithtext_config.vocab_size = 248079. Visual tower dropped (text-only).
Evaluation (todo-probe battery, greedy, todo schema in every prompt)
40 probes, 3 tiers: Tier 1 = 20 real DJLougen todo trajectories held out of training (hash-exact, zero leakage; GT = first todo call). Tier 2 = 10 synthetic multi-step tasks (refactor+tests+docs, CI setup, release prep) → autonomous todo reach-for. Tier 3 = 10 trivial asks (want 0/10) → over-trigger guard.
| tier | model | fired | name=todo | todos_ok | format_exact |
|---|---|---|---|---|---|
| T1 (real, held-out) | v26 | 13/20 | 10/20 | 10/20 | 6/20 |
| T1 | v27 (this model) | 13/20 | 11/20 | 11/20 | 0/20 |
| T2 (novel multi-step) | v26 | 9/10 | 3/10 | 1/10 | 1/10 |
| T2 | v27 | 9/10 | 7/10 | 7/10 | 3/10 |
| T3 (trivial, want 0) | v26 | 7/10 | 1/10 | 1/10 | 3/10 |
| T3 | v27 | 5/10 | 1/10 | 1/10 | 2/10 |
Headline: Tier-1 held-out fidelity held (13/20 = 13/20, name 10→11), and tier-2 autonomous todo reach-for moved 3→7 name=todo / 1→7 todos_ok — the swing metric. 393 SFT demos taught a novel multi-step task to reach for the todo tool without any RLVR. Tier-3 over-trigger improved 7→5 but is not zero (remaining fires are mostly other tools on trivial asks — generic tool-calling eagerness, not todo-specific).
format_exact caveat (important): the format column demands
envelope-only output (^\s*<tool_call>.*?</tool_call>\s*$). v27 scores 0/20
on tier 1 not because its envelopes are malformed — full-output inspection
shows it emits perfect envelopes but entire multi-turn agentic loops in one
completion (<tool_call>todo</tool_call> → simulated user <tool_response> → next <tool_call>search_files</tool_call> → …). That is
the DJLougen validate→mutate→verify curriculum behaving as intended, and the
live harness parses it fine; it just fails the envelope-only regex.
The base distill contributes zero tool-calling (0/45 old eval); the entire capability comes from the fine-tune.
Intended use
Research artifact for experimenters working on tool-call behavior in 9B-class
models — not a product. The tool_call XML envelope and tool schemas match
the Hermes agent runtime this was trained on. This is an intermediate
milestone in an iterative recipe series (v1 → v2 → v25 → v26 → v27 → v28);
v28 is the target default.
What this repo contains
- The BF16 merged weights (this repo)
- All scripts that produced the model and dataset (
scripts/)
Reproducibility
All scripts that produced this model are in scripts/:
| file | purpose | number it produced |
|---|---|---|
build_v26.py | base dataset build (imported by v27 builders for build_system, pad_to_bucket, conv_hash, constants) | shared helpers |
build_v27_todo_real.py | DJLougen trace extractor: event-stream parse, think-block strip, inline tool_call/tool_response split, todo truncation bypass, windowing → 72 rows | todo-real band |
build_v27_condensed.py | same 72 rows with ~90-token system stub → 72 rows | todo-real-condensed band |
assemble_v27.py | final blend: v26 base + gapfill-todo + todo-real + condensed → 1,706 rows | row count, mix |
add_todo_gapfill.py | synthetic todo gap-fill rows (todo + recall tools) | gapfill-todo band |
gate_v27.py | JSON validity gate over the assembled set | 1,706 valid rows |
qwen38_train_v27.py | QLoRA continue-train via Unsloth (1 epoch, 214 steps, loss 0.1822) | training loss/steps |
merge_qwen38_v27_pure.py | shard-streamed BF16 merge (pure torch; MTP head preserved; vocab patched) | 12 shards / 18.4 GB |
build_todo_probes.py | 40-probe battery build (T1 held-out / T2 multi-step / T3 trivial) | the table above |
eval_todo_probes.py | probe battery runner (llama-server on 9088, tier-aware scoring) | the table above |
eval_fc_heldout.py + convert_hf_hermes.py | 45-probe held-out eval + HF-conversion helper (kill-servers pattern source) | legacy eval, import support |
probe_v27_full.py | full-output runner (why format_exact=0 is loop-gen, not malformed XML) | caveat above |
The scripts carry the author's machine paths (F:/models/..., C:/AI/...)
and run on this rig's ROCm stack (Unsloth 2026.8.18, torch 2.11 ROCm,
RX 7700 XT); adjust paths for your environment.
Convert/quantize: llama-rocmfpx fork, convert_hf_to_gguf.py --outtype bf16
→ llama-quantize Q4_0_ROCMFP4_FAST (see GGUF repo).
Quantized version
Q4_0_ROCMFP4_FAST GGUF (ROCmFPX format, AMD RDNA3 kernels) is published in
the companion repo. ROCmFPX quants target AMD ROCm inference; for portable
use, convert from the BF16 merge here.
Training data note
The training data itself is not published. The rows derive from the author's own agent sessions plus DJLougen Hermes traces (Apache-2.0), and some rows contain private strings (hostnames, session identifiers). The scripts that build the dataset are published; the data is not. The weights do not contain those strings.
Acknowledgements
Base model and license inherited from Empero/Qwen3.8-9B (Apache-2.0). Training data derived from the author's own Hermes agent sessions plus publicly-licensed teacher sources (SWE-rebench, APIGen-MT-5k, When2Call, DJLougen/hermes-agent-traces-filtered).