kev-0-5b-coreml / runtime.py
alexwengg's picture
Fix unlabelled Core ML serving and Hub snapshot loading
62d02e9 verified
Raw
History Blame Contribute Delete
3.52 kB
"""Serve unlabelled Kev 0.5B typed decisions from the Core ML artifact."""
from __future__ import annotations
import argparse
import json
import shutil
import tempfile
from pathlib import Path
import coremltools as ct
import numpy as np
from kev.api import output_tokens, to_answers
from transformers import AutoTokenizer
from preprocessing import Shape, prepare_runtime_inputs
COMPUTE_UNITS = {
"all": ct.ComputeUnit.ALL,
"cpu": ct.ComputeUnit.CPU_ONLY,
"cpu-gpu": ct.ComputeUnit.CPU_AND_GPU,
"cpu-ne": ct.ComputeUnit.CPU_AND_NE,
}
PACKAGE = "kev_0_5b_fp16_L128_options32.mlpackage"
def stage_symlinked_package(package: Path) -> tuple[Path, tempfile.TemporaryDirectory | None]:
"""Give Core ML real package files when a Hub snapshot contains blob symlinks."""
if not package.is_symlink() and not any(path.is_symlink() for path in package.rglob("*")):
return package, None
temporary = tempfile.TemporaryDirectory(prefix="kev-coreml-")
staged = Path(temporary.name) / package.name
shutil.copytree(package, staged, symlinks=False)
return staged, temporary
class KevCoreML:
"""Reusable serving session; initialization and request latency stay separate."""
def __init__(self, model_dir: str | Path, *, units: str = "all", package: str | Path | None = None):
root = Path(model_dir)
self.tokenizer = AutoTokenizer.from_pretrained(root / "tokenizer", local_files_only=True)
self.shape = Shape()
model_path = Path(package) if package is not None else root / PACKAGE
model_path, self._staged_package = stage_symlinked_package(model_path)
self.model = ct.models.MLModel(str(model_path), compute_units=COMPUTE_UNITS[units])
def predict(self, request: dict) -> dict:
arrays, encoded, metadata, parsed = prepare_runtime_inputs(self.tokenizer, request, self.shape)
result = np.asarray(self.model.predict(arrays)["probabilities"], dtype=np.float64)
count = len(metadata[0]["keys"])
if result.shape != (1, self.shape.max_options) or not np.isfinite(result).all():
raise ValueError("Invalid Core ML probabilities")
selected = result[0, :count].tolist()
keys = metadata[0]["keys"]
answers = to_answers([selected], metadata)
return {
"model": parsed.model,
"answers": answers,
"usage": {
"input_tokens": len(encoded["ids"]),
"output_tokens": output_tokens(self.tokenizer, answers),
},
"option_keys": keys,
"probabilities": selected,
"chosen": keys[int(np.argmax(selected))],
}
def predict(model_dir: str | Path, request: dict, *, units: str = "all", package: str | Path | None = None) -> dict:
"""One-shot compatibility wrapper; reuse `KevCoreML` for repeated calls."""
return KevCoreML(model_dir, units=units, package=package).predict(request)
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--model-dir", required=True, type=Path)
parser.add_argument("--request-json", type=Path, required=True)
parser.add_argument("--units", choices=COMPUTE_UNITS, default="all")
parser.add_argument("--package", type=Path)
args = parser.parse_args()
request = json.loads(args.request_json.read_text())
print(json.dumps(predict(args.model_dir, request, units=args.units, package=args.package), indent=2))
if __name__ == "__main__":
main()