bbkdevops's picture
download
raw
17.2 kB
"""Train/eval evidence for self-dialogue TinyMind data."""
from __future__ import annotations
import json
import math
import random
from pathlib import Path
import torch
import torch.nn.functional as F
from data.self_dialogue_forge import SelfDialogueForge
from evaluation.claims import build_claim_dossier
from evaluation.local_evidence import _collate, _encode, _make_config, _save_json
from model.architecture import OmegaModel
from model.sparse_int4 import export_sparse_int4_model
SYSTEM_PROMPT = "TinyMind solves by self-dialogue: plan, act, verify, then final."
def _seed_everything(seed: int) -> None:
random.seed(seed)
torch.manual_seed(seed)
def _load_jsonl(path: str | Path) -> list[dict]:
return [json.loads(line) for line in Path(path).read_text(encoding="utf-8").splitlines() if line.strip()]
def _dialogue_text(row: dict) -> str:
return (
f"<bos><system>{SYSTEM_PROMPT}</system>\n"
f"<user>{row['prompt']}</user>\n"
f"<assistant>{row['target']}<eos>"
)
def _prompt_prefix(row: dict) -> str:
return f"<bos><system>{SYSTEM_PROMPT}</system>\n<user>{row['prompt']}</user>\n<assistant>"
def _encode_rows(rows: list[dict], max_len: int, vocab_size: int) -> list[torch.Tensor]:
return [_encode(_dialogue_text(row), max_len, vocab_size) for row in rows]
def _decode_ids(ids: list[int]) -> str:
data = bytes(max(0, min(255, token - 4)) for token in ids if token >= 4)
return data.decode("utf-8", errors="ignore")
def _corrupt_row(row: dict) -> dict:
corrupt = dict(row)
oracle = str(row.get("oracle", ""))
wrong = str(int(oracle) + 7) if oracle.isdigit() else "wrong"
corrupt["target"] = (
"<plan>Trust the first draft without checking.</plan>\n"
"<act>Skip recomputation and keep the inconsistent value.</act>\n"
"<verify>failed_check=true; oracle_mismatch=true</verify>\n"
f"<final>{wrong}</final>"
)
return corrupt
@torch.no_grad()
def _loss(model: OmegaModel, sequences: list[torch.Tensor]) -> float:
model.eval()
input_ids, labels = _collate(sequences)
return float(model(input_ids, labels=labels)["loss"].item())
def _train(model: OmegaModel, sequences: list[torch.Tensor], steps: int) -> tuple[list[float], float]:
optimizer = torch.optim.AdamW(model.parameters(), lr=1.5e-3, weight_decay=0.01)
losses: list[float] = []
grad_norm = 0.0
model.train()
for step in range(max(1, int(steps))):
batch = [sequences[(step + j) % len(sequences)] for j in range(min(4, len(sequences)))]
input_ids, labels = _collate(batch)
out = model(input_ids, labels=labels)
loss = out["loss"]
optimizer.zero_grad()
loss.backward()
norm = torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
optimizer.step()
losses.append(float(loss.item()))
grad_norm = float(norm.item() if hasattr(norm, "item") else norm)
return losses, grad_norm
def _sequence_nll(model: OmegaModel, sequences: list[torch.Tensor]) -> torch.Tensor:
input_ids, labels = _collate(sequences)
out = model(input_ids)
logits = out["logits"][..., :-1, :].contiguous()
targets = labels[..., 1:].contiguous()
token_loss = F.cross_entropy(
logits.view(-1, model.cfg.vocab_size),
targets.view(-1),
ignore_index=-100,
reduction="none",
).view(targets.shape)
mask = (targets != -100).float()
return (token_loss * mask).sum(dim=-1) / mask.sum(dim=-1).clamp(min=1.0)
def _train_with_preference(
model: OmegaModel,
rows: list[dict],
max_len: int,
vocab_size: int,
steps: int,
beta: float = 2.0,
margin: float = 0.25,
) -> dict:
if steps <= 0:
return {"enabled": False, "steps": 0, "losses": [], "final_loss": None, "final_margin": None}
optimizer = torch.optim.AdamW(model.parameters(), lr=2e-3, weight_decay=0.01)
losses: list[float] = []
margins: list[float] = []
model.train()
for step in range(int(steps)):
batch_rows = [rows[(step + j) % len(rows)] for j in range(min(4, len(rows)))]
chosen = [_encode(_dialogue_text(row), max_len, vocab_size) for row in batch_rows]
rejected = [_encode(_dialogue_text(_corrupt_row(row)), max_len, vocab_size) for row in batch_rows]
chosen_nll = _sequence_nll(model, chosen)
rejected_nll = _sequence_nll(model, rejected)
pref_margin = rejected_nll - chosen_nll
preference_loss = F.softplus(-beta * (pref_margin - margin)).mean()
lm_loss = chosen_nll.mean()
loss = lm_loss + preference_loss
optimizer.zero_grad()
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
optimizer.step()
losses.append(float(loss.item()))
margins.append(float(pref_margin.mean().item()))
return {
"enabled": True,
"steps": int(steps),
"losses": losses,
"final_loss": losses[-1],
"final_margin": margins[-1],
"beta": beta,
"target_margin": margin,
}
def _contains_self_dialogue_markers(rows: list[dict]) -> dict:
required = ("<plan>", "<act>", "<verify>", "<final>")
passed = all(all(marker in row["target"] for marker in required) for row in rows)
return {"passed": passed, "required_markers": list(required), "records_checked": len(rows)}
def _ngram_repetition_rate(tokens: list[str], n: int = 4) -> float:
if len(tokens) < n:
return 0.0
grams = [tuple(tokens[i : i + n]) for i in range(len(tokens) - n + 1)]
return 1.0 - (len(set(grams)) / max(len(grams), 1))
def _distinct_ratio(tokens: list[str], n: int = 1) -> float:
if len(tokens) < n:
return 0.0
grams = [tuple(tokens[i : i + n]) for i in range(len(tokens) - n + 1)]
return len(set(grams)) / max(len(grams), 1)
def score_generation_quality(samples: list[dict]) -> dict:
repetitions: list[float] = []
distinct_1: list[float] = []
distinct_2: list[float] = []
final_hits = 0
for sample in samples:
text = str(sample.get("generated_text", ""))
tokens = text.split()
if not tokens and text:
tokens = list(text)
repetitions.append(_ngram_repetition_rate(tokens, 4))
distinct_1.append(_distinct_ratio(tokens, 1))
distinct_2.append(_distinct_ratio(tokens, 2))
oracle = str(sample.get("oracle", ""))
final_hits += int(bool(oracle) and oracle in text)
count = max(len(samples), 1)
avg_repetition = sum(repetitions) / count
avg_distinct_1 = sum(distinct_1) / count
avg_distinct_2 = sum(distinct_2) / count
final_accuracy = final_hits / count
collapse_detected = avg_repetition > 0.35 or avg_distinct_1 < 0.20
passed = not collapse_detected and final_accuracy >= 0.50
return {
"passed": passed,
"collapse_detected": collapse_detected,
"avg_repetition_4gram_rate": avg_repetition,
"avg_distinct_1": avg_distinct_1,
"avg_distinct_2": avg_distinct_2,
"final_answer_accuracy": final_accuracy,
"sample_count": len(samples),
"samples": samples,
}
@torch.no_grad()
def _greedy_generate_text(model: OmegaModel, row: dict, max_new_tokens: int = 80) -> dict:
model.eval()
prompt = _prompt_prefix(row)
ids = _encode(prompt, model.cfg.max_seq_len, model.cfg.vocab_size).tolist()
generated: list[int] = []
for _ in range(max_new_tokens):
context = torch.tensor([ids[-model.cfg.max_seq_len :]], dtype=torch.long)
logits = model(context)["logits"][0, -1]
next_id = int(torch.argmax(logits).item())
if next_id == model.cfg.eos_token_id:
break
ids.append(next_id)
generated.append(next_id)
text = _decode_ids(generated)
return {
"prompt": row["prompt"],
"oracle": str(row.get("oracle", "")),
"generated_text": text,
"generated_token_count": len(generated),
}
def _generation_quality_eval(model: OmegaModel, rows: list[dict], limit: int = 3) -> dict:
samples = [_greedy_generate_text(model, row) for row in rows[: min(limit, len(rows))]]
return score_generation_quality(samples)
def _template_memorization_guard(train_rows: list[dict], eval_rows: list[dict]) -> dict:
train_ids = {row["id"] for row in train_rows}
eval_ids = {row["id"] for row in eval_rows}
train_prompts = {row["prompt"] for row in train_rows}
eval_prompts = {row["prompt"] for row in eval_rows}
return {
"passed": not (train_ids & eval_ids) and not (train_prompts & eval_prompts),
"id_overlap": len(train_ids & eval_ids),
"prompt_overlap": len(train_prompts & eval_prompts),
"train_records": len(train_rows),
"eval_records": len(eval_rows),
}
@torch.no_grad()
def _candidate_preference_eval(model: OmegaModel, rows: list[dict], max_len: int, vocab_size: int) -> dict:
"""Check whether the trained model prefers oracle traces over corrupted traces."""
wins = 0
total = 0
margins: list[float] = []
examples: list[dict] = []
for row in rows:
corrupt = _corrupt_row(row)
correct_seq = _encode(_dialogue_text(row), max_len, vocab_size)
corrupt_seq = _encode(_dialogue_text(corrupt), max_len, vocab_size)
correct_loss = _loss(model, [correct_seq])
corrupt_loss = _loss(model, [corrupt_seq])
preferred = correct_loss <= corrupt_loss
margin = corrupt_loss - correct_loss
margins.append(margin)
wins += int(preferred)
total += 1
if len(examples) < 3:
examples.append(
{
"prompt": row["prompt"],
"correct_loss": correct_loss,
"corrupt_loss": corrupt_loss,
"margin": margin,
"preferred_oracle": preferred,
}
)
return {
"passed": wins >= max(1, total // 2),
"oracle_preference_accuracy": wins / max(total, 1),
"avg_margin": sum(margins) / max(len(margins), 1),
"examples": examples,
}
def run_self_dialogue_train_eval_bundle(
out_dir: str | Path,
train_steps: int = 12,
preference_steps: int = 0,
train_size: int = 48,
eval_size: int = 12,
preference_eval_limit: int = 6,
seed: int = 20260523,
) -> dict:
_seed_everything(seed)
out = Path(out_dir)
out.mkdir(parents=True, exist_ok=True)
manifest = SelfDialogueForge(train_size=train_size, eval_size=eval_size, seed=seed).write_jsonl(out)
train_rows = _load_jsonl(manifest["train_path"])
eval_rows = _load_jsonl(manifest["eval_path"])
cfg = _make_config()
cfg.max_seq_len = 320
train_sequences = _encode_rows(train_rows, cfg.max_seq_len, cfg.vocab_size)
eval_sequences = _encode_rows(eval_rows, cfg.max_seq_len, cfg.vocab_size)
model = OmegaModel(cfg)
initial_eval_loss = _loss(model, eval_sequences)
train_losses, grad_norm = _train(model, train_sequences, train_steps)
preference_optimization = _train_with_preference(
model,
train_rows,
cfg.max_seq_len,
cfg.vocab_size,
preference_steps,
)
eval_loss = _loss(model, eval_sequences)
perplexity = float(math.exp(min(eval_loss, 20.0)))
checkpoint_path = out / "self_dialogue_purefield.pt"
torch.save(
{
"step": int(train_steps),
"model_state": model.state_dict(),
"model_cfg": cfg,
"train_losses": train_losses,
"eval_loss": eval_loss,
"dataset_manifest": str(out / "self_dialogue_manifest.json"),
"training_mode": "self_dialogue_oracle_trace",
},
checkpoint_path,
)
int4_artifact = export_sparse_int4_model(model, quality_gate_delta=cfg.quality_gate_delta)
int4_path = out / "self_dialogue_int4_sparse.pt"
torch.save(int4_artifact, int4_path)
marker_eval = _contains_self_dialogue_markers(train_rows + eval_rows)
memorization_guard = _template_memorization_guard(train_rows, eval_rows)
preference = _candidate_preference_eval(
model,
eval_rows[: min(int(preference_eval_limit), len(eval_rows))],
cfg.max_seq_len,
cfg.vocab_size,
)
generation_quality = _generation_quality_eval(model, eval_rows, limit=min(3, int(preference_eval_limit)))
loss_improved = eval_loss <= initial_eval_loss
train_eval = {
"steps": int(train_steps),
"preference_steps": int(preference_steps),
"initial_eval_loss": initial_eval_loss,
"eval_loss": eval_loss,
"perplexity": perplexity,
"loss_delta": initial_eval_loss - eval_loss,
"final_train_loss": train_losses[-1],
"grad_norm": grad_norm,
}
measurements = {
"quality": {
"passed": bool(torch.isfinite(torch.tensor(eval_loss)).item()) and perplexity < cfg.vocab_size * 2,
"score": eval_loss,
"artifact": str(checkpoint_path),
"notes": f"Self-dialogue eval loss={eval_loss:.4f}, perplexity={perplexity:.2f}.",
},
"self_dialogue": {
"passed": marker_eval["passed"] and memorization_guard["passed"],
"score": preference["oracle_preference_accuracy"],
"artifact": str(out / "self_dialogue_eval.jsonl"),
"notes": "Targets include plan/act/verify/final and eval prompts are disjoint from train prompts.",
},
"anti_memorization": {
"passed": memorization_guard["passed"],
"score": 1.0 - min(1.0, memorization_guard["prompt_overlap"] / max(1, len(eval_rows))),
"artifact": str(out / "self_dialogue_manifest.json"),
"notes": "Train/eval IDs and prompts are disjoint; records are generated from held-out oracle seeds.",
},
"stability": {
"passed": bool(torch.isfinite(torch.tensor(train_losses + [eval_loss, grad_norm])).all().item()),
"score": grad_norm,
"artifact": str(checkpoint_path),
"notes": "Training losses, eval loss, and gradient norm were finite.",
},
"quantization": {
"passed": len(int4_artifact.get("layers", [])) > 0,
"score": len(int4_artifact.get("layers", [])),
"artifact": str(int4_path),
"notes": "INT4 sparse artifact was exported from the self-dialogue trained model.",
},
"oracle_preference": {
"passed": preference["oracle_preference_accuracy"] >= 0.999 and preference["avg_margin"] > 0,
"score": preference["oracle_preference_accuracy"],
"artifact": str(out / "self_dialogue_evidence.json"),
"notes": (
"Chosen oracle traces are compared against corrupted traces; "
f"avg_margin={preference['avg_margin']:.4f}."
),
},
"generation_quality": {
"passed": generation_quality["passed"],
"score": generation_quality["final_answer_accuracy"],
"artifact": str(out / "self_dialogue_evidence.json"),
"notes": (
"Greedy generation must avoid repetition collapse and include the oracle final answer. "
f"collapse_detected={generation_quality['collapse_detected']}, "
f"repetition_4gram={generation_quality['avg_repetition_4gram_rate']:.4f}."
),
},
}
evidence = {
"schema_version": "tinymind-self-dialogue-train-eval-v1",
"model_name": "TinyMind PureField ReGenesis self-dialogue local trainer",
"claim_scope": "self_dialogue_non_memorized_local_capability",
"as_of": "2026-05-23",
"local_evidence_complete": True,
"training_mode": "oracle_generated_self_dialogue_not_answer_copying",
"artifacts": {
"checkpoint": str(checkpoint_path),
"int4_artifact": str(int4_path),
"dataset_manifest": str(out / "self_dialogue_manifest.json"),
"objective_report": str(out / "self_dialogue_evidence.json"),
},
"dataset_manifest": manifest,
"train_eval": train_eval,
"marker_eval": marker_eval,
"memorization_guard": memorization_guard,
"candidate_preference": preference,
"generation_quality": generation_quality,
"preference_optimization": preference_optimization,
"generated_prompt_prefix_example": _prompt_prefix(eval_rows[0]) if eval_rows else "",
"measurements": measurements,
"comparisons": [],
"limitations": [
"This proves local self-dialogue training/eval mechanics, not world-best external rank.",
"The tiny CPU run is intentionally small; larger training is required for strong open-ended conversation.",
],
"loss_improved": loss_improved,
}
evidence["claim_dossier"] = build_claim_dossier(evidence)
evidence_path = out / "self_dialogue_evidence.json"
evidence["evidence_path"] = str(evidence_path)
_save_json(evidence_path, evidence)
return evidence

Xet Storage Details

Size:
17.2 kB
·
Xet hash:
5c6ce0688a78f20d10c5ff4959785c213be3c6689dc6a32c73c884dfee2c10ed

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.