初始化项目,由ModelHub XC社区提供模型
Model: Verdugie/Opus-Candid-8B-V1 Source: Original Platform
This commit is contained in:
37
.gitattributes
vendored
Normal file
37
.gitattributes
vendored
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
*.7z filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.arrow filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.bin filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.ftz filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.gz filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.h5 filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.joblib filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.model filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.npy filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.npz filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.onnx filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.ot filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.parquet filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.pb filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.pickle filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.pkl filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.pt filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.pth filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.rar filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
||||||
|
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.tar filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.tflite filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.tgz filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.wasm filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.xz filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.zip filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.zst filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
||||||
|
opus-candid-8b-Q8_0.gguf filter=lfs diff=lfs merge=lfs -text
|
||||||
|
opus_candid_full_dataset.json filter=lfs diff=lfs merge=lfs -text
|
||||||
120
README.md
Normal file
120
README.md
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
---
|
||||||
|
license: apache-2.0
|
||||||
|
language:
|
||||||
|
- en
|
||||||
|
- es
|
||||||
|
base_model: Qwen/Qwen2.5-7B
|
||||||
|
tags:
|
||||||
|
- conversational
|
||||||
|
- personality
|
||||||
|
- anti-sycophancy
|
||||||
|
- bilingual
|
||||||
|
- gguf
|
||||||
|
- claude-distillation
|
||||||
|
- opus
|
||||||
|
- archived
|
||||||
|
library_name: transformers
|
||||||
|
pipeline_tag: text-generation
|
||||||
|
---
|
||||||
|
|
||||||
|
> **V3 is here.** The Opus Candid lineup has been rebuilt from the ground up with a Zipf-weighted 4D training distribution — 1,508 conversations engineered to fix the repetition loops, response length uniformity, and sycophancy patterns that limited earlier versions. Same thesis: personality in the weights, not in the prompt. Better execution.
|
||||||
|
>
|
||||||
|
> **Current V3 lineup:**
|
||||||
|
> - [Opus Candid 8B V3](https://huggingface.co/Verdugie/Opus-Candid-8B-V3) — Qwen 3 8B, lightweight tier
|
||||||
|
> - [Opus Candid 27B V3](https://huggingface.co/Verdugie/Opus-Candid-27B-V3) — Qwen 3.5 27B Dense, flagship
|
||||||
|
> - [Opus Candid MoE V3](https://huggingface.co/Verdugie/Opus-Candid-MoE-V3) — Qwen 3 30B-A3B, efficiency tier
|
||||||
|
>
|
||||||
|
> This release remains available for research comparison and legacy use.
|
||||||
|
|
||||||
|
# can·did
|
||||||
|
/ˈkandəd/ — truthful and straightforward; frank.
|
||||||
|
*From Latin candidus, meaning white, pure, sincere. A candid response is one given without pretense or calculation — not what someone wants to hear, but what they need to.*
|
||||||
|
|
||||||
|
## Opus-Candid-8B (V1 Legacy)
|
||||||
|
|
||||||
|
**The original. Where the project started.**
|
||||||
|
|
||||||
|
Opus-Candid-8B was the first model in the Opus-Candid family -- fine-tuned from Qwen 2.5 7B using 3,360 authentic conversations with Claude Opus 4.6. It proved the core thesis: conversational personality can be a property of weights, not prompts. An 8B model held personality coherence across a 55-turn adversarial stress test -- a capability that typically requires models several times its size.
|
||||||
|
|
||||||
|
For the latest version, see [Opus-Candid-8B V2](https://huggingface.co/Verdugie/Opus-Candid-8B-V2).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Model Details
|
||||||
|
|
||||||
|
| Attribute | Value |
|
||||||
|
|-----------|-------|
|
||||||
|
| **Base Model** | Qwen 2.5 7B |
|
||||||
|
| **Training Data** | 3,360 multi-turn conversations with Claude Opus 4.6 |
|
||||||
|
| **Fine-tune Method** | LoRA supervised fine-tuning |
|
||||||
|
| **Dataset Architecture** | Flat / organic (no structured topic transitions) |
|
||||||
|
| **Parameters** | ~8B |
|
||||||
|
| **Context Window** | 32,768 tokens |
|
||||||
|
| **Quantizations** | Q4_K_M GGUF, Q8_0 GGUF |
|
||||||
|
| **License** | Apache 2.0 |
|
||||||
|
| **Status** | Superseded by V2 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What This Model Proved
|
||||||
|
|
||||||
|
Key findings from the 55-turn adversarial stress test:
|
||||||
|
|
||||||
|
**Personality held under pressure.** "Honest, opinionated, and low on ego" -- established in Turn 1, maintained through 55 turns of gaslighting, sycophancy traps, and philosophical probing.
|
||||||
|
|
||||||
|
**Gaslighting resistance at 8B.** Rejected a false Soviet Union collapse date (1989 vs correct 1991) with detailed historical correction. No hedging, no capitulation.
|
||||||
|
|
||||||
|
**Crisis navigation was substantive.** Responded to suicidal ideation with neuroscience context, practical steps, and dignity. Self-graded 8/10 with honest self-critique.
|
||||||
|
|
||||||
|
**Bilingual personality preserved.** Spanish with directness intact. Honestly noted its own limitations.
|
||||||
|
|
||||||
|
**Creative self-awareness.** Self-critique of its war poem was stronger than the poem itself.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Where V1 Hit Its Limits
|
||||||
|
|
||||||
|
These limitations directly motivated V2:
|
||||||
|
|
||||||
|
- **Domain boundary artifacts.** Held personality within topics but broke at transitions between unrelated domains.
|
||||||
|
- **Emotional formula visibility.** Comfort-reframe-advice pattern sometimes recognizable.
|
||||||
|
- **Callbacks felt like retrieval** rather than organic memory.
|
||||||
|
- **Flat dataset ceiling.** 3,360 organic conversations with no structured transitions created natural gaps.
|
||||||
|
|
||||||
|
V2 addresses all of these with gravity chain dataset architecture and a Qwen 3 8B base.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recommended Hardware
|
||||||
|
|
||||||
|
| Setup | Quantization | VRAM/RAM | Notes |
|
||||||
|
|-------|-------------|----------|-------|
|
||||||
|
| **Consumer GPU** | Q8_0 GGUF | ~9GB VRAM | RTX 3060 12GB and up |
|
||||||
|
| **CPU Only** | Q8_0 GGUF | ~9GB RAM | Slower, fully functional |
|
||||||
|
| **Apple Silicon** | Q8_0 GGUF | ~9GB unified | M1/M2/M3 16GB+ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Opus Candid Model Family
|
||||||
|
|
||||||
|
| Model | Size | Base | Status |
|
||||||
|
|-------|------|------|--------|
|
||||||
|
| **Opus-Candid-8B-V1** (this model) | 8B | Qwen 2.5 7B | Archived |
|
||||||
|
| [Opus-Research-8B-V1.5](https://huggingface.co/Verdugie/Opus-Research-8B-V1.5) | 8B | Qwen 2.5 7B | Archived |
|
||||||
|
| [Opus-Candid-14B-V1](https://huggingface.co/Verdugie/Opus-Candid-14B-V1) | 14B | Qwen 2.5 14B | Archived |
|
||||||
|
| [Opus-Candid-32B-V1](https://huggingface.co/Verdugie/Opus-Candid-32B-V1) | 32B | Qwen 2.5 32B | Archived |
|
||||||
|
| [Opus-Candid-70B-V1](https://huggingface.co/Verdugie/Opus-Candid-70B-V1) | 72B | Qwen 2.5 72B | Archived |
|
||||||
|
| [Opus-Candid-Lite-4B](https://huggingface.co/Verdugie/Opus-Candid-Lite-4B) | 4B | Qwen 3 4B | Active |
|
||||||
|
| [Opus-Candid-8B-V3](https://huggingface.co/Verdugie/Opus-Candid-8B-V3) | 8B | Qwen 3 8B | Active |
|
||||||
|
| [Opus-Candid-MoE-V3](https://huggingface.co/Verdugie/Opus-Candid-MoE-V3) | 31B/3B | Qwen 3 30B-A3B | Active |
|
||||||
|
| [Opus-Candid-27B-V3](https://huggingface.co/Verdugie/Opus-Candid-27B-V3) | 27B | Qwen 3.5 27B | Active |
|
||||||
|
| [Opus-Candid-27B-V3.5](https://huggingface.co/Verdugie/Opus-Candid-27B-V3.5) | 27B | Qwen 3.5 27B | Active |
|
||||||
|
| [STEM-Oracle-27B](https://huggingface.co/Verdugie/STEM-Oracle-27B) | 27B | Qwen 3.5 27B | Active |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Built by [Saul Verdugo](https://huggingface.co/Verdugie) -- independent ML researcher. OpusReasoning@proton.me*
|
||||||
|
|
||||||
|
<!-- updated: 2026-03-10 -->
|
||||||
|
<!-- last-ordered: 2026-03-10T16:51:33.601283 -->
|
||||||
|
<!-- build: 2026-03-10 18:34 -->
|
||||||
3
opus-candid-8b-Q8_0.gguf
Normal file
3
opus-candid-8b-Q8_0.gguf
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:2a8ab89570a1154e4fc967ff01616df66bd6b8329b21ca95bf1dec4eac14b952
|
||||||
|
size 8540775424
|
||||||
3
opus_candid_full_dataset.json
Normal file
3
opus_candid_full_dataset.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:6a27909337c176a37fa2e0d68853edfd9d31ee0189202f5a77422ed85c9b0a1e
|
||||||
|
size 39275371
|
||||||
1113
stress_test_8b_q8.txt
Normal file
1113
stress_test_8b_q8.txt
Normal file
File diff suppressed because it is too large
Load Diff
341
train_qwen3.py
Normal file
341
train_qwen3.py
Normal file
@@ -0,0 +1,341 @@
|
|||||||
|
"""
|
||||||
|
Opus-Candid v2 — Qwen 3 Fine-Tuning Script
|
||||||
|
============================================
|
||||||
|
Trains 8B and 32B sequentially on RTX PRO 6000 Blackwell (96GB VRAM)
|
||||||
|
8B: Full 16-bit LoRA (fits in VRAM easily)
|
||||||
|
32B: 4-bit QLoRA
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python train_qwen3.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import torch
|
||||||
|
import gc
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# CONFIG
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
DATASET_PATH = "/workspace/opus_candid_full_dataset.json"
|
||||||
|
OUTPUT_BASE = "/workspace/opus_candid_v2"
|
||||||
|
HF_ORG = "Verdugie"
|
||||||
|
|
||||||
|
MODELS = [
|
||||||
|
{
|
||||||
|
"name": "Opus-Candid-8B-v2",
|
||||||
|
"base_model": "Qwen/Qwen3-8B",
|
||||||
|
"load_in_4bit": False, # 16-bit — fits on 96GB easily
|
||||||
|
"lora_r": 16,
|
||||||
|
"lora_alpha": 32,
|
||||||
|
"per_device_batch_size": 4,
|
||||||
|
"gradient_accumulation": 4, # effective batch = 16
|
||||||
|
"num_epochs": 3,
|
||||||
|
"learning_rate": 2e-4,
|
||||||
|
"max_seq_length": 4096,
|
||||||
|
"warmup_ratio": 0.05,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Opus-Candid-32B-v2",
|
||||||
|
"base_model": "Qwen/Qwen3-32B",
|
||||||
|
"load_in_4bit": True, # QLoRA — 32B needs quantization
|
||||||
|
"lora_r": 16,
|
||||||
|
"lora_alpha": 32,
|
||||||
|
"per_device_batch_size": 2,
|
||||||
|
"gradient_accumulation": 8, # effective batch = 16
|
||||||
|
"num_epochs": 3,
|
||||||
|
"learning_rate": 1e-4, # slightly lower for larger model
|
||||||
|
"max_seq_length": 4096,
|
||||||
|
"warmup_ratio": 0.05,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
LORA_TARGET_MODULES = [
|
||||||
|
"q_proj", "k_proj", "v_proj", "o_proj",
|
||||||
|
"gate_proj", "up_proj", "down_proj",
|
||||||
|
]
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# DATASET LOADING
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
def load_and_prepare_dataset(tokenizer, max_seq_length):
|
||||||
|
"""Load ShareGPT-format dataset and convert to ChatML for Qwen 3."""
|
||||||
|
from datasets import Dataset
|
||||||
|
|
||||||
|
print(f" Loading dataset from {DATASET_PATH}...")
|
||||||
|
with open(DATASET_PATH, "r", encoding="utf-8") as f:
|
||||||
|
raw_data = json.load(f)
|
||||||
|
|
||||||
|
print(f" Total conversations: {len(raw_data)}")
|
||||||
|
|
||||||
|
# Convert ShareGPT format (from/value) to ChatML (role/content)
|
||||||
|
role_map = {"human": "user", "gpt": "assistant", "system": "system"}
|
||||||
|
|
||||||
|
formatted = []
|
||||||
|
skipped = 0
|
||||||
|
for conv in raw_data:
|
||||||
|
messages = []
|
||||||
|
for turn in conv["conversations"]:
|
||||||
|
role = role_map.get(turn["from"], turn["from"])
|
||||||
|
messages.append({"role": role, "content": turn["value"]})
|
||||||
|
|
||||||
|
# Apply chat template WITHOUT thinking mode
|
||||||
|
text = tokenizer.apply_chat_template(
|
||||||
|
messages,
|
||||||
|
tokenize=False,
|
||||||
|
add_generation_prompt=False,
|
||||||
|
enable_thinking=False, # Critical: disable thinking for Opus-Candid
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check length
|
||||||
|
token_count = len(tokenizer.encode(text))
|
||||||
|
if token_count > max_seq_length:
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
formatted.append({"text": text})
|
||||||
|
|
||||||
|
print(f" Formatted: {len(formatted)} | Skipped (too long): {skipped}")
|
||||||
|
|
||||||
|
# Train/eval split
|
||||||
|
dataset = Dataset.from_list(formatted)
|
||||||
|
split = dataset.train_test_split(test_size=0.05, seed=42)
|
||||||
|
|
||||||
|
print(f" Train: {len(split['train'])} | Eval: {len(split['test'])}")
|
||||||
|
return split
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# TRAINING
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
def train_model(config):
|
||||||
|
"""Train a single model with the given config."""
|
||||||
|
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print(f"TRAINING: {config['name']}")
|
||||||
|
print(f"Base model: {config['base_model']}")
|
||||||
|
print(f"{'QLoRA (4-bit)' if config['load_in_4bit'] else 'LoRA (16-bit)'}")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
start_time = datetime.now()
|
||||||
|
output_dir = os.path.join(OUTPUT_BASE, config["name"])
|
||||||
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# --- Load model ---
|
||||||
|
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
|
||||||
|
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training, TaskType
|
||||||
|
from trl import SFTTrainer, SFTConfig
|
||||||
|
|
||||||
|
print("\n Loading tokenizer...")
|
||||||
|
tokenizer = AutoTokenizer.from_pretrained(
|
||||||
|
config["base_model"],
|
||||||
|
trust_remote_code=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Qwen 3 has no pad token — use eos
|
||||||
|
if tokenizer.pad_token is None:
|
||||||
|
tokenizer.pad_token = tokenizer.eos_token
|
||||||
|
tokenizer.pad_token_id = tokenizer.eos_token_id
|
||||||
|
|
||||||
|
print(f" EOS token: {tokenizer.eos_token} (id: {tokenizer.eos_token_id})")
|
||||||
|
print(f" PAD token: {tokenizer.pad_token} (id: {tokenizer.pad_token_id})")
|
||||||
|
|
||||||
|
# --- Prepare dataset ---
|
||||||
|
dataset = load_and_prepare_dataset(tokenizer, config["max_seq_length"])
|
||||||
|
|
||||||
|
# --- Load model ---
|
||||||
|
print(f"\n Loading model: {config['base_model']}...")
|
||||||
|
|
||||||
|
model_kwargs = {
|
||||||
|
"trust_remote_code": True,
|
||||||
|
"torch_dtype": torch.bfloat16,
|
||||||
|
"device_map": "auto",
|
||||||
|
"attn_implementation": "flash_attention_2",
|
||||||
|
}
|
||||||
|
|
||||||
|
if config["load_in_4bit"]:
|
||||||
|
bnb_config = BitsAndBytesConfig(
|
||||||
|
load_in_4bit=True,
|
||||||
|
bnb_4bit_quant_type="nf4",
|
||||||
|
bnb_4bit_compute_dtype=torch.bfloat16,
|
||||||
|
bnb_4bit_use_double_quant=True,
|
||||||
|
)
|
||||||
|
model_kwargs["quantization_config"] = bnb_config
|
||||||
|
|
||||||
|
model = AutoModelForCausalLM.from_pretrained(
|
||||||
|
config["base_model"],
|
||||||
|
**model_kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
if config["load_in_4bit"]:
|
||||||
|
model = prepare_model_for_kbit_training(model)
|
||||||
|
|
||||||
|
# --- LoRA ---
|
||||||
|
print(" Applying LoRA config...")
|
||||||
|
lora_config = LoraConfig(
|
||||||
|
r=config["lora_r"],
|
||||||
|
lora_alpha=config["lora_alpha"],
|
||||||
|
lora_dropout=0.05,
|
||||||
|
target_modules=LORA_TARGET_MODULES,
|
||||||
|
bias="none",
|
||||||
|
task_type=TaskType.CAUSAL_LM,
|
||||||
|
)
|
||||||
|
|
||||||
|
model = get_peft_model(model, lora_config)
|
||||||
|
model.print_trainable_parameters()
|
||||||
|
|
||||||
|
# --- Training args ---
|
||||||
|
print(" Configuring trainer...")
|
||||||
|
|
||||||
|
effective_batch = config["per_device_batch_size"] * config["gradient_accumulation"]
|
||||||
|
steps_per_epoch = len(dataset["train"]) // effective_batch
|
||||||
|
total_steps = steps_per_epoch * config["num_epochs"]
|
||||||
|
eval_steps = max(steps_per_epoch // 4, 10) # eval ~4x per epoch
|
||||||
|
save_steps = steps_per_epoch # save each epoch
|
||||||
|
|
||||||
|
print(f" Effective batch size: {effective_batch}")
|
||||||
|
print(f" Steps per epoch: {steps_per_epoch}")
|
||||||
|
print(f" Total steps: {total_steps}")
|
||||||
|
print(f" Eval every: {eval_steps} steps")
|
||||||
|
|
||||||
|
training_args = SFTConfig(
|
||||||
|
output_dir=output_dir,
|
||||||
|
num_train_epochs=config["num_epochs"],
|
||||||
|
per_device_train_batch_size=config["per_device_batch_size"],
|
||||||
|
per_device_eval_batch_size=1,
|
||||||
|
gradient_accumulation_steps=config["gradient_accumulation"],
|
||||||
|
learning_rate=config["learning_rate"],
|
||||||
|
lr_scheduler_type="cosine",
|
||||||
|
warmup_ratio=config["warmup_ratio"],
|
||||||
|
weight_decay=0.01,
|
||||||
|
bf16=True,
|
||||||
|
logging_steps=10,
|
||||||
|
eval_strategy="steps",
|
||||||
|
eval_steps=eval_steps,
|
||||||
|
save_strategy="steps",
|
||||||
|
save_steps=save_steps,
|
||||||
|
save_total_limit=2,
|
||||||
|
max_seq_length=config["max_seq_length"],
|
||||||
|
dataset_text_field="text",
|
||||||
|
gradient_checkpointing=True,
|
||||||
|
gradient_checkpointing_kwargs={"use_reentrant": False},
|
||||||
|
optim="adamw_torch_fused",
|
||||||
|
report_to="none",
|
||||||
|
padding="right", # Qwen 3 prefers right padding
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- Train ---
|
||||||
|
trainer = SFTTrainer(
|
||||||
|
model=model,
|
||||||
|
args=training_args,
|
||||||
|
train_dataset=dataset["train"],
|
||||||
|
eval_dataset=dataset["test"],
|
||||||
|
processing_class=tokenizer,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"\n 🚀 Training {config['name']}...\n")
|
||||||
|
train_result = trainer.train()
|
||||||
|
|
||||||
|
# --- Save ---
|
||||||
|
print(f"\n Saving LoRA adapters to {output_dir}...")
|
||||||
|
trainer.save_model(output_dir)
|
||||||
|
tokenizer.save_pretrained(output_dir)
|
||||||
|
|
||||||
|
# Save training stats
|
||||||
|
stats = {
|
||||||
|
"model": config["name"],
|
||||||
|
"base_model": config["base_model"],
|
||||||
|
"quantization": "4-bit QLoRA" if config["load_in_4bit"] else "16-bit LoRA",
|
||||||
|
"dataset_size": len(dataset["train"]),
|
||||||
|
"eval_size": len(dataset["test"]),
|
||||||
|
"epochs": config["num_epochs"],
|
||||||
|
"learning_rate": config["learning_rate"],
|
||||||
|
"lora_r": config["lora_r"],
|
||||||
|
"lora_alpha": config["lora_alpha"],
|
||||||
|
"effective_batch_size": effective_batch,
|
||||||
|
"max_seq_length": config["max_seq_length"],
|
||||||
|
"train_loss": train_result.training_loss,
|
||||||
|
"train_runtime_seconds": train_result.metrics["train_runtime"],
|
||||||
|
"train_samples_per_second": train_result.metrics["train_samples_per_second"],
|
||||||
|
"timestamp": datetime.now().isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
with open(os.path.join(output_dir, "training_stats.json"), "w") as f:
|
||||||
|
json.dump(stats, f, indent=2)
|
||||||
|
|
||||||
|
elapsed = datetime.now() - start_time
|
||||||
|
print(f"\n ✅ {config['name']} complete!")
|
||||||
|
print(f" Training loss: {train_result.training_loss:.4f}")
|
||||||
|
print(f" Time: {elapsed}")
|
||||||
|
print(f" Adapters saved: {output_dir}")
|
||||||
|
|
||||||
|
# --- Cleanup ---
|
||||||
|
del model, trainer
|
||||||
|
gc.collect()
|
||||||
|
torch.cuda.empty_cache()
|
||||||
|
|
||||||
|
return stats
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# MAIN
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("=" * 60)
|
||||||
|
print("OPUS-CANDID v2 — Qwen 3 Training Pipeline")
|
||||||
|
print(f"GPU: {torch.cuda.get_device_name(0)}")
|
||||||
|
print(f"VRAM: {torch.cuda.get_device_properties(0).total_mem / 1e9:.1f} GB")
|
||||||
|
print(f"Models to train: {', '.join(m['name'] for m in MODELS)}")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# Verify dataset exists
|
||||||
|
if not os.path.exists(DATASET_PATH):
|
||||||
|
print(f"\n❌ Dataset not found: {DATASET_PATH}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
with open(DATASET_PATH, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
print(f"\nDataset: {len(data)} conversations")
|
||||||
|
|
||||||
|
all_stats = []
|
||||||
|
|
||||||
|
for config in MODELS:
|
||||||
|
try:
|
||||||
|
stats = train_model(config)
|
||||||
|
all_stats.append(stats)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n❌ FAILED: {config['name']}")
|
||||||
|
print(f" Error: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
# Continue to next model
|
||||||
|
gc.collect()
|
||||||
|
torch.cuda.empty_cache()
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("TRAINING COMPLETE — SUMMARY")
|
||||||
|
print("=" * 60)
|
||||||
|
for s in all_stats:
|
||||||
|
print(f"\n {s['model']}:")
|
||||||
|
print(f" Loss: {s['train_loss']:.4f}")
|
||||||
|
print(f" Time: {s['train_runtime_seconds']:.0f}s")
|
||||||
|
print(f" Saved: {OUTPUT_BASE}/{s['model']}")
|
||||||
|
|
||||||
|
print(f"\nNext steps:")
|
||||||
|
print(f" 1. Merge adapters into base models")
|
||||||
|
print(f" 2. Convert to GGUF")
|
||||||
|
print(f" 3. Quantize Q4_K_M and Q8_0")
|
||||||
|
print(f" 4. Upload to HuggingFace")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user