gabfon's picture
Upload folder using huggingface_hub
3e363e2 verified
Raw History Blame Contribute Delete
5.13 kB
"""Reloadable inference artifact export."""
from __future__ import annotations
import json
from collections.abc import Mapping
from pathlib import Path
from typing import Any
import torch
from graph.config import GraphConfig, PATConfig
from graph.model import GraphModel
def load_checkpoint_file(
path: str | Path, *, map_location: str | torch.device = "cpu"
) -> dict[str, Any]:
"""Load a GRM checkpoint with ``weights_only=True``.
Checkpoints written by :class:`~graph.training.trainer.GraphTrainer` contain
only tensors and plain containers, so unpickling arbitrary Python objects is
never required. Loading model artifacts with ``weights_only=False`` would
permit code execution from a checkpoint file, which matters because the
registry promotion flow (PLAN section 40) moves artifacts between machines.
"""
# A checkpoint is a heterogeneous payload from outside the process; this is
# the external boundary where dict[str, Any] is the honest type.
payload = torch.load(path, map_location=map_location, weights_only=True)
if not isinstance(payload, dict):
raise TypeError(f"{path}: checkpoint is not a dict payload")
return payload
def save_inference_artifact(
model: GraphModel, directory: str | Path, *, use_safetensors: bool = False
) -> Path:
root = Path(directory)
root.mkdir(parents=True, exist_ok=True)
if use_safetensors:
try:
from safetensors.torch import save_file
except (
ImportError
) as exc: # pragma: no cover - depends on optional export dependency
raise RuntimeError(
"safetensors export requested but safetensors is not installed"
) from exc
weights = root / "model.safetensors"
save_file(
{key: value.detach().cpu() for key, value in model.state_dict().items()},
str(weights),
)
weight_format = "safetensors"
else:
weights = root / "model.pt"
torch.save(model.state_dict(), weights)
weight_format = "torch"
config = model.config.to_dict()
config["weight_format"] = weight_format
(root / "config.json").write_text(
json.dumps(config, indent=2, sort_keys=True), encoding="utf-8"
)
return weights
def load_inference_artifact(
directory: str | Path, *, map_location: str | torch.device = "cpu"
) -> GraphModel:
root = Path(directory)
raw = json.loads((root / "config.json").read_text(encoding="utf-8"))
weight_format = raw.pop("weight_format", "torch")
pat = PATConfig(**raw.pop("pat"))
raw.pop("training", None)
raw.pop("loss", None)
config = GraphConfig(pat=pat, **raw)
model = GraphModel(config)
if weight_format == "safetensors":
try:
from safetensors.torch import load_file
except (
ImportError
) as exc: # pragma: no cover - depends on optional export dependency
raise RuntimeError(
"safetensors artifact requires the safetensors package"
) from exc
state = load_file(str(root / "model.safetensors"), device=str(map_location))
else:
state = torch.load(
root / "model.pt", map_location=map_location, weights_only=True
)
load_state_dict_compat(model, state)
model.eval()
return model
def load_state_dict_compat(
model: GraphModel, state: Mapping[str, torch.Tensor]
) -> None:
"""Load checkpoints across explicitly supported PAT shape migrations.
The structured PAT token expansion added type/attribute tables and a
larger position table. Older registered smoke artifacts remain
loadable by zero-initializing only those new inputs and padding the old
position rows; every other missing or incompatible tensor fails loudly.
"""
expected = model.state_dict()
migrated: dict[str, torch.Tensor] = {}
missing_allowed = {
"pat.type_embedding.weight",
"pat.attribute_embedding.weight",
}
for key, value in state.items():
if key not in expected:
raise RuntimeError(f"checkpoint contains unexpected tensor: {key}")
if value.shape == expected[key].shape:
migrated[key] = value
continue
if (
key == "pat.path_position_embedding.weight"
and value.ndim == 2
and value.shape[1] == expected[key].shape[1]
and value.shape[0] < expected[key].shape[0]
):
padded = torch.zeros_like(expected[key])
padded[: value.shape[0]] = value
migrated[key] = padded
continue
raise RuntimeError(
f"checkpoint tensor shape mismatch for {key}: {tuple(value.shape)} != {tuple(expected[key].shape)}"
)
for key, value in expected.items():
if key not in migrated:
if key in missing_allowed:
migrated[key] = torch.zeros_like(value)
else:
raise RuntimeError(f"checkpoint is missing tensor: {key}")
model.load_state_dict(migrated, strict=True)