multimodalart's picture
multimodalart HF Staff
Upload folder using huggingface_hub
ba6c090 verified
Raw History Blame Contribute Delete
5.67 kB
"""Local and Hugging Face model loading for Continuous Intelligence."""
import json
import math
import re
from contextlib import nullcontext
from functools import cache
from pathlib import Path, PureWindowsPath
import torch
from .ci_proto import Forecaster
_COMMIT = re.compile(r"[0-9a-f]{40}")
def _device(name: str) -> torch.device:
if name == "auto":
name = "cuda" if torch.cuda.is_available() else "cpu"
if name not in ("cpu", "cuda"):
raise ValueError("device must be 'cpu', 'cuda', or 'auto'")
if name == "cuda" and not torch.cuda.is_available():
raise RuntimeError("CUDA was requested but is not available")
return torch.device(name)
def _files(
source: str | Path, tta_config: str | Path | None, revision: str | None
) -> tuple[Path, Path, Path | None]:
path = Path(source).expanduser()
if path.exists():
if revision is not None:
raise ValueError("revision applies only to Hugging Face models")
else:
if (
isinstance(source, Path)
or str(source).startswith(("/", "./", "../", "~", ".\\", "..\\"))
or "\\" in str(source)
or PureWindowsPath(source).drive
or str(source).count("/") != 1
or path.parent.is_dir()
or path.suffix in (".ckpt", ".pt", ".pth")
or tta_config is not None
):
raise FileNotFoundError(path)
from huggingface_hub import hf_hub_download, snapshot_download
info = Path(hf_hub_download(repo_id=str(source), filename="config.json", revision=revision))
commit = info.parent.name if _COMMIT.fullmatch(info.parent.name) else revision
path = Path(
snapshot_download(
repo_id=str(source),
revision=commit,
allow_patterns=["model.ckpt", "model-long.ckpt", "tta.json"],
)
)
info = None
if path.is_dir():
if tta_config is not None:
raise ValueError("a model bundle uses its own tta.json")
info = path / "config.json" if (path / "config.json").is_file() else None
path, tta_config = path / "model.ckpt", path / "tta.json"
elif tta_config is None:
raise ValueError("a standalone checkpoint requires tta_config")
tta = Path(tta_config).expanduser()
for file in (path, tta):
if not file.is_file():
raise FileNotFoundError(file)
return path, tta, info
def _model_info(path: Path, forecaster: Forecaster) -> dict:
"""Read a bundle's ``config.json`` and check it describes the loaded weights."""
info = json.loads(path.read_text())
if not isinstance(info, dict):
raise ValueError("config.json must contain a JSON object")
missing = {"name", "parameters", "quantiles"} - set(info)
if missing:
raise ValueError(f"config.json is missing {sorted(missing)}")
model = forecaster._model
quantiles = forecaster.quantiles.tolist()
if (
not isinstance(info["quantiles"], list)
or len(info["quantiles"]) != len(quantiles)
or not all(
isinstance(given, (int, float)) and math.isclose(given, level, abs_tol=1e-6)
for given, level in zip(info["quantiles"], quantiles, strict=True)
)
):
raise ValueError("config.json quantiles do not match the checkpoint")
if info["parameters"] != sum(parameter.numel() for parameter in model.parameters()):
raise ValueError("config.json parameters do not match the checkpoint")
return info
def load_model(
checkpoint: str | Path,
tta_config: str | Path | None = None,
device: str = "auto",
*,
revision: str | None = None,
) -> Forecaster:
"""Load a local model bundle, Hugging Face model ID, or checkpoint plus TTA file.
Bundles contain ``model.ckpt`` (weights and embedded architecture) and
``tta.json``. Hub bundles also contain ``config.json``, which describes the
model and is returned by ``Forecaster.info``; it is checked against the
weights. Hub files are fetched together from the same revision;
pass a commit SHA to pin it. Hub IDs use ``owner/model`` strings. Use a
``Path`` or ``./`` prefix to mark an ambiguous relative path as local.
An optional ``model-long.ckpt`` shares the bundle's TTA and is selected
when history exceeds the primary checkpoint's context length.
"""
placement = _device(device)
path, tta_path, info_path = _files(checkpoint, tta_config, revision)
payload = torch.load(path, map_location="cpu", weights_only=True, mmap=True)
from .ci_proto._checkpoint import from_checkpoint
forecaster = from_checkpoint(payload, tta_path, placement)
placement = forecaster.device
if info_path is not None:
forecaster._info = _model_info(info_path, forecaster)
long_path = path.with_name("model-long.ckpt")
if tta_config is None and long_path.is_file() and forecaster._model.config.context_len:
quantiles = forecaster.quantiles
future_len = forecaster._model.config.future_len
@cache
def load_long() -> Forecaster:
context = torch.cuda.device(placement) if placement.type == "cuda" else nullcontext()
with context:
long = load_model(long_path, tta_path, device=placement.type)
if not torch.equal(long.quantiles, quantiles) or (
long._model.config.future_len != future_len
):
raise ValueError("long checkpoint must have the same quantiles and forecast window")
return long
forecaster._long_model = load_long
return forecaster