botp
/

File size: 5,513 Bytes
1d2de8a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Verify immutable input snapshots and create a separately checksummed BF16 model."""

import hashlib
import json
import shutil
from importlib.metadata import version
from pathlib import Path

from .artifacts import BASE_REVISION, SOLOMON_REVISION, sha256, verify_release


def verify_base(root, manifest):
    root = Path(root)
    if manifest["revision"] != BASE_REVISION:
        raise ValueError("Wrong pinned base revision")
    result = {}
    for row in manifest["files"]:
        path = root / row["name"]
        if not path.resolve().is_relative_to(root.resolve()):
            raise ValueError("Unsafe base manifest path")
        if path.stat().st_size != row["size"]:
            raise ValueError("Base size mismatch: " + row["name"])
        actual = sha256(path)
        if row["sha256"]:
            if actual != row["sha256"]:
                raise ValueError("Base checksum mismatch: " + row["name"])
        else:
            data = path.read_bytes()
            git_hash = hashlib.sha1(b"blob " + str(len(data)).encode() + b"\0" + data).hexdigest()
            if git_hash != row["blob_id"]:
                raise ValueError("Base Git blob mismatch: " + row["name"])
        result[row["name"]] = actual
    return result


def prepare(base, solomon, output, manifest):

    base, solomon, output = map(Path, (base, solomon, output))
    source_hashes = verify_release(solomon)
    base_hashes = verify_base(base, json.loads(Path(manifest).read_text()))
    if output.exists():
        raise FileExistsError("Use a new output directory; existing conversions are immutable")
    output.mkdir(parents=True)
    convert_bf16(base, output / "backbone")
    shutil.copy2(solomon / "adapter/adapter.safetensors", output / "adapter.safetensors")
    shutil.copy2(solomon / "adapter/heads.npz", output / "heads.npz")
    for name in ("LICENSE", "NOTICE", "MODIFICATIONS.md"):
        shutil.copy2(solomon / name, output / name)
    files = {
        str(p.relative_to(output)): sha256(p)
        for p in sorted(output.rglob("*"))
        if p.is_file() and ".cache" not in p.parts
    }
    binding = {
        "schema": "solomon-mlx-binding-v1",
        "profile": "quality",
        "dtype": "bfloat16",
        "adapter_dtype": "float32",
        "head_dtype": "float32",
        "recurrent_state_dtype": "float32",
        "sensitive_parameters": "FP32 normalization weights, A_log and dt_bias",
        "conversion": "upstream Qwen3.5 sanitization, norms promoted before unit offset",
        "adapter_scale": 2.0,
        "adapter_placement": "question",
        "quantization": None,
        "base_revision": BASE_REVISION,
        "solomon_revision": SOLOMON_REVISION,
        "source_hashes": source_hashes,
        "base_hashes": base_hashes,
        "dependencies": {p: version(p) for p in ("mlx", "mlx-vlm", "transformers", "numpy")},
        "files": files,
    }
    (output / "binding.json").write_text(json.dumps(binding, indent=2, sort_keys=True))
    return binding


def convert_bf16(base, output):
    """Convert one original shard at a time; never copy download caches.

    Keep normalization offsets in FP32 before adding one. Adding the unit
    offset in BF16 would irreversibly round trained normalization parameters.
    Large backbone matrices remain unquantized BF16.
    """
    import mlx.core as mx
    from mlx.utils import tree_flatten
    from mlx_vlm.models.qwen3_5.config import ModelConfig
    from mlx_vlm.models.qwen3_5.qwen3_5 import Model

    base, output = Path(base), Path(output)
    output.mkdir(parents=True, exist_ok=False)
    config = json.loads((base / "config.json").read_text())
    if config.get("model_type") != "qwen3_5" or config.get("quantization"):
        raise ValueError("Expected original unquantized Qwen3.5 architecture")
    model = Model(ModelConfig.from_dict(config))
    expected = {k: v.shape for k, v in tree_flatten(model.parameters())}
    index = {"metadata": {"total_size": 0}, "weight_map": {}}
    source_index = json.loads((base / "model.safetensors.index.json").read_text())
    for filename in sorted(set(source_index["weight_map"].values())):
        arrays = mx.load(str(base / filename))
        for key, value in arrays.items():
            sensitive = value.ndim == 1 and ("norm" in key or key.endswith(("A_log", "dt_bias")))
            arrays[key] = value.astype(mx.float32 if sensitive else mx.bfloat16)
        arrays = model.sanitize(arrays)
        arrays = model.vision_tower.sanitize(arrays)
        for key, value in arrays.items():
            if key not in expected or value.shape != expected[key]:
                raise ValueError("Converted tensor shape mismatch: " + key)
            if key in index["weight_map"]:
                raise ValueError("Duplicate converted tensor: " + key)
            index["weight_map"][key] = filename
            index["metadata"]["total_size"] += value.nbytes
        mx.save_safetensors(str(output / filename), arrays, metadata={"format": "mlx"})
        del arrays
        mx.clear_cache()
        print("Converted " + filename, flush=True)
    if set(index["weight_map"]) != set(expected):
        raise ValueError("Converted backbone is missing required parameters")
    (output / "model.safetensors.index.json").write_text(json.dumps(index, indent=2, sort_keys=True))
    for file in base.iterdir():
        if file.is_file() and file.name != "model.safetensors.index.json" and file.suffix != ".safetensors":
            shutil.copy2(file, output / file.name)