mattbusi commited on
Commit
47270e3
·
verified ·
1 Parent(s): 13b96f8

CI sync from nemotron-embed-quant @ 8ce22703

Browse files
Files changed (3) hide show
  1. nemotron3_embed_mlx.py +26 -9
  2. scripts/convert_8b.py +35 -14
  3. scripts/eval_8b.py +20 -9
nemotron3_embed_mlx.py CHANGED
@@ -8,19 +8,24 @@ Changes: converted the causal-decoder mlx-lm ministral3 implementation into a
8
 
9
  Dependencies: mlx, mlx-lm, transformers, numpy
10
  """
 
11
  import json
12
  from pathlib import Path
13
 
14
  import mlx.core as mx
15
  import mlx.nn as nn
16
  import numpy as np
17
- from mlx_lm.models.ministral3 import (ModelArgs, TransformerBlock,
18
- _get_llama_4_attn_scale)
 
 
 
19
  from transformers import AutoTokenizer
20
 
21
  NEG_INF = -1e9
22
  PREFIXES = {"query": "query: ", "passage": "passage: "}
23
 
 
24
  class NemotronEmbedModel(nn.Module):
25
  def __init__(self, args: ModelArgs):
26
  super().__init__()
@@ -32,7 +37,8 @@ class NemotronEmbedModel(nn.Module):
32
  def __call__(self, input_ids: mx.array, attention_mask: mx.array) -> mx.array:
33
  h = self.embed_tokens(input_ids)
34
  attn_scale = _get_llama_4_attn_scale(
35
- input_ids.shape[1], 0,
 
36
  self.args.rope_parameters["llama_4_scaling_beta"],
37
  self.args.rope_parameters["original_max_position_embeddings"],
38
  ).astype(h.dtype)
@@ -44,17 +50,23 @@ class NemotronEmbedModel(nn.Module):
44
  emb = (h * m).sum(axis=1) / m.sum(axis=1)
45
  return emb / mx.linalg.norm(emb, axis=-1, keepdims=True)
46
 
 
47
  def load(path: str):
48
  p = Path(path)
49
  if not p.is_dir():
50
  from huggingface_hub import snapshot_download
 
51
  p = Path(snapshot_download(path))
52
  cfg = json.loads((p / "config.json").read_text())
53
  model = NemotronEmbedModel(ModelArgs.from_dict(cfg))
54
  q = cfg.get("quantization")
55
  if q:
56
- nn.quantize(model, group_size=q["group_size"], bits=q["bits"],
57
- mode=q.get("mode", "affine"))
 
 
 
 
58
  model.load_weights(str(p / "model.safetensors"))
59
  model.eval()
60
  mx.eval(model.parameters())
@@ -62,14 +74,19 @@ def load(path: str):
62
  tok.padding_side = "right"
63
  return model, tok
64
 
65
- def encode(model, tokenizer, texts, input_type=None, batch_size=8,
66
- max_length=4096):
67
  if input_type is not None:
68
  texts = [PREFIXES[input_type] + t for t in texts]
69
  out = []
70
  for i in range(0, len(texts), batch_size):
71
- b = tokenizer(texts[i:i + batch_size], padding=True, truncation=True,
72
- max_length=max_length, return_tensors="np")
 
 
 
 
 
73
  e = model(mx.array(b["input_ids"]), mx.array(b["attention_mask"]))
74
  out.append(np.asarray(e.astype(mx.float32)))
75
  mx.clear_cache()
 
8
 
9
  Dependencies: mlx, mlx-lm, transformers, numpy
10
  """
11
+
12
  import json
13
  from pathlib import Path
14
 
15
  import mlx.core as mx
16
  import mlx.nn as nn
17
  import numpy as np
18
+ from mlx_lm.models.ministral3 import (
19
+ ModelArgs,
20
+ TransformerBlock,
21
+ _get_llama_4_attn_scale,
22
+ )
23
  from transformers import AutoTokenizer
24
 
25
  NEG_INF = -1e9
26
  PREFIXES = {"query": "query: ", "passage": "passage: "}
27
 
28
+
29
  class NemotronEmbedModel(nn.Module):
30
  def __init__(self, args: ModelArgs):
31
  super().__init__()
 
37
  def __call__(self, input_ids: mx.array, attention_mask: mx.array) -> mx.array:
38
  h = self.embed_tokens(input_ids)
39
  attn_scale = _get_llama_4_attn_scale(
40
+ input_ids.shape[1],
41
+ 0,
42
  self.args.rope_parameters["llama_4_scaling_beta"],
43
  self.args.rope_parameters["original_max_position_embeddings"],
44
  ).astype(h.dtype)
 
50
  emb = (h * m).sum(axis=1) / m.sum(axis=1)
51
  return emb / mx.linalg.norm(emb, axis=-1, keepdims=True)
52
 
53
+
54
  def load(path: str):
55
  p = Path(path)
56
  if not p.is_dir():
57
  from huggingface_hub import snapshot_download
58
+
59
  p = Path(snapshot_download(path))
60
  cfg = json.loads((p / "config.json").read_text())
61
  model = NemotronEmbedModel(ModelArgs.from_dict(cfg))
62
  q = cfg.get("quantization")
63
  if q:
64
+ nn.quantize(
65
+ model,
66
+ group_size=q["group_size"],
67
+ bits=q["bits"],
68
+ mode=q.get("mode", "affine"),
69
+ )
70
  model.load_weights(str(p / "model.safetensors"))
71
  model.eval()
72
  mx.eval(model.parameters())
 
74
  tok.padding_side = "right"
75
  return model, tok
76
 
77
+
78
+ def encode(model, tokenizer, texts, input_type=None, batch_size=8, max_length=4096):
79
  if input_type is not None:
80
  texts = [PREFIXES[input_type] + t for t in texts]
81
  out = []
82
  for i in range(0, len(texts), batch_size):
83
+ b = tokenizer(
84
+ texts[i : i + batch_size],
85
+ padding=True,
86
+ truncation=True,
87
+ max_length=max_length,
88
+ return_tensors="np",
89
+ )
90
  e = model(mx.array(b["input_ids"]), mx.array(b["attention_mask"]))
91
  out.append(np.asarray(e.astype(mx.float32)))
92
  mx.clear_cache()
scripts/convert_8b.py CHANGED
@@ -44,8 +44,11 @@ def main():
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
@@ -62,10 +65,13 @@ def main():
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
@@ -73,23 +79,30 @@ def main():
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
@@ -100,8 +113,16 @@ def main():
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__":
 
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(
48
+ "--mixed",
49
+ action="store_true",
50
+ help="keep embed_tokens at 8-bit (body at --bits)",
51
+ )
52
  args = ap.parse_args()
53
 
54
  import mlx.core as mx
 
65
  weights = {}
66
  for shard in sorted(src.glob("model-*.safetensors")):
67
  weights.update(mx.load(str(shard))) # mmap — not materialized yet
68
+ print(
69
+ f"loaded {len(weights)} tensors from {len(list(src.glob('model-*.safetensors')))} shards"
70
+ )
71
  model.load_weights(list(weights.items()))
72
 
73
  if args.mixed:
74
+
75
  def class_predicate(path, module):
76
  if not hasattr(module, "to_quantized"):
77
  return False
 
79
  return {"group_size": args.group_size, "bits": 8}
80
  return True
81
 
82
+ nn.quantize(
83
+ model,
84
+ group_size=args.group_size,
85
+ bits=args.bits,
86
+ class_predicate=class_predicate,
87
+ )
88
+ quant_cfg = {
89
+ "group_size": args.group_size,
90
+ "bits": args.bits,
91
+ "mode": "affine",
92
+ "overrides": {"embed_tokens": {"bits": 8}},
93
+ }
94
  else:
95
  nn.quantize(model, group_size=args.group_size, bits=args.bits)
96
+ quant_cfg = {"group_size": args.group_size, "bits": args.bits, "mode": "affine"}
 
97
 
98
  mx.eval(model.parameters())
99
 
100
  from mlx.utils import tree_flatten
101
 
102
  flat = dict(tree_flatten(model.parameters()))
103
+ mx.save_safetensors(
104
+ str(out / "model.safetensors"), flat, metadata={"format": "mlx"}
105
+ )
106
 
107
  cfg_out = dict(cfg)
108
  cfg_out["quantization"] = quant_cfg
 
113
  shutil.copy(MLX_1B_DIR / "nemotron3_embed_mlx.py", out / "nemotron3_embed_mlx.py")
114
 
115
  size_gb = (out / "model.safetensors").stat().st_size / 1e9
116
+ print(
117
+ json.dumps(
118
+ {
119
+ "out": str(out),
120
+ "quantization": quant_cfg,
121
+ "n_tensors": len(flat),
122
+ "size_gb": round(size_gb, 2),
123
+ }
124
+ )
125
+ )
126
 
127
 
128
  if __name__ == "__main__":
scripts/eval_8b.py CHANGED
@@ -41,8 +41,12 @@ def main():
41
  ap.add_argument("--make-ref", action="store_true")
42
  ap.add_argument("--mlx-dir", default=None)
43
  ap.add_argument("--out", default=None)
44
- ap.add_argument("--min-cos", type=float, default=0.0,
45
- help="optional gate on min fixture cosine (0 = report only)")
 
 
 
 
46
  args = ap.parse_args()
47
 
48
  import numpy as np
@@ -74,24 +78,31 @@ def main():
74
 
75
  ref = np.load(REF_NPZ)["embeddings"]
76
  m = embed_fn(texts)
77
- cos = [float(np.dot(a / np.linalg.norm(a), b / np.linalg.norm(b)))
78
- for a, b in zip(m, ref)]
 
 
79
 
80
  suite = pooling_fixtures.run(embed_fn, f"mlx-8b:{mlx_dir.name}")
81
  report = {
82
  "mlx_dir": str(mlx_dir),
83
- "quantization": json.loads((mlx_dir / "config.json").read_text()).get("quantization"),
 
 
84
  "fixture_parity_vs_transformers_bf16": [round(c, 6) for c in cos],
85
  "parity_mean": round(float(np.mean(cos)), 6),
86
  "parity_min": round(float(np.min(cos)), 6),
87
  "pooling_suite": suite,
88
- "arch_ok": bool(suite["results"]["padding_invariance"]["pass"]
89
- and suite["results"]["suffix_sensitivity"]["pass"]),
 
 
90
  }
91
  out = Path(args.out) if args.out else ROOT / "runs" / f"mlx_8b_{mlx_dir.name}.json"
92
  out.write_text(json.dumps(report, indent=2))
93
- print(json.dumps({k: v for k, v in report.items() if k != "pooling_suite"},
94
- indent=2))
 
95
  if args.min_cos and min(cos) < args.min_cos:
96
  sys.exit(1)
97
 
 
41
  ap.add_argument("--make-ref", action="store_true")
42
  ap.add_argument("--mlx-dir", default=None)
43
  ap.add_argument("--out", default=None)
44
+ ap.add_argument(
45
+ "--min-cos",
46
+ type=float,
47
+ default=0.0,
48
+ help="optional gate on min fixture cosine (0 = report only)",
49
+ )
50
  args = ap.parse_args()
51
 
52
  import numpy as np
 
78
 
79
  ref = np.load(REF_NPZ)["embeddings"]
80
  m = embed_fn(texts)
81
+ cos = [
82
+ float(np.dot(a / np.linalg.norm(a), b / np.linalg.norm(b)))
83
+ for a, b in zip(m, ref)
84
+ ]
85
 
86
  suite = pooling_fixtures.run(embed_fn, f"mlx-8b:{mlx_dir.name}")
87
  report = {
88
  "mlx_dir": str(mlx_dir),
89
+ "quantization": json.loads((mlx_dir / "config.json").read_text()).get(
90
+ "quantization"
91
+ ),
92
  "fixture_parity_vs_transformers_bf16": [round(c, 6) for c in cos],
93
  "parity_mean": round(float(np.mean(cos)), 6),
94
  "parity_min": round(float(np.min(cos)), 6),
95
  "pooling_suite": suite,
96
+ "arch_ok": bool(
97
+ suite["results"]["padding_invariance"]["pass"]
98
+ and suite["results"]["suffix_sensitivity"]["pass"]
99
+ ),
100
  }
101
  out = Path(args.out) if args.out else ROOT / "runs" / f"mlx_8b_{mlx_dir.name}.json"
102
  out.write_text(json.dumps(report, indent=2))
103
+ print(
104
+ json.dumps({k: v for k, v in report.items() if k != "pooling_suite"}, indent=2)
105
+ )
106
  if args.min_cos and min(cos) < args.min_cos:
107
  sys.exit(1)
108