@@ -181,6 +181,72 @@ def test_half_precision_gaussian_weights_with_many_bins_are_finite(self, dtype):
181181 self .assertEqual (probability .dtype , image .dtype )
182182 self .assertEqual (weight .device , image .device )
183183 self .assertEqual (probability .device , image .device )
184+ torch .testing .assert_close (
185+ weight .float ().sum (dim = - 1 ), torch .ones_like (weight [..., 0 ], dtype = torch .float32 ), rtol = 0.0 , atol = 5e-3
186+ )
187+ torch .testing .assert_close (
188+ probability .float ().sum (dim = - 1 ),
189+ torch .ones_like (probability [..., 0 ], dtype = torch .float32 ),
190+ rtol = 0.0 ,
191+ atol = 5e-3 ,
192+ )
193+
194+ @parameterized .expand ([(torch .float16 ,), (torch .bfloat16 ,)])
195+ def test_module_cast_with_many_bins_remains_finite (self , dtype ):
196+ """Verify module dtype conversion cannot overflow Gaussian parameters."""
197+ image = torch .linspace (0.0 , 1.0 , 64 , dtype = dtype ).reshape (1 , 1 , 8 , 8 ).requires_grad_ ()
198+ target = torch .flip (image .detach (), dims = (- 1 ,))
199+ loss = GlobalMutualInformationLoss (kernel_type = "gaussian" , num_bins = 256 ).to (dtype = dtype )
200+
201+ weight , probability = loss .parzen_windowing_gaussian (image )
202+ result = loss (image , target )
203+
204+ self .assertTrue (torch .isfinite (weight ).all ())
205+ self .assertTrue (torch .isfinite (probability ).all ())
206+ self .assertTrue (torch .isfinite (result ))
207+ result .backward ()
208+ self .assertIsNotNone (image .grad )
209+ self .assertTrue (torch .isfinite (image .grad ).all ())
210+
211+ def test_float16_default_dtype_with_many_bins_remains_finite (self ):
212+ """Verify construction under a float16 default keeps Gaussian parameters finite."""
213+ original_dtype = torch .get_default_dtype ()
214+ try :
215+ torch .set_default_dtype (torch .float16 )
216+ image = torch .linspace (0.0 , 1.0 , 64 ).reshape (1 , 1 , 8 , 8 ).requires_grad_ ()
217+ target = torch .flip (image .detach (), dims = (- 1 ,))
218+ loss = GlobalMutualInformationLoss (kernel_type = "gaussian" , num_bins = 256 )
219+
220+ weight , probability = loss .parzen_windowing_gaussian (image )
221+ result = loss (image , target )
222+
223+ self .assertTrue (torch .isfinite (weight ).all ())
224+ self .assertTrue (torch .isfinite (probability ).all ())
225+ self .assertTrue (torch .isfinite (result ))
226+ result .backward ()
227+ self .assertIsNotNone (image .grad )
228+ self .assertTrue (torch .isfinite (image .grad ).all ())
229+ finally :
230+ torch .set_default_dtype (original_dtype )
231+
232+ @parameterized .expand ([(torch .float16 ,), (torch .bfloat16 ,)])
233+ def test_half_precision_nonconstant_images_match_float32 (self , dtype ):
234+ """Verify nonconstant reduced-precision loss tracks float32."""
235+ pred_float = torch .linspace (0.0 , 1.0 , 64 ).reshape (1 , 1 , 8 , 8 )
236+ target_float = torch .flip (pred_float , dims = (- 1 ,))
237+ loss = GlobalMutualInformationLoss (kernel_type = "gaussian" )
238+ expected = loss (pred_float , target_float )
239+ pred = pred_float .to (dtype = dtype ).requires_grad_ ()
240+ target = target_float .to (dtype = dtype )
241+
242+ result = loss (pred , target )
243+
244+ self .assertTrue (torch .isfinite (result ))
245+ self .assertEqual (result .dtype , dtype )
246+ torch .testing .assert_close (result .float (), expected , rtol = 1e-2 , atol = 1e-2 )
247+ result .backward ()
248+ self .assertIsNotNone (pred .grad )
249+ self .assertTrue (torch .isfinite (pred .grad ).all ())
184250
185251 @parameterized .expand ([(torch .float16 ,), (torch .bfloat16 ,)])
186252 def test_half_precision_large_constant_volume_is_finite (self , dtype ):
@@ -200,6 +266,37 @@ def test_half_precision_large_constant_volume_is_finite(self, dtype):
200266 self .assertEqual (pred .grad .dtype , pred .dtype )
201267 self .assertEqual (pred .grad .device , pred .device )
202268
269+ def test_cpu_float16_autocast_nonconstant_images_match_float32 (self ):
270+ """Verify nonconstant CPU autocast loss matches float32."""
271+ pred = torch .linspace (0.0 , 1.0 , 64 ).reshape (1 , 1 , 8 , 8 ).requires_grad_ ()
272+ target = torch .flip (pred .detach (), dims = (- 1 ,))
273+ loss = GlobalMutualInformationLoss (kernel_type = "gaussian" )
274+ expected = loss (pred , target ).detach ()
275+
276+ with torch .autocast (device_type = "cpu" , dtype = torch .float16 ):
277+ result = loss (pred , target )
278+
279+ self .assertTrue (torch .isfinite (result ))
280+ self .assertEqual (result .dtype , pred .dtype )
281+ torch .testing .assert_close (result , expected )
282+ result .backward ()
283+ self .assertIsNotNone (pred .grad )
284+ self .assertTrue (torch .isfinite (pred .grad ).all ())
285+
286+ def test_scripted_cpu_float16_autocast_large_volume_is_finite (self ):
287+ """Verify scripted loss avoids float16 histogram overflow under autocast."""
288+ pred = torch .zeros ((1 , 1 , 257 , 257 ), requires_grad = True )
289+ target = torch .zeros_like (pred )
290+ loss = torch .jit .script (GlobalMutualInformationLoss (kernel_type = "gaussian" ))
291+
292+ with torch .autocast (device_type = "cpu" , dtype = torch .float16 ):
293+ result = loss (pred , target )
294+
295+ self .assertTrue (torch .isfinite (result ))
296+ result .backward ()
297+ self .assertIsNotNone (pred .grad )
298+ self .assertTrue (torch .isfinite (pred .grad ).all ())
299+
203300 def test_cpu_float16_autocast_large_volume_is_finite (self ):
204301 """Verify CPU float16 autocast avoids histogram accumulation overflow."""
205302 pred = torch .zeros ((1 , 1 , 48 , 48 , 48 ), requires_grad = True )
@@ -210,6 +307,7 @@ def test_cpu_float16_autocast_large_volume_is_finite(self):
210307 result = loss (pred , target )
211308
212309 self .assertTrue (torch .isfinite (result ))
310+ self .assertEqual (result .dtype , pred .dtype )
213311 result .backward ()
214312 self .assertIsNotNone (pred .grad )
215313 self .assertTrue (torch .isfinite (pred .grad ).all ())
0 commit comments