File size: 9,418 Bytes
c83c617 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 | """Prepare V4.2 INT8 client resources and native fixtures; V1/V2 releases stay intact.
Device runs belong to the separate Vime repository; this only writes resources.
"""
import argparse
import shutil
import subprocess
import sys
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 v2, v4_chunk, v4_coreml as v4
from vimeml.deployment.bundle import fresh_directory, read_json, write_json
from vimeml.training.data import file_sha
from vimeml.training.evaluate_ime import score_candidates
from vimeml.tools.phrase_demo import PhraseDemo
MODEL_VERSION = "4.2-epoch20-step1081942-int8-b32-v1"
def compile_package(package, destination):
subprocess.run(["xcrun", "coremlcompiler", "compile", str(package / "model.mlpackage"), str(destination)],
check=True, capture_output=True)
compiled = destination / "model.mlmodelc"
if not (compiled / "coremldata.bin").is_file():
raise ValueError("Missing compiled model.")
return compiled
def package(bundle, stateless, kv, alignments, template, output, chunk=False):
"""`chunk=True`: `kv` is the chunked KV package (resources v2); otherwise the single-token one (v1)."""
if output.exists():
raise ValueError("Resource output exists.")
stateless_manifest = v4.verify_package(stateless)
kv_manifest = v4.verify_package(kv, v4_chunk.FORMAT if chunk else v4.KV_FORMAT)
if any(m["kind"] != "linear8_fp32_compute" or m["minimum_ios"] != 18 for m in (stateless_manifest, kv_manifest)):
raise ValueError("Expected V4 INT8 FP32-compute iOS18 packages.")
gate = read_json(alignments["kv_vs_stateless"])
if (gate.get("format") != (v4_chunk.ALIGNMENT_FORMAT if chunk else "vimeml_kv_alignment_v4") or not gate.get("passed")
or gate.get("comparison") != "same-weight stateless Core ML"
or gate["coreml_manifest_sha256"] != file_sha(kv / "manifest.json")):
raise ValueError("Passed same-weight KV/stateless INT8 comparison required.")
lm = v4.CoreMLLMV4(bundle, stateless)
runtime = v4_chunk.KVChunkRuntimeV4(bundle, kv) if chunk else v4.KVRuntimeV4(bundle, kv)
old = read_json(template)
fixtures = {"format": "vime_client_fixtures_v4", "model": lm.metadata, "tokenization": [], "scoring": [],
"phrases": [], "kv_chunk_steps" if chunk else "kv_steps": []}
deltas = []
for item in old["tokenization"]:
ids = lm.processor.encode(item["text"], out_type=int)
fixtures["tokenization"].append({"text": item["text"], "ids": ids, "decoded": lm.processor.decode(ids)})
for item in old["scoring"]:
scored = score_candidates(lm, item["context"], item["candidates"])
fixtures["scoring"].append({"context": item["context"], "candidates": item["candidates"],
"common_prefix_tokens_including_bos": scored["common_prefix_tokens_including_bos"],
"sums": [c["log_probability_sum"] for c in scored["candidates"]]})
if chunk:
# One chunk call for the common prefix, then single-token decode of the suffix.
common = scored["common_prefix_tokens_including_bos"]
prefix_ids = [lm.special["bos"], *lm.processor.encode(item["context"] + scored["candidates"][0]["text"],
out_type=int)][:common]
prefix_logits, snapshot = runtime.run(prefix_ids)
for candidate in scored["candidates"]:
sequence = [lm.special["bos"], *lm.processor.encode(item["context"] + candidate["text"], out_type=int)]
if sequence[:common] != prefix_ids:
raise ValueError("Common prefix differs from the scored boundary.")
rows, state = [prefix_logits[0, -1]], snapshot
for position in range(common, len(sequence) - 1):
logits, state = runtime.run([sequence[position]], state, position)
rows.append(logits[0, -1])
logp = torch.from_numpy(np.stack(rows)).log_softmax(-1)
targets = sequence[common:]
values = [float(logp[i, t]) for i, t in enumerate(targets)]
deltas.append(abs(sum(values) - candidate["log_probability_sum"]))
fixtures["kv_chunk_steps"].append({"context": item["context"], "text": candidate["text"],
"prefix_ids": prefix_ids, "prefix_chunk_calls": 1, "decode_ids": sequence[common:-1],
"targets": targets, "target_log_probabilities": values, "suffix_sum": sum(values),
"stateless_sum": candidate["log_probability_sum"]})
continue
# KV contract fixture: single-token steps over each joint sequence.
for candidate in scored["candidates"]:
sequence = [lm.special["bos"], *lm.processor.encode(item["context"] + candidate["text"], out_type=int)]
logits, _ = runtime.run(sequence[:-1])
logp = torch.from_numpy(logits[0]).log_softmax(-1)
targets = sequence[1:]
fixtures["kv_steps"].append({"context": item["context"], "text": candidate["text"],
"input_ids": sequence[:-1], "targets": targets,
"score_start": scored["common_prefix_tokens_including_bos"] - 1,
"step_target_log_probabilities": [float(logp[i, t]) for i, t in enumerate(targets)],
"step_argmax_ids": [int(i) for i in logits[0].argmax(-1)]})
demo = PhraseDemo(lm, [])
for index, item in enumerate(old["phrases"]):
result = demo.suggest({"prompt": item["prompt"], "mode": "beam", "count": 5, "max_tokens": 8})
first = result["suggestions"][0]
fixtures["phrases"].append({"prompt": item["prompt"], "firstText": first["text"],
"firstIDs": first["new_token_ids"], "firstSum": first["log_probability_sum"]})
print(f"Native phrase fixture {index + 1}/{len(old['phrases'])}", flush=True)
fresh_directory(output)
resources = output / "Resources"
resources.mkdir()
staging = output / "compile"
compiled = {"stateless": compile_package(stateless, staging / "stateless"),
"kv": compile_package(kv, staging / "kv")}
names = {"stateless": "TinyJapaneseV42INT8.mlmodelc",
"kv": "TinyJapaneseV42KVChunkINT8.mlmodelc" if chunk else "TinyJapaneseV42KVINT8.mlmodelc"}
for key, name in names.items():
shutil.copytree(compiled[key], resources / name)
shutil.rmtree(staging)
shutil.copyfile(Path(bundle) / "tokenizer.model", resources / "VimeJapaneseTokenizerV4.model")
manifest = {"format": "vime_ios_lm_v4", "architecture": v4.ARCHITECTURE, "model_version": MODEL_VERSION,
"minimum_ios": 18, "vocab_size": 8192, "context_length": 128, "compute_units": "CPU_ONLY",
"special_ids": lm.special, "forbidden_generation_ids": list(lm.forbidden),
"reserved_marker_ids": [4, 5, 6], "tokenizer_sha256": lm.metadata["tokenizer_sha256"],
"checkpoint_sha256": lm.metadata["checkpoint_sha256"],
"bundle_manifest_sha256": lm.metadata["bundle_manifest_sha256"],
"models": {key: {"resource": names[key],
"source_manifest_sha256": file_sha((stateless if key == "stateless" else kv) / "manifest.json"),
"compiled_files_sha256": {n: e["sha256"] for n, e in v2.inventory(resources / names[key], hashes=True).items()}}
for key in names},
"stateless_interface": stateless_manifest["interface"], "kv_interface": kv_manifest["interface"],
"cache_shape": kv_manifest["cache_shape"], "cache_dtype": "float32",
"kind": "linear8_fp32_compute", "quantization": "weight-only symmetric INT8 block32; FP32 computation",
"strict_fp32_logits_alignment_passed": False,
"kv_vs_stateless_alignment_sha256": file_sha(alignments["kv_vs_stateless"]),
"kv_graph": "chunk" if chunk else "single_token",
**({"kv_chunk_fixture_max_suffix_sum_delta": max(deltas),
"kv_chunk_fixture_within_1e-5": max(deltas) <= 1e-5} if chunk else {}),
"quality_review": "Candidate only; V2.1 remains the deployed default. See VimeML docs/reports/mac-20261020. Device acceptance is separate."}
write_json(resources / "VimeLMManifestV42.json", manifest)
write_json(output / "VimeLMFixturesV42.json", fixtures)
write_json(output / "manifest.json", {"format": "vimeml_v4_ios_resources_v1", "model": lm.metadata,
"files": v2.inventory(output), "template_sha256": file_sha(template),
"note": "Resources/ is for a future Vime client installer. No client source snapshot or device run included."})
def main():
parser = argparse.ArgumentParser(description=__doc__)
for name in ("bundle", "stateless", "kv", "kv-alignment", "template", "output"):
parser.add_argument("--" + name, type=Path, required=True)
parser.add_argument("--chunk", action="store_true", help="--kv is the chunked KV package")
args = parser.parse_args()
torch.set_num_threads(2)
package(args.bundle, args.stateless, args.kv, {"kv_vs_stateless": args.kv_alignment}, args.template, args.output,
args.chunk)
if __name__ == "__main__":
main()
|