Skip to content

Commit e700a64

Browse files
committed
fix(losses): avoid half precision mutual information overflow
Signed-off-by: kyinhub <kevinpyin@gmail.com>
1 parent 3ee058b commit e700a64

2 files changed

Lines changed: 76 additions & 15 deletions

File tree

monai/losses/image_dissimilarity.py

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -317,15 +317,22 @@ def parzen_windowing_gaussian(self, img: torch.Tensor) -> tuple[torch.Tensor, to
317317
Args:
318318
img: the shape should be B[NDHW].
319319
"""
320-
img = torch.clamp(img, 0, 1)
320+
output_dtype = img.dtype
321+
if output_dtype == torch.float16:
322+
img = img.float()
323+
compute_dtype = torch.float32 if img.dtype in (torch.float16, torch.bfloat16) else img.dtype
324+
img = torch.clamp(img, 0, 1).to(dtype=compute_dtype)
321325
img = img.reshape(img.shape[0], -1, 1) # (batch, num_sample, 1)
322326
if self.bin_centers is None or self.preterm is None:
323327
raise ValueError("bin_centers and preterm must be defined for gaussian parzen windowing.")
324-
weight = torch.exp(
325-
-self.preterm.to(img) * (img - self.bin_centers.to(img)) ** 2
326-
) # (batch, num_sample, num_bin)
328+
preterm = self.preterm.to(device=img.device, dtype=compute_dtype)
329+
bin_centers = self.bin_centers.to(device=img.device, dtype=compute_dtype)
330+
weight = torch.exp(-preterm * (img - bin_centers) ** 2) # (batch, num_sample, num_bin)
327331
weight = weight / torch.sum(weight, dim=-1, keepdim=True) # (batch, num_sample, num_bin)
328332
probability = torch.mean(weight, dim=-2, keepdim=True) # (batch, 1, num_bin)
333+
if output_dtype in (torch.float16, torch.bfloat16):
334+
weight = weight.to(dtype=output_dtype)
335+
probability = probability.to(dtype=output_dtype)
329336
return weight, probability
330337

331338
def forward(self, pred: torch.Tensor, target: torch.Tensor) -> torch.Tensor:
@@ -340,16 +347,28 @@ def forward(self, pred: torch.Tensor, target: torch.Tensor) -> torch.Tensor:
340347
raise ValueError(f"ground truth has differing shape ({target.shape}) from pred ({pred.shape})")
341348
wa, pa, wb, pb = self.parzen_windowing(pred, target) # (batch, num_sample, num_bin), (batch, 1, num_bin)
342349

343-
pab = torch.bmm(wa.permute(0, 2, 1), wb.to(wa)).div(wa.shape[1]) # (batch, num_bins, num_bins)
344-
papb = torch.bmm(pa.permute(0, 2, 1), pb.to(pa)) # (batch, num_bins, num_bins)
345-
mi = torch.sum(
346-
pab * torch.log((pab + self.smooth_nr) / (papb + self.smooth_dr) + self.smooth_dr), dim=(1, 2)
347-
) # (batch)
350+
# Half-precision matrix multiplication can overflow before the joint
351+
# histogram is divided by the number of samples. Accumulate histogram
352+
# products in float32 while preserving float64 inputs.
353+
output_dtype = wa.dtype
354+
compute_dtype = torch.float32 if wa.dtype in (torch.float16, torch.bfloat16) else wa.dtype
355+
wa = wa.to(dtype=compute_dtype)
356+
wb = wb.to(wa)
357+
pa = pa.to(dtype=compute_dtype)
358+
pb = pb.to(pa)
359+
with torch.autocast(device_type=wa.device.type, enabled=False):
360+
pab = torch.bmm(wa.permute(0, 2, 1), wb).div(wa.shape[1]) # (batch, num_bins, num_bins)
361+
papb = torch.bmm(pa.permute(0, 2, 1), pb) # (batch, num_bins, num_bins)
362+
mi = torch.sum(
363+
pab * torch.log((pab + self.smooth_nr) / (papb + self.smooth_dr) + self.smooth_dr), dim=(1, 2)
364+
) # (batch)
348365

349366
if self.reduction == LossReduction.SUM.value:
350-
return torch.sum(mi).neg() # sum over the batch and channel ndims
351-
if self.reduction == LossReduction.NONE.value:
352-
return mi.neg()
353-
if self.reduction == LossReduction.MEAN.value:
354-
return torch.mean(mi).neg() # average over the batch and channel ndims
355-
raise ValueError(f'Unsupported reduction: {self.reduction}, available options are ["mean", "sum", "none"].')
367+
loss = torch.sum(mi).neg() # sum over the batch and channel ndims
368+
elif self.reduction == LossReduction.NONE.value:
369+
loss = mi.neg()
370+
elif self.reduction == LossReduction.MEAN.value:
371+
loss = torch.mean(mi).neg() # average over the batch and channel ndims
372+
else:
373+
raise ValueError(f'Unsupported reduction: {self.reduction}, available options are ["mean", "sum", "none"].')
374+
return loss.to(dtype=output_dtype)

tests/losses/image_dissimilarity/test_global_mutual_information_loss.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,48 @@ def test_ill_opts(self, num_bins, reduction, expected_exception, expected_messag
164164
GlobalMutualInformationLoss(num_bins=num_bins, reduction=reduction)(pred, target)
165165

166166

167+
class TestGlobalMutualInformationLossHalfPrecision(unittest.TestCase):
168+
def test_half_precision_gaussian_weights_with_many_bins_are_finite(self):
169+
image = torch.zeros((1, 1, 2), dtype=torch.float16)
170+
loss = GlobalMutualInformationLoss(kernel_type="gaussian", num_bins=256)
171+
172+
weight, probability = loss.parzen_windowing_gaussian(image)
173+
174+
self.assertTrue(torch.isfinite(weight).all())
175+
self.assertTrue(torch.isfinite(probability).all())
176+
177+
def test_half_precision_large_constant_volume_is_finite(self):
178+
pred = torch.zeros((1, 1, 48, 48, 48), dtype=torch.float16, requires_grad=True)
179+
target = torch.zeros_like(pred)
180+
loss = GlobalMutualInformationLoss(kernel_type="gaussian")
181+
182+
result = loss(pred, target)
183+
184+
self.assertTrue(torch.isfinite(result))
185+
self.assertEqual(result.dtype, pred.dtype)
186+
self.assertEqual(result.device, pred.device)
187+
result.backward()
188+
self.assertIsNotNone(pred.grad)
189+
self.assertTrue(torch.isfinite(pred.grad).all())
190+
self.assertEqual(pred.grad.dtype, pred.dtype)
191+
self.assertEqual(pred.grad.device, pred.device)
192+
193+
def test_cpu_float16_autocast_large_volume_is_finite(self):
194+
pred = torch.zeros((1, 1, 48, 48, 48), requires_grad=True)
195+
target = torch.zeros_like(pred)
196+
loss = GlobalMutualInformationLoss(kernel_type="gaussian")
197+
198+
with torch.autocast(device_type="cpu", dtype=torch.float16):
199+
result = loss(pred, target)
200+
201+
self.assertTrue(torch.isfinite(result))
202+
result.backward()
203+
self.assertIsNotNone(pred.grad)
204+
self.assertTrue(torch.isfinite(pred.grad).all())
205+
self.assertEqual(pred.grad.dtype, pred.dtype)
206+
self.assertEqual(pred.grad.device, pred.device)
207+
208+
167209
class TestGlobalMutualInformationLossBuffers(unittest.TestCase):
168210
def test_gaussian_kernel_registers_buffers(self):
169211
"""Verify gaussian kernel registers preterm and bin_centers as non-trainable, non-persistent buffers."""

0 commit comments

Comments
 (0)