Core AI model zoo

BitCPM-8B — 1.58-bit ternary on Core AI (the zoo’s first sub-int8 kernel)

Lessons from porting openbmb/BitCPM-CANN-8B — the MiniCPM4-8B architecture quantization-aware trained to ternary ({-1, 0, +1}) — to Apple Core AI with a custom 2-bit packed-GEMM Metal kernel, the zoo’s first sub-int8 kernel. It runs on the iPhone 17 Pro GPU at 17 tok/s decode in ~2.1 GB resident (an 8B at a 4B’s footprint).

1. The weights: TQ2_0, and extracting ternary from it

BitCPM-CANN-8B’s main repo ships the bf16 latent master (standard MiniCPM modeling, no BitLinear) — running it as-is is full-precision, not ternary. The ship-ternary truth is the bitcpm4-8b-tq2_0.gguf (2.37 GB). TQ2_0 (llama.cpp) = per 256-element block along the reduction axis K: each weight is a 2-bit code in {0,1,2} → value (code−1) ∈ {-1,0,+1}, times one fp16 scale d per block. gguf.quants.dequantize handles TQ2_0 / Q4_K / Q6_K directly (no hand de-interleave). To recover the kernel inputs from a dequantized weight W[N,K] (== d·(q−1)): d_block = max(|W| in block) (exact — the nonzero magnitude is d), code = round(W/d)+1.

Only the 224 transformer linears (q/k/v/o + gate/up/down × 32 layers) are TQ2_0. The embedding is Q4_K and the untied LM head is Q6_K — BitNet practice keeps those higher-precision.

2. The kernel: simpler than int4 k-means

bitcpm_ternary_metal.py is the int4-k-means matvec (gemma4_metal_mlp.py) minus the codebook:

Numerics: the kernel’s torch_defn is bit-identical to the gguf dequant (maxerr 0). The full 8B decode graph generates “The capital of France is” → “Paris.” and the engine output is token-identical to the torch reference (3/3 prompts, greedy) on M4 Max (62.7 tok/s).

3. The decode contract: M=1 kernel ⇒ S=1 static-ids export (the key trap)

The ternary kernel is M=1 (single-row decode matvec), like every gemma4-metal kernel. A dynamic-input_ids export lets the prefill run S>1; the kernel’s x.reshape(s,k) then produces a dynamic-row tensor MPSGraph can’t constrain, and lowering fails at engine-compile:

error: 'mps_spi.copy_discarding_constraints' op input must have tensor constraints   (op_id ~48, early)

Fix: export --static-idsinput_ids pinned to [1,1] (S=1 always), position_ids + KV dynamic. Prefill then runs as pipelined S=1 steps under COREAI_CHUNK_THRESHOLD=1 (no prefill loss; “prompt tok/s ≈ decode tok/s”). With S=1 the kernel compiles, AOT-survives, and runs. position_ids must carry the full length (the attention offset = seq_len − 1 = the KV write slot).

4. Gating an M=1 static bundle (harness gotchas)

5. iPhone deploy

6. Why this model

The 2026 MLX surge on Apple Silicon makes “match MLX on a Mac” a moving target. The durable Core AI edge is a kernel MLX structurally lacks (its quant is 4/8-bit affine — there is no 2-bit ternary GEMM) on a device MLX doesn’t ship to. 1.58-bit on iPhone is both — and the architecture (MiniCPM4-8B) was already in the zoo’s path, so the whole novelty is the one kernel.