Files
wehub-resource-sync 770d92cb1f
Lint / lint (push) Has been cancelled
Build Docs / Deploy Docs (push) Has been cancelled
Windows CI / Windows (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:23:58 +08:00

200 lines
6.7 KiB
Python

"""Continuous model delivery for MLC LLM models."""
import argparse
import dataclasses
import json
import os
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
from typing import Any, Callable, Dict, List # noqa: UP035
from mlc_llm.support import logging
from mlc_llm.support.argparse import ArgumentParser
from mlc_llm.support.constants import MLC_TEMP_DIR
from mlc_llm.support.style import bold, green, red
logger = logging.getLogger(__name__)
@dataclasses.dataclass
class ModelInfo:
"""Necessary information for the model delivery"""
model_id: str
model: Path
quantization: str
device: str
# overrides the `context_window_size`, `prefill_chunk_size`,
# `sliding_window_size`, `attention_sink_size`, `max_batch_size`
# and `tensor_parallel_shards in mlc-chat-config.json
overrides: Dict[str, int] # noqa: UP006
class DeferredScope:
"""A context manager that defers execution of functions until exiting the scope."""
def __init__(self):
self.deferred_functions = []
def add(self, func: Callable[[], None]):
"""Add a function to be executed when exiting the scope."""
self.deferred_functions.append(func)
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
for func in reversed(self.deferred_functions):
func()
return False
def create_temp_dir(self) -> Path:
"""Create a temporary directory that will be deleted when exiting the scope."""
temp_dir = tempfile.mkdtemp(dir=MLC_TEMP_DIR)
self.add(lambda: shutil.rmtree(temp_dir, ignore_errors=True))
return Path(temp_dir)
def _run_compilation(model_info: ModelInfo, repo_dir: Path) -> bool:
"""Run the compilation of the model library."""
def get_lib_ext(device: str) -> str:
if device in ["cuda", "vulkan", "metal"]:
return ".so"
if device in ["android", "ios"]:
return ".tar"
if device in ["webgpu"]:
return ".wasm"
return ""
succeeded = True
with tempfile.TemporaryDirectory(dir=MLC_TEMP_DIR) as temp_dir:
log_path = Path(temp_dir) / "logs.txt"
model_lib_name = f"{model_info.model_id}-{model_info.quantization}-{model_info.device}"
lib_ext = get_lib_ext(model_info.device)
if lib_ext == "":
raise ValueError(f"Unsupported device: {model_info.device}")
model_lib_name += lib_ext
with log_path.open("a", encoding="utf-8") as log_file:
overrides = ";".join(f"{key}={value}" for key, value in model_info.overrides.items())
cmd = [
sys.executable,
"-m",
"mlc_llm",
"compile",
str(model_info.model),
"--device",
model_info.device,
"--quantization",
model_info.quantization,
"--overrides",
overrides,
"--output",
os.path.join(temp_dir, model_lib_name),
]
print(" ".join(cmd), file=log_file, flush=True)
subprocess.run(cmd, check=True, stdout=log_file, stderr=subprocess.STDOUT)
logger.info("[MLC] Compilation Complete!")
if not (Path(temp_dir) / model_lib_name).exists():
logger.error(
"[%s] Model %s. Device %s. No compiled library found.",
red("FAILED"),
model_info.model_id,
model_info.device,
)
succeeded = False
return succeeded
# overwrite git repo file with the compiled library
repo_filepath = repo_dir / model_info.model_id / model_lib_name
if not repo_filepath.parent.exists():
repo_filepath.parent.mkdir(parents=True, exist_ok=True)
# copy lib from Path(temp_dir) / model_lib_name to repo_filepath
shutil.copy(Path(temp_dir) / model_lib_name, repo_filepath)
logger.info("Saved library %s at %s", model_lib_name, repo_filepath)
return succeeded
def _main(
spec: Dict[str, Any], # noqa: UP006
):
"""Compile the model libs in the spec and save them to the binary_libs_dir."""
failed_cases: List[Any] = [] # noqa: UP006
for task_index, task in enumerate(spec["tasks"], 1):
logger.info(
bold("[{task_index}/{total_tasks}] Processing model: ").format(
task_index=task_index,
total_tasks=len(spec["tasks"]),
)
+ green(task["model_id"])
)
model_info = {
"model_id": task["model_id"],
"model": task["model"],
}
for compile_opt in spec["default_compile_options"] + task.get("compile_options", []):
for quantization in spec["default_quantization"] + task.get("quantization", []):
model_info["quantization"] = quantization
model_info["device"] = compile_opt["device"]
model_info["overrides"] = compile_opt.get("overrides", {})
logger.info(
"[Config] "
+ bold("model_id: ")
+ model_info["model_id"]
+ bold(", quantization: ")
+ model_info["quantization"]
+ bold(", device: ")
+ model_info["device"]
+ bold(", overrides: ")
+ json.dumps(model_info["overrides"])
)
result = _run_compilation(
ModelInfo(**model_info),
repo_dir=Path(spec["binary_libs_dir"]),
)
if not result:
failed_cases.append(model_info)
if failed_cases:
logger.info("Total %s %s:", len(failed_cases), red("failures"))
for case in failed_cases:
logger.info(
"model_id %s, quantization %s, device %s, overrides %s",
case["model_id"],
case["quantization"],
case["device"],
json.dumps(case["overrides"]),
)
def main():
"""Entry point."""
def _load_spec(path_spec: str) -> Dict[str, Any]: # noqa: UP006
path = Path(path_spec)
if not path.exists():
raise argparse.ArgumentTypeError(f"Spec file does not exist: {path}")
with path.open("r", encoding="utf-8") as i_f:
return json.load(i_f)
parser = ArgumentParser("MLC LLM continuous library delivery")
parser.add_argument(
"--spec",
type=_load_spec,
required=True,
help="Path to the spec file",
)
parsed = parser.parse_args()
_main(
spec=parsed.spec,
)
if __name__ == "__main__":
main()