Source code for experimental.server.runtime.engine_build

# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Checkpoint resolution and complete-model engine caching."""

import fcntl
import hashlib
import json
import logging
import os
import shutil
import stat
import tempfile
from contextlib import contextmanager
from dataclasses import dataclass, fields, replace
from pathlib import Path
from typing import Iterator, List, Optional

from . import engine_layout

_CACHE_LAYOUT_VERSION = 2
_DEFAULT_CACHE_DIR = Path.home() / ".cache" / "tensorrt-edgellm"
_DEFAULT_ENGINE_CACHE_MAX_BYTES = 50 * (1 << 30)
_CHECKPOINT_SUFFIXES = (".bin", ".json", ".model", ".pt", ".pth",
                        ".safetensors")
_CONTENT_SAMPLE_BYTES = 64

logger = logging.getLogger("edgellm.server")


@dataclass(frozen=True)
class BuildOptions:
    """Options forwarded to one all-component builder invocation."""

    spec_type: str = "none"
    draft_model_dir: str = ""
    max_input_len: Optional[int] = None
    max_kv_cache_capacity: Optional[int] = None
    max_batch_size: Optional[int] = None
    max_image_tokens: Optional[int] = None
    max_image_tokens_per_image: Optional[int] = None
    tp_size: Optional[int] = None
    plugin_path: str = ""
    tree_base: bool = False

    @property
    def builder_spec_type(self) -> str:
        # A separate MTP checkpoint uses the Gemma assistant contract. Native
        # MTP layers remain part of the base checkpoint.
        if self.spec_type == "mtp" and self.draft_model_dir:
            return "gemma4_mtp"
        return self.spec_type

    def to_argv(self, model_dir: str, bundle_dir: str) -> List[str]:
        argv = [
            "--model-dir",
            model_dir,
            "--engine-dir",
            bundle_dir,
            "--components",
            "all",
            "--externalize-weights",
            "all",
        ]
        if self.builder_spec_type != "none":
            argv += ["--spec-type", self.builder_spec_type]
        if self.draft_model_dir:
            argv += ["--draft-model-dir", self.draft_model_dir]
        if self.plugin_path:
            argv += ["--plugin-path", self.plugin_path]
        if self.tree_base:
            argv.append("--tree-base")
        for flag, value in (
            ("--max-input-len", self.max_input_len),
            ("--max-kv-cache-capacity", self.max_kv_cache_capacity),
            ("--max-batch-size", self.max_batch_size),
            ("--max-image-tokens", self.max_image_tokens),
            ("--max-image-tokens-per-image", self.max_image_tokens_per_image),
            ("--tp-size", self.tp_size),
        ):
            if value is not None:
                argv += [flag, str(value)]
        return argv


@dataclass(frozen=True)
class PreparedModel:
    """Resolved checkpoints and their profile-specific engine bundle."""

    bundle_dir: str
    model_dir: str
    draft_model_dir: str = ""
    built: bool = False


def cache_root(cache_dir: str = "") -> str:
    """Return the one user-controlled artifact root."""
    root = Path(cache_dir).expanduser() if cache_dir else _DEFAULT_CACHE_DIR
    return str(root.resolve())


def resolve_model_dir(model: str, cache_dir: str = "") -> str:
    """Resolve a local checkpoint or Hugging Face model ID."""
    if os.path.isdir(model):
        path = os.path.abspath(model)
        source_type = engine_layout.classify_model_source(path)
        if source_type == "onnx_dir":
            raise ValueError(
                "ONNX model directories are not supported; pass the original "
                "checkpoint")
        if source_type == "engine_dir":
            raise ValueError(
                "engine bundles are cache artifacts; pass the checkpoint or "
                "Hugging Face model ID used to build the model")
        return path
    try:
        from huggingface_hub import snapshot_download
    except ImportError as exc:
        raise RuntimeError(
            "huggingface_hub is required for Hugging Face model IDs; install "
            "the TensorRT Edge-LLM Python package with server dependencies"
        ) from exc
    checkpoint_cache = os.path.join(cache_root(cache_dir), "checkpoints")
    return snapshot_download(model, cache_dir=checkpoint_cache)


def _checkpoint_digest(model_dir: str) -> str:
    """Fingerprint a checkpoint independently of its path and mtimes.

    Small metadata files are hashed completely. Large checkpoint shards use
    their safetensors header or file size plus bounded content samples. The
    samples keep structurally identical checkpoints with different weights in
    separate cache entries without reading full tensor payloads.
    """
    digest = hashlib.sha256()
    for root, dirs, files in os.walk(model_dir):
        dirs[:] = sorted(directory for directory in dirs
                         if directory != ".edgellm")
        for filename in sorted(files):
            if not filename.endswith(_CHECKPOINT_SUFFIXES):
                continue
            path = os.path.join(root, filename)
            digest.update(os.path.relpath(path, model_dir).encode("utf-8"))
            _update_file_digest(path, digest)
    return digest.hexdigest()


def _update_file_digest(path: str, digest) -> None:
    """Hash a checkpoint file with bounded reads for weight payloads."""
    size = os.path.getsize(path)
    digest.update(size.to_bytes(8, "little"))
    with open(path, "rb") as checkpoint_file:
        if path.endswith(".safetensors") and size >= 8:
            prefix = checkpoint_file.read(8)
            header_size = int.from_bytes(prefix, "little")
            if header_size <= size - 8:
                digest.update(prefix)
                digest.update(checkpoint_file.read(header_size))
                _update_content_samples(checkpoint_file, digest,
                                        8 + header_size,
                                        size - 8 - header_size)
                return
            checkpoint_file.seek(0)
        if path.endswith((".json", ".model")):
            for chunk in iter(lambda: checkpoint_file.read(1 << 20), b""):
                digest.update(chunk)
            return
        _update_content_samples(checkpoint_file, digest, 0, size)


def _update_content_samples(checkpoint_file, digest, start: int,
                            size: int) -> None:
    """Hash fixed start, middle, and end ranges of a weight payload."""
    if size <= 0:
        return
    width = min(_CONTENT_SAMPLE_BYTES, size)
    offsets = sorted(set((0, (size - width) // 2, size - width)))
    for offset in offsets:
        checkpoint_file.seek(start + offset)
        digest.update(offset.to_bytes(8, "little"))
        digest.update(checkpoint_file.read(width))


def bundle_cache_path(model_dir: str, cache_dir: str,
                      options: BuildOptions) -> str:
    """Return the cache path for one checkpoint and complete build profile."""
    model_digest = _checkpoint_digest(model_dir)
    draft_digest = (_checkpoint_digest(options.draft_model_dir)
                    if options.draft_model_dir else "")
    profile = {
        "cache_layout_version": _CACHE_LAYOUT_VERSION,
        "model": model_digest,
        "draft_model": draft_digest,
        **{
            field.name: getattr(options, field.name)
            for field in fields(options) if field.name not in {
                "draft_model_dir", "plugin_path"
            }
        },
        "builder_spec_type": options.builder_spec_type,
    }
    profile_digest = hashlib.sha256(
        json.dumps(profile, sort_keys=True).encode("utf-8")).hexdigest()[:12]
    profile_name = (f"i{options.max_input_len or 'default'}-"
                    f"b{options.max_batch_size or 'default'}-"
                    f"kv{options.max_kv_cache_capacity or 'default'}-"
                    f"{options.builder_spec_type}-{profile_digest}")
    return os.path.join(cache_root(cache_dir), "engines",
                        f"model-{model_digest[:16]}", profile_name)


def _is_ready(model_dir: str, bundle_dir: str, options: BuildOptions) -> bool:
    """Verify every component and the requested runtime profile."""
    from experimental.builder.core import contracts
    from experimental.builder.core.bundle import BundleConfig

    try:
        bundle = BundleConfig.from_pretrained(model_dir)
        components = contracts.resolve_components(bundle.root_model_type,
                                                  ("all", ),
                                                  available=bundle.components)
    except (OSError, TypeError, ValueError):
        return False

    config_paths = []
    built_components = set()
    for component in components:
        spec = contracts.component_spec(component)
        if component == contracts.Component.LLM and options.spec_type != "none":
            for role in (contracts.SpecRole.BASE, contracts.SpecRole.DRAFT):
                if not (os.path.isfile(spec.output_path(bundle_dir, role)) and
                        os.path.isfile(spec.config_path(bundle_dir, role))):
                    return False
                config_paths.append(spec.config_path(bundle_dir, role))
            built_components.add(component)
            continue
        if not (os.path.isfile(spec.output_path(bundle_dir))
                and os.path.isfile(spec.config_path(bundle_dir))):
            return False
        config_paths.append(spec.config_path(bundle_dir))
        built_components.add(component)

    has_text_runtime = bool(
        built_components
        & {contracts.Component.LLM, contracts.Component.DLLM})
    has_tts_runtime = {
        contracts.Component.TALKER,
        contracts.Component.CODE_PREDICTOR,
        contracts.Component.CODE2WAV,
    }.issubset(built_components)
    if not (has_text_runtime or has_tts_runtime):
        return False

    try:
        configs = []
        for path in config_paths:
            with open(path, encoding="utf-8") as file:
                configs.append(json.load(file))
    except (OSError, ValueError):
        return False

    base = next((config for config in configs
                 if config.get("engine_role") in ("llm", "dllm", "base")
                 and "max_input_len" in config.get("builder_config", {})), {})
    builder = base.get("builder_config", {})
    expected = {
        "max_input_len": options.max_input_len,
        "max_kv_cache_capacity": options.max_kv_cache_capacity,
        "max_batch_size": options.max_batch_size,
    }
    if any(value is not None and builder.get(name) != value
           for name, value in expected.items()):
        return False
    if (options.spec_type != "none"
            and base.get("spec_decode_type") != options.builder_spec_type):
        return False
    return any("checkpoint_identity" in config for config in configs)


def _resolve_plugin_path(explicit: str) -> str:
    if explicit:
        return explicit
    from .engine import _ensure_plugin_path

    _ensure_plugin_path()
    return os.environ.get("EDGELLM_PLUGIN_PATH", "")


@contextmanager
def _bundle_lock(bundle_dir: str) -> Iterator[None]:
    lock_path = bundle_dir + ".lock"
    os.makedirs(os.path.dirname(lock_path), exist_ok=True)
    with open(lock_path, "w", encoding="utf-8") as lock:
        fcntl.flock(lock, fcntl.LOCK_EX)
        yield


@contextmanager
def _cache_lock(cache_dir: str) -> Iterator[None]:
    """Serialize cache eviction and explicit clear operations."""
    root = cache_root(cache_dir)
    os.makedirs(root, exist_ok=True)
    with open(os.path.join(root, ".engine-cache.lock"), "w",
              encoding="utf-8") as lock:
        fcntl.flock(lock, fcntl.LOCK_EX)
        yield


def _bundle_directories(cache_dir: str) -> List[Path]:
    """List published engine profiles while excluding staging directories."""
    engine_root = Path(cache_root(cache_dir)) / "engines"
    if not engine_root.is_dir():
        return []
    return [
        profile for model in engine_root.iterdir()
        if model.is_dir() and not model.name.startswith(".")
        for profile in model.iterdir()
        if profile.is_dir() and not profile.name.startswith(".")
    ]


def _directory_size(path: Path) -> int:
    """Return logical file bytes below one published bundle."""
    total = 0
    for root, _dirs, files in os.walk(path):
        for filename in files:
            try:
                total += os.path.getsize(os.path.join(root, filename))
            except FileNotFoundError:
                continue
    return total


def _remove_bundle(bundle: Path) -> Optional[int]:
    """Remove an idle bundle, or return None when another process owns it."""
    lock_path = str(bundle) + ".lock"
    with open(lock_path, "w", encoding="utf-8") as lock:
        try:
            fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
        except BlockingIOError:
            return None
        if not bundle.is_dir():
            return 0
        size = _directory_size(bundle)
        shutil.rmtree(bundle)
        return size


[docs] def clear_engine_cache(cache_dir: str = "") -> int: """Remove published engine bundles without deleting checkpoints.""" removed = 0 with _cache_lock(cache_dir): for bundle in _bundle_directories(cache_dir): if _remove_bundle(bundle) is not None: removed += 1 logger.info("Cleared %d engine cache bundle(s)", removed) return removed
[docs] def prune_engine_cache(cache_dir: str = "", max_size_bytes: int = _DEFAULT_ENGINE_CACHE_MAX_BYTES, keep: str = "") -> int: """Evict least-recently-used bundles until the cache is within its cap.""" if max_size_bytes < 1: raise ValueError("max_size_bytes must be positive") keep_path = os.path.realpath(keep) if keep else "" removed = 0 with _cache_lock(cache_dir): entries = [] for bundle in _bundle_directories(cache_dir): try: entries.append((bundle.stat().st_mtime_ns, _directory_size(bundle), bundle)) except FileNotFoundError: continue total = sum(size for _mtime, size, _bundle in entries) for _mtime, size, bundle in sorted(entries): if total <= max_size_bytes: break if keep_path and os.path.realpath(bundle) == keep_path: continue removed_size = _remove_bundle(bundle) if removed_size is None: continue total -= size removed += 1 if removed: logger.info("Evicted %d engine cache bundle(s)", removed) if total > max_size_bytes: logger.warning( "Engine cache remains above its %.2f GiB limit because the active " "or locked bundle cannot be evicted", max_size_bytes / (1 << 30)) return removed
def prepare_model(model: str, cache_dir: str = "", options: Optional[BuildOptions] = None, *, max_cache_size_bytes: int = _DEFAULT_ENGINE_CACHE_MAX_BYTES, clear_cache: bool = False) -> PreparedModel: """Resolve a model and build every component on a cache miss.""" options = options or BuildOptions() model_dir = resolve_model_dir(model, cache_dir) draft_model_dir = "" if options.draft_model_dir: draft_model_dir = resolve_model_dir(options.draft_model_dir, cache_dir) options = replace(options, draft_model_dir=draft_model_dir) if clear_cache: clear_engine_cache(cache_dir) bundle_dir = bundle_cache_path(model_dir, cache_dir, options) built = False with _bundle_lock(bundle_dir): if _is_ready(model_dir, bundle_dir, options): os.utime(bundle_dir) else: parent = os.path.dirname(bundle_dir) os.makedirs(parent, exist_ok=True) staging_dir = tempfile.mkdtemp(prefix=".building-", dir=parent) try: build_options = replace( options, plugin_path=_resolve_plugin_path(options.plugin_path), ) from experimental.builder.cli import main as builder_main builder_main(build_options.to_argv(model_dir, staging_dir)) if not _is_ready(model_dir, staging_dir, build_options): raise RuntimeError( "the checkpoint-native builder did not produce a " f"complete runtime bundle for {model!r}") if os.path.exists(bundle_dir): shutil.rmtree(bundle_dir) os.chmod(staging_dir, stat.S_IMODE(os.stat(parent).st_mode)) os.replace(staging_dir, bundle_dir) built = True finally: shutil.rmtree(staging_dir, ignore_errors=True) prune_engine_cache(cache_dir, max_cache_size_bytes, keep=bundle_dir) return PreparedModel(bundle_dir, model_dir, draft_model_dir, built)