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>
This commit is contained in:
Daniel Han 2026-04-01 06:05:37 -07:00 committed by GitHub
commit 5d7d882ce6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -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: