mattbusi's picture
CI sync from nemotron-embed-quant @ 8ce22703
47270e3 verified
Raw
History Blame Contribute Delete
4.26 kB
"""P3b — convert nvidia/Nemotron-3-Embed-8B-BF16 to a quantized MLX checkpoint.
The NVIDIA HF checkpoint's weight names (embed_tokens/layers.N/norm, no "model."
prefix) and config keys match the proven bidirectional MLX implementation bundled
with the mlx-community 1B port exactly (see runs/mlx_contract_1b_bf16.json: the
implementation is 0.9999-exact vs Transformers). Conversion is therefore:
mmap-load shards -> load into NemotronEmbedModel -> nn.quantize -> save.
One recipe per process invocation (24GB unified memory: 16GB bf16 mmap + the
quantized materialization must not coexist across recipes).
Usage:
source ~/.venvs/nq-mlx/bin/activate
python mlx/convert_8b.py --bits 4 --group-size 32 \
--src ~/.cache/nq-models/nv-8b-bf16 --out ~/.cache/nq-models/mlx-8b-4bit-gs32
python mlx/convert_8b.py --bits 4 --group-size 32 --mixed \
--out ~/.cache/nq-models/mlx-8b-mixed-gs32 # embeddings kept 8-bit
"""
from __future__ import annotations
import argparse
import importlib.util
import json
import shutil
import sys
from pathlib import Path
MLX_1B_DIR = Path.home() / ".cache/nq-models/mlx-1b-4bit" # source of the impl
def load_impl():
spec = importlib.util.spec_from_file_location(
"nemotron3_embed_mlx", MLX_1B_DIR / "nemotron3_embed_mlx.py"
)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--src", default=str(Path.home() / ".cache/nq-models/nv-8b-bf16"))
ap.add_argument("--out", required=True)
ap.add_argument("--bits", type=int, required=True)
ap.add_argument("--group-size", type=int, required=True)
ap.add_argument(
"--mixed",
action="store_true",
help="keep embed_tokens at 8-bit (body at --bits)",
)
args = ap.parse_args()
import mlx.core as mx
import mlx.nn as nn
from mlx_lm.models.ministral3 import ModelArgs
src, out = Path(args.src).expanduser(), Path(args.out).expanduser()
out.mkdir(parents=True, exist_ok=True)
mod = load_impl()
cfg = json.loads((src / "config.json").read_text())
model = mod.NemotronEmbedModel(ModelArgs.from_dict(cfg))
weights = {}
for shard in sorted(src.glob("model-*.safetensors")):
weights.update(mx.load(str(shard))) # mmap — not materialized yet
print(
f"loaded {len(weights)} tensors from {len(list(src.glob('model-*.safetensors')))} shards"
)
model.load_weights(list(weights.items()))
if args.mixed:
def class_predicate(path, module):
if not hasattr(module, "to_quantized"):
return False
if "embed_tokens" in path:
return {"group_size": args.group_size, "bits": 8}
return True
nn.quantize(
model,
group_size=args.group_size,
bits=args.bits,
class_predicate=class_predicate,
)
quant_cfg = {
"group_size": args.group_size,
"bits": args.bits,
"mode": "affine",
"overrides": {"embed_tokens": {"bits": 8}},
}
else:
nn.quantize(model, group_size=args.group_size, bits=args.bits)
quant_cfg = {"group_size": args.group_size, "bits": args.bits, "mode": "affine"}
mx.eval(model.parameters())
from mlx.utils import tree_flatten
flat = dict(tree_flatten(model.parameters()))
mx.save_safetensors(
str(out / "model.safetensors"), flat, metadata={"format": "mlx"}
)
cfg_out = dict(cfg)
cfg_out["quantization"] = quant_cfg
cfg_out["quantization_config"] = quant_cfg
(out / "config.json").write_text(json.dumps(cfg_out, indent=2))
for f in ("tokenizer.json", "tokenizer_config.json"):
shutil.copy(src / f, out / f)
shutil.copy(MLX_1B_DIR / "nemotron3_embed_mlx.py", out / "nemotron3_embed_mlx.py")
size_gb = (out / "model.safetensors").stat().st_size / 1e9
print(
json.dumps(
{
"out": str(out),
"quantization": quant_cfg,
"n_tensors": len(flat),
"size_gb": round(size_gb, 2),
}
)
)
if __name__ == "__main__":
main()