初始化项目,由ModelHub XC社区提供模型
Model: AksaraLLM/Kiel-Pro-0.5B-v3-chat Source: Original Platform
This commit is contained in:
155
train_identity_lora.py
Normal file
155
train_identity_lora.py
Normal file
@@ -0,0 +1,155 @@
|
||||
#!/usr/bin/env python3
|
||||
"""CPU LoRA SFT for identity calibration on AksaraLLM/Kiel-Pro-0.5B-v3.
|
||||
|
||||
- Loads base in bf16.
|
||||
- LoRA r=8, alpha=16, on q_proj, k_proj, v_proj, o_proj.
|
||||
- 50 identity prompts, 3 epochs, batch 1 with grad-accum 4.
|
||||
- Uses Qwen2 ChatML template.
|
||||
"""
|
||||
import os, sys, json, math, time, shutil
|
||||
from pathlib import Path
|
||||
import torch
|
||||
from torch.utils.data import Dataset
|
||||
from transformers import (
|
||||
AutoTokenizer, AutoModelForCausalLM, Trainer, TrainingArguments,
|
||||
DataCollatorForLanguageModeling, set_seed,
|
||||
)
|
||||
from peft import LoraConfig, get_peft_model, TaskType
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
from identity_data import get_dataset
|
||||
|
||||
BASE = 'AksaraLLM/Kiel-Pro-0.5B-v3'
|
||||
OUT = Path('/home/ubuntu/aksara_audit/sft/kiel-pro-0.5b-v3-identity')
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
SEED = 7
|
||||
set_seed(SEED)
|
||||
|
||||
# Qwen2 ChatML template pieces
|
||||
SYS = "Kamu adalah Kiel-Pro, model bahasa Indonesia dari proyek AksaraLLM."
|
||||
|
||||
|
||||
def format_chatml(system: str, user: str, assistant: str) -> str:
|
||||
return (
|
||||
f"<|im_start|>system\n{system}<|im_end|>\n"
|
||||
f"<|im_start|>user\n{user}<|im_end|>\n"
|
||||
f"<|im_start|>assistant\n{assistant}<|im_end|>\n"
|
||||
)
|
||||
|
||||
|
||||
class IdentityDS(Dataset):
|
||||
def __init__(self, tok, max_len=256):
|
||||
data = get_dataset()
|
||||
self.items = []
|
||||
for q, a in data:
|
||||
text = format_chatml(SYS, q, a)
|
||||
enc = tok(text, truncation=True, max_length=max_len, padding='max_length')
|
||||
ids = enc['input_ids']
|
||||
att = enc['attention_mask']
|
||||
# Mask loss on prompt portion — we only want to teach the assistant response.
|
||||
prompt = (
|
||||
f"<|im_start|>system\n{SYS}<|im_end|>\n"
|
||||
f"<|im_start|>user\n{q}<|im_end|>\n"
|
||||
f"<|im_start|>assistant\n"
|
||||
)
|
||||
prompt_len = len(tok(prompt, truncation=True, max_length=max_len)['input_ids'])
|
||||
labels = list(ids)
|
||||
for i in range(min(prompt_len, len(labels))):
|
||||
labels[i] = -100
|
||||
# also mask pad
|
||||
for i, m in enumerate(att):
|
||||
if m == 0:
|
||||
labels[i] = -100
|
||||
self.items.append({
|
||||
'input_ids': torch.tensor(ids, dtype=torch.long),
|
||||
'attention_mask': torch.tensor(att, dtype=torch.long),
|
||||
'labels': torch.tensor(labels, dtype=torch.long),
|
||||
})
|
||||
|
||||
def __len__(self):
|
||||
return len(self.items)
|
||||
|
||||
def __getitem__(self, idx):
|
||||
return self.items[idx]
|
||||
|
||||
|
||||
def main():
|
||||
print('loading tokenizer...', flush=True)
|
||||
tok = AutoTokenizer.from_pretrained(BASE, token=os.environ['HF_TOKEN'])
|
||||
if tok.pad_token is None:
|
||||
tok.pad_token = tok.eos_token
|
||||
|
||||
print('loading model (fp32, CPU; AVX2 only here)...', flush=True)
|
||||
t0 = time.time()
|
||||
model = AutoModelForCausalLM.from_pretrained(
|
||||
BASE,
|
||||
dtype=torch.float32,
|
||||
low_cpu_mem_usage=True,
|
||||
token=os.environ['HF_TOKEN'],
|
||||
)
|
||||
model.config.use_cache = False
|
||||
print(f' loaded in {time.time()-t0:.1f}s, {sum(p.numel() for p in model.parameters())/1e6:.1f}M params', flush=True)
|
||||
|
||||
lora_cfg = LoraConfig(
|
||||
task_type=TaskType.CAUSAL_LM,
|
||||
r=8,
|
||||
lora_alpha=16,
|
||||
lora_dropout=0.05,
|
||||
target_modules=['q_proj', 'k_proj', 'v_proj', 'o_proj'],
|
||||
bias='none',
|
||||
)
|
||||
model = get_peft_model(model, lora_cfg)
|
||||
model.print_trainable_parameters()
|
||||
|
||||
# Small max_len because identity answers are short.
|
||||
ds = IdentityDS(tok, max_len=128)
|
||||
print(f'dataset: {len(ds)} examples', flush=True)
|
||||
|
||||
args = TrainingArguments(
|
||||
output_dir=str(OUT / 'runs'),
|
||||
per_device_train_batch_size=1,
|
||||
gradient_accumulation_steps=2,
|
||||
num_train_epochs=2,
|
||||
learning_rate=2e-4,
|
||||
warmup_ratio=0.1,
|
||||
lr_scheduler_type='cosine',
|
||||
logging_steps=1,
|
||||
save_steps=500,
|
||||
save_total_limit=1,
|
||||
report_to='none',
|
||||
bf16=False, # CPU bf16 autocast via model dtype
|
||||
fp16=False,
|
||||
remove_unused_columns=False,
|
||||
seed=SEED,
|
||||
dataloader_num_workers=0,
|
||||
)
|
||||
|
||||
trainer = Trainer(
|
||||
model=model,
|
||||
args=args,
|
||||
train_dataset=ds,
|
||||
processing_class=tok,
|
||||
)
|
||||
print('starting LoRA training on CPU...', flush=True)
|
||||
t0 = time.time()
|
||||
trainer.train()
|
||||
print(f'done in {(time.time()-t0)/60:.1f} min', flush=True)
|
||||
|
||||
# Save LoRA
|
||||
lora_path = OUT / 'lora'
|
||||
model.save_pretrained(str(lora_path))
|
||||
tok.save_pretrained(str(lora_path))
|
||||
print(f'saved LoRA to {lora_path}', flush=True)
|
||||
|
||||
# Merge + save full model
|
||||
print('merging LoRA into base...', flush=True)
|
||||
merged = model.merge_and_unload()
|
||||
merged_path = OUT / 'merged'
|
||||
merged.save_pretrained(str(merged_path), safe_serialization=True)
|
||||
tok.save_pretrained(str(merged_path))
|
||||
print(f'saved merged model to {merged_path}', flush=True)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user