File size: 8,372 Bytes
81cffe7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Prepare vimeml-tiny-zh-v1 INT8 client resources and native fixtures for the Vime test host.

Resources are named so that Japanese resources are unaffected:
  TinyChineseV1INT8.mlmodelc, TinyChineseV1KVChunkINT8.mlmodelc,
  VimeChineseTokenizerV1.model, VimeLMManifestZhV1.json (+ VimeLMFixturesZhV1.json beside Resources/).

Fixture texts are written here; none comes from the benchmark. Device runs belong to the Vime repository.
"""
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  # noqa: E402
import torch  # noqa: E402

from vimeml.benchmarks.zh13_lm_scores import score_item  # noqa: E402
from vimeml.deployment import v2, zh_v1 as zh  # noqa: E402
from vimeml.deployment.bundle import fresh_directory, read_json, write_json  # noqa: E402
from vimeml.training.data import file_sha  # noqa: E402

TAG = "ZhV1"
TOKENIZATION = ["你好,今天天气不错。", "我们明天下午三点在地铁站见面吧。", "这家餐厅的火锅很好吃,推荐给你。",
                "Vime 输入法支持拼音。", "一二三四五六七八九十"]
SCORING = [("我们晚上去吃", ["火锅", "活过", "伙夫", "或"]),
           ("明天上午我要去", ["开会", "看会", "开灰", "凯"]),
           ("这本书写得", ["很好", "恨好", "很号"]),
           ("", ["你好", "拟好", "泥嚎"])]


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


@torch.inference_mode()
def package(bundle, stateless, chunk, alignment, output):
    if output.exists():
        raise ValueError("Resource output exists.")
    stateless_manifest = zh.verify_package(stateless)
    chunk_manifest = zh.verify_package(chunk, zh.CHUNK_FORMAT)
    for manifest in (stateless_manifest, chunk_manifest):
        if manifest.get("recipe") != zh.DEPLOYED_RECIPE or manifest["minimum_ios"] != 18:
            raise ValueError("Expected the deployed INT8 block32 FP32-compute iOS18 recipe.")
    gate = read_json(alignment)
    if (gate.get("format") != zh.CHUNK_ALIGNMENT_FORMAT or not gate.get("passed")
            or gate.get("comparison") != "same-weight stateless Core ML"
            or gate["coreml_manifest_sha256"] != file_sha(chunk / "manifest.json")
            or gate["baseline_manifest_sha256"] != file_sha(stateless / "manifest.json")):
        raise ValueError("Passed same-weight chunk-KV/stateless INT8 comparison required.")
    lm = zh.CoreMLLMZh(bundle, stateless)
    runtime = zh.ChunkRuntimeZh(bundle, chunk)
    bos = lm.special["bos"]
    fixtures = {"format": "vime_client_fixtures_zh_v1", "model": lm.metadata, "tokenization": [], "scoring": [],
                "kv_chunk_steps": []}
    for text in TOKENIZATION:
        ids = lm.processor.encode(text)
        fixtures["tokenization"].append({"text": text, "ids": ids, "decoded": lm.processor.decode(ids)})
    deltas = []
    for context, candidates in SCORING:
        scored = score_item(lm.model, lm.processor, context, candidates)
        common = scored["common_prefix_tokens_including_bos"]
        fixtures["scoring"].append({"context": context, "candidates": candidates,
                                    "common_prefix_tokens_including_bos": common, "sums": scored["scores"],
                                    "scored_tokens": scored["scored_tokens"]})
        sequences = [[bos, *lm.processor.encode(context + c)] for c in candidates]
        prefix_ids = sequences[0][:common]
        # One chunk call for the common prefix, then single-token decode of each suffix.
        prefix_logits, snapshot = runtime.run(prefix_ids)
        for text, sequence, expected in zip(candidates, sequences, scored["scores"], strict=True):
            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) - expected))
            fixtures["kv_chunk_steps"].append({"context": context, "text": 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": expected})
    fresh_directory(output)
    resources = output / "Resources"
    resources.mkdir()
    staging = output / "compile"
    compiled = {"stateless": compile_package(stateless, staging / "stateless"),
                "kv": compile_package(chunk, staging / "kv")}
    names = {"stateless": "TinyChineseV1INT8.mlmodelc", "kv": "TinyChineseV1KVChunkINT8.mlmodelc"}
    for key, name in names.items():
        shutil.copytree(compiled[key], resources / name)
    shutil.rmtree(staging)
    shutil.copyfile(Path(bundle) / "tokenizer.model", resources / "VimeChineseTokenizerV1.model")
    bundle_manifest = read_json(Path(bundle) / "manifest.json")
    manifest = {"format": "vime_ios_lm_zh_v1", "architecture": zh.ARCHITECTURE, "release_name": zh.RELEASE,
        "model_version": f"zh-v1-step{lm.metadata['checkpoint_step']}-int8-b32-v1",
        "minimum_ios": 18, "vocab_size": zh.VOCAB, "n_layers": zh.LAYERS, "context_length": zh.CONTEXT,
        "compute_units": "CPU_ONLY", "special_ids": lm.special,
        "reserved_ids": bundle_manifest.get("reserved_ids", {}),
        "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 chunk) / "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": chunk_manifest["interface"],
        "cache_shape": chunk_manifest["cache_shape"], "cache_dtype": "float32", "kv_graph": "chunk",
        "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(alignment),
        "kv_chunk_fixture_max_suffix_sum_delta": max(deltas),
        "kv_chunk_fixture_within_1e-4": max(deltas) <= 1e-4,
        "scoring": "BOS + joint context/candidate tokenization; logP sum from the first token differing from the context alone; no EOS",
        "quality_review": "Test-host measurement candidate only; not shipped in Vime. See VimeML docs/reports/mac-*/zh-v1-coreml.md."}
    write_json(resources / f"VimeLMManifest{TAG}.json", manifest)
    write_json(output / f"VimeLMFixtures{TAG}.json", fixtures)
    write_json(output / "manifest.json", {"format": "vimeml_zh_v1_ios_resources_v1", "model": lm.metadata,
        "files": v2.inventory(output),
        "note": "Resources/ is for the Vime test host. No client source snapshot or device run included."})
    print(f"KV chunk fixture max suffix-sum delta: {max(deltas):.3g}", flush=True)


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    for name in ("bundle", "stateless", "chunk", "chunk-alignment", "output"):
        parser.add_argument("--" + name, type=Path, required=True)
    args = parser.parse_args()
    torch.set_num_threads(2)
    package(args.bundle, args.stateless, args.chunk, args.chunk_alignment, args.output)


if __name__ == "__main__":
    main()