File size: 4,563 Bytes
e4ab6c6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
from __future__ import annotations

import argparse
import json
import shutil
import sys
from pathlib import Path
from typing import Dict

import mlx.core as mx
import mlx.nn as nn
from mlx.utils import tree_flatten

REPO_ROOT = Path(__file__).resolve().parents[1]
if str(REPO_ROOT) not in sys.path:
    sys.path.insert(0, str(REPO_ROOT))

from modeling_eurobert_mlx import EuroBertConfig, build_model  # noqa: E402


TOKENIZER_FILES = [
    "tokenizer.json",
    "tokenizer_config.json",
    "special_tokens_map.json",
]


def _save_weights(path: Path, model, metadata: Dict[str, str]) -> None:
    params = tree_flatten(model.parameters(), destination={})
    mx.save_safetensors(str(path), params, metadata=metadata)


def _eval_model(model) -> None:
    params = tree_flatten(model.parameters(), destination={})
    if params:
        mx.eval(*params.values())


def _load_source_config(source_dir: Path) -> dict:
    with open(source_dir / "config.json", "r", encoding="utf-8") as f:
        config = json.load(f)
    weights = mx.load(str(source_dir / "model.safetensors"))
    config["num_labels"] = int(weights["classifier.weight"].shape[0])
    config.setdefault("id2label", {str(i): f"LABEL_{i}" for i in range(config["num_labels"])})
    config.setdefault("label2id", {v: int(k) for k, v in config["id2label"].items()})
    return config


def convert(source_dir: Path, output_dir: Path, include_4bit: bool) -> None:
    source_dir = source_dir.resolve()
    output_dir = output_dir.resolve()
    output_dir.mkdir(parents=True, exist_ok=True)

    config = _load_source_config(source_dir)
    with open(output_dir / "config.json", "w", encoding="utf-8") as f:
        json.dump(config, f, indent=2, sort_keys=True)
        f.write("\n")

    for filename in TOKENIZER_FILES:
        shutil.copy2(source_dir / filename, output_dir / filename)
    if (source_dir / "pulpie-orange.png").exists():
        shutil.copy2(source_dir / "pulpie-orange.png", output_dir / "pulpie-orange.png")

    mlx_config = {
        "format": "mlx",
        "source_model": "feyninc/pulpie-orange-small",
        "architecture": "EuroBertForTokenClassification",
        "implementation": "modeling_eurobert_mlx.py",
        "variants": {
            "model-bf16.safetensors": {
                "dtype": "bfloat16",
                "quantization": None,
                "description": "Native 16-bit BF16 MLX weights converted from the source safetensors.",
            }
        },
    }

    model = build_model(EuroBertConfig(**config))
    model.load_weights(str(source_dir / "model.safetensors"))
    _eval_model(model)
    _save_weights(
        output_dir / "model-bf16.safetensors",
        model,
        {
            "format": "mlx",
            "variant": "bf16",
            "source_model": "feyninc/pulpie-orange-small",
        },
    )

    quantized_specs = [("8bit", 8)]
    if include_4bit:
        quantized_specs.append(("4bit", 4))

    for name, bits in quantized_specs:
        q_model = build_model(EuroBertConfig(**config))
        q_model.load_weights(str(source_dir / "model.safetensors"))
        nn.quantize(q_model, group_size=64, bits=bits, mode="affine")
        _eval_model(q_model)
        weight_name = f"model-{name}.safetensors"
        _save_weights(
            output_dir / weight_name,
            q_model,
            {
                "format": "mlx",
                "variant": name,
                "source_model": "feyninc/pulpie-orange-small",
                "quantization": json.dumps(
                    {"bits": bits, "group_size": 64, "mode": "affine"},
                    sort_keys=True,
                ),
            },
        )
        mlx_config["variants"][weight_name] = {
            "dtype": "quantized",
            "quantization": {"bits": bits, "group_size": 64, "mode": "affine"},
            "description": f"MLX affine weight quantized {bits}-bit variant.",
        }

    with open(output_dir / "mlx_config.json", "w", encoding="utf-8") as f:
        json.dump(mlx_config, f, indent=2, sort_keys=True)
        f.write("\n")


def main() -> None:
    parser = argparse.ArgumentParser(description="Convert Pulpie Orange Small to MLX.")
    parser.add_argument("--source-dir", type=Path, default=Path("source_model"))
    parser.add_argument("--output-dir", type=Path, default=Path("hf_out"))
    parser.add_argument("--include-4bit", action="store_true")
    args = parser.parse_args()
    convert(args.source_dir, args.output_dir, include_4bit=args.include_4bit)


if __name__ == "__main__":
    main()