File size: 9,489 Bytes
29f25be
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
"""Package the selected V2.1 FP32 bundle, INT8 Core ML model and public metrics."""

import argparse
import hashlib
import json
import re
import shutil
import subprocess
from datetime import datetime, timezone
from pathlib import Path

ROOT = Path(__file__).resolve().parents[2]
BUNDLE = ROOT / "artifacts/deployment/tiny-ja-v2.1-extend5-bundle-v1"
COREML = ROOT / "artifacts/deployment/tiny-ja-v2.1-extend5-int8-b32-v1"
TEMPLATES = ROOT / "templates/huggingface-v21"


def read(path):
    return json.loads(Path(path).read_text(encoding="utf-8"))


def write(path, value):
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(value, ensure_ascii=False, indent=2, allow_nan=False) + "\n",
                    encoding="utf-8", newline="\n")


def sha(path):
    with Path(path).open("rb") as stream:
        return hashlib.file_digest(stream, "sha256").hexdigest()


def git(*args):
    return subprocess.check_output(["git", *args], cwd=ROOT).decode("utf-8").strip()


def evaluation_summary(bundle, coreml):
    reports, ranking = {}, {}

    def report(relative):
        path = ROOT / relative
        reports[relative] = sha(path)
        return read(path)

    for role, expected in {"ajimee": (200, 144), "development": (137, 122),
                           "expanded-development": (2000, 1481)}.items():
        data = report(f"outputs/deployment/v21-int8-{role}-v1/metrics.json")
        if (data["model"]["checkpoint_sha256"] != bundle["checkpoint_sha256"]
                or data["model"]["tokenizer_sha256"] != bundle["files"]["tokenizer.model"]["sha256"]
                or data["model"]["coreml_manifest_sha256"] != coreml):
            raise ValueError("Saved quality report belongs to another model.")
        metrics = data["metrics"]["all"]["lm_context_sum"]
        if (metrics["cases"], metrics["top1_correct"]) != expected:
            raise ValueError("Metrics differ from the V2.1 model card.")
        ranking[role] = {
            "original_order": data["metrics"]["all"]["azookey"],
            "fp32_lm": data["baseline_metrics"]["all"]["lm_context_sum"],
            "int8_lm": metrics,
            "fp32_comparison": {k: v for k, v in data["comparison"].items() if k != "changes"},
            "labels_formal_gold": data.get("labels_formal_gold"),
        }
    alignment = {}
    for kind in ("fp32", "int8"):
        data = report(f"outputs/deployment/v21-{kind}-alignment-v1/alignment.json")
        alignment[kind] = {"strict_passed": data["passed"], "tolerances": data["tolerances"],
                           "logits": data["logits"], "invariants": data["invariants"]}
    acceptance = report("outputs/deployment/v21-closeout-v1/acceptance.json")
    device = acceptance["physical_keyboard_extension"].copy()
    device.pop("manual_confirmation", None)
    return {"format": "vimeml_public_evaluation_v2", "model_version": "2.1-extend5-step40000-int8-b32-v1",
            "checkpoint_sha256": bundle["checkpoint_sha256"], "coreml_manifest_sha256": coreml,
            "training": {"full_validation_bpc_fp32": 3.0802686334, "test_used_for_selection": False,
                         "v20_epochs": 4, "restart1_epochs": 2, "extend5_stopped_step": 161095,
                         "selected_extend5_step": 40000},
            "ranking": ranking, "alignment": alignment,
            "fixed_generation": acceptance["same_pool_quality"]["fixed_generation"],
            "quantization_diagnostics": report("outputs/deployment/v21-quantization-analysis-v1/analysis.json"),
            "physical_keyboard_extension": device,
            "limitations": acceptance["failed_or_unresolved"],
            "experiment_decision": "V2 series complete; current INT8 task-quality loss and observed UI tail accepted.",
            "source_reports_sha256": reports,
            "scope": "Saved fixed-pool metrics and bounded device workload; draft expanded labels, no blind scoring. "
                     "No benchmark texts, per-case scores, user inputs or raw device traces distributed."}


def prepare(output, repo_id):
    if not re.fullmatch(r"[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+", repo_id):
        raise ValueError("Use OWNER/REPO.")
    if output.exists():
        raise ValueError("Output exists; use a new release directory.")
    if git("status", "--porcelain"):
        raise ValueError("Commit the source before creating the release snapshot.")
    bundle, model = read(BUNDLE / "manifest.json"), read(COREML / "manifest.json")
    bundle_sha, coreml_sha = sha(BUNDLE / "manifest.json"), sha(COREML / "manifest.json")
    if (bundle["format"] != "vimeml_inference_bundle_v2" or bundle["checkpoint_step"] != 40000
            or bundle["parameter_count"] != 12537920 or not bundle["tied_lm_head"]
            or model["kind"] != "linear8_fp32_compute" or model["minimum_ios"] != 18
            or model["package_bytes"] != 14330856
            or model["bundle"]["bundle_manifest_sha256"] != bundle_sha):
        raise ValueError("Expected selected V2.1 step40000 and its INT8 package.")
    summary = evaluation_summary(bundle, coreml_sha)
    commit, known = git("rev-parse", "HEAD"), {}
    output.mkdir(parents=True, exist_ok=False)

    def copy(source, name, digest=None, size=None):
        if source.is_symlink() or not source.is_file() or (size is not None and source.stat().st_size != size):
            raise ValueError(f"Invalid source file: {source}")
        target = output / name
        target.parent.mkdir(parents=True, exist_ok=True)
        shutil.copyfile(source, target)
        if target.stat().st_size != source.stat().st_size:
            raise ValueError("Copy size mismatch.")
        if digest:
            known[name] = digest

    for name, entry in bundle["files"].items():
        copy(BUNDLE / name, f"inference/{name}", entry["sha256"], entry["bytes"])
    copy(BUNDLE / "manifest.json", "inference/manifest.json", bundle_sha)
    for name, entry in model["package_files"].items():
        copy(COREML / "model.mlpackage" / name, f"coreml/ios18-int8-block32/model.mlpackage/{name}",
             entry.get("sha256"), entry["bytes"])
    copy(COREML / "manifest.json", "coreml/ios18-int8-block32/manifest.json", coreml_sha)
    source_files = subprocess.check_output(
        ["git", "ls-files", "-z", "--", "src", "scripts", "configs", "templates/huggingface-v21",
         "requirements.txt", "requirements-autodl.txt", "requirements-evaluation.txt", "LICENSE"], cwd=ROOT
    ).decode("utf-8").split("\0")
    for name in filter(None, source_files):
        copy(ROOT / name, f"source/{name}")
    (output / "source/README.md").write_text(
        f"VimeML source/configuration from Git commit {commit}. GPL-2.0; see LICENSE.\n"
        "V2 inference uses vimeml.deployment.v2.BundleLM and the original RMSNorm/SwiGLU architecture.\n"
        "Corpus texts, optimizer state, benchmark pools, device traces and the Vime client are excluded.\n",
        encoding="utf-8", newline="\n")
    copy(ROOT / "LICENSE", "LICENSE")
    for name in ("infer.py", "verify_release.py"):
        copy(TEMPLATES / name, name)
    card = (TEMPLATES / "README.md").read_text(encoding="utf-8")
    for key, value in {"SOURCE_COMMIT": commit, "BUNDLE_SHA": bundle_sha, "COREML_SHA": coreml_sha,
                       "TOKENIZER_SHA": bundle["files"]["tokenizer.model"]["sha256"],
                       "CHECKPOINT_SHA": bundle["checkpoint_sha256"]}.items():
        card = card.replace(f"@@{key}@@", value)
    if "@@" in card:
        raise ValueError("Unresolved model card field.")
    (output / "README.md").write_text(card, encoding="utf-8", newline="\n")
    write(output / "evaluation/summary.json", summary)
    # Reuse frozen bundle hashes; hash each new release file at most once here.
    files = {p.relative_to(output).as_posix(): {"bytes": p.stat().st_size,
             "sha256": known.get(p.relative_to(output).as_posix()) or sha(p)}
             for p in sorted(output.rglob("*")) if p.is_file()}
    release = {"format": "vimeml_hf_release_v1", "repo_id": repo_id, "license": "gpl-2.0",
               "model_version": "2.1", "architecture": "tiny_gpt_v2",
               "source_repository": "https://github.com/Voltline/VimeML", "source_commit": commit,
               "created_utc": datetime.now(timezone.utc).isoformat(),
               "bundle_manifest_sha256": bundle_sha, "coreml_manifest_sha256": coreml_sha,
               "files": files, "policy": "FP32 inference and weight-only INT8; original manifests preserved. "
               "Frozen bundle digests reused, new files inventoried once; no training state or credentials."}
    write(output / "RELEASE.json", release)
    sums = {**{name: entry["sha256"] for name, entry in files.items()},
            "RELEASE.json": sha(output / "RELEASE.json")}
    (output / "SHA256SUMS.txt").write_text(
        "".join(f"{digest}  {name}\n" for name, digest in sorted(sums.items())), encoding="utf-8", newline="\n")
    print(json.dumps({"release": str(output), "files": len(files) + 2,
                      "bytes": sum(p.stat().st_size for p in output.rglob("*") if p.is_file()),
                      "source_commit": commit}, indent=2))


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--output", type=Path, required=True)
    parser.add_argument("--repo-id", default="Voltline/vimeml-tiny-ja-v2.1")
    args = parser.parse_args()
    prepare(args.output.resolve(), args.repo_id)