File size: 13,002 Bytes
aa96560 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 | # =============================================================================
# Full-Parameter SFT: Qwen/Qwen3.5-4B-Base on Bc-AI/SFT-Ultra
# On-the-fly streaming | Bad row filtering | HF Hub upload
# =============================================================================
# Requirements:
# pip install torch transformers datasets trl accelerate huggingface_hub
import os
import torch
from datasets import load_dataset
from transformers import AutoModelForCausalLM, AutoTokenizer
from trl import SFTTrainer, SFTConfig
from huggingface_hub import HfApi, login
# =============================================================================
# 0. CONFIG β Edit these as needed
# =============================================================================
MODEL_ID = "Qwen/Qwen3.5-4B"
DATASET_ID = "Bc-AI/SFT-Ultra"
OUTPUT_DIR = "./qwen3.5-4b-full-sft"
# HF Hub β paste your token when prompted at runtime
HF_REPO_ID = "Bc-AI/qwen3.5-4b-sft" # β change this
HF_TOKEN = "" # Leave None β you will be prompted to paste it below
# Sequence
MAX_SEQ_LENGTH = 2048
# Training hyperparameters
NUM_TRAIN_EPOCHS = 1
PER_DEVICE_TRAIN_BATCH_SIZE = 2
GRADIENT_ACCUMULATION_STEPS = 8
LEARNING_RATE = 1e-5
WEIGHT_DECAY = 0.01
WARMUP_RATIO = 0.05
LR_SCHEDULER = "cosine"
MAX_GRAD_NORM = 1.0
# Logging & saving
LOGGING_STEPS = 10
SAVE_STEPS = 500
SAVE_TOTAL_LIMIT = 3
# Precision β bf16 on Ampere+ (A100, 3090, 4090)
# set bf16=False fp16=True on older GPUs (V100, T4)
USE_BF16 = True
USE_FP16 = False
# Streaming = on-the-fly download, no full disk pre-cache
STREAM_DATASET = True
SEED = 42
# =============================================================================
# 1. HF HUB LOGIN β paste token here at runtime
# =============================================================================
print("=" * 60)
print(" Hugging Face Hub Login")
print("=" * 60)
if HF_TOKEN is None:
HF_TOKEN = input(" Paste your HF token (hf_β¦): ").strip()
login(token=HF_TOKEN, add_to_git_credential=False)
print(" β
Logged in successfully.\n")
# =============================================================================
# 2. TOKENIZER
# =============================================================================
print(f"[1/4] Loading tokenizer: {MODEL_ID}")
tokenizer = AutoTokenizer.from_pretrained(
MODEL_ID,
trust_remote_code=True,
)
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "right"
# =============================================================================
# 3. MODEL β full bf16, no quantisation, no adapter
# =============================================================================
print(f"[2/4] Loading full model: {MODEL_ID}")
model = AutoModelForCausalLM.from_pretrained(
MODEL_ID,
trust_remote_code=True,
torch_dtype=torch.bfloat16 if USE_BF16 else torch.float32,
device_map="auto",
)
model.config.use_cache = False
for param in model.parameters():
param.requires_grad = True
total_params = sum(p.numel() for p in model.parameters())
print(f" Trainable parameters: {total_params:,} ({total_params / 1e9:.2f}B)")
# =============================================================================
# 4. DATASET β streaming + bad row filtering + on-the-fly tokenisation
# =============================================================================
print(f"[3/4] Loading dataset: {DATASET_ID} (streaming={STREAM_DATASET})")
raw_dataset = load_dataset(
DATASET_ID,
split="train",
streaming=STREAM_DATASET,
trust_remote_code=True,
)
# ββ Bad row validator ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Catches every known failure mode so no single row can crash training
REQUIRED_ROLES = {"user", "assistant"} # Minimum roles a valid convo must have
def is_valid_row(example):
"""
Returns True only if the row is safe to train on.
Filters out:
- Missing / non-list messages field
- Empty message list
- Messages with missing role or content keys
- Messages where role or content is not a string
- Messages where content is an empty / whitespace-only string
- Conversations missing at least one user AND one assistant turn
- Rows where the entire rendered text would be empty
"""
try:
messages = example.get("messages", None)
# Must exist and be a non-empty list
if not isinstance(messages, list) or len(messages) == 0:
return False
seen_roles = set()
for msg in messages:
# Each message must be a dict
if not isinstance(msg, dict):
return False
role = msg.get("role", None)
content = msg.get("content", None)
# role and content must be non-empty strings
if not isinstance(role, str) or not role.strip():
return False
if not isinstance(content, str) or not content.strip():
return False
seen_roles.add(role.strip().lower())
# Must have at least one user turn and one assistant turn
if not REQUIRED_ROLES.issubset(seen_roles):
return False
return True
except Exception:
# Catch-all: any unexpected structure is silently dropped
return False
# ββ Safe formatter βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def format_messages(example):
"""
Applies the Qwen3.5 chat template.
Wrapped in try/except so any template rendering failure is handled
gracefully β the row is marked with an empty text field and later dropped.
"""
try:
text = tokenizer.apply_chat_template(
example["messages"],
tokenize=False,
add_generation_prompt=False,
)
# Final safety: rendered text must be non-trivial
if not text or not text.strip():
return {"text": ""}
return {"text": text}
except Exception:
return {"text": ""}
def is_non_empty_text(example):
"""Drop any row where formatting produced an empty string."""
text = example.get("text", "")
return isinstance(text, str) and len(text.strip()) > 0
# ββ Apply pipeline βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
print(" Step 1 β Filtering malformed rows β¦")
clean_dataset = raw_dataset.filter(is_valid_row)
print(" Step 2 β Applying chat template on the fly β¦")
formatted_dataset = clean_dataset.map(format_messages)
print(" Step 3 β Dropping any rows with empty rendered text β¦")
formatted_dataset = formatted_dataset.filter(is_non_empty_text)
print(" β
Dataset pipeline ready.\n")
# =============================================================================
# 5. TRAINER
# =============================================================================
print("[4/4] Configuring SFTTrainer β¦")
sft_config = SFTConfig(
# ββ Output βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
output_dir=OUTPUT_DIR,
# ββ Sequence βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
max_seq_length=MAX_SEQ_LENGTH,
# ββ Training schedule ββββββββββββββββββββββββββββββββββββββββββββββββββββ
num_train_epochs=NUM_TRAIN_EPOCHS,
per_device_train_batch_size=PER_DEVICE_TRAIN_BATCH_SIZE,
gradient_accumulation_steps=GRADIENT_ACCUMULATION_STEPS,
learning_rate=LEARNING_RATE,
weight_decay=WEIGHT_DECAY,
warmup_ratio=WARMUP_RATIO,
lr_scheduler_type=LR_SCHEDULER,
max_grad_norm=MAX_GRAD_NORM,
# ββ Optimizer ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
optim="adamw_torch_fused",
# ββ Precision ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
bf16=USE_BF16,
fp16=USE_FP16,
# ββ Gradient checkpointing βββββββββββββββββββββββββββββββββββββββββββββββ
gradient_checkpointing=True,
gradient_checkpointing_kwargs={"use_reentrant": False},
# ββ Loss βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
completion_only_loss=True,
# ββ Dataset ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
dataset_text_field="text",
dataset_num_proc=1, # Must be 1 for IterableDataset (streaming)
# ββ Logging & checkpointing ββββββββββββββββββββββββββββββββββββββββββββββ
logging_steps=LOGGING_STEPS,
save_steps=SAVE_STEPS,
save_total_limit=SAVE_TOTAL_LIMIT,
report_to="none", # Swap to "wandb" or "tensorboard" if needed
# ββ Misc βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
seed=SEED,
remove_unused_columns=True,
)
trainer = SFTTrainer(
model=model,
args=sft_config,
train_dataset=formatted_dataset,
tokenizer=tokenizer,
)
# =============================================================================
# 6. TRAIN
# =============================================================================
print("\nπ Starting full fine-tuning β¦\n")
trainer.train()
# =============================================================================
# 7. SAVE LOCALLY
# =============================================================================
print(f"\nπΎ Saving full model + tokenizer to: {OUTPUT_DIR}")
trainer.save_model(OUTPUT_DIR)
tokenizer.save_pretrained(OUTPUT_DIR)
print(" β
Local save complete.\n")
# =============================================================================
# 8. PUSH TO HF HUB
# =============================================================================
print(f"βοΈ Uploading to Hugging Face Hub: {HF_REPO_ID}")
print(" (This may take a while depending on your upload speed β¦)\n")
try:
# Push model
model.push_to_hub(
HF_REPO_ID,
token=HF_TOKEN,
commit_message="Full SFT β Qwen3.5-4B-Base on Bc-AI/SFT-Ultra",
private=True, # Set False if you want a public repo
)
# Push tokenizer
tokenizer.push_to_hub(
HF_REPO_ID,
token=HF_TOKEN,
commit_message="Add tokenizer",
)
# Push a minimal model card so the repo is well-documented
api = HfApi()
model_card = f"""---
language:
- en
license: apache-2.0
base_model: {MODEL_ID}
datasets:
- {DATASET_ID}
tags:
- full-fine-tune
- sft
- qwen3.5
---
# Qwen3.5-4B β Full SFT
- **Base model:** `{MODEL_ID}`
- **Dataset:** `{DATASET_ID}`
- **Training type:** Full parameter supervised fine-tuning (no LoRA)
- **Max sequence length:** {MAX_SEQ_LENGTH}
- **Epochs:** {NUM_TRAIN_EPOCHS}
- **Learning rate:** {LEARNING_RATE}
- **Precision:** {"bf16" if USE_BF16 else "fp16"}
"""
api.upload_file(
path_or_fileobj=model_card.encode("utf-8"),
path_in_repo="README.md",
repo_id=HF_REPO_ID,
token=HF_TOKEN,
commit_message="Add model card",
)
print(f"\nβ
Model successfully uploaded to: https://huggingface.co/{HF_REPO_ID}")
except Exception as e:
print(f"\nβ Upload failed: {e}")
print(f" Your model is still saved locally at: {OUTPUT_DIR}")
print(" You can retry the upload manually with:")
print(f" model.push_to_hub('{HF_REPO_ID}')")
print(f" tokenizer.push_to_hub('{HF_REPO_ID}')")
print("\nβ
All done!") |