A training run OOMs at step 8,000 with no code change — leak or peak, and how do you tell?
answer
- growing line versus flat line
- what does the loop still reference
- the failing step number is not reproducible
- check where in the epoch it died
- a metric still attached to its graph
basics
~20 sLog memory per step. Steady growth with a constant increment means retention, usually a logged metric still attached to its graph. Flat memory with one tall spike means a peak: the longest sample, or the evaluation pass.
solid answer
~50 sTwo failure shapes, and one plot separates them. Log allocated memory and peak-allocated per step. If the line climbs monotonically by a roughly constant amount, something is being retained: the classic culprit is a running loss or metric accumulated as a live tensor that still carries its autograd graph, so every step's saved activations stay alive; the fix is to accumulate the detached scalar value instead. If the line is flat and the failing step is an outlier, the peak was always this high and you were simply lucky — a single very long sample sets the true peak, and length-sorted batching can hide it until a reshuffle puts it somewhere else. The third suspect is the evaluation pass: gradients still tracked during eval, or a larger eval batch, so training survives every step and epoch-end does not.
go deeper
Remember the first move: log memory every step and look at the shape of the line. Growing means something is being kept; flat with a spike means one batch is unusually large.
Explain why holding a loss tensor for logging keeps a whole computation graph alive, and why activation memory tracks the longest sample in a batch. These two mechanisms cover most of what you will meet.
Demonstrate the triage under pressure: instrument first, classify the failure shape, then fix the matching cause — and say how you would bound the worst-case batch so the failure cannot come back under a new seed.
Own the guardrail. Decide that jobs are sized against a measured peak step, that the evaluation path is budgeted like the training path, and that per-step memory is a logged metric on every run rather than something someone reconstructs after a failure.
## Two failure shapes An out-of-memory error thousands of steps into an unchanged run is one of two things, and they need opposite fixes. 1. **A leak.** Something is retained across iterations, so the live set grows without bound. It was always going to fail; step 8,000 is just where the curve crossed the ceiling. 2. **A peak.** Memory is flat in aggregate, but one particular step demands far more than the typical step. The run was never comfortably inside the budget — you had not yet met the batch that proves it. ## The instrument that separates them Log, per step: allocated memory at the end of the step, the step's **peak** allocated, and the shape of the batch (largest sequence length, or resolution and batch size). Two or three hundred steps of this is enough. - **Straight climbing line, constant increment** → retention. The increment is informative: if it is close to one step's activation footprint, an entire computation graph is being kept per step. - **Flat line with occasional tall spikes** → peak. Correlate the spike heights with the logged batch shapes; they will track the largest sample in the batch. - **Flat line that fails exactly at an epoch boundary** → the evaluation pass, not the training loop. ## The classic retention bug The most common cause has nothing to do with the model. A training loop keeps a running loss or accuracy for logging, and accumulates the loss *tensor* rather than its numeric value. That tensor is still attached to the graph that produced it, and the graph holds references to every intermediate activation saved for the backward pass. Add one per step and after ten thousand steps you are holding ten thousand computation graphs. The signature is unmistakable in the plot: perfectly linear growth, identical increment, and no code change to blame because the bug was there from the first commit and only recently crossed the ceiling as batch size or sequence length crept up. The fix is to accumulate the detached scalar. Related variants: appending per-step predictions or logits to a list "for later analysis"; a cache keyed by batch that is never evicted; a history buffer of model snapshots. All produce the same straight line, and all are found by asking *what does this loop still reference at the end of a step?* ## The peak that hides Activation memory scales with how much data flows through the network, so on variable-length data the **longest sample in a batch** sets the step's peak. Consider a corpus of thirty-minute call transcriptions: almost every utterance is short, and one is enormous. Whichever batch contains it will demand several times the typical step's memory. If the pipeline groups similar lengths together, that one giant sample lands in a batch of its own kind and the peak is even sharper. Worse, that grouping can *hide* the problem: for several epochs the ordering happens to place the outlier where enough memory is free, then a reshuffle — or a different seed, or a resumed run — moves it and the job dies at a step number that means nothing. The tell is that the failing step number is not reproducible across runs, while the failing *batch content* is. Log the batch's maximum length at failure; if it is the corpus maximum, you have your answer. The fix is a budget rather than a count: cap the tokens or pixels per batch, or cap the length of an individual sample, so the worst-case step is bounded by construction instead of by luck. ## The evaluation pass The third suspect explains the most confusing variant: every training step succeeds, and the run dies at the end of the epoch. Two causes dominate. First, gradients are still being tracked during evaluation, so the forward pass saves activations it will never use — evaluation then costs *more* per sample than training did, on top of whatever training state is still resident. Second, evaluation is run at a larger batch because "there is no backward pass", which is true right up until the first cause is also present. Both are one-line fixes, and both are invisible until you look at where in the epoch the failure lands. ## The triage order Ask in this sequence: does memory grow monotonically? If yes, hunt retention — it is the only failure that gets worse with time. If no, does the failing step carry an outlier batch? If yes, bound the batch by a budget. If no, does the failure land at an epoch boundary? If yes, inspect the evaluation path. Only when all three are excluded is this a genuine capacity problem, and only then do you spend the day on the memory levers. One discipline underneath all of it: size the job against the **peak** step, never the average one. A run that survives its typical batch and has never met its largest is not a run that fits.
- Memory rises by exactly the same amount every step. What does the size of that increment tell you?It identifies what is being retained. An increment close to one step's activation footprint means a whole computation graph is being held per iteration — the running-metric-as-live-tensor bug. A much smaller, constant increment points at something bounded per step, such as appended predictions or a growing log structure, and a jagged increment points at fragmentation rather than retention.
- The job survives every training step but dies on the first validation pass. What do you check first?Whether gradients are still being tracked during evaluation. If they are, the forward pass saves activations nobody will use, so evaluation costs more per sample than training. Check second whether the evaluation batch is larger than the training batch, which is a reasonable choice only once the first problem is gone.
- Why can changing only the shuffle seed turn a stable run into one that fails?Because the peak is set by content, not by step count. A different order changes which samples share a batch, so the longest sample can land beside other long ones or at a point where more memory is already in use. The run was always sitting inside its margin by luck; the seed only changed the day the luck ran out.
- How do you make the worst-case step bounded rather than hoping the outlier is rare?Budget the batch instead of counting it: cap total tokens or pixels per step so a long sample simply means fewer samples share the batch, and cap the length of any individual example. Then the peak step is a property of the configuration, and it can be measured once at start-up rather than discovered mid-epoch.
saying these in an interview costs you the question
- Reduces batch size before knowing whether memory is growing
- Assumes an unchanged codebase means the bug is new
- Ignores the evaluation pass because it has no backward pass
- Sizes the job against the average batch, not the largest
- Treats a moving failure step as randomness rather than ordering