ardy-motion-api / ardy /model /load_model.py
cs686's picture
Deploy ARDY ZeroGPU Blender motion API
c1e2af3 verified
Raw
History Blame Contribute Delete
10.8 kB
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
"""Load Ardy diffusion models from local checkpoints or Hugging Face."""
from pathlib import Path
from typing import Optional
import torch
from huggingface_hub import snapshot_download
from omegaconf import OmegaConf
from .loading import (
DEFAULT_MODEL,
DEFAULT_TEXT_ENCODER_URL,
get_env_var,
instantiate_from_dict,
)
from .registry import hf_repo_id, resolve_model_name
DEFAULT_TEXT_ENCODER = "llm2vec"
TEXT_ENCODER_PRESETS = {
"llm2vec": {
"target": "ardy.model.LLM2VecEncoder",
"kwargs": {
# The original McGill adapter points at Meta's gated repository.
# Load the same Llama 3.0 architecture from an ungated mirror, then
# apply the original MNTP and supervised adapters explicitly.
"base_model_name_or_path": get_env_var(
"LLM2VEC_BASE_MODEL",
"unsloth/llama-3-8b-Instruct",
),
"mntp_model_name_or_path": get_env_var(
"LLM2VEC_MNTP_MODEL",
"McGill-NLP/LLM2Vec-Meta-Llama-3-8B-Instruct-mntp",
),
"peft_model_name_or_path": get_env_var(
"LLM2VEC_SUPERVISED_MODEL",
"McGill-NLP/LLM2Vec-Meta-Llama-3-8B-Instruct-mntp-supervised",
),
"prompt_model_family": "llama3-instruct",
"dtype": "bfloat16",
"llm_dim": 4096,
"device": "auto",
},
}
}
def _download_from_hf(full_name: str) -> Path:
"""Download a released model from Hugging Face; returns the local snapshot dir.
With LOCAL_CACHE=true, tries the local HF cache first and falls back online.
"""
repo_id = hf_repo_id(full_name)
local_cache = get_env_var("LOCAL_CACHE", "False").lower() == "true"
if local_cache:
try:
return Path(snapshot_download(repo_id=repo_id, local_files_only=True))
except Exception:
pass # cache miss -> download online below
return Path(snapshot_download(repo_id=repo_id))
def _build_api_text_encoder_conf(text_encoder_url: str) -> dict:
return {
"_target_": "ardy.model.text_encoder_api.TextEncoderAPI",
"url": text_encoder_url,
}
def _build_local_text_encoder_conf(text_encoder_fp32: bool = False) -> dict:
text_encoder_name = get_env_var("TEXT_ENCODER", DEFAULT_TEXT_ENCODER)
if text_encoder_name not in TEXT_ENCODER_PRESETS:
available = ", ".join(sorted(TEXT_ENCODER_PRESETS))
raise ValueError(f"Unknown TEXT_ENCODER='{text_encoder_name}'. Available: {available}")
preset = TEXT_ENCODER_PRESETS[text_encoder_name]
# Copy before overriding so the shared preset dict is never mutated.
kwargs = dict(preset["kwargs"])
if text_encoder_fp32:
kwargs["dtype"] = "float32"
return {
"_target_": preset["target"],
**kwargs,
}
def _select_text_encoder_conf(
text_encoder_url: str,
text_encoder_fp32: bool = False,
mode: str = "auto",
) -> tuple:
"""Return ``(conf, probe)``: the selected encoder conf plus the already-instantiated encoder
when auto-mode probing built one (reused by the caller so the API client is not constructed
twice).
``mode`` is resolved and validated by load_text_encoder:
- "api": force TextEncoderAPI
- "local": force local LLM2VecEncoder
- "auto": try API first, fallback to local if unreachable
"""
if mode == "local":
return _build_local_text_encoder_conf(text_encoder_fp32), None
if mode == "api":
return _build_api_text_encoder_conf(text_encoder_url), None
api_conf = _build_api_text_encoder_conf(text_encoder_url)
try:
text_encoder = instantiate_from_dict(api_conf)
# Probe availability early so inference doesn't fail later.
text_encoder(["healthcheck"])
return api_conf, text_encoder
except Exception as error:
print(
"Text encoder service is unreachable, falling back to local LLM2Vec "
f"encoder. ({type(error).__name__}: {error})"
)
return _build_local_text_encoder_conf(text_encoder_fp32), None
def load_text_encoder(
mode: Optional[str] = None,
url: Optional[str] = None,
fp32: bool = False,
device: Optional[str] = None,
):
"""Select and instantiate a text encoder, ready for inference.
This is the single place that owns text-encoder selection + instantiation,
so it can be built once and reused across multiple models (e.g. core / g1 /
soma) by passing the result into ``load_model(..., text_encoder=...)``.
Args:
mode: Backend selection ("auto"/"api"/"local"). When None, falls back
to the TEXT_ENCODER_MODE env var (default "auto").
url: Remote service URL. When None, falls back to the TEXT_ENCODER_URL
env var.
fp32: Use float32 instead of the default bfloat16.
device: Target device. When None, uses cuda if available else cpu.
Returns:
The instantiated text encoder placed on ``device``.
"""
if mode is None:
mode = get_env_var("TEXT_ENCODER_MODE", "auto")
mode = str(mode).lower()
if mode not in ("auto", "api", "local"):
raise ValueError(
f"Unknown text-encoder mode {mode!r}. Choose 'auto', 'api' or 'local'; "
"to load a model without a text encoder, pass text_encoder=False to "
"load_model()."
)
resolved_url = url or get_env_var("TEXT_ENCODER_URL", DEFAULT_TEXT_ENCODER_URL)
print(
f"Setting up text encoder (mode={mode}); first run may take a while...",
flush=True,
)
conf, text_encoder = _select_text_encoder_conf(resolved_url, fp32, mode=mode)
if text_encoder is None:
# Placement is handled below via .to(); drop any device kwarg the preset
# may carry so it doesn't conflict (e.g. accelerate device_map="auto").
conf = dict(conf)
conf.pop("device", None)
text_encoder = instantiate_from_dict(conf)
if device is None:
device = "cuda" if torch.cuda.is_available() else "cpu"
dtype = torch.float32 if fp32 else torch.bfloat16
return text_encoder.to(device=device, dtype=dtype)
def load_model(
modelname=None,
device=None,
eval_mode: bool = True,
default_family: Optional[str] = None,
text_encoder=None,
text_encoder_fp32: bool = False,
text_encoder_mode: Optional[str] = None,
text_encoder_url: Optional[str] = None,
return_config: bool = False,
checkpoints_dir: Optional[str] = None,
):
"""Load a released Ardy model.
``modelname`` may be a short key ("core"/"g1"/"soma") or a full folder name
("Ardy-Core-RP-20FPS-Horizon40"). If a local checkpoints dir is given (via
``checkpoints_dir`` or the ``CHECKPOINTS_DIR`` env var) the model is loaded
from ``<checkpoints_dir>/<full_name>``; otherwise it is downloaded from HF.
Args:
modelname: Short key or full folder name; uses DEFAULT_MODEL if None.
device: Target device for the model (e.g. 'cuda', 'cpu').
eval_mode: If True, set model to eval mode.
default_family: Ignored (kept for call-site compatibility).
text_encoder: Pre-built text encoder to reuse, or False to load the
model without any text encoder (model.text_encoder is left as
None). When None (the default), one is built via
``load_text_encoder``.
text_encoder_fp32: If True, uses fp32 for the text encoder rather than default bfloat16.
text_encoder_mode: Backend selection ("auto"/"api"/"local"). When None,
falls back to the TEXT_ENCODER_MODE env var (default "auto").
Ignored unless ``text_encoder`` is None.
text_encoder_url: URL of the remote text-encoder service. When None,
falls back to the TEXT_ENCODER_URL env var.
checkpoints_dir: Local dir holding released model folders. When None,
falls back to the CHECKPOINTS_DIR env var; if neither is set the
model is downloaded from Hugging Face.
Returns:
Loaded model in eval mode, or (model, confg) if return_config is true
Raises:
ValueError: If modelname cannot be resolved to a released model.
FileNotFoundError: If config.yaml is missing in the model folder.
"""
if modelname is None:
modelname = DEFAULT_MODEL
# Local dir if CHECKPOINTS_DIR is set (arg or env), otherwise download from HF.
checkpoints_dir = checkpoints_dir or get_env_var("CHECKPOINTS_DIR")
# Resolve after checkpoints_dir so local-only folders (beyond the three
# released models) are accepted when loading from a local dir.
full_name = resolve_model_name(modelname, checkpoints_dir=checkpoints_dir)
if checkpoints_dir:
model_path = Path(checkpoints_dir) / full_name
if not model_path.exists():
raise FileNotFoundError(f"Model {full_name!r} not found under CHECKPOINTS_DIR {checkpoints_dir!r}.")
else:
model_path = _download_from_hf(full_name)
model_config_path = model_path / "config.yaml"
if not model_config_path.exists():
raise FileNotFoundError(f"The model folder exists but config.yaml is missing: {model_config_path}")
model_conf = OmegaConf.load(model_config_path)
# Resolve the text encoder: False means load the model without one, a
# pre-built instance is reused as-is, and None (the default) builds one
# here via load_text_encoder (which resolves a None mode through the
# TEXT_ENCODER_MODE env var). Identity checks, not truthiness: False and
# None mean different things.
if text_encoder is False:
text_encoder = None
elif text_encoder is None:
text_encoder = load_text_encoder(
mode=text_encoder_mode,
url=text_encoder_url,
fp32=text_encoder_fp32,
device=device,
)
runtime_conf = OmegaConf.create({"checkpoint_dir": str(model_path)})
model_cfg = OmegaConf.to_container(OmegaConf.merge(model_conf, runtime_conf), resolve=True)
model_cfg.pop("checkpoint_dir", None)
# The text encoder is attached after construction (or left as None for
# text_encoder=False), so prevent Hydra from instantiating one during
# construction.
model_cfg["text_encoder"] = None
model = instantiate_from_dict(model_cfg, overrides={"device": device})
if text_encoder is not None:
model.text_encoder = text_encoder
if eval_mode:
model = model.eval()
if return_config:
return model, model_cfg
return model