ML Best Practices
Last updated on 2026-08-24 | Edit this page
Estimated time: 40 minutes
Overview
Questions
- How do you ensure reproducibility of ML experiments?
- What are common pitfalls in HPC ML workflows?
- How do you track experiments and deploy models?
- What habits separate research-ready ML from one-off scripts?
Objectives
- Follow MLOps best practices for reproducibility
- Monitor and log experiments systematically
- Avoid common HPC ML pitfalls
- Export trained models for deployment and sharing
Why Best Practices Matter More on HPC
A research project that produces a single trained model is a one-off script. A project that produces a dozen variants over six months, with multiple co-authors and a paper deadline, is a system. The difference between the two is whether you can answer questions like “which version of the code produced figure 3?” and “what hyperparameters did the model in checkpoint 47 use?”.
On HPC the stakes are higher than on a laptop because you spend real GPU-hours per experiment. Losing track of which run produced a result means redoing the run, which is sometimes a day of A100 time. The habits below are the ones that pay back many times over their setup cost.
Reproducibility and MLOps
1. Version Control Everything
This is the absolute minimum. Beyond it, commit at every meaningful
experiment boundary, tag the commit that produced each significant
result, and never store data files in git (use .gitignore
for /data and /results). Six months later, the
only reliable way to know exactly what code produced a specific result
is to read its git hash off the saved checkpoint.
2. Configuration Management
YAML
# config.yaml - Never hardcode hyperparameters
model:
architecture: "resnet50"
pretrained: true
training:
learning_rate: 0.001
batch_size: 32
epochs: 50
data:
path: "/bigdata/lab/<labname>/datasets/cifar10"
train_split: 0.8
PYTHON
import yaml
with open('config.yaml', 'r') as f:
config = yaml.safe_load(f)
lr = config['training']['learning_rate']
Hardcoded hyperparameters scattered through your code mean tweaking one value can require touching ten files. Externalizing them to a config file means experiments differ from each other only in the YAML, which is easy to diff, easy to share, and easy to commit.
For sweeps, generate one config file per trial and save it alongside the result. Future-you will thank present-you when figuring out what produced run 47.
3. Experiment Tracking with TensorBoard
PYTHON
from torch.utils.tensorboard import SummaryWriter
writer = SummaryWriter(log_dir='/scratch/$USER/runs/exp_lr0.001_bs32')
for epoch in range(epochs):
train_loss = train_one_epoch(...)
val_loss, val_acc = validate(...)
writer.add_scalar('Loss/train', train_loss, epoch)
writer.add_scalar('Loss/val', val_loss, epoch)
writer.add_scalar('Accuracy/val', val_acc, epoch)
writer.close()
TensorBoard is the simplest experiment tracker for PyTorch and
TensorFlow workflows. Run it locally or via OnDemand to compare 30 sweep
runs in one chart and immediately spot the trial that converged twice as
fast as the others. For more sophisticated tracking, Weights and Biases
(wandb) and MLflow are popular choices that add
cloud-hosted dashboards and richer artifact tracking.
4. Complete Checkpoints
PYTHON
import subprocess
from datetime import datetime
checkpoint = {
'epoch': epoch,
'model_state': model.state_dict(),
'optimizer_state': optimizer.state_dict(),
'hyperparameters': config,
'timestamp': datetime.now().isoformat(),
'git_hash': subprocess.check_output(['git', 'rev-parse', 'HEAD']).decode().strip()
}
torch.save(checkpoint, f'/scratch/$USER/checkpoints/model_epoch_{epoch}.pt')
Saving the git hash inside the checkpoint is the single most useful
reproducibility habit. Six months later, when reviewers ask which
version of the code trained your headline model, you can
git checkout HASH and see exactly that state.
Common HPC ML Pitfalls
Pitfall 1: Forgetting to Scale Learning Rate
When you increase batch size, adjust learning rate:
Larger batches give more accurate gradients, which permits larger steps. Square-root scaling is a reasonable rule for most cases; linear scaling (lr ~ batch_size) is more aggressive and works for some setups.
Pitfall 2: Tuning on Test Set
PYTHON
# WRONG: Using test set for hyperparameter selection
# CORRECT: Use three separate sets
train, val, test = split_data(dataset, [0.6, 0.2, 0.2])
# Tune on val_data, report final metrics on test_data once
This is research integrity 101 but still happens regularly under deadline pressure. Touch the test set exactly once, at the end, when reporting the final number. Tuning on the test set inflates apparent performance and the gap will not survive deployment.
Pitfall 3: GPU Memory Leaks During Sweeps
PYTHON
import gc
for lr in learning_rates:
model = MyModel()
train(model, lr)
del model
gc.collect()
torch.cuda.empty_cache()
Without explicit cleanup, the GPU accumulates state from previous iterations and eventually OOMs. The pattern above is the safe boilerplate.
Pitfall 4: Not Monitoring Training
BASH
watch -n 1 nvidia-smi # GPU usage
squeue -u $USER -l # Job status
tail -f training.log # Log output
If your loss has been NaN for the last 20 epochs and you do not notice until the job finishes, you have wasted GPU-hours. Set up monitoring early and check it at least once per training run.
Model Deployment
Export for Production
PYTHON
# PyTorch: TorchScript for production
scripted_model = torch.jit.script(model)
scripted_model.save('model.pt')
# TensorFlow: SavedModel format
model.save('model_saved_model/')
TorchScript and SavedModel are the framework-native serialization formats. They preserve the model architecture and weights together, so the consumer does not need access to your Python code to run inference. Use them whenever you want to share a model with someone outside your immediate codebase.
Prediction Pipeline
PYTHON
def predict(image_path, model_path):
model = MyModel()
model.load_state_dict(torch.load(model_path))
model.eval()
image = transform(Image.open(image_path)).unsqueeze(0).cuda()
with torch.no_grad():
probs = torch.softmax(model(image), dim=1)
predicted_class = torch.argmax(probs, dim=1)
return predicted_class.item(), probs[0].cpu().numpy()
Two details matter for any inference pipeline:
model.eval() switches off dropout and uses running
statistics for batch norm, and torch.no_grad() disables
gradient tracking which both speeds up inference and roughly halves
memory use.
A research-grade run log
For each significant experiment, record:
- Date and SLURM job ID
- Git hash of the code
- Path to the config.yaml used
- Path to the dataset (with version or checksum)
- Wall clock time and final epoch reached
- Best metric on validation
- Headline observation in one sentence
Keep this in a single Markdown file (or a spreadsheet) per project. Three months later, when a co-author asks “what was the best run we did with the new architecture?”, you have an answer in 30 seconds instead of 30 minutes of grep through SLURM logs.
Challenge
Challenge: Create a Reproducible Experiment
Set up a complete experiment workflow: 1. Create a config.yaml with model hyperparameters 2. Add TensorBoard logging to your training loop 3. Save checkpoints that include the git hash 4. Export the final model for inference
Challenge
Challenge: Audit a Project for Reproducibility Gaps
You inherit a colleague’s ML project. The repo has
train.py (uses hardcoded hyperparameters),
model.pt (no metadata), and a folder of result CSVs. List
four things you would do, in order, to make this reproducible.
Initialize git in the project and commit the current state. At minimum you can now refer to “the version that produced these results” later.
Externalize hyperparameters into a
config.yamland refactortrain.pyto read it. This makes future experiments diff-able.Add an experiment-log Markdown file documenting what each existing CSV came from (best effort) and start logging new experiments going forward.
Re-train the model with the new pipeline so the resulting checkpoint includes git hash, hyperparameters, and date. The old
model.ptis essentially unreproducible; it stays for reference but is not authoritative.
- Track experiments with TensorBoard, config files, and version control
- Always use separate train/val/test sets; touch test exactly once at the end
- Scale learning rate with batch size (sqrt or linear)
- Monitor GPU memory to avoid leaks during sweeps
- Save complete checkpoints including hyperparameters, git hash, and date
- Export models with TorchScript or SavedModel for deployment
- Keep a per-project run log so you can answer “which run produced figure 3?”