This is the 6bit variant. Also available: czl/CLM-v0.1-8B-MLX-4bit, czl/CLM-v0.1-8B-MLX-8bit, and the bf16 reference.

Size without the dead head: the same weights with lm_head at 2-bit — measured to leave the embeddings indistinguishable from a re-run, 233 MB smaller — are in czl/CLM-v0.1-8B-MLX-6bit-outq2. Pooling only, not for generation.

CLM-v0.1-8B — MLX encoder (6bit)

The encoder half of Contrastive-LM/CLM-v0.1-8B, quantised for MLX on Apple Silicon.

⚠️ Not recommended for ranking: 4bit. agreement on decisive (>1 nat) decisions 0.889651 < 0.995; planner accuracy falls 31.95 points, beyond the 3.0-point budget. Published with the measured numbers so the result is reproducible, not because it is a drop-in replacement for the bf16 encoder.

What is in this repo

path what
config.json, model*.safetensors, tokenizer* the 6bit quantised encoder, standard mlx-community layout with the weights at the repo root so mlx_lm.load and the Hub file browser both work
clm_mlx/ the pooling pass and an OpenAI-compatible /v1/embeddings server
heads/ CLM_v0.1-8B.pt as safetensors, so no PyTorch is needed
clm_mlx.json the pooling / embedding / scale contract

mlx_lm is generate-only — it has no embeddings entrypoint, and mlx_lm.server routes just /v1/completions, /v1/chat/completions, /v1/models and /health. So clm_mlx supplies the pooling pass over mlx_lm internals: Qwen3Model.__call__ already returns self.norm(h), the post-final-RMSNorm hidden states, which is what vLLM's pooling runner returns in last-token mode.

How to use

# From the Hub, directly (mlx-lm loads MLX repos natively):
mlx_lm.load("czl/CLM-v0.1-8B-MLX-6bit")

# Or serve it for CLM:
python -m clm_mlx.server --model <dir> --port 8092
clm-serve --emb-url http://127.0.0.1:8092/v1/embeddings

Or in process:

from clm_mlx import Encoder
vecs, tokens = Encoder("<dir>", max_tokens=2048).embed(["What causes tides on Earth?"])

Evaluation

Agreement against a bf16 MLX reference of the same encoder in the same runtime, over 23,926 scored System One questions, through the real head stack (argmax(scale · cos), scale = 100.0 — cosine error is amplified 100×).

variant cos min cos mean top-1 top-1 (decisive) planner acc. Δ verdict
bf16 – – 1.0000 (ref) 1.0000 (ref) – reference
8bit 0.98294 0.99985 0.9888 1.0000 -0.46 pts yes
6bit 0.89642 0.99918 0.9828 1.0000 +0.70 pts yes
4bit 0.87825 0.99191 0.6186 0.8897 -31.95 pts no

Pass line. top-1 >= 1.0000 — the measured bf16-vs-bf16 noise floor of this corpus in this runtime — and top-1 (decisive) >= 0.995, where decisive means the reference's own top-1 led by more than 1 nat. A third condition applies: planner accuracy Δ, the change in accuracy against the T-Rex physics planner's label. A variant that holds every decisive decision but still loses measurable accuracy is marked usable rather than recommended.

Against the numbers published in the parent model card

case model card (vLLM bf16) this runtime (bf16 reference)
anchor-invoice/department billing billing — argmax matches, billing=0.98818, technical=0.01182
anchor-tides/rank 0 0 — argmax matches, 0=0.99386, 1=0.00003, 2=0.00611

Those probabilities came from vLLM bf16. This runtime reproduces the arg-max on both cases and agrees with an independent llama.cpp bf16 build to a cosine of 0.998680 minimum and 0.999958 mean over all 4,448 texts, so the residual difference is cross-runtime bf16 numerics rather than a pooling or tokenisation error. Tokenisation is byte-identical to AutoTokenizer, with no BOS and no EOS. All quantisation comparisons here are therefore against the same-runtime bf16 reference.

Quantization details

Tool: mlx-lm 0.31.3, one mlx_lm.convert pass per width:

mlx_lm.convert --hf-path Qwen/Qwen3-8B --mlx-path <dir> -q --q-bits N --q-group-size 128
  • affine mode, per-group bf16 scale and bias.

  • group_size = 32 — and this is the one number that mattered most in this whole exercise, and it is the opposite of both defaults. mlx-lm's own default is 64; Qwen's mlx-community Qwen3-8B-{4,6,8}bit repos use 128. Both are worse here.

    MLX affine stores a bf16 scale and a bf16 bias per group, so it spends 32 bits per group on quantisation metadata. That makes the group size a first-class quality knob rather than a rounding detail: gs=128 is 4x coarser than ggml's 32-weight blocks and stores its scales at bf16's 8-bit significand against ggml's fp16 11-bit. Every 0.25 bits/weight spent on finer groups bought back more accuracy than it cost, monotonically:

    width group bpw top-1 decisive (>1 nat) 0.25–1.0 nat planner acc. Δ
    8-bit 128 8.250 0.9461 7440/7440 0.9588 −2.60 pts
    8-bit 64 8.500 0.9849 7440/7440 0.9990 −0.59 pts
    8-bit 32 9.000 0.9888 7440/7440 1.0000 −0.46 pts
    6-bit 128 6.250 0.9459 7440/7440 0.9487 −4.23 pts
    6-bit 64 6.500 0.9477 7440/7440 0.9566 −1.94 pts
    6-bit 32 7.000 0.9828 7440/7440 0.9971 +0.70 pts

    At group 32 the decisive bucket is perfect at every width and essentially all remaining disagreement sits below 0.10 nats, i.e. in genuine near-ties. The 6-bit gain is inside the noise of the metric and should be read as "no measurable cost", not as an improvement.

    The published artifacts are the group 32 ones. The group 64 and 128 models are not shipped; they exist here only as the measurements above.

  • --q-bits 6 is a supported affine width, not a silent no-op.

Deliberately not used: mlx_lm.awq and mlx_lm.dwq. DWQ distils 16-bit down to 6- and 8-bit and upstream warns it "often doesn't work well" at those widths; AWQ is statistically indistinguishable from naive round-to-nearest on embedding encoders. See the GGUF repo's quantisation notes for the measurements behind that choice.

Limitations

  • The projection heads are encoder-locked to Qwen3-8B last-token-pooled 4096-d embeddings. The pooling and the head checkpoint must both match.
  • This is a ranker, not a generator. lm_head is never evaluated under pooling.
  • The head stack amplifies cosine error 100×. A 0.001 cosine error is 0.10 nats. Read the confident-decision column, not the mean cosine.
  • Right-padding and the last token. clm_mlx.Encoder reads each row's own last real index, never h[:, -1, :]; on a right-padded batch the latter returns the pad token's hidden state for every short row. Attention is causal, so trailing pads cannot change a real token — but only if the right element is indexed.
  • truncate_prompt_tokens is honoured server-side. vLLM truncates to fit; llama.cpp answers ERROR_TYPE_EXCEED_CONTEXT_SIZE and 400s instead. CLM sends the field on every request, so ignoring it would break the client on long inputs.
  • Probabilities are relative to the candidate set, and quantisation moves the logits by 100× the cosine error — see above.
  • Upstream asymmetry, reproduced here: CLM's training recipe keeps the last tokens of a state while the serving path truncates from the head.

License

Apache-2.0, inherited from Qwen/Qwen3-8B and Contrastive-LM/CLM-v0.1-8B. This repo is not gated.

Citation

@misc{kwok2026contrastivelanguagemodels,
  title={Contrastive Language Models: A System One Model for Fast and Generalizable Decision-Making},
  author={Jacky Kwok and Hangoo Kang and Tarun Suresh and Jon Saad-Falcon and Marco Pavone and Christopher Ré and Azalia Mirhoseini},
  year={2026},
  note={Notion Blog},
  url={https://contrastive-lm.notion.site}
}
Downloads last month
71
Safetensors
Model size
8B params
Tensor type
U32
·
BF16
·
MLX
Hardware compatibility
Log In to add your hardware

6-bit

Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support

Model tree for czl/CLM-v0.1-8B-MLX-6bit

Finetuned
Qwen/Qwen3-8B
Quantized
(9)
this model