| |
| |
| |
| |
| |
|
|
| |
|
|
| 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 |
|
|
| |
| |
| |
| MODEL_ID = "Qwen/Qwen3.5-4B" |
| DATASET_ID = "Bc-AI/SFT-Ultra" |
| OUTPUT_DIR = "./qwen3.5-4b-full-sft" |
|
|
| |
| HF_REPO_ID = "Bc-AI/qwen3.5-4b-sft" |
| HF_TOKEN = "" |
|
|
| |
| MAX_SEQ_LENGTH = 2048 |
|
|
| |
| 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_STEPS = 10 |
| SAVE_STEPS = 500 |
| SAVE_TOTAL_LIMIT = 3 |
|
|
| |
| |
| USE_BF16 = True |
| USE_FP16 = False |
|
|
| |
| STREAM_DATASET = True |
|
|
| SEED = 42 |
|
|
| |
| |
| |
| 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") |
|
|
| |
| |
| |
| 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" |
|
|
| |
| |
| |
| 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)") |
|
|
| |
| |
| |
| 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, |
| ) |
|
|
| |
| |
|
|
| REQUIRED_ROLES = {"user", "assistant"} |
|
|
| 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) |
|
|
| |
| if not isinstance(messages, list) or len(messages) == 0: |
| return False |
|
|
| seen_roles = set() |
| for msg in messages: |
| |
| if not isinstance(msg, dict): |
| return False |
|
|
| role = msg.get("role", None) |
| content = msg.get("content", None) |
|
|
| |
| 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()) |
|
|
| |
| if not REQUIRED_ROLES.issubset(seen_roles): |
| return False |
|
|
| return True |
|
|
| except Exception: |
| |
| return False |
|
|
|
|
| |
|
|
| 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, |
| ) |
| |
| 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 |
|
|
|
|
| |
| 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") |
|
|
| |
| |
| |
| print("[4/4] Configuring SFTTrainer β¦") |
|
|
| sft_config = SFTConfig( |
| |
| output_dir=OUTPUT_DIR, |
|
|
| |
| max_seq_length=MAX_SEQ_LENGTH, |
|
|
| |
| 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, |
|
|
| |
| optim="adamw_torch_fused", |
|
|
| |
| bf16=USE_BF16, |
| fp16=USE_FP16, |
|
|
| |
| gradient_checkpointing=True, |
| gradient_checkpointing_kwargs={"use_reentrant": False}, |
|
|
| |
| completion_only_loss=True, |
|
|
| |
| dataset_text_field="text", |
| dataset_num_proc=1, |
|
|
| |
| logging_steps=LOGGING_STEPS, |
| save_steps=SAVE_STEPS, |
| save_total_limit=SAVE_TOTAL_LIMIT, |
| report_to="none", |
|
|
| |
| seed=SEED, |
| remove_unused_columns=True, |
| ) |
|
|
| trainer = SFTTrainer( |
| model=model, |
| args=sft_config, |
| train_dataset=formatted_dataset, |
| tokenizer=tokenizer, |
| ) |
|
|
| |
| |
| |
| print("\nπ Starting full fine-tuning β¦\n") |
| trainer.train() |
|
|
| |
| |
| |
| 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") |
|
|
| |
| |
| |
| print(f"βοΈ Uploading to Hugging Face Hub: {HF_REPO_ID}") |
| print(" (This may take a while depending on your upload speed β¦)\n") |
|
|
| try: |
| |
| 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, |
| ) |
|
|
| |
| tokenizer.push_to_hub( |
| HF_REPO_ID, |
| token=HF_TOKEN, |
| commit_message="Add tokenizer", |
| ) |
|
|
| |
| 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!") |