6161 get_weight_block_size ,
6262 get_weight_scaling_factor ,
6363 get_weight_scaling_factor_2 ,
64+ process_layer_quant_config ,
6465 to_quantized_weight ,
6566)
6667
@@ -169,6 +170,7 @@ def __init__(
169170 self .all_rules = self ._populate_rule_book ()
170171 self .rules = self .all_rules [self .arch ]
171172 self .exclude_modules = []
173+ self .layer_config_dict = {}
172174
173175 if not hasattr (model , "_modelopt_state" ):
174176 return
@@ -324,22 +326,32 @@ def save_pretrained(
324326 print (f"Successfully loaded { len (mtp_state_dict )} MTP tensors" )
325327
326328 combined_exclude_modules = self ._gather_exclude_modules ()
329+ combined_layer_config_dict = self ._gather_layer_config_dict ()
327330
328331 if is_last_stage_main_rank and quantization is not None :
329- self ._hf_quant_config = {
332+ if combined_layer_config_dict :
333+ quantization_config = process_layer_quant_config (combined_layer_config_dict )
334+ quantization_config ["exclude_modules" ] = combined_exclude_modules
335+ else :
336+ quantization_config = {
337+ "quant_algo" : quantization ,
338+ "exclude_modules" : combined_exclude_modules ,
339+ }
340+ if quantization == "NVFP4" : # update block size
341+ quantization_config ["group_size" ] = 16
342+
343+ if hasattr (self , "kv_cache_dtype" ):
344+ quantization_config ["kv_cache_quant_algo" ] = self .kv_cache_dtype
345+
346+ raw_hf_quant_config = {
330347 "producer" : {
331348 "name" : "modelopt" ,
332349 "version" : __version__ ,
333350 },
334- "quantization" : {
335- "quant_algo" : quantization ,
336- "exclude_modules" : combined_exclude_modules ,
337- },
351+ "quantization" : quantization_config ,
338352 }
339- if quantization == "NVFP4" : # update block size
340- self ._hf_quant_config ["quantization" ]["group_size" ] = 16
341- if hasattr (self , "kv_cache_dtype" ):
342- self ._hf_quant_config ["quantization" ]["kv_cache_quant_algo" ] = self .kv_cache_dtype
353+ # Use one serving-facing config for both hf_quant_config.json and config.json.
354+ self ._hf_quant_config = convert_hf_quant_config_format (raw_hf_quant_config )
343355 with open (save_directory + "/hf_quant_config.json" , "w" ) as f :
344356 json .dump (self ._hf_quant_config , f , indent = 4 )
345357
@@ -359,10 +371,9 @@ def save_pretrained(
359371 # Newer versions of VLLM expect config.json with hf_quant_config
360372 config_json_file = save_directory + "/config.json"
361373 if self ._hf_quant_config and os .path .exists (config_json_file ):
362- converted_quant_config = convert_hf_quant_config_format (self ._hf_quant_config )
363374 with open (config_json_file ) as f :
364375 config_dict = json .load (f )
365- config_dict ["quantization_config" ] = converted_quant_config
376+ config_dict ["quantization_config" ] = self . _hf_quant_config
366377 with open (config_json_file , "w" ) as f :
367378 json .dump (config_dict , f , indent = 4 )
368379
@@ -803,9 +814,7 @@ def _get_quantized_state(
803814 name_to_value = {}
804815 qformat : str = self ._get_quantization_format (module )
805816 if qformat is None and "norm" not in prefix :
806- # Add exclude layers for hf_quant_config. Note that if the prefix is not an empty
807- # string then it usually ends with "." which needs to be removed.
808- self .exclude_modules .append (prefix .removesuffix ("." ))
817+ self ._record_excluded_module (prefix )
809818 block_size = get_weight_block_size (module )
810819
811820 name_to_value = self ._get_weight_bias (module , dtype , name_to_value )
@@ -850,6 +859,27 @@ def _get_weight_scales(self, quantized_state: dict[str, Any], qformat: str):
850859
851860 return weight_scale , weight_scale_2
852861
862+ def _record_layer_quant_config (self , prefix : str , qformat : str | None , block_size : int ):
863+ """Record per-HF-layer quantization metadata for mixed precision exports."""
864+ if qformat in (None , QUANTIZATION_NONE ):
865+ return
866+
867+ layer_name = prefix .removesuffix ("." )
868+ if "{" in layer_name or not layer_name :
869+ return
870+
871+ self .layer_config_dict [layer_name + ".quantization" ] = qformat
872+ self .layer_config_dict [layer_name + ".awq_block_size" ] = block_size
873+
874+ def _record_excluded_module (self , prefix : str ):
875+ """Record an unquantized HF module prefix for hf_quant_config."""
876+ layer_name = prefix .removesuffix ("." )
877+ if "{" in layer_name or not layer_name :
878+ return
879+
880+ if layer_name not in self .exclude_modules :
881+ self .exclude_modules .append (layer_name )
882+
853883 def _name_remapping (
854884 self ,
855885 module : torch .nn .Module | torch .Tensor ,
@@ -866,6 +896,7 @@ def _name_remapping(
866896 return
867897
868898 name_to_value , qformat , block_size = self ._get_quantized_state (module , dtype , prefix = prefix )
899+ self ._record_layer_quant_config (prefix , qformat , block_size )
869900
870901 weight = name_to_value .pop ("weight" )
871902 weight_scale , weight_scale_2 = self ._get_weight_scales (name_to_value , qformat )
@@ -906,6 +937,8 @@ def _gated_mlp_slicing(
906937
907938 gate_proj_prefix = prefix + gate_proj_name + "."
908939 up_proj_prefix = prefix + up_proj_name + "."
940+ self ._record_layer_quant_config (gate_proj_prefix , qformat , block_size )
941+ self ._record_layer_quant_config (up_proj_prefix , qformat , block_size )
909942
910943 ffn_hidden_size = module .config .ffn_hidden_size
911944 gate_proj_weight = weight [:ffn_hidden_size , :]
@@ -986,6 +1019,7 @@ def _grouped_mlp_slicing(self, module, prefix, parallel_config=None):
9861019
9871020 for expert_id in range (num_experts ):
9881021 expert_prefix = prefix .format (expert_id ) + "."
1022+ self ._record_layer_quant_config (expert_prefix , qformat , block_size )
9891023 weight_key = f"weight{ expert_id } "
9901024
9911025 if weight_key not in state_dict :
@@ -1030,6 +1064,18 @@ def _qkv_slicing(
10301064 q_proj_prefix = prefix + q_proj_name + "."
10311065 k_proj_prefix = prefix + k_proj_name + "."
10321066 v_proj_prefix = prefix + v_proj_name + "."
1067+ self ._record_layer_quant_config (q_proj_prefix , qformat , block_size )
1068+ self ._record_layer_quant_config (k_proj_prefix , qformat , block_size )
1069+ self ._record_layer_quant_config (v_proj_prefix , qformat , block_size )
1070+ if qformat in (None , QUANTIZATION_NONE ):
1071+ # MCore stores Q/K/V as one fused linear_qkv module, but HF exports them
1072+ # as separate q_proj/k_proj/v_proj modules. Record the HF names so
1073+ # runtime quant configs do not miss excluded fused-QKV projections.
1074+ fused_prefix = prefix .removesuffix ("." )
1075+ self .exclude_modules = [m for m in self .exclude_modules if m != fused_prefix ]
1076+ self ._record_excluded_module (q_proj_prefix )
1077+ self ._record_excluded_module (k_proj_prefix )
1078+ self ._record_excluded_module (v_proj_prefix )
10331079
10341080 config = module .config
10351081 hidden_size = config .hidden_size
@@ -1179,6 +1225,7 @@ def _pack_name_remapping(self, module, prefix, layer_type=None):
11791225 weight_scale_list .append (weight_scale )
11801226 weight_scale_2_list .append (weight_scale_2 )
11811227 input_scale_list .append (input_scale )
1228+ self ._record_layer_quant_config (prefix , qformat , block_size )
11821229
11831230 merged_weight = torch .stack (weight_list , dim = 0 )
11841231
@@ -1247,6 +1294,7 @@ def _pack_name_remapping_gpt_oss(self, module, prefix, layer_type=None):
12471294 weight_scale_2_list .append (weight_scale_2 )
12481295 input_scale_list .append (input_scale )
12491296 bias_list .append (bias )
1297+ self ._record_layer_quant_config (prefix , qformat , block_size )
12501298
12511299 merged_weight = torch .stack (weight_list , dim = 0 )
12521300
@@ -1349,6 +1397,19 @@ def _gather_exclude_modules(self):
13491397 combined_exclude_modules .update (modules )
13501398 return sorted (combined_exclude_modules )
13511399
1400+ def _gather_layer_config_dict (self ):
1401+ """Get per-layer quantization metadata from all ranks for hf_quant_config."""
1402+ if not torch .distributed .is_initialized ():
1403+ return dict (sorted (self .layer_config_dict .items ()))
1404+
1405+ all_layer_config_dicts = [None ] * torch .distributed .get_world_size ()
1406+ torch .distributed .all_gather_object (all_layer_config_dicts , self .layer_config_dict )
1407+ combined_layer_config_dict = {}
1408+ for layer_config_dict in all_layer_config_dicts :
1409+ if layer_config_dict :
1410+ combined_layer_config_dict .update (layer_config_dict )
1411+ return dict (sorted (combined_layer_config_dict .items ()))
1412+
13521413
13531414def export_mcore_gpt_to_hf (
13541415 model : torch .nn .Module ,
0 commit comments