Status: Stage 1, Stage 2 (iter2), and Ablation runs complete. The pipeline numbers below reflect the finalized
tunifolk-rlft-iter2run following the contrastive reward recalibration.
| job | state | duration | |
|---|---|---|---|
| Stage 1 | tunifolk-sft-base · 3008210220993216512 |
SUCCEEDED | 1 h 06 m |
| Stage 2 (initial) | tunifolk-rlft-from-sft · 7141142488407867392 |
FAILED (Export Error) | 1 d 22 h (Code 13 at step 158) |
| Stage 2 (iter2) | tunifolk-rlft-iter2 · 2554307082035658752 |
SUCCEEDED | 1 d 19 h |
| Ablation | tunifolk-rlft-v5 · 5782572726988308480 |
SUCCEEDED | 2 d 02 h 28 m |
The ablation is the control: the same reinforcement stage — same prompt set,
same reward, same hyperparameters except thinkingLevel — started from base
gemini-3.5-flash instead of from stage 1
(configs/baseline_rl_from_base.yaml). It is what the pipeline has to beat to
justify existing.
/train_total_loss 3.052 → 2.425
/eval_total_loss 2.951 → 2.481 gap 0.056 — no overfitting
next-token acc 0.461 → 0.528 train | 0.473 → 0.512 eval
151 steps · 3 checkpoints (step 51 ep 2, 102 ep 3, 151 ep 3)
Held-out loss tracks train throughout, which on 1,868 source texts seen three times is the thing most worth checking.
Note the step count: 151, not the 340 that 3631 × 3 / 32 would suggest.
The supervised service batches differently from the reinforcement one; do not
assume the RLFT formula carries over.
| ablation: base → RL | pipeline: base → SFT → RL | |
|---|---|---|
structure at step 0 |
0.4229 | 0.7624 |
poetic_quality at step 0 |
0.1803 | −0.9260 |
/train_mean_reward at step 0 |
0.3501 | 0.2559 |
/train_mean_reward |
0.9154 (final, 158) | 0.9250 (step 86) |
structure |
0.8805 (final) | 0.9246 (step 86) |
poetic_quality |
0.9969 (final) | 0.9257 (step 86) |
/eval_mean_reward |
0.9025 (step 159) | 0.9216 (step 78) |
| generation length | 12,782 tokens | 927 tokens |
thinkingLevel |
HIGH |
MINIMAL |
The supervised stage transfers. Stage 2 opens at structure 0.762 where the
ablation opened at 0.423 — it begins roughly a third of the way into the
ablation's trajectory.
It ends higher, not just starts higher. At step 86 of 158 the pipeline has
passed the ablation's final score on /train_mean_reward, structure and
/eval_mean_reward. The margin is widest on structure (0.925 vs 0.881), the
deterministic half of the reward rather than the judge.
13.8× less generation. 927 tokens against 12,782, because the ablation runs
thinkingLevel: HIGH and spends ~98% of its output on thinking tokens while the
pipeline spends almost none. Same or better reward, an order of magnitude
cheaper per sample.
poetic_quality opened at −0.926, against +0.180 for the ablation — the
judge initially rejected stage 1's output almost totally, dragging the composite
below the ablation's opening despite the far better structure.
It recovered fully: −0.926 → +0.926 by step 86. So the reinforcement stage
resolved it rather than being sunk by it. But it was not predicted, and the
cause is still not established. The plausible candidates — that MINIMAL
thinking yields terser work the rubric marks down, or that the rubric reacts to
something specific in the supervised register — have not been distinguished. It
is recorded as an open question, not explained.
Fixed before any result was known, so the analysis cannot be shaped by the
outcome. Run by scripts/evaluate.py once stage 2 finishes.
Held-out prompts. The 174 RL validation prompts, from source poems disjoint from training.
Comparison. Base gemini-3.5-flash against the final pipeline endpoint,
same prompts, same decoding, each at the thinkingLevel its config specifies.
Primary metric. Mean structure reward on held-out prompts, scored by the
same score_response the Cloud Run service uses in training.
Secondary, per form. form and each form's defining constraint in isolation
(ruju' for malzūma, closing_rhyme for mawqif, sadr_rhyme/ajuz_rhyme for
qasim); tunisianity; novelty; poetic_quality.
Failure conditions, declared in advance. The pipeline fails if any of:
- held-out
structuredoes not improve over base; noveltydegrades materially — the policy is reciting the corpus. This is the risk the pipeline is most exposed to, because stage 1 trains on full poems three times over and nothing in a supervised objective penalises reproducing one verbatim;structureimproves whilepoetic_qualitydeclines (structural gaming);- mean generation length collapses toward
min_lines(length gaming).
scripts/evaluate.py emits a line for every condition, so none can be
silently absent. Condition 3 needs the judge, which that script does not call;
it is reported there as not evaluated and checked instead against the job's
own structure/eval_mean_reward vs poetic_quality/eval_mean_reward.
Reported regardless of outcome. Per-form results including musaddas, whose
n of 2 validation prompts cannot support a claim either way and which is
reported as inconclusive rather than folded into an average.
Kept because each cost real time and none is in the documentation.
The step-count formula. Total RLFT steps is
(examples × epochs) // batchSize, not (examples // batchSize) × epochs. The
ablation finished on 158; the per-epoch form predicts 156, so progress
appeared to pass 100% as it finished. TuningClient.total_steps uses the
corrected form.
Generation-length extrapolation. At step 40 the ablation's output was growing +205 tokens/step, and a linear fit put it at the 32,768 cap around step 126–147 — flagged at the time as the run's main risk. It was not linear: growth flattened, peaked at 15,913 (49% of the cap) at step 104, and finished at 12,782. No truncation occurred. A linear extrapolation from a third of a run was the wrong instrument.
Judge saturation in the ablation. poetic_quality reached ~0.99 by step 20
and stayed there, so five-sixths of that run optimised structure alone at
weight 0.7. The pipeline does not show this — its judge score climbs from −0.93
and is still climbing at 0.926 — which is a further, unplanned argument for the
two-stage order: the judge remains informative for far longer.