Download source/scripts/deployment/coreml_kv.py from Voltline/vimeml-tiny-ja-v4.2: direct link, hf CLI and curl.
- Browser
- Download file 12.9 kB
-
https://huggingface.co/Voltline/vimeml-tiny-ja-v4.2/resolve/main/source/scripts/deployment/coreml_kv.py
- Command line
-
hf download hf://Voltline/vimeml-tiny-ja-v4.2/source/scripts/deployment/coreml_kv.py
-
curl -L -o coreml_kv.py https://huggingface.co/Voltline/vimeml-tiny-ja-v4.2/resolve/main/source/scripts/deployment/coreml_kv.py
12.9 kB
| """Convert, gate, compare and package the independent V2 KV-cache experiment.""" | |
| import argparse | |
| import json | |
| import shutil | |
| import sys | |
| import time | |
| from pathlib import Path | |
| ROOT = Path(__file__).resolve().parents[2] | |
| sys.path.insert(0, str(ROOT / "src")) | |
| import numpy as np | |
| import torch | |
| from vimeml.deployment import kv_cache as kv, v2 | |
| from vimeml.deployment.bundle import fresh_directory, read_json, write_json | |
| from vimeml.deployment.validation import numeric_error | |
| from vimeml.training.data import file_sha | |
| from vimeml.training.evaluate_ime import common_prefix_length | |
| def align(bundle, directory, reference, output, baseline=None): | |
| runtime = kv.Runtime(bundle, directory) | |
| old = v2.CoreMLLM(bundle, baseline) if baseline else None | |
| errors, invariants, saved = [], [], {} | |
| info = read_json(reference / "reference.json") | |
| if info.get("format") != "vimeml_v2_mac_preparation_reference_v1" or info["checkpoint_step"] != runtime.metadata["checkpoint_step"]: | |
| raise ValueError("Invalid frozen reference identity.") | |
| if old and any(old.metadata[key] != runtime.metadata[key] for key in ("checkpoint_sha256", "tokenizer_sha256", "model")): | |
| raise ValueError("Stateless baseline model/tokenizer identity differs.") | |
| with np.load(reference / "logits.npz", allow_pickle=False) as arrays: | |
| for name, case in info["cases"].items(): | |
| ids = arrays[name + "_input_ids"][0] | |
| expected = old.model(torch.tensor([ids.tolist()])).numpy() if old else arrays[name + "_logits"] | |
| patterns = [[len(ids)], [1] * len(ids)] | |
| if len(ids) > 3: patterns.append([3, len(ids)-3]) | |
| for chunks in patterns: | |
| parts, past, state = [], 0, None | |
| for size in chunks: | |
| logits, state = runtime.step(ids[past:past+size], state, past) | |
| parts.append(logits); past += size | |
| actual = np.concatenate(parts, axis=1) | |
| valid = case["valid_prefix"] | |
| error = numeric_error(expected[:, :valid], actual[:, :valid], 3e-4, 3e-4) | |
| errors.append({"case": name, "chunks": chunks, **error}) | |
| if chunks == [len(ids)]: saved[name] = actual | |
| last, _ = runtime.step(ids, last_only=True) | |
| errors.append({"case": name, "last_only": True, **numeric_error(expected[:, -1:], last, 3e-4, 3e-4)}) | |
| for name in ("right_pad", "changed_future"): | |
| invariants.append({"case": name, **numeric_error(saved["short"], saved[name][:, :16], 3e-4, 3e-4)}) | |
| # Immutable shared snapshots, stale suffix overwrite, and A/B/A branch | |
| # order catch errors that fresh-prefill comparisons cannot detect. | |
| prefix = arrays["short_input_ids"][0][:7] | |
| _, state = runtime.step(prefix, last_only=True) | |
| original = tuple(a.copy() for a in state) | |
| for suffix in ([9], [47, 81, 102], [9]): | |
| actual, _ = runtime.step(suffix, state, len(prefix)) | |
| expected = (old.model(torch.tensor([[*prefix.tolist(), *suffix]])).numpy() if old else | |
| v2.BundleLM(bundle).model(torch.tensor([[*prefix.tolist(), *suffix]])).detach().numpy())[:, len(prefix):] | |
| errors.append({"branch": suffix, **numeric_error(expected, actual, 3e-4, 3e-4)}) | |
| immutable = all(np.array_equal(a, b) for a, b in zip(state, original)) | |
| dirty = tuple(a.copy() for a in state) | |
| for a in dirty: a[:, :, :, len(prefix):, :] = 999 | |
| a, _ = runtime.step([9], state, len(prefix)) | |
| b, _ = runtime.step([9], dirty, len(prefix)) | |
| invariants.append({"case": "unused_tail", **numeric_error(a, b, 0, 0)}) | |
| report = {"format": "vimeml_kv_alignment_v1", "passed": immutable and all(e["outside_tolerance"] == 0 for e in errors+invariants), | |
| "coreml_manifest_sha256": runtime.metadata["coreml_manifest_sha256"], "model": runtime.metadata, | |
| "comparison": "same-weight stateless Core ML" if old else "frozen Windows FP32", | |
| "baseline_manifest_sha256": old.metadata["coreml_manifest_sha256"] if old else None, | |
| "tolerances": {"atol": 3e-4, "rtol": 3e-4}, "immutable_branch_inputs": immutable, | |
| "errors": errors, "invariants": invariants} | |
| fresh_directory(output); write_json(output / "alignment.json", report) | |
| return report | |
| class CachedForward: | |
| """Evaluation adapter: share exact batch token prefixes within one call.""" | |
| def __init__(self, runtime): | |
| self.runtime, self.config = runtime, runtime.config | |
| def __call__(self, inputs): | |
| ids = inputs.numpy().astype(np.int32) | |
| common = common_prefix_length(ids.tolist()) | |
| if common: | |
| prefix, state = self.runtime.step(ids[0, :common]) | |
| result = [] | |
| for row in ids: | |
| if common == len(row): logits = prefix | |
| elif common: | |
| suffix, _ = self.runtime.step(row[common:], state, common) | |
| logits = np.concatenate((prefix, suffix), axis=1) | |
| else: logits, _ = self.runtime.step(row) | |
| result.append(logits) | |
| return torch.from_numpy(np.concatenate(result, axis=0)) | |
| class CachedLM(v2.BundleLM): | |
| def __init__(self, bundle, directory): | |
| super().__init__(bundle) | |
| self.runtime = kv.Runtime(bundle, directory) | |
| self.model, self.metadata = CachedForward(self.runtime), self.runtime.metadata | |
| self._generating = False | |
| self._ids = self._state = None | |
| def next_logits(self, ids): | |
| if self._generating and self._ids and ids[:len(self._ids)] == self._ids and len(ids) > len(self._ids): | |
| logits, state = self.runtime.step(ids[len(self._ids):], self._state, len(self._ids), last_only=True) | |
| else: | |
| logits, state = self.runtime.step(ids, last_only=True) | |
| if self._generating: self._ids, self._state = list(ids), state | |
| return torch.from_numpy(logits[0,-1]) | |
| def generate(self, *args, **kwargs): | |
| self._generating, self._ids, self._state = True, None, None | |
| try: return super().generate(*args, **kwargs) | |
| finally: self._generating, self._ids, self._state = False, None, None | |
| def timing(work, repeats=20): | |
| for _ in range(3): work() | |
| times = [] | |
| for _ in range(repeats): | |
| start = time.perf_counter(); work(); times.append((time.perf_counter()-start)*1000) | |
| return {"n": len(times), "p50_ms": float(np.median(times)), "p95_ms": float(np.percentile(times, 95)), "max_ms": max(times)} | |
| def benchmark(bundle, directory, baseline, output): | |
| runtime, old = kv.Runtime(bundle, directory), v2.CoreMLLM(bundle, baseline) | |
| rng = np.random.default_rng(716) | |
| rows = [] | |
| for length in (8, 32, 64, 120): | |
| ids = rng.integers(4, 16384, length).tolist(); ids[0] = 2 | |
| _, state = runtime.step(ids[:-1], last_only=True) | |
| rows.append({"prefix_tokens": length-1, | |
| "stateless": timing(lambda: old.model(torch.tensor([ids]))), | |
| "cached_decode": timing(lambda: runtime.step(ids[-1:], state, length-1, last_only=True)), | |
| "prefill_last_only": timing(lambda: runtime.step(ids, last_only=True))}) | |
| def generated(cached, ids, tokens=8): | |
| sequence = list(ids) | |
| if cached: logits, state = runtime.step(sequence, last_only=True) | |
| else: logits = old.model(torch.tensor([sequence])).numpy()[:, -1:] | |
| for i in range(tokens): | |
| token = int(logits[0,-1].argmax()); sequence.append(token) | |
| if i == tokens-1: break | |
| if cached: logits, state = runtime.step([token], state, len(sequence)-1, last_only=True) | |
| else: logits = old.model(torch.tensor([sequence])).numpy()[:, -1:] | |
| return sequence | |
| generation = [] | |
| for length in (8, 32, 64): | |
| ids = [2, *rng.integers(4, 16384, length-1).tolist()] | |
| generation.append({"prefix_tokens": length, "new_tokens": 8, | |
| "tokens_equal": generated(True,ids) == generated(False,ids), | |
| "cached_total": timing(lambda: generated(True,ids), 10), | |
| "stateless_total": timing(lambda: generated(False,ids), 10)}) | |
| fresh_directory(output) | |
| write_json(output / "benchmark.json", {"scope": "Mac CPU_ONLY Python/Core ML; includes Python and array-validation overhead, not iPhone or keyboard extension", | |
| "model": runtime.metadata, "decode": rows, "generation": generation, | |
| "cache_bytes_per_snapshot": runtime.manifest["cache_bytes_per_snapshot"]}) | |
| def package(bundle, source, compiled, comparison, output): | |
| m = kv.verify_package(source) | |
| gate = read_json(comparison) | |
| if m["kind"] != "linear8_fp32_compute" or not (compiled / "coremldata.bin").is_file(): | |
| raise ValueError("Expected compiled INT8 KV resources.") | |
| if (gate.get("format") != "vimeml_kv_alignment_v1" or not gate.get("passed") | |
| or gate.get("comparison") != "same-weight stateless Core ML" | |
| or gate["coreml_manifest_sha256"] != file_sha(source / "manifest.json")): | |
| raise ValueError("Passed same-weight stateless comparison required before packaging.") | |
| fresh_directory(output) | |
| resources = output / "Resources"; resources.mkdir() | |
| shutil.copytree(compiled, resources / "TinyJapaneseV21KVINT8.mlmodelc") | |
| # Reuses the exact V2 tokenizer already shipped with the baseline, not a | |
| # second tokenizer copy. Installer checks this existing identity. | |
| write_json(resources / "VimeLMManifestV21KV.json", {"format": "vime_ios_lm_v2_kv_v1", | |
| "architecture": v2.ARCHITECTURE, "model_version": "2.1-extend5-step40000-int8-b32-kv-v1", | |
| "minimum_ios": 18, "vocab_size": 16384, "context_length": 128, "compute_units": "CPU_ONLY", | |
| "special_ids": {"pad": 0, "unk": 1, "bos": 2, "eos": 3}, | |
| "tokenizer_sha256": m["bundle"]["tokenizer_sha256"], "checkpoint_sha256": m["bundle"]["checkpoint_sha256"], | |
| "cache_shape": m["cache_shape"], "cache_dtype": "float32", | |
| "source_manifest_sha256": file_sha(source / "manifest.json"), | |
| "stateless_alignment_sha256": file_sha(comparison), | |
| "compiled_files_sha256": {name: item["sha256"] for name,item in v2.inventory(compiled,hashes=True).items()}, | |
| "policy": "Experimental opt-in; explicit FP32 request-local immutable caches; baseline remains default."}) | |
| write_json(output / "manifest.json", {"format": "vimeml_kv_client_resources_v1", "model": m, | |
| "files": v2.inventory(output)}) | |
| def main(): | |
| p = argparse.ArgumentParser(description=__doc__) | |
| p.add_argument("--threads", type=int, default=4) | |
| stages = p.add_subparsers(dest="stage", required=True) | |
| for name in ("convert", "align", "compress", "evaluate", "samples", "benchmark", "package"): | |
| s = stages.add_parser(name); s.add_argument("--output", type=Path, required=True) | |
| if name != "compress": s.add_argument("--bundle", type=Path, required=True) | |
| if name in ("align", "evaluate", "samples", "benchmark"): s.add_argument("--model", type=Path, required=True) | |
| if name in ("compress", "package"): s.add_argument("--source", type=Path, required=True) | |
| if name == "compress": s.add_argument("--alignment", type=Path, required=True) | |
| if name == "align": | |
| s.add_argument("--reference", type=Path, required=True); s.add_argument("--baseline", type=Path) | |
| if name in ("evaluate", "samples", "benchmark"): s.add_argument("--baseline", type=Path, required=True) | |
| if name == "evaluate": s.add_argument("--benchmark", type=Path, required=True) | |
| if name == "package": | |
| s.add_argument("--compiled", type=Path, required=True) | |
| s.add_argument("--comparison", type=Path, required=True) | |
| args = p.parse_args() | |
| if args.output.exists() or args.threads < 1: p.error("Fresh output and positive thread count required.") | |
| torch.set_num_threads(args.threads) | |
| if args.stage == "convert": kv.convert(args.bundle,args.output) | |
| elif args.stage == "compress": kv.compress(args.source,args.output,args.alignment) | |
| elif args.stage == "align": | |
| report = align(args.bundle,args.model,args.reference,args.output,args.baseline) | |
| print(json.dumps({"passed": report["passed"], "max_abs": max(e["max_abs"] for e in report["errors"])}, indent=2)) | |
| if not report["passed"]: raise SystemExit("Alignment failed; evidence preserved.") | |
| elif args.stage == "benchmark": benchmark(args.bundle,args.model,args.baseline,args.output) | |
| elif args.stage == "package": package(args.bundle,args.source,args.compiled,args.comparison,args.output) | |
| else: | |
| lm = CachedLM(args.bundle,args.model) | |
| if args.stage == "evaluate": v2.evaluate(lm,args.benchmark,args.baseline,args.output) | |
| else: v2.samples(lm,args.baseline,args.output) | |
| print(f"Output: {args.output.resolve()}") | |
| if __name__ == "__main__": main() | |