nicholpt's picture
Release Dino Snake and Tetris fine-tuned Laya decision models
9220532 verified
Raw History Blame Contribute Delete
7.24 kB
"""Load one game adapter from the shared repository and score supplied options."""
from __future__ import annotations
import json
import math
from pathlib import Path, PurePosixPath
from typing import Any, Sequence
import torch
from . import _core as core
GAMES = ("dino", "snake", "tetris")
def _root(path: str | Path, game: str, revision: str | None) -> Path:
if game not in GAMES:
raise core.ModelError("game must be dino, snake or tetris")
local = Path(path)
if local.is_dir():
return local
from huggingface_hub import snapshot_download
return Path(snapshot_download(str(path), revision=revision, allow_patterns=[
"manifest.json", "checksums.sha256", f"{game}/*",
]))
class GameModel:
"""A fine-tuned Laya model. Inputs must include the game's engineered options."""
def __init__(self, model: core.SystemOneDecisionModel, game: str):
self.model, self.game = model, game
@classmethod
def from_pretrained(cls, path: str | Path = "getSTEAV/system-one-games", *, game: str,
revision: str | None = None, base: str | Path | None = None,
device: str | None = None, dtype: str = "auto") -> "GameModel":
root = _root(path, game, revision)
manifest = json.loads((root / "manifest.json").read_text())
if manifest.get("schema") != "steav/system-one-games/v1":
raise core.ModelError("unsupported game release manifest")
for rel, expected in manifest["models"][game]["files"].items():
relative = PurePosixPath(rel)
target = root / relative
# Hub snapshots use symlinks into their shared blob cache. Validate
# the manifest path itself, then verify the referenced file's bytes.
if relative.is_absolute() or ".." in relative.parts or not target.is_file():
raise core.ModelError("missing or invalid release file: " + rel)
if core._sha256(target) != expected:
raise core.ModelError("release checksum mismatch: " + rel)
model = core.SystemOneDecisionModel.from_pretrained(root / game, base=base,
device=device, dtype=dtype)
return cls(model, game)
def _temperature(self, question: dict) -> float:
value = self.model.temperature(question)
if not math.isfinite(value) or value <= 0:
raise core.ModelError("calibration temperature must be finite and positive")
return value
@torch.inference_mode()
def predict_many(self, rows: Sequence[dict], *, batch_size: int = 32) -> list[dict]:
"""Score rows {state, question}; each row may supply different options.
Returns an option id and a calibrated probability per supplied option.
Batch size 32 and original file order match the historical evaluation.
"""
if isinstance(batch_size, bool) or not isinstance(batch_size, int) or batch_size < 1:
raise core.ModelError("batch_size must be a positive integer")
answers = []
for start in range(0, len(rows), batch_size):
chunk = [row.get("system_one", row) for row in rows[start:start + batch_size]]
for row in chunk:
if not isinstance(row.get("question"), dict) or row["question"].get("type") != "choice":
raise core.ModelError("game models require a choice question with supplied options")
encoded = [self.model.encode(row["state"], row["question"]) for row in chunk]
for row, item, logits in zip(chunk, encoded, self.model._logits(encoded)):
values = torch.softmax(logits / self._temperature(row["question"]), -1).cpu().tolist()
slots = core.question_slots(row["question"])
if len(values) != len(slots) or not all(math.isfinite(p) and 0 <= p <= 1 for p in values):
raise core.ModelError("invalid decision probabilities")
best = max(range(len(values)), key=values.__getitem__)
answers.append({"choice": slots[best]["id"], "confidence": values[best],
"probabilities": {slot["id"]: p for slot, p in zip(slots, values)},
"tokens": len(item.ids), "dropped_state_tokens": item.dropped_state_tokens})
return answers
def predict(self, state: Any, question: dict) -> dict:
return self.predict_many([{"state": state, "question": question}], batch_size=1)[0]
class StockLayaModel(GameModel):
"""The pinned unadapted Laya checkpoint, evaluated with the same input layout."""
@classmethod
def from_pretrained(cls, *, max_seq_length: int, base: str | Path | None = None,
device: str | None = None, dtype: str = "auto") -> "StockLayaModel":
from safetensors.torch import load_file
from transformers import AutoTokenizer
base_dir = core._local_dir(base or core.LAYA_BASE_REPO, core.LAYA_BASE_REVISION,
[*core.LAYA_BASE_FILES, "rl_agent_config.json"])
agent = json.loads((base_dir / "rl_agent_config.json").read_text())
device_obj = torch.device(device or ("cuda" if torch.cuda.is_available() else "cpu"))
dtype_obj = core._resolve_dtype(dtype, device_obj)
encoder = core.load_laya_encoder(base_dir, dtype_obj, device_obj, core.LAYA_BASE_SHA256)
weights_path, _, tokenizer_dir = core.base_layout(base_dir)
weights = {k: v for k, v in load_file(str(weights_path)).items() if not k.startswith("encoder.")}
head = core.LayaDecisionHead(int(encoder.get_input_embeddings().weight.shape[1]),
int(agent.get("head_layers", 2)),
len(agent.get("act_costs", {"escalate": .5})) + 1).float()
head.load_state_dict(weights, strict=True)
head.to(device_obj).eval()
tokenizer = AutoTokenizer.from_pretrained(str(tokenizer_dir), trust_remote_code=False,
local_files_only=True)
tokenizer.padding_side = "right"
model = core.SystemOneDecisionModel(encoder.eval(), tokenizer, head,
{"global_temperature": 1.0, "buckets": {}}, {}, {},
device_obj, dtype_obj, max_seq_length, None, None, False)
result = cls(model, "stock-laya")
result.agent_config = agent
return result
def _temperature(self, question: dict) -> float:
count = len(core.question_slots(question))
size = "2" if count <= 2 else "3-5" if count <= 5 else "6-10" if count <= 10 else "11+"
by_options = self.agent_config.get("temperature_by_options", {})
by_type = self.agent_config.get("temperature", [1.0, 1.0, 1.0])
value = float(by_options.get(question["type"] + ":" + size,
by_type[core.LAYA_QTYPES[question["type"]]]))
if not math.isfinite(value) or value <= 0:
raise core.ModelError("stock calibration temperature must be finite and positive")
return value