1616import gc
1717
1818import pytest
19+ import safetensors .torch
1920import torch
2021
2122from diffusers import (
@@ -423,6 +424,14 @@ def _test_quantization_device_map(self, config_kwargs):
423424 assert hasattr (model , "hf_device_map" ), "Model should have hf_device_map attribute"
424425 assert model .hf_device_map is not None , "hf_device_map should not be None"
425426
427+ map_devices = {torch .device (d ).type for d in model .hf_device_map .values ()}
428+ for name , buffer in model .named_buffers ():
429+ assert buffer .device .type != "meta" , f"Buffer { name } was left on the meta device"
430+ if len (map_devices ) == 1 :
431+ assert buffer .device .type == next (iter (map_devices )), (
432+ f"Expected device { next (iter (map_devices ))} for buffer { name } , got { buffer .device } "
433+ )
434+
426435 inputs = self .get_dummy_inputs ()
427436 output = model (** inputs , return_dict = False )[0 ]
428437 assert output is not None , "Model output is None"
@@ -625,15 +634,32 @@ def test_bnb_quantization_memory_footprint(self, config_name):
625634 def test_bnb_quantization_inference (self , config_name ):
626635 self ._test_quantization_inference (BitsAndBytesConfigMixin .BNB_CONFIGS [config_name ])
627636
628- @pytest .mark .parametrize ("config_name" , ["4bit_nf4" ], ids = ["4bit_nf4" ])
637+ @pytest .mark .parametrize ("config_name" , ["4bit_nf4" , "8bit" ], ids = ["4bit_nf4" , "8bit " ])
629638 def test_bnb_quantization_dtype_assignment (self , config_name ):
630639 self ._test_quantization_dtype_assignment (BitsAndBytesConfigMixin .BNB_CONFIGS [config_name ])
631640
641+ def test_bnb_device_assignment (self ):
642+ """Test that a 4-bit model moves between CPU and accelerator without changing its memory footprint."""
643+ model = self ._create_quantized_model (BitsAndBytesConfigMixin .BNB_CONFIGS ["4bit_nf4" ])
644+ mem_before = model .get_memory_footprint ()
645+
646+ model .to ("cpu" )
647+ assert model .device .type == "cpu"
648+ assert model .get_memory_footprint () == pytest .approx (mem_before )
649+
650+ model .to (torch_device )
651+ assert model .device .type == torch .device (torch_device ).type
652+ assert model .get_memory_footprint () == pytest .approx (mem_before )
653+
632654 @pytest .mark .parametrize ("config_name" , ["4bit_nf4" ], ids = ["4bit_nf4" ])
633655 def test_bnb_quantization_lora_inference (self , config_name ):
634656 self ._test_quantization_lora_inference (BitsAndBytesConfigMixin .BNB_CONFIGS [config_name ])
635657
636- @pytest .mark .parametrize ("config_name" , ["4bit_nf4" ], ids = ["4bit_nf4" ])
658+ @pytest .mark .parametrize (
659+ "config_name" ,
660+ list (BitsAndBytesConfigMixin .BNB_CONFIGS .keys ()),
661+ ids = list (BitsAndBytesConfigMixin .BNB_CONFIGS .keys ()),
662+ )
637663 def test_bnb_quantization_serialization (self , config_name , tmp_path ):
638664 self ._test_quantization_serialization (BitsAndBytesConfigMixin .BNB_CONFIGS [config_name ], tmp_path )
639665
@@ -660,15 +686,51 @@ def test_bnb_keep_modules_in_fp32(self):
660686 self ._test_keep_modules_in_fp32 (BitsAndBytesConfigMixin .BNB_CONFIGS ["4bit_nf4" ])
661687
662688 def test_bnb_modules_to_not_convert (self ):
663- """Test that modules_to_not_convert parameter works correctly."""
689+ """Test that `llm_int8_skip_modules` (the BitsAndBytesConfig module-exclusion option) works correctly."""
664690 modules_to_exclude = getattr (self , "modules_to_not_convert_for_test" , None )
665691 if modules_to_exclude is None :
666692 pytest .skip ("modules_to_not_convert_for_test not defined for this model" )
667693
668- self ._test_quantization_modules_to_not_convert (
669- BitsAndBytesConfigMixin .BNB_CONFIGS ["4bit_nf4" ], modules_to_exclude
694+ config_kwargs = {** BitsAndBytesConfigMixin .BNB_CONFIGS ["8bit" ], "llm_int8_skip_modules" : modules_to_exclude }
695+ model = self ._create_quantized_model (config_kwargs )
696+
697+ found_excluded = False
698+ for name , module in model .named_modules ():
699+ if isinstance (module , torch .nn .Linear ):
700+ if any (excluded in name for excluded in modules_to_exclude ):
701+ found_excluded = True
702+ assert module .weight .dtype != torch .int8 , f"Module { name } should not be quantized"
703+ else :
704+ assert isinstance (module , bnb .nn .Linear8bitLt ), f"Module { name } should be quantized"
705+ assert module .weight .dtype == torch .int8 , f"Module { name } weight should be int8"
706+
707+ assert found_excluded , f"No linear layers found in excluded modules: { modules_to_exclude } "
708+
709+ @pytest .mark .parametrize ("config_name" , ["8bit" ], ids = ["8bit" ])
710+ def test_bnb_quantization_sharded_serialization (self , config_name , tmp_path ):
711+ self ._test_quantization_serialization (
712+ BitsAndBytesConfigMixin .BNB_CONFIGS [config_name ], tmp_path , max_shard_size = "16KB"
670713 )
671714
715+ def test_bnb_errors_loading_incorrect_state_dict (self , tmp_path ):
716+ """Test that loading a checkpoint with a corrupted quantized weight raises a helpful error."""
717+ model = self ._create_quantized_model (BitsAndBytesConfigMixin .BNB_CONFIGS ["4bit_nf4" ])
718+ model .save_pretrained (str (tmp_path ))
719+ del model
720+ gc .collect ()
721+ backend_empty_cache (torch_device )
722+
723+ weights_file = tmp_path / "diffusion_pytorch_model.safetensors"
724+ state_dict = safetensors .torch .load_file (str (weights_file ))
725+ key_to_target = next (k for k in state_dict if k .endswith (".weight" ) and state_dict [k ].dtype == torch .uint8 )
726+ corrupted_param = torch .randn (state_dict [key_to_target ].shape [0 ] - 1 , 1 )
727+ state_dict [key_to_target ] = bnb .nn .Params4bit (corrupted_param , requires_grad = False )
728+ safetensors .torch .save_file (state_dict , str (weights_file ))
729+
730+ with pytest .raises (ValueError ) as err_context :
731+ _ = self .model_class .from_pretrained (str (tmp_path ))
732+ assert key_to_target in str (err_context .value )
733+
672734 @pytest .mark .parametrize ("config_name" , ["4bit_nf4" , "8bit" ], ids = ["4bit_nf4" , "8bit" ])
673735 def test_bnb_device_map (self , config_name ):
674736 """Test that device_map='auto' works correctly with quantization."""
@@ -678,9 +740,10 @@ def test_bnb_dequantize(self):
678740 """Test that dequantize() works correctly."""
679741 self ._test_dequantize (BitsAndBytesConfigMixin .BNB_CONFIGS ["4bit_nf4" ])
680742
681- def test_bnb_training (self ):
743+ @pytest .mark .parametrize ("config_name" , ["4bit_nf4" , "8bit" ], ids = ["4bit_nf4" , "8bit" ])
744+ def test_bnb_training (self , config_name ):
682745 """Test that quantized models can be used for training with adapters."""
683- self ._test_quantization_training (BitsAndBytesConfigMixin .BNB_CONFIGS ["4bit_nf4" ])
746+ self ._test_quantization_training (BitsAndBytesConfigMixin .BNB_CONFIGS [config_name ])
684747
685748 @pytest .mark .parametrize (
686749 "config_name" ,
@@ -812,6 +875,10 @@ def test_quanto_quantization_inference(self, weight_type_name):
812875 def test_quanto_quantized_layers (self , weight_type_name ):
813876 self ._test_quantized_layers (QuantoConfigMixin .QUANTO_WEIGHT_TYPES [weight_type_name ])
814877
878+ @pytest .mark .parametrize ("weight_type_name" , ["int8" ], ids = ["int8" ])
879+ def test_quanto_quantization_dtype_assignment (self , weight_type_name ):
880+ self ._test_quantization_dtype_assignment (QuantoConfigMixin .QUANTO_WEIGHT_TYPES [weight_type_name ])
881+
815882 @pytest .mark .parametrize ("weight_type_name" , ["int8" ], ids = ["int8" ])
816883 def test_quanto_quantization_lora_inference (self , weight_type_name ):
817884 self ._test_quantization_lora_inference (QuantoConfigMixin .QUANTO_WEIGHT_TYPES [weight_type_name ])
@@ -1016,6 +1083,47 @@ def test_torchao_device_map(self):
10161083 """Test that device_map='auto' works correctly with quantization."""
10171084 self ._test_quantization_device_map (TorchAoConfigMixin .TORCHAO_QUANT_TYPES ["int8wo" ])
10181085
1086+ @torch .no_grad ()
1087+ def test_torchao_cpu_disk_offload_device_map (self , tmp_path ):
1088+ """Test custom device maps with cpu/disk offload: offloaded modules stay unquantized, inference works."""
1089+ from torchao .utils import TorchAOBaseTensor
1090+
1091+ model = self ._create_quantized_model (TorchAoConfigMixin .TORCHAO_QUANT_TYPES ["int8wo" ])
1092+
1093+ # Offload the first two linear-bearing top-level modules to cpu and disk, keep the rest on the accelerator.
1094+ device_map = {}
1095+ offload_targets = []
1096+ for name , child in model .named_children ():
1097+ if len (offload_targets ) < 2 and any (isinstance (m , torch .nn .Linear ) for m in child .modules ()):
1098+ device_map [name ] = "disk" if offload_targets else "cpu"
1099+ offload_targets .append (name )
1100+ else :
1101+ device_map [name ] = str (torch_device )
1102+ del model
1103+ gc .collect ()
1104+ backend_empty_cache (torch_device )
1105+ if len (offload_targets ) < 2 :
1106+ pytest .skip ("Model does not have enough linear-bearing top-level modules for offload testing" )
1107+
1108+ model = self ._create_quantized_model (
1109+ TorchAoConfigMixin .TORCHAO_QUANT_TYPES ["int8wo" ], device_map = device_map , offload_folder = str (tmp_path )
1110+ )
1111+
1112+ # Weights offloaded to cpu/disk are not quantized, only the weights on the accelerator are.
1113+ for name , module in model .named_modules ():
1114+ if isinstance (module , torch .nn .Linear ):
1115+ if name .split ("." )[0 ] in offload_targets :
1116+ assert not isinstance (module .weight , TorchAOBaseTensor ), (
1117+ f"Offloaded module { name } should not be quantized"
1118+ )
1119+ else :
1120+ assert isinstance (module .weight , TorchAOBaseTensor ), f"Module { name } should be quantized"
1121+
1122+ inputs = self .get_dummy_inputs ()
1123+ output = model (** inputs , return_dict = False )[0 ]
1124+ assert output is not None , "Model output is None"
1125+ assert not torch .isnan (output ).any (), "Model output contains NaN"
1126+
10191127 @pytest .mark .parametrize (
10201128 "quant_type" ,
10211129 [
@@ -1103,6 +1211,38 @@ class GGUFTesterMixin(GGUFConfigMixin, QuantizationTesterMixin):
11031211 def test_gguf_quantization_inference (self ):
11041212 self ._test_quantization_inference ({"compute_dtype" : torch .bfloat16 })
11051213
1214+ def test_gguf_quantized_layers (self ):
1215+ compute_dtype = getattr (self , "torch_dtype" , torch .bfloat16 )
1216+ model = self ._create_quantized_model ({"compute_dtype" : compute_dtype })
1217+
1218+ num_quantized = 0
1219+ for name , module in model .named_modules ():
1220+ if isinstance (module , torch .nn .Linear ) and hasattr (module .weight , "quant_type" ):
1221+ self ._verify_if_layer_quantized (name , module )
1222+ if module .bias is not None :
1223+ assert module .bias .dtype == compute_dtype , f"{ name } bias should be { compute_dtype } "
1224+ num_quantized += 1
1225+
1226+ assert num_quantized > 0 , "No quantized linear layers found in model"
1227+
1228+ @torch .no_grad ()
1229+ def test_gguf_memory_usage (self ):
1230+ expected_gb = getattr (self , "expected_memory_use_in_gb" , None )
1231+ if expected_gb is None :
1232+ pytest .skip ("expected_memory_use_in_gb not defined for this model" )
1233+
1234+ compute_dtype = getattr (self , "torch_dtype" , torch .bfloat16 )
1235+ model = self ._create_quantized_model ({"compute_dtype" : compute_dtype })
1236+ model .to (torch_device )
1237+ assert (model .get_memory_footprint () / 1024 ** 3 ) < expected_gb
1238+
1239+ inputs = self .get_dummy_inputs ()
1240+ backend_reset_peak_memory_stats (torch_device )
1241+ backend_empty_cache (torch_device )
1242+ model (** inputs )
1243+ max_memory = backend_max_memory_allocated (torch_device )
1244+ assert (max_memory / 1024 ** 3 ) < expected_gb
1245+
11061246 def test_gguf_keep_modules_in_fp32 (self ):
11071247 if not hasattr (self .model_class , "_keep_in_fp32_modules" ):
11081248 pytest .skip (f"{ self .model_class .__name__ } does not have _keep_in_fp32_modules" )
0 commit comments