Checkpointing
Checkpointing is a technique to trade compute for memory during training. Instead of storing all intermediate activations (outputs layers) for backprop, which consumes a lot of memory, checkpointing discards some and recomputes them during the backward pass. Thus, this saves memory at the expense of additional computation
1
2
3
4
5
6
7
8
9
class MyModel(nn.Module):
def __init__(self):
super(MyModel, self).__init__()
self.inc = nn.Conv2d(3, 16, kernel_size=3, padding=1) # Example layer
self.inc = checkpoint.checkpoint(self.inc) # Enable checkpointing
def forward(self, x):
x = self.inc(x) # Checkpointed layer
return x
checkpointing can be used on functions as well.
A full training checkpoint may also include weights, optimizer state, epoch, scheduler, etc. PyTorch recommends saving more than just the model state when resuming training.
Seeding
With seeding, we want to reproduce the same:
| What | Example | Controlled by |
|---|---|---|
| Initial weights | Random init of conv / linear layers | torch.manual_seed() before building the model |
| Random operations in data loading | Selecting random seabed crops; subsampling points when a cloud exceeds 1,024 | random / np.random / torch seeds inside each DataLoader worker (worker_init_fn) |
| Order of training samples | Which samples land in each batch with shuffle=True |
The DataLoader’s generator |
The data-loading row is the easy one to miss: with num_workers > 0, each worker is a separate process, so seeding the main process alone does not make NumPy or Python’s random reproducible inside the workers. The pattern from the PyTorch reproducibility notes:
1
2
3
4
5
6
7
8
9
10
11
def seed_worker(worker_id):
# torch already gives each worker a distinct seed derived from the base seed
worker_seed = torch.initial_seed() % 2**32
np.random.seed(worker_seed)
random.seed(worker_seed)
g = torch.Generator()
g.manual_seed(0)
loader = DataLoader(dataset, batch_size=32, shuffle=True, num_workers=4,
worker_init_fn=seed_worker, generator=g)
Seeding makes the random numbers repeat; making the GPU math itself repeat is the next section.
Op Determinisim
Here is a good reference on Op Determinism. Below is how this story goes
- Tensor operations are not necessarily deterministic:
tf.nn.softmax_cross_entropy_with_logits(From a quick search, it’s still not clear to me why this is non-deterministic. Mathematically, the quantity should be deterministic.)
- Op Determinisim will make sure you get the same output with the same code, same hardware. But it will disable asynchronicity, so it will slow down these operations
- Use the same software environment in every run (OS, checkpoints, version of CUDA and TensorFlow, environmental variables, etc). Note that determinism is not guaranteed across different versions of TensorFlow.
- How to enable Op Determinism?
- PyTorch
-
TensorFlow:
python tf.keras.utils.set_random_seed(1) tf.config.experimental.enable_op_determinism()- This effectively sets the pseudorandom number generators (PRNGs) in Python seed, the NumPy seed, and the TensorFlow seed.
- Without setting the seed,
tf.random.normalwould raiseRuntimeError, but Python and Numpy won’t
- Have more consistent training loss, one can do
1
2
3
4
5
6
7
8
# Setup random seed
torch.manual_seed(1)
np.random.seed(1)
random.seed(1)
# Optional to enforce sequential execution
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
Data Provenance
In ML, provenance means the history of where something came from and what happened to it. For data, provenance answers questions like:
| Question | Example record |
|---|---|
| Where was this sample collected? | Site, run name, GPS / map region |
| Who or what labeled it? | Annotator ID, or the auto-labeling model and its version |
| What sensor produced it? | Sensor model, serial number, calibration file |
| When was it recorded? | Timestamp of the source bag / log |
| What preprocessing was applied? | Crop, resample, normalization, and the code commit that ran it |
| Which dataset split did it enter? | train / val / test, and the split seed or list file |
| Which model version trained on it? | Checkpoint name or training run ID |
Seeding and op determinism make a training run repeatable; provenance is what lets you trace a bad prediction back to the exact samples, labels, and preprocessing behind it, and check that no test sample leaked into training.
HuggingFace Trainer
Hugging Face has a Trainer class that has distributed training by default, and mixed precision training in a Trainer class. It goes hand-in-hand with the TrainerArgument class
Boiler plate for image segmentation:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
def compute_metrics(pred):
preds = pred.predictions.argmax(-1)
labels = pred.label_ids
# Flatten the tensors
preds = preds.flatten()
labels = labels.flatten()
# Compute IoU
metric = load_metric("iou")
iou = metric.compute(predictions=preds, references=labels, average="macro")
return {"iou": iou}
training_args = TrainingArguments(
output_dir="./segmentation_results",
num_train_epochs=10,
per_device_train_batch_size=4,
per_device_eval_batch_size=4,
evaluation_strategy="epoch",
save_strategy="epoch",
logging_dir="./segmentation_logs",
logging_steps=10,
load_best_model_at_end=True,
metric_for_best_model="iou",
greater_is_better=True,
)
trainer = Trainer(
model=model,
args=training_args,
train_dataset=train_dataset,
eval_dataset=eval_dataset,
compute_metrics=compute_metrics,
)
eval_results = trainer.evaluate()
print(f"Evaluation results: {eval_results}")
trainer.save_model("./trained_segmentation_model")