caf324b09d
Build documentation / build (push) Failing after 0s
Deploy "method_comparison" Gradio to Spaces / deploy (push) Has been cancelled
Deploy "PEFT shop" Gradio app to Spaces / deploy (push) Has been cancelled
tests on transformers main / tests (push) Has been cancelled
tests / check_code_quality (push) Has been cancelled
tests / tests (ubuntu-latest, 3.10) (push) Has been cancelled
tests / tests (ubuntu-latest, 3.11) (push) Has been cancelled
tests / tests (ubuntu-latest, 3.12) (push) Has been cancelled
tests / tests (ubuntu-latest, 3.13) (push) Has been cancelled
tests / tests (windows-latest, 3.10) (push) Has been cancelled
tests / tests (windows-latest, 3.11) (push) Has been cancelled
tests / tests (windows-latest, 3.12) (push) Has been cancelled
tests / tests (windows-latest, 3.13) (push) Has been cancelled
Secret Leaks / trufflehog (push) Has been cancelled
CI security linting / zizmor latest via Cargo (push) Has been cancelled
183 lines
6.9 KiB
Python
183 lines
6.9 KiB
Python
# Copyright 2026-present the HuggingFace Inc. team.
|
||
#
|
||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||
# you may not use this file except in compliance with the License.
|
||
# You may obtain a copy of the License at
|
||
#
|
||
# http://www.apache.org/licenses/LICENSE-2.0
|
||
#
|
||
# Unless required by applicable law or agreed to in writing, software
|
||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
# See the License for the specific language governing permissions and
|
||
# limitations under the License.
|
||
|
||
import os
|
||
from dataclasses import dataclass, field
|
||
from typing import Optional
|
||
|
||
import torch
|
||
from datasets import load_dataset
|
||
from transformers import AutoModelForCausalLM, AutoTokenizer, HfArgumentParser
|
||
from trl import SFTConfig, SFTTrainer
|
||
|
||
from peft import PsoftConfig, get_peft_model
|
||
|
||
|
||
@dataclass
|
||
class ScriptArguments(SFTConfig):
|
||
# --- model ---
|
||
base_model_name_or_path: Optional[str] = field(
|
||
default=None, metadata={"help": "The name or path of the fp32/16 base model."}
|
||
)
|
||
bits: str = field(
|
||
default="fp32",
|
||
metadata={"help": "Precision to load the base model. Choices: ['bf16', 'fp16', 'fp32']."},
|
||
)
|
||
|
||
# --- PSOFT ---
|
||
r: int = field(default=32, metadata={"help": "Rank (r): dimension of trainable R."})
|
||
psoft_alpha: int = field(default=32, metadata={"help": "Scaling factor (typically set to r)."})
|
||
target_modules: list[str] = field(
|
||
default_factory=lambda: ["q_proj", "v_proj"],
|
||
metadata={"help": "Target module names, e.g. ['q_proj','k_proj','v_proj','o_proj', ...]."},
|
||
)
|
||
|
||
# SVD / init
|
||
ab_svd_init: str = field(
|
||
default="psoft_init",
|
||
metadata={"help": "Principal-subspace init identifier (e.g. 'psoft_init')."},
|
||
)
|
||
psoft_svd: str = field(
|
||
default="full",
|
||
metadata={"help": "SVD method. Typical choices: ['full', 'lowrank']."},
|
||
)
|
||
psoft_svd_lowrank_niter: Optional[int] = field(
|
||
default=None,
|
||
metadata={"help": "If psoft_svd='lowrank', number of iterations for lowrank SVD (optional)."},
|
||
)
|
||
|
||
# Orth / relaxation
|
||
psoft_orth: bool = field(default=True, metadata={"help": "Use orthogonal R (Cayley parameterization)."})
|
||
psoft_mag_a: bool = field(default=True, metadata={"help": "Enable tunable vector alpha (relaxed mode)."})
|
||
psoft_mag_b: bool = field(default=True, metadata={"help": "Enable tunable vector beta (relaxed mode)."})
|
||
|
||
# Cayley–Neumann approximation
|
||
use_cayley_neumann: bool = field(default=False, metadata={"help": "Enable Cayley-Neumann approximation."})
|
||
num_cayley_neumann_terms: int = field(default=5, metadata={"help": "Number of Neumann series terms."})
|
||
cayley_neumann_eps: Optional[float] = field(
|
||
default=None, metadata={"help": "Optional eps for numerical stability."}
|
||
)
|
||
|
||
# --- data ---
|
||
data_path: str = field(default="imdb", metadata={"help": "Dataset name/path for training."})
|
||
dataset_split: str = field(default="train[:1%]", metadata={"help": "Dataset split, e.g. 'train[:1%]'."})
|
||
dataset_field: Optional[list[str]] = field(
|
||
default=None,
|
||
metadata={
|
||
"help": (
|
||
"Fields used to build SFT text. "
|
||
"If provided, will build: '### USER: <field0>\\n### ASSISTANT: <field1>'. "
|
||
"If None, must already have a 'text' column."
|
||
)
|
||
},
|
||
)
|
||
|
||
|
||
def _dtype_from_bits(bits: str) -> torch.dtype:
|
||
bits = bits.lower()
|
||
if bits == "bf16":
|
||
return torch.bfloat16
|
||
if bits == "fp16":
|
||
return torch.float16
|
||
if bits == "fp32":
|
||
return torch.float32
|
||
raise ValueError(f"Unknown bits={bits}. Use one of: bf16, fp16, fp32.")
|
||
|
||
|
||
def main():
|
||
parser = HfArgumentParser(ScriptArguments)
|
||
script_args = parser.parse_args_into_dataclasses()[0]
|
||
print(script_args)
|
||
|
||
if script_args.base_model_name_or_path is None:
|
||
raise ValueError("--base_model_name_or_path is required.")
|
||
|
||
# PSOFT does NOT support quantized layers (nf4/int8/etc.).
|
||
# We only allow fp16/bf16/fp32 here to avoid accidental quantized loading.
|
||
if script_args.bits.lower() not in {"bf16", "fp16", "fp32"}:
|
||
raise ValueError("PSOFT example only supports bits in ['bf16','fp16','fp32'] (no quantization).")
|
||
|
||
torch_dtype = _dtype_from_bits(script_args.bits)
|
||
|
||
# Load base model
|
||
model = AutoModelForCausalLM.from_pretrained(
|
||
script_args.base_model_name_or_path,
|
||
dtype=torch_dtype,
|
||
device_map="auto",
|
||
)
|
||
tokenizer = AutoTokenizer.from_pretrained(script_args.base_model_name_or_path)
|
||
if tokenizer.pad_token_id is None:
|
||
tokenizer.pad_token_id = tokenizer.eos_token_id
|
||
|
||
# Build PSOFT config
|
||
psoft_kwargs = {
|
||
"r": script_args.r,
|
||
"psoft_alpha": script_args.psoft_alpha,
|
||
"target_modules": script_args.target_modules,
|
||
"ab_svd_init": script_args.ab_svd_init,
|
||
"psoft_svd": script_args.psoft_svd,
|
||
"psoft_orth": script_args.psoft_orth,
|
||
"psoft_mag_a": script_args.psoft_mag_a,
|
||
"psoft_mag_b": script_args.psoft_mag_b,
|
||
"use_cayley_neumann": script_args.use_cayley_neumann,
|
||
"num_cayley_neumann_terms": script_args.num_cayley_neumann_terms,
|
||
"cayley_neumann_eps": script_args.cayley_neumann_eps,
|
||
"task_type": "CAUSAL_LM",
|
||
}
|
||
# Only pass lowrank_niter when user sets it (and typically when psoft_svd='lowrank')
|
||
if script_args.psoft_svd_lowrank_niter is not None:
|
||
psoft_kwargs["psoft_svd_lowrank_niter"] = script_args.psoft_svd_lowrank_niter
|
||
|
||
peft_config = PsoftConfig(**psoft_kwargs)
|
||
model = get_peft_model(model, peft_config)
|
||
|
||
model.print_trainable_parameters()
|
||
|
||
# Load dataset
|
||
dataset = load_dataset(script_args.data_path, split=script_args.dataset_split)
|
||
|
||
# Ensure a "text" field for SFTTrainer
|
||
if script_args.dataset_field is not None:
|
||
if len(script_args.dataset_field) != 2:
|
||
raise ValueError("dataset_field must be a list of exactly 2 field names: [input_field, output_field].")
|
||
|
||
in_f, out_f = script_args.dataset_field[0], script_args.dataset_field[1]
|
||
|
||
def to_sft_text(example):
|
||
return {"text": f"### USER: {example[in_f]}\n### ASSISTANT: {example[out_f]}"}
|
||
|
||
dataset = dataset.map(to_sft_text)
|
||
else:
|
||
if "text" not in dataset.column_names:
|
||
raise ValueError("dataset_field is None but dataset has no 'text' column. Provide dataset_field.")
|
||
|
||
# Train
|
||
trainer = SFTTrainer(
|
||
model=model,
|
||
args=script_args,
|
||
train_dataset=dataset,
|
||
processing_class=tokenizer,
|
||
)
|
||
trainer.train()
|
||
trainer.save_state()
|
||
|
||
# Save adapter (PSOFT)
|
||
os.makedirs(script_args.output_dir, exist_ok=True)
|
||
model.save_pretrained(os.path.join(script_args.output_dir, "psoft_ft"))
|
||
tokenizer.save_pretrained(os.path.join(script_args.output_dir, "psoft_ft"))
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|