Download source/src/vimeml/posttraining/infer.py from Voltline/vimeml-tiny-ja-v4.2: direct link, hf CLI and curl.
- Browser
- Download file 8.65 kB
-
https://huggingface.co/Voltline/vimeml-tiny-ja-v4.2/resolve/main/source/src/vimeml/posttraining/infer.py
- Command line
-
hf download hf://Voltline/vimeml-tiny-ja-v4.2/source/src/vimeml/posttraining/infer.py
-
curl -L -o infer.py https://huggingface.co/Voltline/vimeml-tiny-ja-v4.2/resolve/main/source/src/vimeml/posttraining/infer.py
8.65 kB
| """Conditional OUT-sum scoring on explicitly allowlisted nonblind snapshots.""" | |
| from contextlib import contextmanager | |
| import json | |
| from pathlib import Path | |
| from types import MethodType | |
| import unicodedata | |
| import torch | |
| from torch.nn import functional as F | |
| from vimeml.posttraining.data import ConversionCodec | |
| from vimeml.posttraining.readings import hiragana | |
| from vimeml.training.data import file_sha, write_json | |
| from vimeml.training.model_v4 import TinyGPTV4 | |
| ROOT = Path(__file__).resolve().parents[3] | |
| SCORE_CONTRACT = "full_vocabulary_OUT_logp_sum_excluding_EOS_v1" | |
| READING_CONTRACT = "NFKC_then_katakana_to_hiragana_preserve_punctuation_v1" | |
| # Fixed paths and hashes: no directory discovery, arbitrary split or blind mode. | |
| TRACKS = { | |
| "standard-development": (728, "bf8fc3d6d0bf7323ff6d196a13d555f2f6d099bbf946adf434827e450b687a06"), | |
| "legacy-regression": (500, "dc2b9430c5676b95dae5c83156741bac35ca2d4c33ebff520b1394c224668e66"), | |
| "ajimee": (200, "2c18523afc3c23f7341b096f72dd7d84e81a5a2e143d822fbf78512d008fef85"), | |
| "historical-development": (137, "cd356733e6d6b743ad61cf8cc3f28303de27a84a5238a204615319897f71c8b5"), | |
| } | |
| def read_snapshot(track, root=ROOT): | |
| if track not in TRACKS: | |
| raise ValueError("Only explicitly frozen development/regression tracks are allowed") | |
| count, expected = TRACKS[track] | |
| if not expected: | |
| raise ValueError("Unpinned snapshot") | |
| path = Path(root) / f"outputs/evaluation-v4-20261015/inputs/{track}.json" | |
| if file_sha(path) != expected: | |
| raise ValueError(f"Frozen snapshot changed: {track}") | |
| rows = json.loads(path.read_text(encoding="utf-8")) | |
| if len(rows) != count: | |
| raise ValueError("Frozen snapshot count mismatch") | |
| return rows | |
| def fp32_evaluation(model): | |
| """Evaluate primitive FP32 operations, avoiding a second compiled graph. | |
| Weights stay FP32 under BF16 AMP training. Never clone a second CUDA model. | |
| """ | |
| training, forward = model.training, model.forward | |
| tf32 = torch.backends.cuda.matmul.allow_tf32 | |
| model.eval() | |
| model.forward = MethodType(TinyGPTV4.forward, model) | |
| torch.backends.cuda.matmul.allow_tf32 = False | |
| try: | |
| with torch.inference_mode(), torch.autocast(next(model.parameters()).device.type, enabled=False): | |
| yield | |
| finally: | |
| torch.backends.cuda.matmul.allow_tf32 = tf32 | |
| model.forward = forward | |
| model.train(training) | |
| def normalize_reading(query): | |
| return hiragana(unicodedata.normalize("NFKC", query)) | |
| def azookey_order(row): | |
| pool = [x["text"] for x in row["candidates"]] | |
| order = row.get("orders", {}).get("azookey", pool) | |
| if not pool or len(set(pool)) != len(pool) or len(order) != len(pool) or set(order) != set(pool): | |
| raise ValueError("Frozen candidate order/pool invalid") | |
| return order | |
| def score_candidates(model, codec, context, reading, candidates): | |
| """Whole-case fallback on overflow; never crop context during evaluation.""" | |
| encoded = [] | |
| for candidate in candidates: | |
| try: | |
| item = codec.encode(context, reading, candidate) | |
| except ValueError as error: | |
| return {"scores": None, "fallback": "invalid_structured_field", "detail": str(error)} | |
| if item is None or item[2]: | |
| return {"scores": None, "fallback": "structured_context_overflow"} | |
| ids, first, _ = item | |
| encoded.append((ids, first)) | |
| if not encoded: | |
| raise ValueError("Empty candidate pool") | |
| device = next(model.parameters()).device | |
| width = max(len(ids) - 1 for ids, _ in encoded) | |
| inputs = torch.zeros((len(encoded), width), dtype=torch.long, device=device) | |
| rows, positions, targets = [], [], [] | |
| for row, (ids, first) in enumerate(encoded): | |
| inputs[row, :len(ids) - 1] = torch.tensor(ids[:-1], device=device) | |
| # EOS is a training target; the fixed inference score excludes it. | |
| for position in range(first, len(ids) - 1): | |
| rows.append(row) | |
| positions.append(position - 1) | |
| targets.append(ids[position]) | |
| logits = model(inputs) | |
| row_ids = torch.tensor(rows, device=device) | |
| selected = logits[row_ids, torch.tensor(positions, device=device)] | |
| logp = -F.cross_entropy(selected.float(), torch.tensor(targets, device=device), reduction="none") | |
| scores = torch.zeros(len(encoded), device=device).scatter_add_(0, row_ids, logp) | |
| if not bool(torch.isfinite(scores).all()): | |
| raise FloatingPointError("Non-finite conditional candidate score") | |
| return {"scores": scores.cpu().tolist(), "fallback": None} | |
| def evaluate_snapshot(model, codec, rows, destination=None, condition="context_reading"): | |
| if condition not in {"context_reading", "empty_reading", "empty_context"}: | |
| raise ValueError("Unknown diagnostic condition") | |
| counts, by_source, records = {"total": 0, "correct": 0, "azookey_correct": 0, "corrected": 0, "damaged": 0, "fallbacks": 0}, {}, [] | |
| for row in rows: | |
| order = azookey_order(row) | |
| # The user's query alone supplies READ. Answers are used only AFTER | |
| # scoring for correctness; never synthesize reading from surface/gold. | |
| reading = normalize_reading(row["query"]) | |
| context = row["left_context"] | |
| actual_reading = "" if condition == "empty_reading" else reading | |
| actual_context = "" if condition == "empty_context" else context | |
| result = score_candidates(model, codec, actual_context, actual_reading, order) | |
| ranked = order if result["scores"] is None else [order[i] for i in sorted( | |
| range(len(order)), key=lambda i: (-result["scores"][i], i))] | |
| correct, baseline = ranked[0] in row["answers"], order[0] in row["answers"] | |
| source = row.get("source", "unspecified") | |
| sub = by_source.setdefault(source, {"total": 0, "correct": 0}) | |
| sub["total"] += 1 | |
| sub["correct"] += int(correct) | |
| counts["total"] += 1 | |
| counts["correct"] += int(correct) | |
| counts["azookey_correct"] += int(baseline) | |
| counts["corrected"] += int(correct and not baseline) | |
| counts["damaged"] += int(baseline and not correct) | |
| counts["fallbacks"] += int(result["fallback"] is not None) | |
| records.append({"id": row["id"], "original_query": row["query"], "normalized_reading": reading, | |
| "condition": condition, "context": actual_context, "answers": row["answers"], | |
| "candidate_order": order, "scores": result["scores"], "ranked": ranked, | |
| "correct": correct, "azookey_correct": baseline, "fallback": result["fallback"]}) | |
| counts["net_improvement"] = counts["corrected"] - counts["damaged"] | |
| counts["top1"] = counts["correct"] / counts["total"] | |
| report = {**counts, "by_source": by_source, "condition": condition, "precision": "fp32", | |
| "score_contract": SCORE_CONTRACT, "reading_contract": READING_CONTRACT, | |
| "overflow_policy": "whole-case original AzooKey order; full denominator; no context crop"} | |
| if destination is not None: | |
| destination = Path(destination) | |
| destination.mkdir(parents=True, exist_ok=True) | |
| write_json(destination / "metrics.json", report) | |
| with (destination / "scores.jsonl").open("w", encoding="utf-8", newline="\n") as output: | |
| for record in records: | |
| output.write(json.dumps(record, ensure_ascii=False) + "\n") | |
| return report | |
| def load_conditional_checkpoint(path, tokenizer, device="cpu"): | |
| from vimeml.training import model_factory_v4 | |
| from vimeml.training.model_factory import model_from_checkpoint | |
| from vimeml.posttraining.runtime import TASK_CONTRACT, validate_task_checkpoint | |
| checkpoint_path = Path(path) | |
| saved = torch.load(checkpoint_path, map_location="cpu", weights_only=True) | |
| validate_task_checkpoint(saved) | |
| if saved["posttraining"]["task_contract"] != TASK_CONTRACT: | |
| raise ValueError("Incompatible conditional scoring contract") | |
| for name, path in (("conversion", ROOT / saved["config"]["conversion_dir"] / "manifest.json"), | |
| ("tokens", ROOT / saved["config"]["token_dir"] / "manifest.json")): | |
| if file_sha(path) != saved["signatures"][name]: | |
| raise ValueError(f"Conditional checkpoint {name} identity mismatch") | |
| codec = ConversionCodec(tokenizer) | |
| model = model_from_checkpoint(saved).to(device).eval() | |
| return model, codec, {"step": saved["step"], "stage": saved["posttraining"]["stage"], | |
| "checkpoint_sha256": file_sha(checkpoint_path), "source": saved["initialization"]} | |