Files
L'électron rareandClaude Opus 4.6 7b5c3fa795 lot-12: training pipeline réel (Unsloth/TRL + eval)
## Scripts Python
- scripts/train_unsloth.py: wrapper Unsloth/TRL pour fine-tuning LoRA/QLoRA
  Supporte JSONL multi-format (instruction/output, messages, text)
  Output JSON sur stdout avec métriques (loss, duration)
- scripts/eval_model.py: évaluation modèle sur set de prompts
  Score F1 token overlap, support adapter LoRA
  Output JSON détaillé (réponses, scores, tokens)

## Worker V2
- Training nodes (lora_training, qlora_training): exécution réelle via
  child_process → train_unsloth.py (fallback stub si dry-run)
- Evaluation nodes (benchmark, prompt_test): exécution réelle via
  child_process → eval_model.py (fallback stub si pas de prompts)
- Config: PYTHON_BIN, SCRIPTS_DIR, TRAINING_TIMEOUT_MS

## Prérequis serveur (kxkm-ai)
- Python 3.12 + venv avec PyTorch 2.10+cu128, Unsloth 2026.3.4,
  TRL 0.24.0, transformers 5.2, peft 0.18, bitsandbytes 0.49

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
2026-03-16 17:34:47 +01:00

207 lines
6.4 KiB
Python

#!/usr/bin/env python3
"""
KXKM_Clown — Model Evaluation Script
Evaluates a model (base or fine-tuned adapter) on a set of prompts.
Called by the Node Engine worker for benchmark/prompt_test nodes.
Usage:
python scripts/eval_model.py \
--model unsloth/llama-3-8b \
[--adapter /path/to/adapter] \
--prompts /path/to/prompts.jsonl \
--output /path/to/eval_result.json \
[--max-new-tokens 256] [--temperature 0.7]
Prompts JSONL format:
{"prompt": "...", "expected": "..."} (expected is optional)
Output: JSON with evaluation scores.
"""
import argparse
import json
import os
import sys
import time
def parse_args():
p = argparse.ArgumentParser(description="KXKM Model Evaluation")
p.add_argument("--model", required=True, help="Base model name or path")
p.add_argument("--adapter", default=None, help="Path to LoRA adapter (optional)")
p.add_argument("--prompts", required=True, help="Path to prompts JSONL file")
p.add_argument("--output", required=True, help="Path to output JSON result")
p.add_argument("--max-new-tokens", type=int, default=256)
p.add_argument("--temperature", type=float, default=0.7)
p.add_argument("--quantize", choices=["4bit", "none"], default="4bit")
return p.parse_args()
def load_prompts(path):
"""Load prompts from JSONL."""
prompts = []
with open(path, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line:
continue
try:
item = json.loads(line)
if isinstance(item, dict) and "prompt" in item:
prompts.append(item)
except json.JSONDecodeError:
continue
return prompts
def compute_similarity(generated, expected):
"""Simple token overlap similarity score (0-1)."""
if not expected:
return None
gen_tokens = set(generated.lower().split())
exp_tokens = set(expected.lower().split())
if not exp_tokens:
return None
overlap = gen_tokens & exp_tokens
precision = len(overlap) / len(gen_tokens) if gen_tokens else 0
recall = len(overlap) / len(exp_tokens) if exp_tokens else 0
if precision + recall == 0:
return 0.0
return 2 * (precision * recall) / (precision + recall) # F1
def main():
args = parse_args()
start_time = time.time()
result = {
"status": "failed",
"model": args.model,
"adapter": args.adapter,
"error": None,
}
try:
from unsloth import FastLanguageModel
print(f"[eval] Loading model: {args.model}", file=sys.stderr)
load_in_4bit = args.quantize == "4bit"
model, tokenizer = FastLanguageModel.from_pretrained(
model_name=args.model,
max_seq_length=2048,
load_in_4bit=load_in_4bit,
)
# Load adapter if specified
if args.adapter and os.path.isdir(args.adapter):
print(f"[eval] Loading adapter: {args.adapter}", file=sys.stderr)
from peft import PeftModel
model = PeftModel.from_pretrained(model, args.adapter)
# Enable inference mode
FastLanguageModel.for_inference(model)
# Load prompts
prompts = load_prompts(args.prompts)
if not prompts:
raise ValueError(f"No valid prompts in {args.prompts}")
print(f"[eval] Evaluating {len(prompts)} prompts", file=sys.stderr)
scores = []
responses = []
total_tokens = 0
for i, item in enumerate(prompts):
prompt_text = item["prompt"]
expected = item.get("expected", None)
inputs = tokenizer(prompt_text, return_tensors="pt").to(model.device)
input_len = inputs["input_ids"].shape[1]
outputs = model.generate(
**inputs,
max_new_tokens=args.max_new_tokens,
temperature=args.temperature,
do_sample=args.temperature > 0,
)
generated_tokens = outputs[0][input_len:]
generated_text = tokenizer.decode(generated_tokens, skip_special_tokens=True)
total_tokens += len(generated_tokens)
similarity = compute_similarity(generated_text, expected)
response_entry = {
"prompt": prompt_text[:100],
"generated": generated_text[:500],
"tokens": len(generated_tokens),
}
if expected is not None:
response_entry["expected"] = expected[:100]
response_entry["similarity"] = similarity
if similarity is not None:
scores.append(similarity)
responses.append(response_entry)
print(
f"[eval] {i+1}/{len(prompts)} tokens={len(generated_tokens)}"
+ (f" sim={similarity:.3f}" if similarity is not None else ""),
file=sys.stderr,
)
duration = time.time() - start_time
avg_score = sum(scores) / len(scores) if scores else None
eval_data = {
"score": avg_score,
"total_prompts": len(prompts),
"scored_prompts": len(scores),
"total_tokens": total_tokens,
"avg_tokens_per_prompt": total_tokens / len(prompts) if prompts else 0,
"duration": round(duration, 2),
"responses": responses,
}
# Write detailed result to output file
os.makedirs(os.path.dirname(args.output) or ".", exist_ok=True)
with open(args.output, "w", encoding="utf-8") as f:
json.dump(eval_data, f, indent=2, ensure_ascii=False)
result = {
"status": "completed",
"model": args.model,
"adapter": args.adapter,
"score": avg_score,
"metrics": {
"score": avg_score,
"total_prompts": len(prompts),
"total_tokens": total_tokens,
"duration": round(duration, 2),
},
"outputFile": args.output,
"error": None,
}
print(f"[eval] Done in {duration:.1f}s, score={avg_score}", file=sys.stderr)
except Exception as e:
duration = time.time() - start_time
result["error"] = str(e)
result["metrics"] = {"duration": round(duration, 2)}
print(f"[eval] ERROR: {e}", file=sys.stderr)
# Output JSON result on stdout
print(json.dumps(result))
if __name__ == "__main__":
main()