mattbusi commited on
Commit
e71c131
·
verified ·
1 Parent(s): 1276073

add scripts/convert_8b.py

Browse files
Files changed (1) hide show
  1. scripts/convert_8b.py +108 -0
scripts/convert_8b.py ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """P3b — convert nvidia/Nemotron-3-Embed-8B-BF16 to a quantized MLX checkpoint.
2
+
3
+ The NVIDIA HF checkpoint's weight names (embed_tokens/layers.N/norm, no "model."
4
+ prefix) and config keys match the proven bidirectional MLX implementation bundled
5
+ with the mlx-community 1B port exactly (see runs/mlx_contract_1b_bf16.json: the
6
+ implementation is 0.9999-exact vs Transformers). Conversion is therefore:
7
+ mmap-load shards -> load into NemotronEmbedModel -> nn.quantize -> save.
8
+
9
+ One recipe per process invocation (24GB unified memory: 16GB bf16 mmap + the
10
+ quantized materialization must not coexist across recipes).
11
+
12
+ Usage:
13
+ source ~/.venvs/nq-mlx/bin/activate
14
+ python mlx/convert_8b.py --bits 4 --group-size 32 \
15
+ --src ~/.cache/nq-models/nv-8b-bf16 --out ~/.cache/nq-models/mlx-8b-4bit-gs32
16
+ python mlx/convert_8b.py --bits 4 --group-size 32 --mixed \
17
+ --out ~/.cache/nq-models/mlx-8b-mixed-gs32 # embeddings kept 8-bit
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import argparse
23
+ import importlib.util
24
+ import json
25
+ import shutil
26
+ import sys
27
+ from pathlib import Path
28
+
29
+ MLX_1B_DIR = Path.home() / ".cache/nq-models/mlx-1b-4bit" # source of the impl
30
+
31
+
32
+ def load_impl():
33
+ spec = importlib.util.spec_from_file_location(
34
+ "nemotron3_embed_mlx", MLX_1B_DIR / "nemotron3_embed_mlx.py"
35
+ )
36
+ mod = importlib.util.module_from_spec(spec)
37
+ spec.loader.exec_module(mod)
38
+ return mod
39
+
40
+
41
+ def main():
42
+ ap = argparse.ArgumentParser()
43
+ ap.add_argument("--src", default=str(Path.home() / ".cache/nq-models/nv-8b-bf16"))
44
+ ap.add_argument("--out", required=True)
45
+ ap.add_argument("--bits", type=int, required=True)
46
+ ap.add_argument("--group-size", type=int, required=True)
47
+ ap.add_argument("--mixed", action="store_true",
48
+ help="keep embed_tokens at 8-bit (body at --bits)")
49
+ args = ap.parse_args()
50
+
51
+ import mlx.core as mx
52
+ import mlx.nn as nn
53
+ from mlx_lm.models.ministral3 import ModelArgs
54
+
55
+ src, out = Path(args.src).expanduser(), Path(args.out).expanduser()
56
+ out.mkdir(parents=True, exist_ok=True)
57
+ mod = load_impl()
58
+
59
+ cfg = json.loads((src / "config.json").read_text())
60
+ model = mod.NemotronEmbedModel(ModelArgs.from_dict(cfg))
61
+
62
+ weights = {}
63
+ for shard in sorted(src.glob("model-*.safetensors")):
64
+ weights.update(mx.load(str(shard))) # mmap — not materialized yet
65
+ print(f"loaded {len(weights)} tensors from {len(list(src.glob('model-*.safetensors')))} shards")
66
+ model.load_weights(list(weights.items()))
67
+
68
+ if args.mixed:
69
+ def class_predicate(path, module):
70
+ if not hasattr(module, "to_quantized"):
71
+ return False
72
+ if "embed_tokens" in path:
73
+ return {"group_size": args.group_size, "bits": 8}
74
+ return True
75
+
76
+ nn.quantize(model, group_size=args.group_size, bits=args.bits,
77
+ class_predicate=class_predicate)
78
+ quant_cfg = {"group_size": args.group_size, "bits": args.bits,
79
+ "mode": "affine",
80
+ "overrides": {"embed_tokens": {"bits": 8}}}
81
+ else:
82
+ nn.quantize(model, group_size=args.group_size, bits=args.bits)
83
+ quant_cfg = {"group_size": args.group_size, "bits": args.bits,
84
+ "mode": "affine"}
85
+
86
+ mx.eval(model.parameters())
87
+
88
+ from mlx.utils import tree_flatten
89
+
90
+ flat = dict(tree_flatten(model.parameters()))
91
+ mx.save_safetensors(str(out / "model.safetensors"), flat,
92
+ metadata={"format": "mlx"})
93
+
94
+ cfg_out = dict(cfg)
95
+ cfg_out["quantization"] = quant_cfg
96
+ cfg_out["quantization_config"] = quant_cfg
97
+ (out / "config.json").write_text(json.dumps(cfg_out, indent=2))
98
+ for f in ("tokenizer.json", "tokenizer_config.json"):
99
+ shutil.copy(src / f, out / f)
100
+ shutil.copy(MLX_1B_DIR / "nemotron3_embed_mlx.py", out / "nemotron3_embed_mlx.py")
101
+
102
+ size_gb = (out / "model.safetensors").stat().st_size / 1e9
103
+ print(json.dumps({"out": str(out), "quantization": quant_cfg,
104
+ "n_tensors": len(flat), "size_gb": round(size_gb, 2)}))
105
+
106
+
107
+ if __name__ == "__main__":
108
+ main()