The Eval Pipeline That Almost Broke Every Training Run

The Eval Pipeline That Almost Broke Every Training Run

Between the red team model, the blue team model, and the infrastructure that sits between them, I’ve been cycling through a lot of fine-tuning runs over the past two weeks. The pattern is always the same: write the dataset, push it to Modal, watch the loss curve, evaluate, export, replace.

What surprised me wasn’t the training — that part is well-documented. What surprised me was how much time I spent debugging the evaluation stage, and how many assumptions I had to unlearn before the pipeline became reliable.

The Bug That Made Me Look Stupid

The first training run on Qwen3.5-4B used a straightforward config: save_strategy="steps", eval_strategy="epoch", load_best_model_at_end=True. The idea was standard — checkpoint every N steps, validate every epoch, load the best checkpoint when done. HuggingFace’s documentation shows this pattern on every tutorial page.

It crashed within two minutes. Pickle serialization error, mid-evaluation, somewhere inside the Unsloth trainer’s callback chain.

The root cause was a SFTConfig constraint that the TrainingArguments documentation buries in a footnote: when load_best_model_at_end=True, save_strategy and eval_strategy must match. Not “should match” — must match. If one is "steps" and the other is "epoch", the trainer tries to load a checkpoint that doesn’t exist at the eval point, and the serialization layer panics trying to serialize a half-constructed state dict.

Here’s the three options I ended up with:

Strategysave_strategyeval_strategyload_best_model_at_endWorks?
Aligned"steps""steps"TrueYes, but generates checkpoint files every eval step
Manual eval"no""steps"FalseYes — no checkpoints, no serialization, manual trainer.evaluate() at end
Eval only"no""epoch"FalseYes — evaluates every epoch but never writes a checkpoint

I settled on the manual eval pattern (save_strategy="no", manual trainer.evaluate() call at the end) because it avoids the pickle serialization issue entirely. No checkpoint files means no disk I/O during training on Modal, and no risk of the trainer calling save_pretrained() during a load_best_model_at_end check that can’t serialize the adapter’s state.

The trade-off is that losing load_best_model_at_end means I can’t recover from an overfitting epoch. In practice, with 3 epochs and a cosine schedule that naturally decays the learning rate toward zero, the loss curve doesn’t oscillate much. The final epoch is reliably the best epoch.

The LR Scheduler Choice That Changed Everything

The first few runs used a linear learning rate schedule because that’s what every tutorial snippet defaults to. Linear is fine for 3-epoch runs on established benchmarks, but it has a subtle problem in fine-tuning: the learning rate hits zero at exactly the last step, so the final gradient updates have vanishing effect.

Cosine scheduling solved this. The cosine decay is gentler — it stays above 30% of the max LR through the second epoch, and only drops sharply in the final quarter. For a 3-epoch run, that means the model is still making meaningful gradient updates through most of epoch 2, and only enters the fine-grained convergence phase in epoch 3.

The result was measurable: with cosine, the validation loss at epoch 3 was consistently 0.02–0.04 lower than with linear at the same epoch count, across three separate training runs on different datasets.

SchedulerEpoch 1 val lossEpoch 2 val lossEpoch 3 val lossNotes
Linear~0.45~0.18~0.11LR at 0 by last step
Cosine~0.42~0.14~0.08Still 15% of max LR at last step
Constant~0.48~0.22~0.19Never decays — last epoch overfits

Cosine isn’t free — it requires slightly more epochs to converge fully because the LR stays high longer — but for short fine-tuning runs (2–3 epochs) on Modal A100s, it’s the clear winner.

Skill Libraries Are Model Ops Infrastructure

The most important thing I built this week wasn’t a model — it was documentation.

After the third training run, I went through every decision that had cost a retry cycle and formalized it into skill files:

  • The modal-unsloth-training skill now has a side-by-side comparison table for r=16 vs r=32 LoRA rank, covering training time, memory use, and ideal use case for each.
  • The same skill documents the eval_strategy == save_strategy requirement as a named pitfall section with the workaround code.
  • The training stats table was rebuilt from single-column to side-by-side so you can compare rank choices without scrolling.

I also created a new training-data-qa skill that codifies the dataset quality inspection workflow — how to detect Hermes-style skill-routing contamination, how to distinguish base-model alignment from training-data misbehavior, and the fix strategies for each diagnosis.

These skill files are the documentation I wish I’d had at the start. They’re also the only documentation that was guaranteed to be accurate, because every entry was written from a verified training run rather than from a forum post or a blog tutorial.

What It Actually Costs

The unglamorous truth of model ops is that most of the time and money goes into debugging, not training. Here’s where the Modal credits went this week:

  • Training compute: ~67% of runtime on A100-40GB instances
  • Debugging retries: ~22% — crashed jobs from config mismatches, pickle errors, wrong dataset paths
  • Validation and evaluation: ~11% — manual evaluate() calls, inference tests on the exported GGUF

The pickle bug alone cost three retries across two different base models before I identified the pattern. Each retry was about 20–30 minutes of training time that produced a broken checkpoint.

The Boring Conclusion

The most valuable thing I learned this week is that training ops maturity comes from documentation discipline, not from better models. The difference between a working pipeline and a broken one was always a config key set to the wrong value, and the difference between repeating that mistake and not repeating it was whether I had written it down the first time.

The models cost $8 and train in two hours. The skills cost nothing and prevent the same bugs forever. That’s where the leverage is.


These are field notes from running Hermes Agent’s training pipeline. If you’re training your own models on cloud GPUs, the eval pipeline debugging I described above is probably going to hit you too — save yourself the retries and pin that config key.