@@ -499,11 +499,6 @@ def test_getattr_is_correct(self, caplog):
499499
500500 assert str (error .value ) == f"'{ type (model ).__name__ } ' object has no attribute 'does_not_exist'"
501501
502- @require_accelerator
503- @pytest .mark .skipif (
504- torch_device not in ["cuda" , "xpu" ],
505- reason = "float16 and bfloat16 can only be used with an accelerator" ,
506- )
507502 def test_keep_in_fp32_modules (self , tmp_path ):
508503 model = self .model_class (** self .get_init_dict ())
509504 fp32_modules = model ._keep_in_fp32_modules
@@ -516,11 +511,71 @@ def test_keep_in_fp32_modules(self, tmp_path):
516511 model .save_pretrained (tmp_path )
517512 model = self .model_class .from_pretrained (tmp_path , torch_dtype = torch .float16 ).to (torch_device )
518513
514+ # The rule is applied to every floating point checkpoint tensor, so persistent buffers are covered too.
515+ # Non-persistent buffers are not in the checkpoint — they are regenerated by `__init__` and left alone.
516+ for name , tensor in named_persistent_module_tensors (model , recurse = True ):
517+ if not tensor .is_floating_point ():
518+ continue
519+ if any (module_to_keep_in_fp32 in name .split ("." ) for module_to_keep_in_fp32 in fp32_modules ):
520+ assert tensor .dtype == torch .float32 , f"{ name } should be float32 but got { tensor .dtype } "
521+ else :
522+ assert tensor .dtype == torch .float16 , f"{ name } should be float16 but got { tensor .dtype } "
523+
524+ def test_keep_in_fp32_modules_as_str (self , tmp_path , monkeypatch ):
525+ model = self .model_class (** self .get_init_dict ())
526+ fp32_modules = model ._keep_in_fp32_modules
527+
528+ if fp32_modules is None or len (fp32_modules ) == 0 :
529+ pytest .skip ("Model does not have _keep_in_fp32_modules defined." )
530+
531+ # Pick an entry that owns at least one parameter of the tiny test config, otherwise the assertions below
532+ # would hold trivially.
533+ parameter_name_parts = [name .split ("." ) for name , _ in model .named_parameters ()]
534+ fp32_module = next (
535+ (module for module in fp32_modules if any (module in parts for parts in parameter_name_parts )), None
536+ )
537+ if fp32_module is None :
538+ pytest .skip ("No _keep_in_fp32_modules entry owns a parameter of this model." )
539+
540+ # `from_pretrained` also accepts `_keep_in_fp32_modules` declared as a bare string.
541+ monkeypatch .setattr (self .model_class , "_keep_in_fp32_modules" , fp32_module )
542+
543+ model .save_pretrained (tmp_path )
544+ model = self .model_class .from_pretrained (tmp_path , torch_dtype = torch .float16 ).to (torch_device )
545+
546+ for name , param in model .named_parameters ():
547+ expected_dtype = torch .float32 if fp32_module in name .split ("." ) else torch .float16
548+ assert param .dtype == expected_dtype , f"Parameter { name } should be { expected_dtype } but got { param .dtype } "
549+
550+ def test_keep_in_fp32_modules_layerwise_casting (self ):
551+ # Lives here rather than next to the other layerwise casting tests because it asserts
552+ # `_keep_in_fp32_modules` semantics (`enable_layerwise_casting` folds it into the skip patterns) and needs
553+ # no accelerator, while the layerwise casting mixin is accelerator-gated.
554+ model = self .model_class (** self .get_init_dict ())
555+ fp32_modules = model ._keep_in_fp32_modules
556+
557+ if fp32_modules is None or len (fp32_modules ) == 0 :
558+ pytest .skip ("Model does not have _keep_in_fp32_modules defined." )
559+
560+ if all (
561+ any (module_to_keep_in_fp32 in name .split ("." ) for module_to_keep_in_fp32 in fp32_modules )
562+ for name , _ in model .named_parameters ()
563+ ):
564+ pytest .skip ("Every parameter is kept in fp32, so layerwise casting has nothing to cast." )
565+
566+ # float16 storage instead of float8 so the assertions hold on every device — the skip patterns are applied
567+ # the same way whatever the storage dtype is.
568+ model .enable_layerwise_casting (storage_dtype = torch .float16 , compute_dtype = torch .float32 )
569+
519570 for name , param in model .named_parameters ():
520571 if any (module_to_keep_in_fp32 in name .split ("." ) for module_to_keep_in_fp32 in fp32_modules ):
521572 assert param .dtype == torch .float32 , f"Parameter { name } should be float32 but got { param .dtype } "
522- else :
523- assert param .dtype == torch .float16 , f"Parameter { name } should be float16 but got { param .dtype } "
573+
574+ # The skip patterns keep several other modules in fp32 as well, so the loop above cannot check the
575+ # complement. Assert that casting happened at all instead, otherwise it would pass on an untouched model.
576+ assert any (param .dtype == torch .float16 for param in model .parameters ()), (
577+ "No parameter was cast to the storage dtype, so the assertions above hold trivially"
578+ )
524579
525580 def test_to_keep_in_fp32_modules_warns (self , caplog ):
526581 fp32_modules = self .model_class ._keep_in_fp32_modules
0 commit comments