An SFT run shows a striking pattern: loss drops smoothly within each epoch, then jumps back UP at the moment a new epoch starts, producing a sharp saw tooth aligned exactly with epoch boundaries. Training is otherwise stable. Identify the most likely cause, why the jumps happen specifically at epoch boundaries, and what one line fix usually resolves it. Also name the secondary suspect to rule out.
The data loader repeats the same batch order each epoch, so loss restarts at the same hard-batch sequence. Fix: per-epoch shuffling with an epoch-dependent seed, plus sampler.set_epoch under DDP or FSDP.
Picture practising a piano programme of twenty pieces in the same fixed order every day. Within a day you warm up on the first piece, get smoother by the tenth, and play the last few beautifully. Tomorrow you start again on piece one, which is hard from cold, and your score for that piece looks worse than where you ended yesterday. The score chart looks like a saw, jumping up every morning. The fix is to shuffle the order each day so you do not always restart on the same cold piece. In a training run, the same trick is to reshuffle the data on every full pass so the first chunks are not always the same hard ones.
Concept explanation~2 min read
Everything you need to truly understand this topic: intuition, mechanics, step by step explanation, code, formulas, and worked example. Click to expand.
Concept explanation~2 min read
Everything you need to truly understand this topic: intuition, mechanics, step by step explanation, code, formulas, and worked example. Click to expand.
A saw-tooth loss curve aligned exactly with epoch boundaries is one of the most frequently misdiagnosed patterns in fine-tuning. It looks alarming on a training dashboard, the loss jumps up at every epoch start, looks like instability, and the natural first reaction is to lower the learning rate or check gradient clipping. Both responses slow training without fixing anything, because the cause is not in the optimiser at all. It is in the data loader.
The diagnostic is structural: the jumps align precisely with epoch boundaries, not with random steps, and they repeat with the same shape across epochs. That precision is the tell. Only one class of cause can produce a curve whose discontinuities align exactly with the iteration-counter resets that define epochs, and that class is data-order artifacts. The loader is presenting the same batches in the same order each epoch, and the loss reflects that repetition.
This deep dive walks the mechanism in detail, explains why per-epoch shuffling fixes it, covers the DistributedSampler gotcha that catches most multi-GPU runs, and lists the secondary suspects (curriculum samplers, cosine-restart schedules) whose visual signatures overlap. The audience is engineers debugging a real training run, not theorists, the goal is to make the correct diagnosis fast and apply the one-line fix without disturbing the rest of the recipe.
Why the same batch order produces the same loss curve
An SFT training run sees a sequence of batches and updates the model parameters after each one. Within an epoch the loss curve is a function of two things: the model's current parameters and the specific sequence of batches it sees. If you fix both inputs, the curve is deterministic up to numerical noise.
Now suppose the loader repeats the same batch order each epoch. At iteration zero of epoch one, the model sees batch A1 with its initial parameters and computes some loss L1A. By the end of epoch one, the parameters have moved and the final batch produces a much lower loss.
At iteration zero of epoch two, the loader hands back batch A1 again. The parameters have moved, so the loss on A1 is now lower than L1A, but it is higher than the loss on the final batch of epoch one was, because the model was specifically tuned on that final batch right before the boundary and is now back to batch A1. The jump from the last batch of epoch one to the first batch of epoch two is the saw-tooth.
The shape repeats across epochs because the batch sequence repeats. Each epoch's curve is roughly the same shape, shifted lower because the model has improved overall. The boundary between any two epochs produces the same kind of jump because it is always the transition from the last (well-fitted) batch back to the first (less-fitted) batch.
It is critical to recognise that the model IS learning during all of this. The within-epoch curve falls, epoch two reaches lower loss values than epoch one at comparable iteration positions, and the long-run trend is downward. The saw-tooth is purely a function of measuring loss on a non-stationary sample order.
Situations where this technique stops working.
2–4 min · Everything important, quickly.
Real products, models, and research that use this idea.
- PyTorch's DistributedSampler documentation explicitly warns that set_epoch must be called or shuffling will repeat, a warning many teams discover only after seeing this curve.
- Hugging Face Trainer handles set_epoch automatically since transformers 4.x, but custom training loops with raw PyTorch DDP often miss it.
What an interviewer would ask next. Try answering before peeking at the approach.
QHow would you distinguish the data-order saw-tooth from a cosine-restart artifact if both happen to align with epoch boundaries?
Look at the direction. Data-order produces jumps UP at the boundary, because the new epoch starts on the hardest batch of the cycle. Cosine restart produces drops or initial spikes because the learning rate jumps back to its high value, briefly destabilising loss before it settles. Also check the LR-versus-step plot, restarts are visible there.
Red flags & common mistakes
The phrases that signal junior thinking. Click to expand.
Red flags & common mistakes
The phrases that signal junior thinking. Click to expand.
Chasing this as a training-instability bug. It is a data-order artifact, the model is still learning, the loss curve is just being measured on a non-stationary sample order. Look at the loader, not the optimiser.
60 second bullets to scan on the way to the call.
What a saw-tooth pattern aligned with epoch boundaries indicates
Why the same batch order across epochs produces the same loss curve shape
Primary sources. Browse if you want the original framing.
Same topic, related formats. Practice these next.