ml-training-pipeline
Debug ML training pipelines that stop early, OOM on eval, or produce wrong epoch counts.
SKILL.mdskills/ml-training-pipeline/SKILL.md
GitHubML Training Pipeline Skill
Problem it solves
ML training frameworks have silent failure modes that look like model bugs but are actually pipeline infrastructure issues. TensorFlow's keras.utils.Sequence adapter may produce too few batches per epoch when __len__ is small, epoch-end evaluation spikes memory and silently kills the process on small GPUs, and virtual environment Python executables are redirector stubs that report wrong PIDs. These issues waste hours because the error messages point at the model, not the pipeline.
Detection triggers
Activate when:
- Training stops after exactly 1 epoch with no error
- Process silently disappears during epoch-end evaluation
StopIterationor "input ran out of data" during training- GPU memory usage spikes at epoch boundary then process dies
Start-Processreturns a PID with 0 CPU and tiny memory (venv stub)- Training loss only shows 1 epoch in history despite running for hours
Protocol
1. Check if the data adapter loops
TensorFlow's model.fit iterates the adapter once per epoch. If __len__ returns a small number, you get few batches per epoch and the training appears to stall. If __len__ is sys.maxsize and shuffle=True, the adapter does list(range(len)) and OOMs.
Verify:
import tensorflow as tf
# Check your Sequence's __len__ — is it reasonable?
seq = YourSequence()
print(f"__len__ = {len(seq)}") # Should be ~samples/batch_size
Fix — make __len__ return a moderate multiple and wrap __getitem__:
class LoopingSequence(tf.keras.utils.Sequence):
def __init__(self, original):
self.original = original
def __len__(self):
# Moderate multiple — NOT sys.maxsize (causes OOM with shuffle=True)
return len(self.original) * 1000
def __getitem__(self, idx):
real_idx = idx % len(self.original)
item = self.original[real_idx]
if real_idx == 0:
self.original.on_epoch_end()
return item
2. Disable epoch-end evaluation on small GPUs
Per-epoch mAP/accuracy evaluation builds a second model and can silently OOM-kill the process on 6GB GPUs.
# BAD — evaluation spikes memory at every epoch boundary
python train.py --epochs 12
# GOOD — skip per-epoch eval, run once at the end
python train.py --epochs 12 --no-evaluation --compute-val-loss
# After training completes:
python evaluate.py --model best_model.h5 --val-data ...
3. Track the real process PID
Virtual environment python.exe on Windows is a redirector stub. Start-Process returns the stub PID (0 CPU, ~7MB RAM). The real training process is its child.
# BAD — this kills the stub, not the trainer
Stop-Process -Id $stubPid
# GOOD — find the actual trainer by its child relationship
$stub = Get-Process -Id $stubPid -ErrorAction SilentlyContinue
$trainer = Get-CimInstance Win32_Process -Filter "ParentProcessId=$($stub.Id)" | Where-Object { $_.Name -eq "python.exe" }
if ($trainer) { Stop-Process -Id $trainer.ProcessId }
4. Verify multi-epoch completion
Check the training history keys:
history = model.fit(...)
print(f"Epochs completed: {len(history.history['loss'])}")
# Should match --epochs, not 1
If len(history.history['loss']) == 1, the adapter isn't looping.
5. Match training and inference dimensions
Training image size must match inference:
# Training
python train.py --image-min-side 400 --image-max-side 400
# Inference in app
resize_image(image, min_side=400) # MUST match
Mismatched dimensions cause silent accuracy degradation, not crashes.
When NOT to use
- PyTorch DataLoader (handles looping natively)
- Pure inference code (no training loop)
- Non-GPU training (no memory spike issues)
Cross-references
- debugging-and-error-recovery — Apply structured debugging when training silently fails.
- anti-phantom-symbols — Verify TF/Keras API calls exist in the installed version.
Sequence,on_epoch_end, and adapter internals change across versions.
Lessons learned
Real bugs caught by this skill:
- TF
KerasSequenceAdapterproduces few batches when__len__is small — make__len__returnlen(seq)*1000and wrap__getitem__with modulo +on_epoch_end sys.maxsizeas__len__causes OOM — the adapter doeslist(range(len))whenshuffle=True- Per-epoch evaluation silently kills the process on 6GB GPUs — use
--no-evaluationand evaluate once after training - venv
python.exeon Windows is a redirector stub — the real PID is the child process