CAMeLBERT-da-MLX
An MLX build of CAMeL-Lab/bert-base-arabic-camelbert-da for on-device Arabic inference on Apple
Silicon — the same checkpoint as the Core AI build in this series, in Apple's array framework instead of
the .aimodel container.
mlx_lm has no BERT-shaped architecture, so for a long time this looked impossible; mlx_vlm carries
bert, modernbert and xlm_roberta in full, including the classification heads. That is where this
build comes from.
Verified equivalence
Compared against the original PyTorch model on 8 Arabic sentences (including one dialect sentence and one bare proper noun), mean-pooled and L2-normalised so the embeddings mean the same thing in both builds:
| Input | max abs diff | cosine |
|---|---|---|
| الخدمة كانت ممتازة وأنصح به بشدة. | 4.22e-07 | 1.0000000000 |
| تجربة سيئة جدا ولن أعود مرة أخرى. | 7.48e-07 | 1.0000000000 |
| المنتج وصل أمس. | 5.70e-07 | 1.0000000000 |
| فريق الدعم ساعدني بسرعة وحل المشكلة. | 5.95e-07 | 1.0000000000 |
| الطلب اتأخر أسبوعين وما حد رد عليا. | 8.90e-07 | 1.0000000000 |
| كيف أستعيد كلمة المرور؟ | 5.97e-07 | 1.0000000000 |
| ابغى اعرف ايش صار بالظبط | 5.35e-07 | 1.0000000000 |
| الرياض | 3.01e-07 | 1.0000000000 |
Worst absolute difference 8.900e-07 · minimum cosine 1.0000000000.
The Core AI build of the same checkpoint, measured against the same reference, lands at 1.490e-07. The two differ because the frameworks accumulate in a different order, not because either is wrong: both graphs are compared against the identical PyTorch outputs in validation.json.
Arithmetic equivalence only — no quality benchmark, no accuracy score, and no comparison with upstream's reported numbers.
Use
import json, numpy as np, mlx.core as mx
from transformers import AutoTokenizer
from mlx_vlm.models import bert as B
cfg = json.load(open("config.json"))
model = B.Model(B.ModelConfig.from_dict(cfg))
model.load_weights(list(mx.load("model.safetensors").items()), strict=False)
tok = AutoTokenizer.from_pretrained(".")
enc = tok(["نص عربي"], padding="max_length", truncation=True, max_length=512, return_tensors="np")
out = model(mx.array(enc["input_ids"].astype("int32")),
attention_mask=mx.array(enc["attention_mask"].astype("int32")))
hidden = np.asarray(out.last_hidden_state, dtype="float32") # [1, 512, 768]
mask = enc["attention_mask"][..., None]
pooled = (hidden * mask).sum(1) / mask.sum(1).clip(1e-9)
emb = pooled / np.linalg.norm(pooled, axis=-1, keepdims=True)
What is in here
| File | |
|---|---|
model.safetensors |
weights (fp32) |
config.json |
architecture config |
validation-mlx.json |
every measured number above |
| tokenizer files | so the tokenizer loads from this repository |
Limits
- Fixed at 512 tokens; longer inputs need chunking.
- 8 sentences measured. Dialect text, long documents and domain vocabulary were not.
- No quality benchmark and no speed or memory measurement in this card.
- The graph returns hidden states; pooling, normalisation and any task head are the caller's steps.
Licence and attribution
apache-2.0, as declared by upstream. A derivative work of CAMeL-Lab/bert-base-arabic-camelbert-da; credit for the model is theirs.