Skip to content

Commit 5d76467

Browse files
committed
Add option to disable AMP during training/validation
- enabling AMP can decrease compression ratios during inference - all current models were trained using AMP. We need to be able to test training without.
1 parent 16c0226 commit 5d76467

4 files changed

Lines changed: 87 additions & 14 deletions

File tree

scripts/train_bm4dnet.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ def train(train_cache_dir, val_cache_dir):
8181
lr=lr,
8282
max_epochs=max_epochs,
8383
model=model,
84+
use_amp=use_amp,
85+
use_amp_validation=use_amp_validation,
8486
fg_weight=fg_weight,
8587
checkpoint_weights=checkpoint_weights,
8688
num_workers=0,
@@ -97,6 +99,8 @@ def train(train_cache_dir, val_cache_dir):
9799
"resume_path": resume_path,
98100
"transform_cfg": transform.cfg,
99101
"preserve_foreground": preserve_foreground,
102+
"use_amp": use_amp,
103+
"use_amp_validation": use_amp_validation,
100104
}
101105
)
102106

@@ -131,7 +135,11 @@ def train(train_cache_dir, val_cache_dir):
131135
# Training parameters
132136
batch_size = 32
133137
lr = 1e-3
134-
max_epochs = 30
138+
max_epochs = 50
139+
# Use AMP for optimization, but keep validation/checkpoint selection in
140+
# FP32 so its cratio matches production FP32 inference.
141+
use_amp = False
142+
use_amp_validation = False
135143
# Validate (and consider a checkpoint) after every full-cache epoch.
136144
val_every = 1
137145
seed = 42

src/aind_exaspim_image_compression/machine_learning/train.py

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
99
"""
1010

11-
from contextlib import nullcontext
1211
from datetime import datetime
1312
from numcodecs import blosc
1413
from torch.optim.lr_scheduler import CosineAnnealingLR
@@ -71,6 +70,7 @@ def __init__(
7170
max_epochs=400,
7271
model=None,
7372
use_amp=True,
73+
use_amp_validation=False,
7474
checkpoint_weights=None,
7575
fg_weight=20.0,
7676
num_workers=None,
@@ -96,7 +96,12 @@ def __init__(
9696
model : None or nn.Module, optional
9797
Model to be trained on the given datasets. Default is None.
9898
use_amp : bool, optional
99-
Indication of whether to use mixed precision. Default is True.
99+
Whether to use CUDA float16 autocast and gradient scaling during
100+
training. Default is True.
101+
use_amp_validation : bool, optional
102+
Whether to use CUDA float16 autocast during validation and
103+
checkpoint selection. Default is False so validation matches FP32
104+
production inference.
100105
val_every : int, optional
101106
Run validation (and checkpoint selection) every this many epochs;
102107
the final epoch is always validated. The count-space metrics are
@@ -120,6 +125,8 @@ def __init__(
120125
self.prefetch = prefetch
121126
self.val_every = max(1, int(val_every))
122127
self.seed = seed
128+
self.use_amp = bool(use_amp)
129+
self.use_amp_validation = bool(use_amp_validation)
123130

124131
self.codec = blosc.Blosc(cname="zstd", clevel=5, shuffle=blosc.SHUFFLE)
125132
self.criterion = SignalPreservingLoss(fg_weight=fg_weight)
@@ -134,15 +141,10 @@ def __init__(
134141
self.scheduler = CosineAnnealingLR(self.optimizer, T_max=max_epochs)
135142
self.writer = SummaryWriter(log_dir=log_dir)
136143

137-
if use_amp:
138-
self.autocast = torch.autocast(device_type="cuda", dtype=torch.float16)
139-
else:
140-
self.autocast = nullcontext()
141-
142144
# Scale the loss before backward so small float16 gradients do not
143145
# underflow (and are unscaled before the step). Disabled => no-op, so
144146
# the same code path is correct with and without AMP.
145-
self.scaler = torch.amp.GradScaler("cuda", enabled=use_amp)
147+
self.scaler = torch.amp.GradScaler("cuda", enabled=self.use_amp)
146148

147149
# --- Core Routines ---
148150
def run(self, train_dataset, val_dataset):
@@ -232,7 +234,9 @@ def train_step(self, train_dataloader, epoch):
232234
self.model.train()
233235
for x, y, fg_mask in train_dataloader:
234236
# Forward pass
235-
hat_y, loss = self.forward_pass(x, y, fg_mask)
237+
hat_y, loss = self.forward_pass(
238+
x, y, fg_mask, use_amp=self.use_amp
239+
)
236240

237241
# Backward pass (loss-scaled for AMP stability)
238242
self.optimizer.zero_grad()
@@ -276,7 +280,9 @@ def validate_step(self, val_dataloader, epoch):
276280
self.model.eval()
277281
for x, y, raw, fg_mask in val_dataloader:
278282
# Run model
279-
hat_y, loss = self.forward_pass(x, y, fg_mask)
283+
hat_y, loss = self.forward_pass(
284+
x, y, fg_mask, use_amp=self.use_amp_validation
285+
)
280286

281287
# Evaluate result
282288
losses.append(loss.detach().cpu())
@@ -313,7 +319,7 @@ def validate_step(self, val_dataloader, epoch):
313319
self.save_model(epoch, score)
314320
return loss, cratio, is_best
315321

316-
def forward_pass(self, x, y, fg_mask):
322+
def forward_pass(self, x, y, fg_mask, use_amp=None):
317323
"""
318324
Performs a forward pass through the model and computes loss.
319325
@@ -325,6 +331,9 @@ def forward_pass(self, x, y, fg_mask):
325331
Target tensor with shape (B, C, D, H, W).
326332
fg_mask : torch.Tensor
327333
Foreground mask (0/1) with shape (B, C, D, H, W).
334+
use_amp : bool, optional
335+
Whether to autocast this forward pass to float16. Defaults to the
336+
training AMP setting.
328337
329338
Returns
330339
-------
@@ -333,7 +342,13 @@ def forward_pass(self, x, y, fg_mask):
333342
loss : torch.Tensor
334343
Computed loss value.
335344
"""
336-
with self.autocast:
345+
if use_amp is None:
346+
use_amp = self.use_amp
347+
with torch.autocast(
348+
device_type=torch.device(self.device).type,
349+
dtype=torch.float16,
350+
enabled=bool(use_amp),
351+
):
337352
x = x.to(self.device)
338353
y = y.to(self.device)
339354
fg_mask = fg_mask.to(self.device)
@@ -437,6 +452,8 @@ def save_config(self, config):
437452
"prefetch": self.prefetch,
438453
"val_every": self.val_every,
439454
"seed": self.seed,
455+
"use_amp": self.use_amp,
456+
"use_amp_validation": self.use_amp_validation,
440457
"fg_weight": getattr(self.criterion, "fg_weight", None),
441458
"checkpoint_weights": self.checkpoint_weights,
442459
"lr": self.optimizer.param_groups[0]["lr"],

tests/test_full_cache_training.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import os
55
import tempfile
66
import unittest
7+
from contextlib import nullcontext
78
from types import SimpleNamespace
89
from unittest.mock import MagicMock, call, patch
910

@@ -118,6 +119,42 @@ def test_validation_is_ordered_and_final_partial_batch_is_retained(self):
118119
class TrainerShuffleTest(unittest.TestCase):
119120
"""Tests Trainer wiring for shuffled training and ordered validation."""
120121

122+
def test_training_and_validation_amp_are_configured_separately(self):
123+
"""Training can use AMP while validation remains full precision."""
124+
model = torch.nn.Linear(1, 1)
125+
values = torch.zeros((1, 1, 1, 1, 1))
126+
with tempfile.TemporaryDirectory() as directory:
127+
trainer = Trainer(
128+
directory,
129+
device="cpu",
130+
model=model,
131+
max_epochs=1,
132+
use_amp=True,
133+
use_amp_validation=False,
134+
)
135+
try:
136+
with patch(
137+
"aind_exaspim_image_compression.machine_learning."
138+
"train.torch.autocast",
139+
side_effect=[nullcontext(), nullcontext()],
140+
) as autocast:
141+
trainer.forward_pass(
142+
values, values, values, use_amp=trainer.use_amp
143+
)
144+
trainer.forward_pass(
145+
values,
146+
values,
147+
values,
148+
use_amp=trainer.use_amp_validation,
149+
)
150+
151+
self.assertTrue(autocast.call_args_list[0].kwargs["enabled"])
152+
self.assertFalse(
153+
autocast.call_args_list[1].kwargs["enabled"]
154+
)
155+
finally:
156+
trainer.writer.close()
157+
121158
def test_trainer_sets_epoch_and_persists_seed(self):
122159
"""Trainer passes its seed, sets every epoch, and saves the seed."""
123160
model = torch.nn.Linear(1, 1)
@@ -177,7 +214,10 @@ def test_trainer_sets_epoch_and_persists_seed(self):
177214
os.path.join(trainer.log_dir, "config.json"),
178215
encoding="utf-8",
179216
) as file:
180-
self.assertEqual(json.load(file)["seed"], 42)
217+
config = json.load(file)
218+
self.assertEqual(config["seed"], 42)
219+
self.assertFalse(config["use_amp"])
220+
self.assertFalse(config["use_amp_validation"])
181221
finally:
182222
trainer.writer.close()
183223

tests/test_train_bm4dnet.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ def test_training_uses_only_cached_datasets(self):
101101
"lr": 1e-3,
102102
"max_epochs": 3,
103103
"model": object(),
104+
"use_amp": True,
105+
"use_amp_validation": False,
104106
"fg_weight": 0,
105107
"checkpoint_weights": {"fg_mae": 1.0},
106108
"val_every": 2,
@@ -159,11 +161,17 @@ def test_training_uses_only_cached_datasets(self):
159161
self.assertEqual(config["train_cache_dir"], str(train_cache))
160162
self.assertEqual(config["val_cache_dir"], str(val_cache))
161163
self.assertEqual(config["transform_cfg"], transform.cfg)
164+
self.assertTrue(config["use_amp"])
165+
self.assertFalse(config["use_amp_validation"])
162166
self.assertNotIn("n_train_examples_per_epoch", config)
163167
self.assertNotIn("brain_ids_path", config)
164168
self.assertNotIn("sigma_bm4d", config)
165169
trainer_call = trainer_factory.call_args
166170
self.assertEqual(trainer_call.kwargs["seed"], 42)
171+
self.assertTrue(trainer_call.kwargs["use_amp"])
172+
self.assertFalse(
173+
trainer_call.kwargs["use_amp_validation"]
174+
)
167175
finally:
168176
for key, value in previous.items():
169177
if value is None:

0 commit comments

Comments
 (0)