File size: 13,179 Bytes
3da88e3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
"""Publish verified release components after redistribution rights are established.

This command changes Hugging Face state. It never accepts model access requests
or sends announcements. Components may be uploaded one at a time to conserve
scratch space. The main repository is finalized only after every component.
"""

from __future__ import annotations

import argparse
import json
import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))

from h3_mlx.hub import read_json, sha256  # noqa: E402

PROJECT = "devin-lai/MiniMax-H3-Image-mlx"
RELEASE = "v0.4.0"
COMPONENTS = ("shared", "bf16", "packed12", "q8", "q4", "examples")


def write_json(path, value):
    path.write_text(json.dumps(value, indent=2) + "\n")


def verify_local(folder):
    manifest = read_json(folder / "files.json")
    if manifest.get("format") != "h3-mlx-files-v1":
        raise ValueError("Unknown file manifest")
    for name, spec in manifest["files"].items():
        from h3_mlx.hub import relative_path

        file = folder / relative_path(name)
        if not file.is_file() or file.stat().st_size != spec["bytes"]:
            raise ValueError(f"Missing or truncated local file: {file}")
        if sha256(file) != spec["sha256"]:
            raise ValueError(f"Local SHA-256 mismatch: {file}")
    quantization = folder / "model/core/quantization.json"
    if quantization.exists():
        files = read_json(quantization)["files"]
        if len(files) != 52:
            raise ValueError("Expected all fifty main blocks and two token refiners")
        for file in files:
            expected = manifest["files"].get("model/core/" + file["file"])
            if expected != {"bytes": file["bytes"], "sha256": file["sha256"]}:
                raise ValueError(f"Quantized export/manifest mismatch: {file['file']}")
    return manifest


def verify_remote(api, repo_id, manifest, *, repo_type="model"):
    from huggingface_hub import hf_hub_download

    info = api.repo_info(repo_id, repo_type=repo_type, files_metadata=True)
    files = {file.rfilename: file for file in info.siblings}
    for name, spec in manifest["files"].items():
        remote = files.get(name)
        if remote is None or remote.size != spec["bytes"]:
            raise ValueError(f"Remote file missing or wrong size: {repo_id}/{name}")
        lfs = getattr(remote, "lfs", None)
        digest = getattr(lfs, "sha256", None) if lfs is not None else None
        if isinstance(lfs, dict):
            digest = lfs.get("sha256")
        if digest is None:
            if remote.size > 10 * 1024**2:
                raise ValueError(
                    f"Cannot verify remote weight digest: {repo_id}/{name}"
                )
            digest = sha256(
                hf_hub_download(repo_id, name, revision=info.sha, repo_type=repo_type)
            )
        if digest != spec["sha256"]:
            raise ValueError(f"Remote SHA-256 mismatch: {repo_id}/{name}")
    return info.sha


def ensure_owned_repository(api, repo_id, folder, *, gated):
    from huggingface_hub import hf_hub_download

    marker = {"project": PROJECT, "release": RELEASE}
    if api.repo_exists(repo_id):
        try:
            existing = read_json(hf_hub_download(repo_id, "release-owner.json"))
        except Exception as error:
            raise RuntimeError(
                f"Existing repository has no matching release marker: {repo_id}"
            ) from error
        if existing != marker:
            raise RuntimeError(f"Refusing to modify unrelated repository: {repo_id}")
    else:
        api.create_repo(repo_id, private=True)
        # Establish ownership immediately so an interrupted upload is resumable.
        api.upload_file(
            repo_id=repo_id,
            path_in_repo="release-owner.json",
            path_or_fileobj=(json.dumps(marker) + "\n").encode(),
            commit_message=f"Initialize {RELEASE} release staging",
        )
    write_json(folder / "release-owner.json", marker)
    if gated:
        api.update_repo_settings(repo_id, gated="manual")


def refresh_small_file(folder, name):
    manifest = read_json(folder / "files.json")
    file = folder / name
    manifest["files"][name] = {"bytes": file.stat().st_size, "sha256": sha256(file)}
    write_json(folder / "files.json", manifest)


def publish_component(api, component, folder):
    repo_id = f"{PROJECT}-{component}"
    if component != "examples" and not (folder / "model").is_dir():
        raise ValueError(f"Model files are not prepared for {component}")
    if (
        component in ("q8", "q4")
        and not (folder / "model/core/quantization.json").is_file()
    ):
        raise ValueError(f"Quantized export incomplete: {component}")
    if (
        component == "packed12"
        and len(list((folder / "model/core").glob("blocks.*.safetensors"))) != 50
    ):
        raise ValueError("Expected all fifty packed blocks")
    card = (
        (folder / "README.md")
        .read_text()
        .replace(
            "**Preparation status:** this model repository is staged locally; publication\n"
            "and immutable commit IDs are pending confirmation of redistribution rights.",
            f"**Release:** {RELEASE}. Access requests are reviewed manually under the retained license.",
        )
        .replace(
            "**Preparation status:** staged locally; publication is pending confirmation\n"
            "of redistribution rights.",
            f"**Release:** {RELEASE}. Access requests are reviewed manually.",
        )
    )
    (folder / "README.md").write_text(card)
    refresh_small_file(folder, "README.md")
    manifest = verify_local(folder)
    ensure_owned_repository(api, repo_id, folder, gated=True)
    api.upload_large_folder(repo_id, folder, repo_type="model", num_workers=4)
    revision = verify_remote(api, repo_id, manifest)
    api.create_tag(repo_id, tag=RELEASE, revision=revision, exist_ok=True)
    if api.model_info(repo_id, revision=RELEASE).sha != revision:
        raise ValueError(f"Existing release tag points at another commit: {repo_id}")
    api.update_repo_settings(repo_id, gated="manual", private=False)
    info = api.model_info(repo_id)
    if info.private or info.gated != "manual":
        raise RuntimeError(f"Unexpected publication/access state: {repo_id}")
    return {
        "repo_id": repo_id,
        "revision": revision,
        "gated": "manual",
        "verified": True,
    }


def finalize_main(api, output, state):
    missing = [name for name in COMPONENTS if not state.get(name, {}).get("verified")]
    if missing:
        raise ValueError(
            f"Publish and verify these components first: {', '.join(missing)}"
        )
    from huggingface_hub import ModelCard

    catalog = read_json(ROOT / "h3_mlx/model_index.json")
    catalog["status"] = "published"
    catalog["shared"]["revision"] = state["shared"]["revision"]
    for name in catalog["variants"]:
        catalog["variants"][name]["revision"] = state[name]["revision"]
    main_folder = output / PROJECT.split("/")[-1]
    write_json(main_folder / "h3_mlx/model_index.json", catalog)
    collection = api.create_collection(
        "MiniMax H3 Image · Apple silicon / MLX",
        namespace="devin-lai",
        private=True,
        description="Local image generation and editing. BF16, lossless packed BF16, Q8 and Q4; selective downloads, shared components and measured comparisons.",
        exists_ok=True,
    )
    readme = (main_folder / "README.md").read_text()
    readme = readme.replace(
        "The current preparation has **not yet been\npublished** while redistribution rights are clarified. The commands below are\nthe prepared release workflow.",
        f"Release **{RELEASE}** is available with manual access review for model components.",
    )
    if f"[Collection]({collection.url})" not in readme:
        readme = readme.replace(
            "[Repository map](MODEL_INDEX.md)",
            f"[Collection]({collection.url}) · [Repository map](MODEL_INDEX.md)",
        )
    for name in ("q8", "q4"):
        files = read_json(output / f"MiniMax-H3-Image-mlx-{name}/files.json")["files"]
        size = (
            sum(
                spec["bytes"]
                for path, spec in files.items()
                if path.startswith("model/")
            )
            / 1024**3
        )
        readme = readme.replace(
            "About 20 GiB²" if name == "q8" else "About 11 GiB²", f"{size:.2f} GiB"
        )
    readme = readme.replace(
        " ² Estimates until the prepared export manifest is finalized.", ""
    )
    readme = readme.replace(
        " ² Q8 is an estimate until its export manifest is finalized.", ""
    )
    (main_folder / "README.md").write_text(readme)
    chinese = (
        (main_folder / "README.zh-CN.md")
        .read_text()
        .replace(
            "**发布状态:目前仅本地准备,尚未上传。**",
            f"**发布版本:{RELEASE},模型组件采用人工审核访问。**",
        )
        .replace(
            "在确认发布方具有所需权限之前暂停公开发布。", "使用前请确认具备所需权限。"
        )
    )
    (main_folder / "README.zh-CN.md").write_text(chinese)
    release_file = main_folder / "RELEASE.md"
    release_notes = release_file.read_text()
    if "<!-- COLLECTION_LINK -->" in release_notes:
        release_notes = release_notes.replace(
            "<!-- COLLECTION_LINK -->", f"[Browse the collection]({collection.url})."
        )
    elif collection.url not in release_notes:
        release_notes += f"\n[Browse the collection]({collection.url}).\n"
    release_file.write_text(release_notes)
    ModelCard.load(main_folder / "README.md").validate()
    ensure_owned_repository(api, PROJECT, main_folder, gated=False)
    from scripts.prepare_hf_release import manifest as build_manifest

    build_manifest(main_folder, {})
    main_manifest = read_json(main_folder / "files.json")
    commit = api.upload_folder(
        repo_id=PROJECT,
        folder_path=main_folder,
        commit_message=f"Release {RELEASE}: selective MLX model downloads",
    )
    if verify_remote(api, PROJECT, main_manifest) != commit.oid:
        raise RuntimeError("Main repository changed during publication verification")
    api.create_tag(PROJECT, tag=RELEASE, revision=commit.oid, exist_ok=True)
    if api.model_info(PROJECT, revision=RELEASE).sha != commit.oid:
        raise ValueError("Main release tag points at another commit")
    api.update_repo_settings(PROJECT, private=False)
    notes = {
        "main": "Start here: runtime, quick start, model selection, authentic outputs and measured results.",
        "packed12": "Recommended exact storage choice: lossless BF16 packing, not quantization.",
        "bf16": "Unchanged original transformer files: native BF16/FP32 storage control.",
        "q8": "Smaller affine Q8 core; image changes and no established generation-speed advantage.",
        "q4": "Smallest affine Q4 core; stronger detail and lettering changes.",
        "shared": "VAE, native support, PDD heads, configurations and frozen six-step tables; reused across variants.",
        "examples": "Generated examples and visual comparisons, with access review under the retained license.",
    }
    for name in ("main", "packed12", "bf16", "q8", "q4", "shared", "examples"):
        repo_id = PROJECT if name == "main" else state[name]["repo_id"]
        api.add_collection_item(
            collection.slug, repo_id, "model", note=notes[name], exists_ok=True
        )
    api.update_collection_metadata(collection.slug, private=False, position=0)
    write_json(ROOT / "h3_mlx/model_index.json", catalog)
    return {
        "repo_id": PROJECT,
        "revision": commit.oid,
        "collection": collection.url,
        "verified": True,
    }


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--output", type=Path, default=ROOT / ".release/huggingface")
    parser.add_argument("--component", choices=(*COMPONENTS, "main"), required=True)
    parser.add_argument(
        "--rights-confirmed",
        action="store_true",
        help="Publisher has established the required redistribution rights",
    )
    args = parser.parse_args()
    if not args.rights_confirmed:
        parser.error("Establish redistribution rights first; no Hub changes were made")
    from huggingface_hub import HfApi

    api = HfApi()
    if api.whoami()["name"] != "devin-lai":
        raise RuntimeError("Expected the devin-lai publishing account")
    state_path = args.output.resolve().parent / "publication.json"
    state = read_json(state_path) if state_path.exists() else {}
    if args.component == "main":
        state["main"] = finalize_main(api, args.output.resolve(), state)
    else:
        folder = args.output / f"MiniMax-H3-Image-mlx-{args.component}"
        state[args.component] = publish_component(api, args.component, folder)
    write_json(state_path, state)
    print(json.dumps(state[args.component], indent=2))


if __name__ == "__main__":
    main()