From 5d7d882ce6184c990246d41eb0e512a8fb7805a7 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Wed, 1 Apr 2026 06:05:37 -0700 Subject: [PATCH] Fix save_pretrained_merged for full-finetuned models (#4755) * Fix save_pretrained_merged for full-finetuned models save_pretrained_merged and push_to_hub_merged silently do nothing when the model is not a PeftModel (i.e. full finetuning without LoRA). merge_and_overwrite_lora returns None immediately for non-PeftModel, and unsloth_generic_save does not check the return value. Add a non-PeftModel branch in unsloth_generic_save that falls back to model.save_pretrained / model.push_to_hub. When save_method contains "16bit", cast weights to bfloat16 (or float16) via a state_dict copy to honor the user's intent without mutating the live model. The existing PeftModel (LoRA) code path is unchanged. * Forward create_pr and revision to tokenizer.push_to_hub The tokenizer push_to_hub call was missing create_pr and revision, which could cause the tokenizer to push to the wrong branch or bypass PR creation when the model push uses them. * Honor merged_16bit dtype contract for full-finetuned models Cast state_dict to bfloat16/float16 when save_method contains "16bit" to match the documented behavior of save_pretrained_merged. Also pass state_dict and save kwargs consistently to both save_pretrained and push_to_hub paths. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Address review feedback for PR #4755 - Simplify PeftModel isinstance check (PeftModelForCausalLM inherits from PeftModel) - Add is_main_process guard for distributed training - Forward variant to save_pretrained - Set tokenizer padding_side to "left" before saving (matches other save paths) * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- unsloth/save.py | 86 +++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 73 insertions(+), 13 deletions(-) diff --git a/unsloth/save.py b/unsloth/save.py index 1759d86fb1..178178980d 100644 --- a/unsloth/save.py +++ b/unsloth/save.py @@ -2777,19 +2777,79 @@ def unsloth_generic_save( elif save_method == "merged_4bit_forced": save_method = "merged_4bit" - merge_and_overwrite_lora( - get_model_name, - model = model, - tokenizer = tokenizer, - save_directory = save_directory, - push_to_hub = push_to_hub, - private = private, - token = token, - save_method = save_method, - output_dtype = None, - low_disk_space_usage = True, - use_temp_file = False, - ) + # Full-finetuned models (no LoRA) cannot use merge_and_overwrite_lora + # since there are no adapters to merge. Fall back to save_pretrained. + # This mirrors the non-PeftModel handling in save_pretrained_torchao + # and the GGUF save path. + _is_peft = isinstance(model, PeftModel) + if not _is_peft: + if not is_main_process: + return + + # Honor merged_16bit by casting to the target dtype if needed + _save_kwargs = dict( + safe_serialization = safe_serialization, + max_shard_size = max_shard_size, + variant = variant, + ) + if "16bit" in save_method: + _target_dtype = ( + torch.bfloat16 if torch.cuda.is_bf16_supported() else torch.float16 + ) + _save_kwargs["state_dict"] = { + k: v.to(dtype = _target_dtype) if v.is_floating_point() else v + for k, v in model.state_dict().items() + } + + if push_to_hub: + print(f"Unsloth: Pushing full fine-tuned model to '{save_directory}' ...") + model.push_to_hub( + repo_id = save_directory, + token = token, + private = private, + commit_message = commit_message, + create_pr = create_pr, + revision = revision, + commit_description = commit_description, + tags = tags, + **_save_kwargs, + ) + if tokenizer is not None: + old_padding_side = tokenizer.padding_side + tokenizer.padding_side = "left" + tokenizer.push_to_hub( + save_directory, + token = token, + private = private, + commit_message = commit_message, + create_pr = create_pr, + revision = revision, + ) + tokenizer.padding_side = old_padding_side + else: + print(f"Unsloth: Saving full fine-tuned model to '{save_directory}' ...") + model.save_pretrained(save_directory, **_save_kwargs) + if tokenizer is not None: + old_padding_side = tokenizer.padding_side + tokenizer.padding_side = "left" + tokenizer.save_pretrained(save_directory) + tokenizer.padding_side = old_padding_side + + print(f"Unsloth: Model saved successfully to '{save_directory}'") + else: + merge_and_overwrite_lora( + get_model_name, + model = model, + tokenizer = tokenizer, + save_directory = save_directory, + push_to_hub = push_to_hub, + private = private, + token = token, + save_method = save_method, + output_dtype = None, + low_disk_space_usage = True, + use_temp_file = False, + ) if push_to_hub and datasets: try: