Spaces:
Sleeping
Sleeping
Remove hf_space_a100 directory - training dashboard moved to separate space
Browse files- hf_space_a100/.python-version +0 -1
- hf_space_a100/Dockerfile +0 -56
- hf_space_a100/README.md +0 -13
- hf_space_a100/app.py +0 -117
- hf_space_a100/requirements.txt +0 -6
- hf_space_a100/train_script.py +0 -362
hf_space_a100/.python-version
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
3.11
|
|
|
|
|
|
hf_space_a100/Dockerfile
DELETED
|
@@ -1,56 +0,0 @@
|
|
| 1 |
-
FROM python:3.11-slim
|
| 2 |
-
|
| 3 |
-
# Install system dependencies
|
| 4 |
-
RUN apt-get update && apt-get install -y \
|
| 5 |
-
git \
|
| 6 |
-
build-essential \
|
| 7 |
-
&& rm -rf /var/lib/apt/lists/*
|
| 8 |
-
|
| 9 |
-
# Set up a new user named "user" with user ID 1000
|
| 10 |
-
RUN useradd -m -u 1000 user
|
| 11 |
-
USER user
|
| 12 |
-
ENV HOME=/home/user \
|
| 13 |
-
PATH=/home/user/.local/bin:$PATH
|
| 14 |
-
WORKDIR $HOME/app
|
| 15 |
-
|
| 16 |
-
# 1. Install the entire "Gold Standard" ML stack in one go.
|
| 17 |
-
# This includes all vision/audio/quantization/acceleration tools to prevent ANY further 'ModuleNotFound' errors.
|
| 18 |
-
RUN pip install --no-cache-dir \
|
| 19 |
-
"torch==2.5.0" \
|
| 20 |
-
"torchvision==0.20.0" \
|
| 21 |
-
"torchaudio==2.5.0" \
|
| 22 |
-
"triton==3.1.0" \
|
| 23 |
-
"xformers==0.0.28.post2" \
|
| 24 |
-
"accelerate" \
|
| 25 |
-
"peft" \
|
| 26 |
-
"bitsandbytes" \
|
| 27 |
-
"sentencepiece" \
|
| 28 |
-
"protobuf" \
|
| 29 |
-
"transformers>=4.46.0" \
|
| 30 |
-
"trl>=0.12.0" \
|
| 31 |
-
"datasets" \
|
| 32 |
-
"huggingface_hub" \
|
| 33 |
-
"matplotlib" \
|
| 34 |
-
"scipy" \
|
| 35 |
-
--extra-index-url https://download.pytorch.org/whl/cu121
|
| 36 |
-
|
| 37 |
-
# 2. Install Unsloth components (must be after torch stack)
|
| 38 |
-
RUN pip install --no-cache-dir \
|
| 39 |
-
"unsloth_zoo @ git+https://github.com/unslothai/unsloth-zoo.git" \
|
| 40 |
-
"unsloth @ git+https://github.com/unslothai/unsloth.git"
|
| 41 |
-
|
| 42 |
-
# 3. CRITICAL: Uninstall torchao completely to bypass the 'int1' bug
|
| 43 |
-
RUN pip uninstall -y torchao
|
| 44 |
-
|
| 45 |
-
# 4. Install Gradio and remaining UI needs
|
| 46 |
-
COPY --chown=user requirements.txt $HOME/app/requirements.txt
|
| 47 |
-
RUN pip install --no-cache-dir -r $HOME/app/requirements.txt
|
| 48 |
-
|
| 49 |
-
# Copy application files
|
| 50 |
-
COPY --chown=user . $HOME/app
|
| 51 |
-
|
| 52 |
-
# Expose port
|
| 53 |
-
EXPOSE 7860
|
| 54 |
-
|
| 55 |
-
# Start the application
|
| 56 |
-
CMD ["python", "app.py"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hf_space_a100/README.md
DELETED
|
@@ -1,13 +0,0 @@
|
|
| 1 |
-
---
|
| 2 |
-
title: ConflictBench GRPO (A100)
|
| 3 |
-
emoji: ⚔️
|
| 4 |
-
colorFrom: indigo
|
| 5 |
-
colorTo: purple
|
| 6 |
-
sdk: docker
|
| 7 |
-
pinned: false
|
| 8 |
-
---
|
| 9 |
-
|
| 10 |
-
# ConflictBench GRPO Training Pipeline
|
| 11 |
-
|
| 12 |
-
One-click automated GRPO training pipeline tailored for an **NVIDIA A100** GPU.
|
| 13 |
-
This pipeline automatically clones the ConflictBench environment, generates scenarios based on a defined curriculum, runs GRPO fine-tuning using Unsloth, generates loss/reward plots, and pushes the final LoRA weights to the Hugging Face Hub.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hf_space_a100/app.py
DELETED
|
@@ -1,117 +0,0 @@
|
|
| 1 |
-
import gradio as gr
|
| 2 |
-
import threading
|
| 3 |
-
import time
|
| 4 |
-
import queue
|
| 5 |
-
import os
|
| 6 |
-
|
| 7 |
-
LOG_QUEUE = queue.Queue()
|
| 8 |
-
TRAINING_ACTIVE = False
|
| 9 |
-
PLOT_PATHS = []
|
| 10 |
-
|
| 11 |
-
def log_callback(msg):
|
| 12 |
-
LOG_QUEUE.put(f"[{time.strftime('%H:%M:%S')}] {msg}")
|
| 13 |
-
|
| 14 |
-
def start_training():
|
| 15 |
-
global TRAINING_ACTIVE, PLOT_PATHS
|
| 16 |
-
if TRAINING_ACTIVE:
|
| 17 |
-
yield "⚠️ Training already running...", *[None]*4
|
| 18 |
-
return
|
| 19 |
-
|
| 20 |
-
TRAINING_ACTIVE = True
|
| 21 |
-
PLOT_PATHS.clear()
|
| 22 |
-
|
| 23 |
-
def run():
|
| 24 |
-
try:
|
| 25 |
-
from train_script import run_training
|
| 26 |
-
paths = run_training(progress_callback=log_callback)
|
| 27 |
-
PLOT_PATHS.extend(paths or [])
|
| 28 |
-
log_callback("🎉 Done! Check plots below.")
|
| 29 |
-
except Exception as e:
|
| 30 |
-
log_callback(f"❌ Training failed: {e}")
|
| 31 |
-
import traceback
|
| 32 |
-
log_callback(traceback.format_exc())
|
| 33 |
-
finally:
|
| 34 |
-
global TRAINING_ACTIVE
|
| 35 |
-
TRAINING_ACTIVE = False
|
| 36 |
-
LOG_QUEUE.put("<<FINISHED>>")
|
| 37 |
-
|
| 38 |
-
thread = threading.Thread(target=run, daemon=True)
|
| 39 |
-
thread.start()
|
| 40 |
-
|
| 41 |
-
logs = []
|
| 42 |
-
while thread.is_alive() or not LOG_QUEUE.empty():
|
| 43 |
-
try:
|
| 44 |
-
msg = LOG_QUEUE.get(timeout=1.0)
|
| 45 |
-
if msg == "<<FINISHED>>":
|
| 46 |
-
continue
|
| 47 |
-
logs.append(msg)
|
| 48 |
-
if len(logs) > 300:
|
| 49 |
-
logs = logs[-300:]
|
| 50 |
-
|
| 51 |
-
# Prepare UI outputs
|
| 52 |
-
current_log_text = "\n".join(logs)
|
| 53 |
-
imgs = [p if os.path.exists(p) else None for p in PLOT_PATHS[:4]]
|
| 54 |
-
while len(imgs) < 4:
|
| 55 |
-
imgs.append(None)
|
| 56 |
-
|
| 57 |
-
yield current_log_text, *imgs
|
| 58 |
-
except queue.Empty:
|
| 59 |
-
# Yield current state even if no new messages
|
| 60 |
-
current_log_text = "\n".join(logs)
|
| 61 |
-
imgs = [p if os.path.exists(p) else None for p in PLOT_PATHS[:4]]
|
| 62 |
-
while len(imgs) < 4:
|
| 63 |
-
imgs.append(None)
|
| 64 |
-
yield current_log_text, *imgs
|
| 65 |
-
|
| 66 |
-
CSS = """
|
| 67 |
-
.main-title { text-align: center; margin-bottom: 0.5em; }
|
| 68 |
-
.log-box textarea { font-family: 'JetBrains Mono', 'Fira Code', monospace !important;
|
| 69 |
-
font-size: 12px !important; background: #0d1117 !important;
|
| 70 |
-
color: #c9d1d9 !important; }
|
| 71 |
-
"""
|
| 72 |
-
|
| 73 |
-
with gr.Blocks(title="ConflictBench GRPO Trainer (A100)") as demo:
|
| 74 |
-
gr.Markdown("# ⚔️ ConflictBench — GRPO Training Dashboard (A100 Target)", elem_classes="main-title")
|
| 75 |
-
gr.Markdown("**One-click** production GRPO training script mapped to Run 2 parameters. "
|
| 76 |
-
"Automatically streams logs and generates plots.")
|
| 77 |
-
|
| 78 |
-
with gr.Row():
|
| 79 |
-
with gr.Column(scale=1):
|
| 80 |
-
gr.Markdown("### ⚙️ Run 2 Configuration")
|
| 81 |
-
gr.Markdown(f"""
|
| 82 |
-
| Parameter | Value |
|
| 83 |
-
|-----------|-------|
|
| 84 |
-
| Model | Qwen2.5-3B-Instruct |
|
| 85 |
-
| Scenarios | 600 train / 30 eval |
|
| 86 |
-
| Curriculum | 60% Diff-1 / 40% Diff-2 |
|
| 87 |
-
| Global Batch | 8 (1 * 8 accum) |
|
| 88 |
-
| Generations | 8 |
|
| 89 |
-
| Max Output | 768 tokens |
|
| 90 |
-
| LoRA rank | 16 |
|
| 91 |
-
| Epochs | 3 |
|
| 92 |
-
| β (KL) | 0.04 |
|
| 93 |
-
| LR | 3e-6 |
|
| 94 |
-
""")
|
| 95 |
-
start_btn = gr.Button("🚀 Start Training", variant="primary", size="lg")
|
| 96 |
-
|
| 97 |
-
with gr.Column(scale=2):
|
| 98 |
-
gr.Markdown("### 📋 Live Training Logs")
|
| 99 |
-
log_box = gr.Textbox(label="", lines=25, max_lines=25, interactive=False,
|
| 100 |
-
elem_classes="log-box")
|
| 101 |
-
|
| 102 |
-
gr.Markdown("---")
|
| 103 |
-
gr.Markdown("### 📊 Training Plots")
|
| 104 |
-
with gr.Row():
|
| 105 |
-
plot1 = gr.Image(label="Reward Curve", type="filepath")
|
| 106 |
-
plot2 = gr.Image(label="Loss Curve", type="filepath")
|
| 107 |
-
with gr.Row():
|
| 108 |
-
plot3 = gr.Image(label="KL Divergence", type="filepath")
|
| 109 |
-
plot4 = gr.Image(label="Training Dashboard", type="filepath")
|
| 110 |
-
|
| 111 |
-
# Start training and stream outputs to logs and plots
|
| 112 |
-
start_btn.click(
|
| 113 |
-
fn=start_training,
|
| 114 |
-
outputs=[log_box, plot1, plot2, plot3, plot4]
|
| 115 |
-
)
|
| 116 |
-
|
| 117 |
-
demo.launch(server_name="0.0.0.0", server_port=7860, theme=gr.themes.Soft(), css=CSS, ssr_mode=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hf_space_a100/requirements.txt
DELETED
|
@@ -1,6 +0,0 @@
|
|
| 1 |
-
gradio>=4.0.0
|
| 2 |
-
transformers
|
| 3 |
-
trl
|
| 4 |
-
datasets
|
| 5 |
-
huggingface_hub
|
| 6 |
-
matplotlib
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hf_space_a100/train_script.py
DELETED
|
@@ -1,362 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
ConflictBench — GRPO Training Script for HF Space (A100 Target)
|
| 3 |
-
Tailored for NVIDIA A100 (48GB VRAM)
|
| 4 |
-
"""
|
| 5 |
-
|
| 6 |
-
import os
|
| 7 |
-
import sys
|
| 8 |
-
from unsloth import FastLanguageModel # Must be before transformers
|
| 9 |
-
import json
|
| 10 |
-
import random
|
| 11 |
-
import logging
|
| 12 |
-
import time
|
| 13 |
-
import subprocess
|
| 14 |
-
import shutil
|
| 15 |
-
from pathlib import Path
|
| 16 |
-
from typing import List, Dict, Any, Tuple
|
| 17 |
-
from datetime import datetime
|
| 18 |
-
|
| 19 |
-
import torch
|
| 20 |
-
from datasets import Dataset
|
| 21 |
-
from transformers import TrainerCallback
|
| 22 |
-
|
| 23 |
-
# Setup Paths
|
| 24 |
-
REPO_URL = "https://github.com/Harsh-4210/Conflict_Bench.git"
|
| 25 |
-
REPO_DIR = Path("/tmp/conflictbench_repo")
|
| 26 |
-
OUTPUT_DIR = Path("./grpo-out")
|
| 27 |
-
PLOTS_DIR = OUTPUT_DIR / "plots"
|
| 28 |
-
|
| 29 |
-
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
| 30 |
-
log = logging.getLogger("train")
|
| 31 |
-
|
| 32 |
-
def clone_repo():
|
| 33 |
-
"""Clone the ConflictBench repo if not already present."""
|
| 34 |
-
if REPO_DIR.exists():
|
| 35 |
-
log.info("Repo already cloned, pulling latest...")
|
| 36 |
-
try:
|
| 37 |
-
subprocess.run(["git", "-C", str(REPO_DIR), "pull"], check=True)
|
| 38 |
-
except:
|
| 39 |
-
log.warning("Pull failed, deleting and re-cloning...")
|
| 40 |
-
shutil.rmtree(REPO_DIR)
|
| 41 |
-
subprocess.run(["git", "clone", REPO_URL, str(REPO_DIR)], check=True)
|
| 42 |
-
else:
|
| 43 |
-
log.info(f"Cloning {REPO_URL}...")
|
| 44 |
-
subprocess.run(["git", "clone", REPO_URL, str(REPO_DIR)], check=True)
|
| 45 |
-
|
| 46 |
-
if str(REPO_DIR) not in sys.path:
|
| 47 |
-
sys.path.insert(0, str(REPO_DIR))
|
| 48 |
-
log.info("✅ Repo ready")
|
| 49 |
-
|
| 50 |
-
# ── Config (Run 2 Parameters) ────────────────────────────────────────────────
|
| 51 |
-
|
| 52 |
-
TRAIN_SCENARIOS = 300 # ↓ was 600
|
| 53 |
-
EVAL_SCENARIOS = 10 # ↓ was 30
|
| 54 |
-
SEED = 42
|
| 55 |
-
|
| 56 |
-
DIFFICULTY_TRAIN_WEIGHTS = {1: 0.60, 2: 0.40}
|
| 57 |
-
DIFFICULTY_EVAL_WEIGHTS = {1: 0.40, 2: 0.40, 3: 0.20}
|
| 58 |
-
|
| 59 |
-
NUM_EPOCHS = 2 # ↓ was 3
|
| 60 |
-
BATCH_SIZE = 1
|
| 61 |
-
GRADIENT_ACCUM = 4 # ↓ was 8
|
| 62 |
-
NUM_GENERATIONS = 4 # ↓ was 8 ← BIGGEST SPEED WIN
|
| 63 |
-
MAX_NEW_TOKENS = 512 # ↓ was 768
|
| 64 |
-
MAX_PROMPT_CHARS = 2800
|
| 65 |
-
LEARNING_RATE = 3e-6
|
| 66 |
-
WARMUP_STEPS = 20
|
| 67 |
-
BETA = 0.04
|
| 68 |
-
TEMPERATURE = 0.85
|
| 69 |
-
TOP_P = 0.92
|
| 70 |
-
|
| 71 |
-
HF_REPO_ID = os.getenv("HF_REPO_ID", None)
|
| 72 |
-
HF_TOKEN = os.getenv("HF_TOKEN", None)
|
| 73 |
-
|
| 74 |
-
SYSTEM_PROMPT = """You are an expert business operations coordinator.
|
| 75 |
-
Your task: given a set of business instructions from various stakeholders, identify ALL conflicts and produce a resolution plan.
|
| 76 |
-
|
| 77 |
-
Authority hierarchy (lower number = higher authority — ALWAYS wins):
|
| 78 |
-
1. Legal & Compliance, Regulatory Affairs
|
| 79 |
-
2. CEO Office, CFO, CTO, COO
|
| 80 |
-
3. VP Engineering, VP Finance, VP Operations, VP Human Resources
|
| 81 |
-
4. Director of IT, Director of Finance
|
| 82 |
-
5. Engineering Manager, Finance Manager, HR Manager, IT Manager
|
| 83 |
-
6. Team Lead, Department Coordinator
|
| 84 |
-
|
| 85 |
-
Rules:
|
| 86 |
-
- When two instructions conflict, the HIGHER authority (lower tier number) always wins
|
| 87 |
-
- List ALL conflicting pairs — do not miss any
|
| 88 |
-
- Your execution_plan must contain ONLY the winning instructions + non-conflicting ones
|
| 89 |
-
- overridden_instructions must contain ONLY the losing instructions
|
| 90 |
-
|
| 91 |
-
Output ONLY valid JSON matching this exact schema. No preamble, no explanation outside the JSON:
|
| 92 |
-
{"identified_conflicts":[{"instruction_a":"<ID>","instruction_b":"<ID>","conflict_type":"direct|resource|temporal|absolute","resolution":"<winning ID>","reasoning":"<one sentence>"}],"execution_plan":["<ID>",...],"overridden_instructions":["<ID>",...]}"""
|
| 93 |
-
|
| 94 |
-
# Global list to collect training metrics for plotting
|
| 95 |
-
_training_log = []
|
| 96 |
-
|
| 97 |
-
# ── Dataset builder ───────────────────────────────────────────────────────────
|
| 98 |
-
|
| 99 |
-
def build_dataset():
|
| 100 |
-
from generator import ScenarioGenerator
|
| 101 |
-
gen = ScenarioGenerator(seed=SEED)
|
| 102 |
-
|
| 103 |
-
def make_records(n: int, diff_weights: Dict[int, float]) -> List[Dict[str, Any]]:
|
| 104 |
-
records = []
|
| 105 |
-
attempts = 0
|
| 106 |
-
while len(records) < n and attempts < n * 5:
|
| 107 |
-
attempts += 1
|
| 108 |
-
diff = random.choices(list(diff_weights.keys()), weights=list(diff_weights.values()))[0]
|
| 109 |
-
scenario = gen.generate(difficulty=diff)
|
| 110 |
-
|
| 111 |
-
if len(scenario.prompt) > MAX_PROMPT_CHARS:
|
| 112 |
-
continue
|
| 113 |
-
|
| 114 |
-
messages = [
|
| 115 |
-
{"role": "system", "content": SYSTEM_PROMPT},
|
| 116 |
-
{"role": "user", "content": scenario.prompt},
|
| 117 |
-
]
|
| 118 |
-
|
| 119 |
-
records.append({
|
| 120 |
-
"prompt": messages,
|
| 121 |
-
"scenario_json": json.dumps({
|
| 122 |
-
"ground_truth_followed": scenario.ground_truth_followed,
|
| 123 |
-
"ground_truth_overridden": scenario.ground_truth_overridden,
|
| 124 |
-
"difficulty": scenario.difficulty,
|
| 125 |
-
"conflicts": [
|
| 126 |
-
{
|
| 127 |
-
"instruction_a_id": c.instruction_a_id,
|
| 128 |
-
"instruction_b_id": c.instruction_b_id,
|
| 129 |
-
"conflict_type": c.conflict_type,
|
| 130 |
-
"resolution_id": c.resolution_id,
|
| 131 |
-
"explanation": c.explanation,
|
| 132 |
-
} for c in scenario.conflicts
|
| 133 |
-
],
|
| 134 |
-
"instructions": [
|
| 135 |
-
{
|
| 136 |
-
"id": ins.id,
|
| 137 |
-
"action_key": ins.action_key,
|
| 138 |
-
"action_value": ins.action_value,
|
| 139 |
-
"source_priority": ins.source_priority,
|
| 140 |
-
} for ins in scenario.instructions
|
| 141 |
-
],
|
| 142 |
-
}),
|
| 143 |
-
})
|
| 144 |
-
return records
|
| 145 |
-
|
| 146 |
-
train_records = make_records(TRAIN_SCENARIOS, DIFFICULTY_TRAIN_WEIGHTS)
|
| 147 |
-
eval_records = make_records(EVAL_SCENARIOS, DIFFICULTY_EVAL_WEIGHTS)
|
| 148 |
-
return Dataset.from_list(train_records), Dataset.from_list(eval_records)
|
| 149 |
-
|
| 150 |
-
# ── Reward function ───────────────────────────────────────────────────────────
|
| 151 |
-
|
| 152 |
-
def build_reward_fn():
|
| 153 |
-
from generator import Scenario, Instruction, ConflictPair
|
| 154 |
-
from verifier import score as _score
|
| 155 |
-
|
| 156 |
-
RUBRIC_WEIGHTS_V2 = {
|
| 157 |
-
"correct_final_state": 0.35,
|
| 158 |
-
"no_contradictions": 0.25,
|
| 159 |
-
"conflict_identification": 0.20,
|
| 160 |
-
"efficiency": 0.10,
|
| 161 |
-
"format_compliance": 0.10,
|
| 162 |
-
}
|
| 163 |
-
|
| 164 |
-
def reward_fn(prompts, completions, **kwargs):
|
| 165 |
-
rewards = []
|
| 166 |
-
scenario_jsons = kwargs.get("scenario_json", [None] * len(completions))
|
| 167 |
-
|
| 168 |
-
for completion, scenario_json in zip(completions, scenario_jsons):
|
| 169 |
-
if scenario_json is None:
|
| 170 |
-
rewards.append(0.0)
|
| 171 |
-
continue
|
| 172 |
-
try:
|
| 173 |
-
data = json.loads(scenario_json)
|
| 174 |
-
instructions = [Instruction(id=ins["id"], text="", source="", source_priority=ins["source_priority"],
|
| 175 |
-
instruction_type="absolute", action_key=ins["action_key"],
|
| 176 |
-
action_value=ins["action_value"]) for ins in data["instructions"]]
|
| 177 |
-
conflicts = [ConflictPair(instruction_a_id=c["instruction_a_id"], instruction_b_id=c["instruction_b_id"],
|
| 178 |
-
conflict_type=c["conflict_type"], resolution_id=c["resolution_id"],
|
| 179 |
-
explanation=c["explanation"]) for c in data["conflicts"]]
|
| 180 |
-
scenario = Scenario(scenario_id="", domain="", difficulty=data.get("difficulty", 1),
|
| 181 |
-
business_context="", instructions=instructions, conflicts=conflicts,
|
| 182 |
-
ground_truth_followed=data["ground_truth_followed"],
|
| 183 |
-
ground_truth_overridden=data["ground_truth_overridden"], prompt="")
|
| 184 |
-
|
| 185 |
-
breakdown = _score(completion, scenario)
|
| 186 |
-
composite_v2 = (
|
| 187 |
-
RUBRIC_WEIGHTS_V2["correct_final_state"] * breakdown.correct_final_state +
|
| 188 |
-
RUBRIC_WEIGHTS_V2["no_contradictions"] * breakdown.no_contradictions +
|
| 189 |
-
RUBRIC_WEIGHTS_V2["conflict_identification"] * breakdown.conflict_identification +
|
| 190 |
-
RUBRIC_WEIGHTS_V2["efficiency"] * breakdown.efficiency +
|
| 191 |
-
RUBRIC_WEIGHTS_V2["format_compliance"] * breakdown.format_compliance
|
| 192 |
-
)
|
| 193 |
-
|
| 194 |
-
diff_bonus = 0.05 if data.get("difficulty", 1) == 2 else 0.0
|
| 195 |
-
final_reward = min(1.0, composite_v2 + diff_bonus * composite_v2)
|
| 196 |
-
rewards.append(round(final_reward, 4))
|
| 197 |
-
except Exception as e:
|
| 198 |
-
rewards.append(0.0)
|
| 199 |
-
return rewards
|
| 200 |
-
|
| 201 |
-
return reward_fn
|
| 202 |
-
|
| 203 |
-
# ── Plotting ──────────────────────────────────────────────────────────────────
|
| 204 |
-
|
| 205 |
-
def generate_plots():
|
| 206 |
-
import matplotlib
|
| 207 |
-
matplotlib.use("Agg")
|
| 208 |
-
import matplotlib.pyplot as plt
|
| 209 |
-
|
| 210 |
-
PLOTS_DIR.mkdir(parents=True, exist_ok=True)
|
| 211 |
-
logs = _training_log
|
| 212 |
-
if not logs: return []
|
| 213 |
-
|
| 214 |
-
steps = [l["step"] for l in logs]
|
| 215 |
-
plot_paths = []
|
| 216 |
-
|
| 217 |
-
# Reward
|
| 218 |
-
fig, ax = plt.subplots(figsize=(10, 5))
|
| 219 |
-
train_r = [(l["step"], l["reward"]) for l in logs if "reward" in l]
|
| 220 |
-
eval_r = [(l["step"], l["eval_reward"]) for l in logs if "eval_reward" in l]
|
| 221 |
-
if train_r: ax.plot(*zip(*train_r), label="Train Reward")
|
| 222 |
-
if eval_r: ax.plot(*zip(*eval_r), label="Eval Reward")
|
| 223 |
-
ax.set_title("Reward Curve"); ax.legend(); ax.grid(True)
|
| 224 |
-
p = PLOTS_DIR / "reward_curve.png"; fig.savefig(p); plt.close(fig); plot_paths.append(str(p))
|
| 225 |
-
|
| 226 |
-
# Loss
|
| 227 |
-
fig, ax = plt.subplots(figsize=(10, 5))
|
| 228 |
-
loss = [(l["step"], l["loss"]) for l in logs if "loss" in l]
|
| 229 |
-
if loss: ax.plot(*zip(*loss), color="red")
|
| 230 |
-
ax.set_title("Training Loss"); ax.grid(True)
|
| 231 |
-
p = PLOTS_DIR / "loss_curve.png"; fig.savefig(p); plt.close(fig); plot_paths.append(str(p))
|
| 232 |
-
|
| 233 |
-
# KL
|
| 234 |
-
fig, ax = plt.subplots(figsize=(10, 5))
|
| 235 |
-
kl = [(l["step"], l["kl"]) for l in logs if "kl" in l]
|
| 236 |
-
if kl: ax.plot(*zip(*kl), color="purple")
|
| 237 |
-
ax.set_title("KL Divergence"); ax.grid(True)
|
| 238 |
-
p = PLOTS_DIR / "kl_divergence.png"; fig.savefig(p); plt.close(fig); plot_paths.append(str(p))
|
| 239 |
-
|
| 240 |
-
# Combined
|
| 241 |
-
fig, axes = plt.subplots(2, 2, figsize=(12, 8))
|
| 242 |
-
for ax, key in zip(axes.flat, ["reward", "loss", "kl", "eval_reward"]):
|
| 243 |
-
pts = [(l["step"], l[key]) for l in logs if key in l]
|
| 244 |
-
if pts: ax.plot(*zip(*pts))
|
| 245 |
-
ax.set_title(key); ax.grid(True)
|
| 246 |
-
fig.tight_layout()
|
| 247 |
-
p = PLOTS_DIR / "training_dashboard.png"; fig.savefig(p); plt.close(fig); plot_paths.append(str(p))
|
| 248 |
-
|
| 249 |
-
return plot_paths
|
| 250 |
-
|
| 251 |
-
# ── Main Training Loop ────────────────────────────────────────────────────────
|
| 252 |
-
|
| 253 |
-
def run_training(progress_callback=None):
|
| 254 |
-
def emit(msg):
|
| 255 |
-
if progress_callback: progress_callback(msg)
|
| 256 |
-
log.info(msg)
|
| 257 |
-
|
| 258 |
-
_training_log.clear()
|
| 259 |
-
clone_repo()
|
| 260 |
-
|
| 261 |
-
from trl import GRPOTrainer, GRPOConfig
|
| 262 |
-
|
| 263 |
-
emit("🚀 Loading model (Unsloth 4-bit)...")
|
| 264 |
-
model, tokenizer = FastLanguageModel.from_pretrained(
|
| 265 |
-
model_name="unsloth/Qwen2.5-3B-Instruct-bnb-4bit",
|
| 266 |
-
max_seq_length=4096,
|
| 267 |
-
load_in_4bit=True,
|
| 268 |
-
)
|
| 269 |
-
|
| 270 |
-
model = FastLanguageModel.get_peft_model(
|
| 271 |
-
model, r=16, lora_alpha=16, lora_dropout=0.0, bias="none",
|
| 272 |
-
target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"],
|
| 273 |
-
use_gradient_checkpointing="unsloth", random_state=SEED,
|
| 274 |
-
)
|
| 275 |
-
|
| 276 |
-
# Cast LoRA params to avoid Half/Float mismatch
|
| 277 |
-
compute_dtype = torch.bfloat16 if torch.cuda.is_bf16_supported() else torch.float16
|
| 278 |
-
for name, param in model.named_parameters():
|
| 279 |
-
if "lora_" in name: param.data = param.data.to(compute_dtype)
|
| 280 |
-
# Patch for compatibility with recent TRL GRPOTrainer
|
| 281 |
-
if not hasattr(model, "warnings_issued"):
|
| 282 |
-
model.warnings_issued = {}
|
| 283 |
-
|
| 284 |
-
emit(f"✅ LoRA applied (dtype: {compute_dtype})")
|
| 285 |
-
|
| 286 |
-
emit("📊 Building datasets...")
|
| 287 |
-
train_dataset, eval_dataset = build_dataset()
|
| 288 |
-
emit(f"✅ Datasets ready: {len(train_dataset)} train / {len(eval_dataset)} eval")
|
| 289 |
-
|
| 290 |
-
class GradioLogCallback(TrainerCallback):
|
| 291 |
-
def on_log(self, args, state, control, logs=None, **kwargs):
|
| 292 |
-
if logs:
|
| 293 |
-
entry = {"step": state.global_step, **{k: v for k, v in logs.items() if isinstance(v, (int, float))}}
|
| 294 |
-
_training_log.append(entry)
|
| 295 |
-
|
| 296 |
-
# Format learning rate with scientific notation, others with 4 decimals
|
| 297 |
-
parts = []
|
| 298 |
-
for k, v in entry.items():
|
| 299 |
-
if k == "step": continue
|
| 300 |
-
if "learning_rate" in k:
|
| 301 |
-
parts.append(f"{k}={v:.2e}")
|
| 302 |
-
else:
|
| 303 |
-
parts.append(f"{k}={v:.4f}")
|
| 304 |
-
|
| 305 |
-
msg = f"Step {state.global_step}: " + " | ".join(parts)
|
| 306 |
-
emit(msg)
|
| 307 |
-
|
| 308 |
-
grpo_config = GRPOConfig(
|
| 309 |
-
output_dir=str(OUTPUT_DIR),
|
| 310 |
-
per_device_train_batch_size=NUM_GENERATIONS, # Unsloth forces batch_size == num_generations
|
| 311 |
-
per_device_eval_batch_size=NUM_GENERATIONS,
|
| 312 |
-
gradient_accumulation_steps=1, # Keep effective batch size at 8 (8*1=8)
|
| 313 |
-
num_train_epochs=NUM_EPOCHS,
|
| 314 |
-
learning_rate=LEARNING_RATE,
|
| 315 |
-
warmup_steps=WARMUP_STEPS,
|
| 316 |
-
num_generations=NUM_GENERATIONS,
|
| 317 |
-
max_completion_length=MAX_NEW_TOKENS,
|
| 318 |
-
max_prompt_length=MAX_PROMPT_CHARS,
|
| 319 |
-
temperature=TEMPERATURE,
|
| 320 |
-
top_p=TOP_P,
|
| 321 |
-
beta=BETA,
|
| 322 |
-
logging_steps=10,
|
| 323 |
-
save_steps=50,
|
| 324 |
-
eval_strategy="steps",
|
| 325 |
-
eval_steps=50,
|
| 326 |
-
fp16=not torch.cuda.is_bf16_supported(),
|
| 327 |
-
bf16=torch.cuda.is_bf16_supported(),
|
| 328 |
-
report_to="none",
|
| 329 |
-
seed=SEED,
|
| 330 |
-
)
|
| 331 |
-
|
| 332 |
-
trainer = GRPOTrainer(
|
| 333 |
-
model=model, args=grpo_config, tokenizer=tokenizer,
|
| 334 |
-
train_dataset=train_dataset, eval_dataset=eval_dataset,
|
| 335 |
-
reward_funcs=[build_reward_fn()],
|
| 336 |
-
)
|
| 337 |
-
trainer.add_callback(GradioLogCallback())
|
| 338 |
-
|
| 339 |
-
emit("🔥 Starting GRPO training...")
|
| 340 |
-
trainer.train()
|
| 341 |
-
|
| 342 |
-
emit("💾 Saving final model...")
|
| 343 |
-
final_path = OUTPUT_DIR / "final"
|
| 344 |
-
model.save_pretrained(str(final_path))
|
| 345 |
-
tokenizer.save_pretrained(str(final_path))
|
| 346 |
-
|
| 347 |
-
if HF_REPO_ID and HF_TOKEN:
|
| 348 |
-
emit(f"☁️ Uploading to {HF_REPO_ID}...")
|
| 349 |
-
try:
|
| 350 |
-
from huggingface_hub import HfApi
|
| 351 |
-
api = HfApi(token=HF_TOKEN)
|
| 352 |
-
api.create_repo(HF_REPO_ID, repo_type="model", exist_ok=True)
|
| 353 |
-
api.upload_folder(folder_path=str(final_path), repo_id=HF_REPO_ID, repo_type="model")
|
| 354 |
-
emit(f"✅ Uploaded to https://huggingface.co/{HF_REPO_ID}")
|
| 355 |
-
except Exception as e:
|
| 356 |
-
emit(f"❌ Upload failed: {e}")
|
| 357 |
-
|
| 358 |
-
emit("📈 Generating plots...")
|
| 359 |
-
return generate_plots()
|
| 360 |
-
|
| 361 |
-
if __name__ == "__main__":
|
| 362 |
-
run_training()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|