All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog.
v0.7.0-beta.1 - 2026-09-04
Epic #1431 pinned llama.cpp release b10621 (commit c1d0e7a0, published 2026-08-25), extracted its whole server surface (249 help entries, 323 long-option spellings, 136 environment variables, 53 routes, 74 native request fields) and reduced it to 376 manifest entries across 18 shards under compat/llama-server/b10621/. Every entry now carries a terminal state: 169 supported, 22 aliased onto an mlxcel spelling, 35 recorded as a deliberate divergence with a written rationale, 150 not applicable to a runtime that does not execute GGUF. Nothing is deferred, and a CI gate fails the build if an entry regresses. Full map in llama-server compatibility.
- HTTP transport, authentication, TLS and CORS (#1462, #1465). Socket timeouts, keep-alive, request-size limits, path prefixes, Unix domain sockets, TLS certificates, CORS origins, and multi-key authentication from a list, a file, or both combined.
- The native completion, embedding, tokenization, template and infill routes (#1467, #1480).
/completion,/embedding,/tokenize,/detokenize,/apply-templateand/infillare served separately from the OpenAI-compatible surface, withresponse_fields,stream_optionsand real prefill timings on the responses. - The sampler remainder and a GBNF grammar engine (#1487, #1506). Mirostat v1 and v2, dynamic temperature, adaptive-p,
min_keep,logit_bias, b10621 DRY breaker-string semantics,n_probsandpost_sampling_probs, and the uint32 seed fold;--grammar,--grammar-file,--json-schema,--json-schema-file, and the nativejson_schemafield with itsgrammaralias, lazy grammars, triggers and preserved tokens. - Prompt cache, batching, context retention and YaRN (#1468, #1503, #1522).
--rope-scaling yarnand the five--yarn-*knobs run on the shared RoPE path,--parallel autoresolves, and the prompt-cache and slot-scheduling shapes match the pinned binary. - Router mode (#1495, #1501).
--models-dirdiscovers checkpoints, a model cache is a second source, INI presets are read, and the management surface is served from an in-process model pool where upstream spawns child processes. - LoRA adapters served unfused with live scales (#1497, #1520). Multiple adapters stay unfused for per-request or hot-swapped scale changes, or are fused for zero decode overhead.
- mp3 and flac transcription, streamed per token (#1523). The container is identified from the clip's own magic bytes rather than the client's
formatstring, and the ASR route emits one delta per decoded token instead of one delta at the end. - Embedding and reranking mode and pooling (#1483), speculative flags mapped to MTP and DFlash (#1488), and the Vertex AI custom-container predict adapter behind
AIP_MODE=PREDICTION(#1493). - Observability and slots (#1492, #1524).
/props,/slots,/metrics,/healthand slot persistence, with a slot bound on a request's first progress signal rather than at route entry, so a queued request no longer holds a slot away from one the worker is serving. - Idle sleep (#1528, #1530).
--sleep-idle-secondsdrops the serving worker and its weights after the idle window; the next request reloads and is served by the rebuilt scheduler, exactly as upstream's blocking wake is. - Assistant prefill, echo, and streamed thinking tags (#1526, #1527), with
reasoning_in_contentsettled against the pinned binary. - Unsupported options fail instead of being ignored (#1463, #1469, #1481, #1489). GGUF model-source semantics, GGML runtime flags and KV quantizers, projector flags, and control-vector options are refused at startup with a diagnostic naming the option.
- The Inkling family, across four modalities (#1532, #1535, #1540, #1546, #1548). The text backbone brings hybrid sliding/global NoPE attention, learned banded relative-position logits, four f32 short-convolution states per layer, and logsigmoid-normalized routed and shared experts, loading original bf16/f32 weights, native ModelOpt NVFP4 expert planes, and pre-converted affine MLX 4-bit experts. On top of it: HMLP image input with the reference 40x40 tiler, adjacent-frame video for CLI
--videoand servervideo_urlunder one request-wide 16-pair budget, dMel audio for--audioandinput_audio, and a native chained-MTP drafter whose default verify width isnum_nextn_predict_layers + 2. - IQuest-Coder (
iquestcoder) and text-only Youtu-LLM (youtu,youtu_llm) (#1594, #1593). Both are decoders mlxcel already runs under a label detection rejected, so the checkpoints failed withUnsupported model type. Each is a newModelTypearm on an existing loader with no new architecture code; IQuest-Coder additionally refuses the two config switches that would stop it being a Llama decoder and fixes a SentencePiece tokenizer defect the checkpoint exposed. - Qwen-VL video input and Responses-native image parts (#1510, #1310), and LFM2-VL image splitting (#1405).
- Locally typical sampling (
typical_p) and top-n-sigma logit filtering (#1482, #1479). Both are row-wise graph transformations applied throughapply_row_filtersbefore the single fused sampling dispatch, so requests stay on the batched path. - A live runtime settings endpoint (#1516), optionally authenticated, and per-request speculative acceptance on the response (#1588): the drafter kind, verify rounds, and drafted and accepted token counts, which previously reached only a
tracingline no client can read. tool_choiceacceptsrequiredand a named function (#1319), and tools are declared to the template only when the request actually sends them (#1598).mlxcel inspect --jsonandmlxcel arch --json(#1605, #1606, #1608). The first emits the memory estimator's byte fields as a stable contract instead of a human-readable banner; the second derives a per-family registry of runtime, modality, backend, distributed, drafter and KV facts fromALL_MODEL_TYPES.make recipes-registrywrites a versioned snapshot underrecipes/registry/for downstream recipe builders.- Chat Completions reasoning is available under both common field names (#1308). Streaming deltas and non-streaming assistant messages now emit an OpenRouter-compatible
reasoningalias alongsidereasoning_content, with identical values and joint omission when no reasoning exists;--reasoning-alias-field noneorMLXCEL_REASONING_ALIAS_FIELD=nonedisables only the duplicate for byte-sensitive deployments, while Responses API events and Anthropic thinking blocks remain unchanged. - OpenAI-shaped reasoning controls now drive chat-template thinking behavior (#1307). Chat Completions
reasoning_effort, Responsesreasoning.effort, and compatible extra-bodyreasoningvalues deriveenable_thinking;none,off,disabled,false, and0disable thinking without becoming invalid level kwargs, enabled effort is forwarded verbatim through a template'sreasoning_effortorreasoning_strengthspelling, and explicit per-request chat-template kwargs keep per-key precedence. - Embeddings are available through
mlxcel embedand the OpenAI-compatiblePOST /v1/embeddingsendpoint (#1408, #1410-#1416). The server can load an embedding checkpoint alone or next to a chat model with--embedding-model; supported families cover BERT/XLM-RoBERTa, ModernBERT, SigLIP text, EmbeddingGemma, Qwen3 and Qwen3-VL embeddings, bidirectional Llama/Nemotron/LFM2.5, Llama-Nemotron-VL, and ColBERT-style multimodal late-interaction models. Pooling, L2 normalization, Matryoshka dimensions, token and image inputs, bounded worker admission, and per-checkpoint length limits are shared by the CLI and server paths. - Reranking is available through
mlxcel rerankand the Cohere/Jina-compatiblePOST /v1/rerankendpoint (#1417). One-label BERT/XLM-RoBERTa/ModernBERT cross-encoders, Qwen3 generative rerankers, and Qwen3-VL multimodal rerankers return ranked relevance probabilities;--reranker-modelcan serve one beside the chat and embedding workers. - DeepSeek-V4 (
deepseek_v4) text model support (#523). A genuinely new architecture rather than a fifth member of the DeepSeek MLA family: HyperConnections replace the plain residual stream, attention runs one shared 512-wide KV head across 64 query heads inside a 128-token rotating window with per-head softmax sinks, per-layer compression (compress_ratios) picks local, pooled, or sparse-compressed attention, sparse layers add a HiSA indexer for top-k pooled-row selection, and MoE routing hashes the first three layers by token id (tid2eid) rather than argpartitioning a score row. Ported from the in-treereferences/mlx-vlmreference after an earlier V3-wrapper attempt (PR #592) could not load a real checkpoint. Validated againstmlx-community/DeepSeek-V4-Flash-4bit(43 layers, ~151 GB on disk): a strict weight-coverage check at load, plus three real-checkpoint generation gates covering a capital-of-France sanity decode, a decode that crosses a pooling-window boundary, and a prompt past 2100 tokens that pushes the HiSA path into its sparse selection branch. Review closed several boundaries where untrusted checkpoint or config data reached MLX unguarded (an out-of-range hash-routed expert id, a wrong-length correction bias, an indivisiblewo_areshape plane, and architecture scalars that could overflow ani32cast on their way into a kernel), each now a load-timeResultinstead of an uncatchable abort. MTP drafting, the reference's fused HyperConnection Metal kernel, and tensor/pipeline-parallel sharding are follow-ups.
- CUDA
qmm_naivesizes its CTA tile against the device's real shared-memory budget, and opts into more of it when the tile needs it (#1541). The quantized matmul behind CUDA prefill picked its N tile withenough_smem = sm80 && itemsize <= 2 && group_size <= 64, folding two unrelated decisions into one name. The shape terms are a shared-memory rule and are now written as one: a comparison againstcudaDevAttrMaxSharedMemoryPerBlockOptinqueried from the running device. The architecture term is not, and a Tesla V100 disproves it, offering 96 KB per block against the 24 KB the widest eligible tile needs. Three defects fall out of writing the budget down. A tile larger than the 48 KB a launch gets withoutcuFuncSetAttributenow opts in first, which f32 activations at group size 128 need and never had, so that shape stops failing inside the driver on every architecture. A tile that does not fit at all is refused at the launch site with a message naming the tile, the requirement and the budget, instead of reaching the driver; Turing's 64 KB budget is the case that hits. And the N tile joins the JIT module name, which is the sole key for both the in-process module cache and the persistent PTX cache. The wide tile itself was measured on Volta and refused. At bf16 and group size 64 the 128-wide instantiation needs 255 registers and spills 128 bytes per thread where the 64-wide one needs 224 and spills nothing, both reach the same 2 blocks per SM so the extra width buys no occupancy, and halving the CTA count on a grid already smaller than an 80-SM part makesqmm_naive1.50x slower at a 106-token prompt, 1.06x at 516 and 1.02x at 4,106, at identical launch counts. There is no crossover: prefill is chunked at 2,048 tokens, so the M extent of the grid is capped and the wide tile's best case is bounded rather than approached. The selected tile is therefore identical to upstream's on every architecture, asserted by enumerating every(itemsize, group_size, m)combination against every real per-block budget in a host-side unit test that needs no GPU.MLXCEL_QMM_NAIVE_TILE_Npins the width andMLXCEL_TRACE_QMM_TILEprints tile, registers, spill and occupancy, so the sweep is one command to re-run against #1543's Volta tensor-core MMA. Full record indocs/benchmark_results/qmm-naive-tile-v100-2026-08-31.md. - CUDA
qmvaccumulates bf16 in float below Ampere, at every bit width (#1539). The quantized GEMV that serves every single-stream decode step picked its accumulator from the weight bit width alone, float atbits >= 8and the element type below that, which handed a bf16 checkpoint a bf16 accumulator at 4 bits. No pre-Ampere part has a bf16 ALU, so each FMA in the k-loop ran as convert-to-float, fma, convert-back. Below Ampere the kernel now accumulates bf16 in float regardless of bit width, behind a__CUDA_ARCH__ < 800guard in device code; the float specializations it selects already existed for thebits >= 8case. Measured on a Tesla V100-PCIE-32GB:qwen3.8-27B-4bitdecodes at 117.83 ms/token against 219.96 before (4.55 to 8.49 tok/s, 1.87x) andgemma-4-12B-it-4bitat 71.66 against 124.26 (8.05 to 13.95 tok/s, 1.73x). In an nsys profile at identical launch countsqmvitself runs 2.01x faster and accounts for 99.4% of the end-to-end gain, whileqmm_naive, which already accumulated in float, moves by less than 0.6%. The 8-bit sibling checkpoint, the control, is unmoved at 66.73 ms/token.qmvroofline attainment rises from 7.27% to 14.80% of 900 GB/s, short of the 25% the issue asked for and close to the 2.14x ceiling the controlled 4-bit against 8-bit measurement had put on it. Pre-Ampere output changes: float accumulation is strictly more accurate than the emulated bf16 accumulation it replaces, so quality improves, but greedy decode on a Volta host is no longer token-identical to a build from before this change. No published Volta baseline exists for it to regress against. Determinism is unaffected and still tested. sm_80 and later are untouched: the emitted SASS for the sm_80 and sm_121 device passes is byte-identical before and after. Full record indocs/benchmark_results/qmv-float-accum-v100-2026-08-31.md. --models-dirchanges meaning, and the old meaning moves to--model-store-root(#1495). Onmlxcel serveandmlxcel-server,--models-dirandLLAMA_ARG_MODELS_DIRnow select b10621 router-mode discovery; the mlxcel store root that spelling used to name is--model-store-root. A command line carrying both logs a migration diagnostic at startup.--timeoutandLLAMA_ARG_TIMEOUTchange meaning (#1432). They are now the HTTP socket read/write timeout with b10621's 3600-second default. The per-request decode watchdog they used to configure is--decode-timeout/MLXCEL_DECODE_TIMEOUT, with its 600-second default unchanged; setting the old spelling without the new one logs a migration warning.- Constrained decoding keeps the grammar mask packed (#1578). The token bitmask stays in
u32form from the matcher to the GPU, where four graph ops expand it andwhere_condapplies it. On the Qwen3.8-27B geometry that removes two 248k-element host loops and 97% of the per-step host-to-device copy from the scheduler thread, on every tick of every constrained sequence. - Chat templates are compiled once and cached (#1518), response stores are bounded by bytes rather than entry count (#1519), and the JSON body limit is derived from the image budget (#1515) instead of being a fixed number that either truncated valid image requests or admitted oversized ones.
- CUDA compute capability is visible to the runtime (#1551), and the Volta sm_70 baseline is recorded and gated in CI (#1556) so the pre-Ampere arms above cannot regress unmeasured. The Volta first-token fixed cost is instrumented rather than guessed (#1545).
- OpenXLA compiles a set of context capacities and routes each request to the smallest that fits (#1302), its warning backlog is cleared with warnings denied in CI (#1381), and CI links an OpenXLA binary so a link-only regression cannot reach
main(#1305). - The Python client is tested across CPython 3.9 through 3.13 (#1572).
- A malformed
MLXCEL_PAGED_SLAB_BLOCKSnow falls back to the derived slab size, as its warning always claimed (#1137).resolve_paged_slab_blocksreturned early on a parse error, which is the0pin: the pool kept the historical 32-block slab while the log said the derived size was in use, so a typo in the variable quietly made the fused paged decode path unreachable past one slab. The parse-error arm now falls through to the derivation,0still pins the pool default, and a unit test keeps the three arms distinct. - Speculative-burst responses report
prompt_msandpredicted_msthe way classic decode does (#1592). The B=1 and batched bursts run every verify round before streaming, and the first-token stamp was taken when the finished token vector was replayed, soprompt_msabsorbed drafter load, prefill and all verify rounds whilepredicted_msreported the sub-millisecond replay (predicted_per_secondread 95000 on a 96-token DFlash request). The burst now carries the instant its target prefill finished and stamps the sequence with it, sopredicted_mscovers the verify rounds; the drafter's once-per-process disk load is excluded from everytimingsfield and stays on theDrafter loadedlog line. Classic decode is unchanged. - Youtu-VL restores raster order after the windowed vision tower (#1600).
reverse_window_indicessorted(value, index)pairs and wrote the rank back by index, which for a permutation of0..Nreproduceswindow_indexitself rather than its inverse, so the merged vision tokens were un-reordered by applying the window permutation a second time. That only restores raster order when the permutation is an involution, and with Youtu-VL's 8x8 merged-token window even a 512x512 image (a 16x16 merged grid) is not one: 192 of its 256 merged tokens reached the language model out of place. The helper now buildsargsort(window_index)directly, bounds-checked like the Qwen2.5-VL fix in #1601, with checkpoint-free tests pinning the non-involution, single-window, and multi-image cases. This corrects the token order at that stage only, and does not on its own make the family describe images correctly: the processor emits patches in raster order where the tower's rotary positions and the patch merger both expect merge-block-major, which is filed separately as #1610. mlxcel_core::is_gpu_available()is renameddefault_device_is_gpu(), andgpu_backend_available()answers whether a GPU backend exists (#1421). The old name read as a hardware query but reported whether the MLX default device was currently the GPU, so it turnedfalsethe moment anything calledset_default_device(false)on a machine with a GPU.gpu_backend_available()is the unclampeddevice_count(Device::gpu) > 0:trueon Metal and on a CUDA build with a device,falseon a CPU-only build and on a CUDA build without a driver, and unchanged by the default device.is_gpu_available()remains as a deprecated shim for one release.initialize_runtime()now resolvesRuntimeSetup.devicefrom backend availability and reportsMLXCEL_DEVICE=cpuseparately in the newRuntimeSetup.cpu_overridefield, which the server and CLI startup lines print, so a CPU device is distinguishable as requested or as the only option. Every CPU resolution is applied to MLX's default device, not only the operator's override: the CUDA backend answersgpu::is_available()unconditionally whiledevice_count(Device::gpu)reports what the driver found, so a CUDA build on a driverless host would otherwise reportCpuwhile MLX kept dispatching to the unusable GPU. Test modules that moved the default device to the CPU inside aOnceand never moved it back, which made every real-checkpoint gate sorting after them measure the CPU backend under--test-threads=1, now hold the newmlxcel_core::streams::DefaultDeviceGuardper test, and the unconditional GPU pin PR #1420 added tomlx_test_guardis reduced to an assertion so a future leak fails at the first gate after it instead of being silently repaired. Restoring the device is not the same as excluding other movers, so there is also one process-wide lock for it,mlxcel_core::streams::lock_default_device, held by every test that moves the default device and bymlx_test_guardfor the duration of the tests it serializes: a lock private to each module leaves the modules interleaving, which under the parallelcargo test --libinscripts/run_quality_gate.shreported another module's live guard as a leak in 4 of 300 runs and would have let a real-checkpoint gate score on the CPU backend without saying so.- CUDA grouped GEMM no longer tells CUTLASS a Volta part is a Turing part (#1544).
dispatch_cutlass_archmapped every device below compute capability 8.0 tocutlass::arch::Sm75, which names them16n8k8MMA that Turing introduced and an sm_70 part does not have, andget_grouped_mm_funcionopened with a matchingSm75placeholder. The pre-Ampere arm now selectscutlass::arch::Sm70and the placeholder is gone. The branch is live, and on the checkpoint the epic benchmarks. #629's sorted-MoE prefill fast path routes a quantizedGatherQMMintocutlass_grouped_gemm_unalignedonce the batch clearsB >= 8 * num_experts, which forgemma-4-26b-a4b-it-4bitmeans a prompt of 128 tokens or more; an nsys profile at 573 prompt tokens shows 180cutlass::Kernel<GemmGrouped>launches taking 3.8% of GPU time, and the same profile at the 46-token prompt the Volta baseline used shows none, which is why nobody had seen it. Nothing was computing wrong. The pre-Ampere arm resolves toGemmConfiguration's primary template, which isOpClassSimtwithInstructionShape<1, 1, 1>, so no arch tag can reach an MMA atom there and CUTLASS erases the tag: the running kernel is anMmaSimt/OpMultiplyAdd/MmaPipelinedinstantiation whose mangled name carries no architecture token at all. Correctness was measured rather than assumed, because a mismatched CUTLASS path can return plausible wrong numbers that greedy decoding hides: the grouped path and the legacyqmm_naivepath produce a byte-identical 64-token greedy continuation, and new unit tests comparegather_mmagainst anf64dense per-expert reference on the model's real expert dims across both entry points, both output-alignment arms, both operand layouts and f32, bf16 and f16. The retag moves no device code on any architecture, which was checked rather than argued: compiling the translation unit before and after atcompute_70,compute_80andcompute_121yields the same 51 device symbols with byte-identical SASS bodies at all three, over 144 MB of dump compared per symbol. The architecture decision now lives ingemms/grouped_gemm_arch.has a pure function of the compute capability major version, enumerated over every architecture by a host-side test through a C shim with no GPU involved, which is what closes this issue's "zero change on sm_80+" criterion locally instead of deferring it to GB10. Twostatic_asserts pin the preconditions: that the pre-Ampere configuration is still SIMT, without which one arm could not cover Volta and Turing together, and that its stage count is 2, since the 3-stageSM80_CP_ASYNCpipeline is bound to an explicitcutlass::arch::Sm80specialization andcp.asyncdoes not exist before Ampere. MoE decode and prefill are unmoved on a V100, as byte-identical device code requires. Full record indocs/benchmark_results/grouped-gemm-arch-v100-2026-08-31.md. mlxcel serveandmlxcel-servermatch the low-risk llama-server b10621 deployment surface (#1430). CanonicalLLAMA_ARG_*variables and--temperaturenow work on both entry points, server sampling defaults match the nightly, OpenAI-shaped requests accept llama aliases, scalar stops and seed-1, safe health/reranking route aliases reach the existing handlers,f16maps exactly to FP16 split-cache storage, and unsupported cache-reuse chunk sizes or GGML cache quantizers fail instead of changing unrelated behavior.- Qwen2.5-VL runs its vision tower norm in f32 and restores raster order correctly (#1601), and the raw Hugging Face patch-embed layout is normalized at load rather than assumed (#1582).
- Four RoPE scaling defects across shared decoder paths. Qwen3 ignored a configured
rope_scaling(#1398); the shared Llama 3 path dropped the scaled frequencies (#1385); Gemma 3 skipped the linear factor on global-attention layers (#1386); InternLM doubled RoPE positions under dynamic NTK scaling (#1389). Each produced degraded long-context output with nothing failing. - Phi-3 selects its LongRoPE table by whole-prompt position (#1580) rather than by the current chunk, so a prompt that crosses the short/long boundary mid-chunk no longer switches tables partway through.
- Falcon-Mamba applies the B/C/dt RMS norm once (#1574), not once per chunk.
- Recurrent decode state is persisted across requests (#1513), and KV modes are applied to model-owned caches (#1400) instead of only to the scheduler's.
- Symmetric Turbo4 stops reading past its sign vectors on MLA latent caches (#1395).
- Sampling filters on the untempered distribution and scales by temperature last (#1391), which is the order every filter's threshold was defined against.
- The prompt cache no longer donates or adopts KV-less shadow paged entries (#1390), and preemption victim selection has a total order (#1301), so two equally-ranked sequences no longer produce a nondeterministic eviction.
- Gemma 4 Unified keeps its vision overlay when audio is present (#1403), and SmolVLM split-image framing is aligned with the reference (#1402).
- LoRA adapters that do not map onto the loaded model are refused (#1576) instead of loading and silently contributing nothing.
- XML tool-call arguments are typed by the request's own schema (#1575), and Python
reprvalues in tool calls are parsed (#1404). - Thinking state is derived from the checkpoint rather than guessed. The primed-open case reads the tokenizer's markers (#1554) and the
thinking_modesentinel comes from the chat template (#1547). - Chat-template failures surface. A template rejection is no longer swallowed (#1511), a missing map key returns
Noneinstead of raising (#1394), and thetojsonfilter matchesjson.dumps(#1382). - Boundary snapshots are gated by model capability and sized by model (#1507, #1509), so a model that cannot produce one no longer allocates for it.
- The audio frontend shares one bounded real-FFT implementation (#1517), and an Inkling sub-config that carries both a field and its alias is accepted (#1561).
benchreports KV sizes in binary byte units (#1514) and refuses a--targetpath with no file name (#1569).
v0.6.0 - 2026-08-22
- Gemma 4 MTP is gated on a measured exactness probe instead of an unconditional yes (#1188, #1258). The three Gemma 4 arms of
mtp_capable_targetreturnedtruewithout checking anything, which advertised temperature-0 byte-identity the hardware does not always provide: on Apple GPU generation 15 and later, MLX routesM >= 2affine-quantized matmuls toqmv_wide, whose K-reduction order differs from the single-tokenqmv, and the result is a systematic token divergence rather than f16 jitter. The Gemma 4 wrapper now runs the same block-vs-chain probe the Qwen 3.5 family has run since #1186, on throwaway caches built from the inner model so no scheduler-owned sequence slot is touched, with both arms projected through the tied LM head at their own width so the head's dispatch is covered too. On a failing probe the gate first retries withqmv_widedisabled and keeps the narrow kernel when that restores exactness, which is what generation 15+ hosts now do by default: measured on M5 Max,gemma-4-12b-it-4bitplus its 4-bit assistant serves 93.2 tok/s against 43.8 classic (2.13x) with byte-identity kept, where the fast kernel would serve 120.4 (2.76x) without it. Upgrade note: a Gemma 4 MTP deployment on M3, M4 or M5 will see roughly 23% lower speculative throughput than v0.5.2 and byte-identical temperature-0 output in exchange.MLXCEL_MTP_ALLOW_INEXACT=1together withMLXCEL_QMV_WIDE=1restores the previous behavior and forfeits the contract. - The B=1 MTP burst is gated on GPU generation rather than the Neural Accelerator (#1217). The static default for batch-capable targets keyed on
has_neural_accelerator, an M5-only proxy that lumped M3 Ultra with M1 Ultra although every recent measurement places it much closer to M5. The discriminator is theuse_qmv_widesplit, so the gate now enables from generation 15 (M3, M4, M5). M3 Ultra, never measured on this pairing before, reads 1.95x (prose), 2.41x (source code) and 2.65x (enumeration) at a verify-round cost of 1.51 classic steps against 2.71 on M1 Ultra, which keeps declining. Seedocs/benchmark_results/mtp-b1-gate-m3ultra-2026-08-20.md. - The MTP verify width is decided by measured throughput, not by an acceptance proxy (#1207). The adaptive controller held a drafter at its configured depth until the configured prefix was usually fully accepted, a proxy that cannot pass on the Gemma 4 12B pairing (acceptance 0.585 over a 3-proposal prefix) no matter how profitable widening would be. The B=1 round loop now alternates 32-round measurement windows between the configured depth and the requested ceiling and holds whichever measures more emitted tokens per millisecond, with a 2% adoption margin, a 4/16/64-window re-challenge backoff, and an early abort that drops a collapsing challenger after 4 rounds. Measured on M5 Max at pinned widths: 89.15 tok/s at 4 against 94.91 at 5, and the controller adopts 5; at a requested 8 (85.96 tok/s) it refuses.
MLXCEL_MTP_BLOCK_CONTROLLER=proxyrestores the previous gate and=requestedpins the requested width for width sweeps. The batched (B>1) loop keeps the proxy, because the row-averaged accept length is the only per-round signal it measures. mlxcel-xlatracks the root version (this release). The crate was exempted from the workspace version check as a default-off backend outside the release contract. It now carries model support users select (Molmo2 indexed attention pooling, LLaVA and Qwen2-VL image context floors), its worker sits on the server's prompt-cache path, and CI compiles its feature combinations, so a version standing still at 0.4.0 while the code moved was telling readers the backend was dormant. It moves 0.4.0 to 0.6.0 with the rest of the workspace, andscripts/ci/check_crate_versions.pynow has no exemptions at all.
The OpenXLA / StableHLO backend leaves the experimental shelf and ships as alpha. It stays off by default behind the xla-backend and xla-iree features, and alpha means what it says: the surface is real enough to run and to report bugs against, not to deploy.
Its crate was exempted from the workspace version check on the reasoning that a default-off backend sits outside the release contract, so mlxcel-xla sat at 0.4.0 while 23 pull requests landed in it and none of them reached a release note. That was the wrong trade. The work below shipped between v0.4.0 and now, and is collected here because this is the release that folds the crate back into the workspace version line.
- Multimodal execution through IREE. LLaVA vision (#913) against a reference architecture validated end to end (#897), Qwen2-VL vision (#915), Phi4MM audio with per-slot adapters (#914), Gemma3n text runtime with dense PLE prefill (#892), sparse DeepStack prefill (#893), multimodal RoPE position state (#894), Molmo2 indexed attention pooling (#916), and image requests admitted through both the CLI and continuous-batch serving (#895). Image context floors are derived for LLaVA and Qwen2-VL (#1280), so a graph too small to admit an image fails at startup instead of dropping it silently.
- Session and prefill plumbing. A prefill-embeddings entry point (#879), IREE sessions seeded from prepared embeddings (#888), and static context capacity as a parameter rather than a constant (#880).
- Operator numeric contracts. Auxiliary dtype contracts are versioned (#934) and operator contracts are bound to them (#937), with a bounded numeric oracle harness (#938) and probes for dense matmul (#940), the core numeric set (#941), CUDA prefix scan (#943) and affine Q4 dequant (#944), sharing one emitter helper layer (#942). Issue #932, which defines the contracts these probes check, stays open.
- Fixes. The terminating EOS token is no longer emitted as output (#968), diagnostic local-task threads are bounded (#945), backend-seam tests take the env lock on
xla-backendbuilds (#1169), the OpenXLA worker handlesPromptCacheWarmupinstead of falling through (#1273), and integration tests link again withlibcafter the IREE archives (#1275). CI compiles the feature combinations (#1282), which is what keeps this list from going stale again.
GET /v1/internal/mtp-policyreports the adaptive MTP verdict (#1257, #1268). A versioned read interface for what the policy settled on and why, so an operator does not have to parse the private hint files, whose empty directory used to mean "still profiling", "no MTP configured" and "the cache root resolved elsewhere" all at once. States aresettled,profiling,forced,unavailable(with a reason) and, since #1298,exactness_declined. The same state rides the/healthsnapshot underobservability.mtp_policy. Seedocs/mtp-policy-api.md.- MTP can draft and verify a token tree instead of a chain, behind
MLXCEL_MTP_TREE, default off (#1204, #1212, #1214). The tree path carries per-node RoPE positions (DraftTree::depths), because a flattened tree handed to the target as a contiguous span puts every node after a sibling one place too far along, which is a correctness fix for any branching tree and was invisible while only linear trees ran. The measured verdict is negative and the flag stays off: a linear tree costs 1.9% to 4.1% against the chain and branching costs 8.1% to 10.1%, with an oracle upper bound of about 6.5%, because branching rescues only the rounds whose chain breaks early while paying for the leaf on every round. The drafter's own top-two confidence does not predict which rounds those are. scripts/bench_speculative.shmeasures speculative decoding with the protocol the published numbers were taken under (#1215): baked-in prompts, a quiet-host gate, ABBA alternation with discarded warm-ups, and a spread limit above which a run is reported as untrustworthy rather than averaged. Three-host records for M5 Max, M3 Ultra and M1 Ultra are indocs/benchmark_results/.
MLXCEL_ENABLE_MTP_DEFERRED(#1179). The flag gated a "deferred" verify path that deferred nothing: it split the verify forward from the LM-head projection, producing identical compute with one extra bridge crossing and one extra intermediate tensor. The comment above the gate still described a per-position projection loop that had been rewritten into a single batched graph long before. The flag, the path, and its four orphaned helpers are gone.
/v1/internal/mtp-policyno longer reportsprofilingforever when the exactness probe vetoes MTP (#1298). A vetoed pairing never dispatches a burst, so the adaptive policy never accumulates a sample and the endpoint invited the operator to wait for a verdict that structurally could not arrive. The veto is now the stateexactness_declined, carrying the probe's own one-line reason in a newdecline_detailfield, withmtp_enabled: falseand no verdict. The label addition stays insideschema_version1 under the documented growth rule. This is what an operator sees today on the Gemma 4 31B plus bf16 assistant pairing, which fails the probe under both kernel selections on every generation 15+ host measured (#1279).- A
--lang-bias-configYAML file with two or more languages now steers deterministically (#1267). Thebias:block deserialized into aHashMap, whose per-instance iteration order became the first-language-wins priority order. Han is shared byja,zhandko, so a config naming two or more CJK languages assigned a different bias to every shared Han token on every run, with no error and no warning, and the shipped schema example is itself a three-CJK config. The block is now collected throughMapAccessinto an orderedVec, so index 0 is the first language written in the file, matching what--lang-biasandLLAMA_ARG_LANG_BIAShave always done. The accepted YAML syntax is unchanged. - A language code repeated inside one YAML
bias:block is rejected (#1267).serde_yamlresolved a repeated key last-wins with no diagnostic, which made the duplicate check unreachable and let the YAML path accept input the flag parser has always rejected. - Four more
HashMapiteration-order defects, all silent (#1266, #1281, #1284, #1288, #1299). RT-DETRv2'sneeds_sanitizedecided on the first key aHashMapwalk produced; the distributed registry's node accessors returned an arbitrary order; the pipeline cache manager's eviction sort and the server's four LRUmin_by_keycalls broke ties by whatever the map yielded, so two runs could preempt different sequences from identical state; and the synthetic model fixtures seeded from an unordered key walk. Each is now ordered by a stable key. The defect class is recorded indocs/code-guidelines.md. - The MTP round-loop diagnostics report the width the loop actually drafted at (#1206, #1208). The log line carried the requested
block_size, so a run overridden to the configured depth was indistinguishable in the record from one that ran at the requested width, which made any width sweep unreadable.effective_block_minandeffective_block_maxare reported alongside the request, and a controller override that the token budget did not force now warns once. - macOS release signing is gated on a Developer ID certificate (#1216). The workflow signed with whatever identity the keychain offered, which is how v0.0.26 and v0.0.27 shipped assets signed with a since-revoked Apple Distribution certificate that Gatekeeper blocks on launch. The job now gates on the certificate type, pins the identifier, asserts the signature after the fact, and fails the build rather than shipping an asset that will be blocked.
- The Metal test gate runs single-threaded, like the CUDA one (#1210), removing the workspace-parallel SIGSEGV that made the nightly red without any code being wrong.
- The test harness pins f32 GEMMs to full precision (#1259, #1260). MLX dispatches f32 matmuls to a TF32-class Neural Accelerator kernel on generation 17 when
MLX_ENABLE_TF32is unset, which defaults on, so 17 algorithmic-equivalence tests (chunked GLA against the sequential recurrence, prefill against the decode chain, absorbed MLA against the decompressed block) failed their full-precision tolerances on M5 hardware. Both lib test binaries now pinMLX_ENABLE_TF32=0at startup unless the environment sets it explicitly. Unit tests assert full-precision equivalence; shipping numerics are decided by the runtime probes. The same commit accepts minijinja 2.24's Python-style boolean rendering, which matches HF transformers.
- The dense KV tail trim is a logical rewind instead of a buffer rewrite (#1209). Every speculative round ended by re-slicing the entire live window to discard a few rejected tail tokens, an O(context) copy paid twice: the re-slice also threw away the step-aligned capacity, forcing the next append through its regrow path. Fp16 and Int8 trims now move only the offset and keep the allocation, the contract
RotatingKVCache::trimhas always had. Measured on M5 Max withqwen2.5-7b-instruct-4bitplus a 0.5B drafter: +0.9% at 1k context, +2.5% at 8k, +5.3% at 24k, with the saved term scaling linearly in context. Output is byte-identical. Turbo modes keep the physical trim, because their packed sidecars feed fused kernels that have not been audited for a stale tail. - The MTP accept hook builds its paired-hidden block with one slice (#1185), replacing a chain of per-position
concatenatecalls, and multi-token drafter forwards route through the shared causal wrapper instead of materializing an[s, s+offset]mask on every call. - The early-exit verify walk was decided by measurement and not built (#1179). The LM head is weight-read-bound, so a sequential walk re-reads the full 566 to 715 MB weight per walked position and costs 1.2 to 1.6x the batched projection at the production widths (K = 3 to 4, acceptance 0.80 to 0.88), against a ceiling of 2 to 4% of the verify forward. Where a wide block would make the ceiling larger, padding the projection past the qmv batch limit into the matrix-matrix kernel collects it more cheaply (2.56 ms against 4.51 at width 8). Recorded in
docs/benchmark_results/mtp-verify-early-exit-decision-m5max-2026-08-22.mdwith the newexamples/mtp_projection_width_benchharness. - The qmv_wide narrow pin's collateral is priced (#1261, #1278). The exactness gate's retry pins the narrow kernel process-wide, so batched decode that never asked for byte-identity pays for it too. Measured on M3 Ultra and recorded in
docs/benchmark_results/qmv-wide-pin-tax-m3ultra-2026-08-22.md; #1289 tracks the order-preserving streamed kernel that would remove the tradeoff.
- h2 to 0.4.16 for RUSTSEC-2026-0258 (#1213).
- The minor-and-patch group across 11 updates (#1162), plus a cargo package refresh.
v0.5.2 - 2026-08-18
- The pinned MLX C++ commit moves to
9a795735(2026-08-17), 168 commits on from2c46b953. Four in-tree overlays were three-way merged against the new base and the rest were left alone, which is the whole reconciliation: upstream touched onlymlx/backend/cuda/matmul.cpp,mlx/backend/cuda/jit_module.cpp,mlx/backend/cuda/reduce/init_reduce.cuandmlx/ops.cpp(the last through 23 commits and 286/151 lines) in that range, and the two Metal overlays (compiled.cpp,kernels/utils.h) are byte-identical across it. Three merged cleanly;matmul.cppconflicted once, where upstream's#3929addedout.size() == 0toGatherMM::eval_gpu's empty-input guard on the same lines our overlay renameda_pre/b_pretoa/b, and the resolution takes upstream's added condition under our names. Every merged overlay's delta against the new base is the same +/- count it had against the old one, so upstream's changes landed and ours survived.mlx/backend/cuda/quantized/is untouched upstream in this range andqmm/qmm.his byte-identical, so the dispatch call sites thequantized.cppoverlay warns about needed no adjustment; its sync comment records that rather than repeating the previous bump's reasoning. Theastypeoverload takingstd::optional<bool> copyis gone (#4207) andlinspacegained a requiredendpointparameter on its primary overload (#4184), neither of which reaches this tree: everyastypecall site uses the two- or three-argument form andlinspaceresolves to the defaulted inline overload. Validated on Apple M5 Max (Metal) only. The three in-tree fused Metal kernel launchers re-validate well inside their RMS < 5e-3 contract (sparse_v_kernel_threshold_zero_matches_graphpasses;delegated_fused_kernel_matches_reference_over_200_stepsat 1.7263e-4;delegated_steel_envelope_matches_cold_only_fused_over_200_stepsat 1.5259e-4), and the isolated failure sets are identical at both pins and deterministic over three runs each:-p mlxcel --libfails the same 13 tests and-p mlxcel-core --libreports 1472 passed / 4 failed. All 17 pass underMLX_ENABLE_TF32=0and are the pre-existing M5 reduced-precision class tracked by #1065, not anything this bump introduced. The four merged overlays are CUDA-only and were not compiled or run here; perCONTRIBUTING.mdthe CUDA backend has no CI test gate at all, somake verify-test-cudaon NVIDIA hardware remains the only gate they get. - The MTP drafter's projections are quantized at load, so a bf16 drafter costs what a 4-bit one costs (#1185). The drafter is read once per drafted token, so its cost is weight traffic:
qwen3.8-27b-mtp-bf16is 810 MiB for one decoder layer plus anfcprojection, and quantizing its eight 2-D projections to the scheme its config declares (affine, group 64, 4-bit) takes that to 228 MiB. Measured on M5 Max againstqwen3.8-27b-4bit, two reps per arm, alternated:draft_blockgoes from 10.40 and 10.71 to 2.70 and 2.69 ms per round, the accept hook from 10.35 and 10.60 to 2.74 and 2.71, and the verify forward is unchanged. Throughput goes from 37.90 and 35.57 to 49.71 and 50.33 tok/s at 120 generated tokens, which is about 1.19x to 1.5x against classic decode. Acceptance does not move: 0.6831 to 0.6601 at 120 tokens and 0.6500 to 0.6589 at 300, disagreeing in sign, which is noise rather than degradation. Output stays byte-identical to classic decode, structurally rather than by tolerance: the target verifies every proposal, so drafter numerics cannot reach the output and the only exposure is acceptance. Done at load rather than as a second checkpoint, so every existing bf16 drafter gets it without being re-downloaded; a tensor whose.scalessibling already exists is left alone and one whose contraction axis is not a multiple of the group size stays dense.MLXCEL_MTP_QUANTIZE_DRAFTER=0keeps the checkpoint's precision, andscripts/tools/quantize_mtp_drafter.pyconverts offline. - MTP engages on Apple GPU generation 15 and later instead of always declining (#1187).
use_qmv_widein MLX sendsM >= 2affine quantized matmuls to a kernel that reduces along K differently from theqmvthatM == 1takes, so a verify block was never byte-equal to the single-token chain there and the #1189 gate, which fails closed, declined every time. The gate now re-probes withqmv_widedisabled and keeps it off when that is what makes the block exact, which is the path generations 13 and 14 already take. Upstream exposes no knob for this, somlx/backend/metal/quantized.cppis overlaid with one;MLXCEL_QMV_WIDE=1pins the faster kernel and declines MTP instead. The switch costs 17 to 20 percent on the verify forward, and measured on M5 Max over four alternating reps the result is 1.04x classic decode against 1.00x for declining and 1.16x for forfeiting the contract.
MLXCEL_METAL4_ATTENTION=0turns off the M5 neural-accelerator fused attention route (#1065). An off-switch only: the hardware test still gates the route, so a stray value on an M1 is inert. Both copies of that hardware test,layers::should_use_metal4_attentionand the inline one inlib.rs's causal-attention dispatch, now go through the shared predicate, because a half-applied switch produces a run that looks like the route is off and is not. Measured on M5 Max acrossllama-3.2-1b-4bit,gemma-3-4b-it-4bitandqwen2.5-7b-instruct-4bit: the route engages on all three (528, 1122 and 924 dispatches over a 32-token generation) and disabling it changes neither the generated text nor throughput beyond noise, so it is a diagnostic rather than a tuning knob.mlx-community/Qwen3.8-27B-4bitis qualified on the existingqwen3_5path (#1163). Qwen3.8 declaresmodel_type: "qwen3_5"/Qwen3_5ForConditionalGenerationand is architecturally identical to Qwen3.5-27B down to a byte-identical weight-map key set, so it already loaded and ran onmainwith zero code changes; that was accidental rather than guaranteed, sinceQwen35Configcarries nodeny_unknown_fieldsand silently dropped three keys the generation added that are load-bearing upstream (output_gate_type,rope_parameters.mrope_interleaved, and the top-levellanguage_model_only). Those three are now read at load time and rejected with a named error when they ask for behavior mlxcel does not implement (output_gate_type: "sigmoid",mrope_interleaved: false,language_model_only: true), across every Qwen3.5-family parse site including the MiniCPM-V 4.6 text backbone, instead of being dropped silently and producing wrong output. Upgrade note:vision_start_token_idis now mandatory for the Qwen3.5-family VLM path; the stale248045default this replaces was wrong for every shipped checkpoint in the family (all use248053), and loading with the wrong id used to mis-segment MRoPE vision spans without failing. Every known checkpoint in the family supplies the key, and the new failure is fail-closed at startup with a message naming the missing key, but a deployment running a checkpoint whoseconfig.jsonhappens to omit it will see a new startup failure on upgrade rather than the previous silent misbehavior.- Qwen 3.5 MTP speculative decoding, driven by the
qwen3_5_mtpdrafter family (mlx-community/Qwen3.8-27B-MTP-bf16and siblings) (#1165). The Qwen 3.5 (dense and MoE, text and VLM) targets now implementMtpTargetalongside Gemma 4, so the sameMtpGeneratorround loop, the tick-cooperative slice path, and the run-to-completion burst all drive this pairing on both the offline CLI and the server. Temperature-0 output is byte-identical to classic decode on this pairing only when the Metal chain-parity gated-delta kernel (gated_delta_step_seqpar,MLXCEL_GDN_CHAIN_PARITY) is taken: the standard kernel carries float32 recurrent state across a verify block and rounds it to the storage dtype once at the end, while the classic single-token decode chain rounds after every token, so without the parity kernel a near-tie argmax can flip about once every 100-250 generated tokens.mtp_capable_targetgates this pairing on both Metal availability and the checkpoint's GDN shape satisfying the parity kernel's contract, declining to classic decode rather than silently forfeiting exactness when either does not hold. Measured verdict (M1 Ultra, Mac Studio): MTP decode is 0.70x classic at the drafter's own configured block size (3) and 0.59x at the historical block-4 default, both below the 1.0x break-even, because the target's multi-token verify forward does not amortize across the block on this GPU generation (about 29 ms per extra block position, roughly two thirds of a full decode step); the adaptive MTP policy (issue #333) profiles and declines this pairing by default on pre-M5 hardware (mtp_b1_default), matching the precedent already recorded for Gemma 4 MTP. M5-class hardware is not measured; that cell is open. Seedocs/benchmark_results/qwen38-mtp-m1ultra-2026-08-16.mdfor the full record, including the DFlash blast-radius A/B (forward_speculativeis shared verify machinery) and the explicitly unmeasured acceptance-under-concurrent-slice-rotation cell. The drafter-configuredblock_size(3) is now the default when--draft-block-sizeis not passed, instead of the flat Gemma-4-derived constant (4); the PR's own measurement found the configured value faster (16.48 vs 13.81 tok/s, 0.591 vs 0.465 acceptance).
-
mlxcel generate --draft-model <dflash-drafter>now fails with a named error instead of a misleadingWeight not found: model.embed_tokens.weight(#1168). A DFlash drafter borrowsembed_tokensandlm_headfrom its target when it binds, so it ships neither, but itsconfig.jsonstill declares an ordinarymodel_type(qwen3, say), so the offline path classified it as a standalone model and drove it through the fullLoadedModelloader, which failed on the first missing tensor with a message that named a tensor rather than the problem. Detection now rejects a directory that is structurally a DFlash drafter (a nesteddflash_configobject and/orarchitectures: ["DFlashDraftModel"]) before dispatch, both for-m/--model(get_model_type, which also covers server startup and the distributed stage loaders) and for offline--draft-model(a new pre-load check inmlxcel generate), and points atmlxcel-server --draft-kind dflashinstead. The discriminator is the checkpoint's own markers, not the resolvedDrafterKind: an ordinary small full model still auto-resolves toDrafterKind::Dflashby default (DEFAULT_DRAFTER_KIND) and keeps loading through the classicSpeculativeGeneratorpath unaffected, confirmed by a non-regression run (qwen3-0.6b-4bitas both target and drafter, acceptance_rate 1.0000, 137.54 tok/s).docs/supported-models.mdanddocs/speculative-acceptance.mdare corrected to match: DFlash's offline entry ismlxcel-serveronly. -
A chat template that refuses a caller-supplied value now fails the request with
400instead of answering200from a stripped prompt, and the OpenAI-standard top-levelreasoning_effortfield reaches the template instead of being dropped (#1164). A refusal raised through Jinja'sraise_exceptionused to be swallowed intorender_simple_fallbackand served as200from a prompt with no chat framing, no system message, and no tool declarations, with only a server-sideWARNrecording it; the request now fails with400carrying the template's own message. The discriminator is type-level rather than a heuristic:raise_exceptionattaches aTemplateRejectionsentinel as theminijinja::Errorsource andtemplate_rejection_messagerecovers it by walking the error chain, becauseErrorKind::InvalidOperationis the same kind minijinja raises for genuine engine problems and matching the message text would depend on wording the template author picks. A template mlxcel genuinely cannot render (an unimplemented filter, a malformed template, a fuel-budget blowout) still degrades to the plain prompt exactly as before, pinned by test rather than asserted. Both fallback sites are covered, the typed path and the raw/multimodal path that any request carrying tool calls, a prior-turnreasoningfield, or typed media parts routes through. The error propagates out ofprepare_chat_request_with_cache, which the chat, Responses, and Anthropic routes already map to a400before generation starts, so the streaming routes return it in place of the SSE stream rather than mid-stream; the disaggregated router front (src/server/router_front.rs) is the one exception and reports it as a500like every other request-preparation error on that surface.reasoning_effortwas not onChatCompletionRequestat all, so serde dropped it before anything could act; it is now resolved through the same three-tier chain asprompt_cache_keyanduser(top-level field, flattened OpenAI-SDKextra_body, nestedextra_body) and mapped onto thereasoning_effortchat-template kwarg behind two guards: an explicitchat_template_kwargs.reasoning_effortwins, and the loaded template must actually mention the name, so a checkpoint that ignores the field does not silently acquire a kwarg. Values are not translated: OpenAI's vocabulary isminimal/low/medium/highand Qwen3.8's isxhigh/medium/low, sohighis valid OpenAI and invalid there whilexhighis the reverse, and remapping would silently set a reasoning budget the caller did not ask for and could not detect, sohighsurfaces the template's400naming the accepted set instead. The mapped value participates in the prompt cache'stemplate_sig, so two requests differing only in effort do not share a bucket, and the next-turn warm-up render resolves its kwargs through the same helper so the warmed vector matches the bucket it is filed under; the prompt-cachepreserve_thinking=truedefault is deliberately kept out of that shared resolver, so no existing deployment'stemplate_sigchanges on upgrade. Measured onmodels/qwen3.8-27b-4bitwithmax_tokens: 1and the message"hi", whereprompt_tokensis the discriminator because the template injects a reasoning-instruction system message whose length is a direct function of the resolved effort:chat_template_kwargsxhighand unset both render 53 prompt tokens,low41, andmedium11, whilehighandHIGHused to answer200from a 7-token stripped prompt and now return400; top-levelreasoning_effort: lowwent from an ignored 200/53 to 200/41 andmediumto 200/11, top-levelhighreturns400, and the streaming path returns the400with no stream opened. The controlmodels/qwen3-0.6b-4bit, whose template never mentions the name, is byte-identical before and after across every case. Upgrade note: a deployment serving a checkpoint whose template validates caller input will see that traffic turn from200with a silently degraded prompt into400. On the four Qwen3.8/Qwen3.5-27B checkpoints in this tree the two highest-traffic new 400s are neither an unsupported role nor an unknown kwarg: asystemmessage at any index other than 0 ('System message must be at the beginning.'), which fires on the common mid-conversation system-reminder injection and on any second system message, and a conversation with nousermessage at all ('No user query found in messages.'), which covers assistant-prefill and system-only requests and also fires when every user turn is<tool_response>-wrapped; both are sticky per conversation once triggered.qwen3.5-27b-4bitcarries eight of the nineraise_exceptionsites in the Qwen3.8 template but not itsreasoning_effortguard, so it gains the role/ordering 400s above without gaining the effort one. That is the defect being fixed, but it is visible as new 4xx on upgrade rather than as a quiet improvement. -
A VLM wrapper no longer re-enables a padded prefill its own text backbone disabled (#1201).
LanguageModel::supports_padded_prefilldefaults totrueand every hybrid and recurrent family overrides it tofalse, because a tile-aligned prefill appends up to 31 pad positions and, while the causal mask and cache trimming undo their effect on the KV caches, a Mamba / GatedDeltaNet / RWKV / DeltaCache state that has already absorbed them cannot be rewound.Qwen35VLModelandMiniCPMV46VLModelboth hold aQwen35Model, which answersfalse, and both forwarded four other capability predicates while leaving this one defaulted. On Neural Accelerator hardware a text-only run through either wrapper therefore padded the prompt and corrupted the backbone's recurrent state, silently: it compiled, it ran, and greedy output changed whenever the prompt length was not already a multiple of 32.qwen3.8-27b-4bitis affected because itsarchitecturesfield routes even a text-only run through the VLM wrapper. A source-level test now fails when a wrapper holds a backbone that refuses padded prefill and does not forward the predicate, and it derives the refusing set by scanningsrc/modelsrather than hard-coding it. -
The server gates tile-aligned prefill on the model, not on the hardware alone (#1201).
execute_full_prefilland both chunked-prefill paths padded to a 32-token tile whenever the host had a neural accelerator, without asking whether the model tolerates it, so every hybrid family served on an M5-class host had its recurrent state corrupted whenever a prompt or chunk was not already tile-aligned. Unlike the wrapper case this needed no wrapper: it reached Mamba, Mamba2, Jamba, RWKV, Nemotron-H, Falcon-H1, Kimi Linear, LFM2, Plamo2, GraniteMoeHybrid and Qwen 3.5 directly. The same scheduler already declined to pad on its boundary-snapshot path and explained why in a comment, and its batched path read the predicate into a local; these three sites simply never asked. Confirmed by A/B on one endpoint: the same/v1/completionsrequest at temperature 0 returns different text before and after the guard. The guard test grows a second case for the second way to lose a guard, a call site that never consults the predicate. -
The MTP exactness probe compares three inputs before reporting equality (#1186). One input is not enough to conclude that two arms took the same kernels: measured at op level, mxfp4 group 32 at 5120 to 5120 moves 1 to 11 bytes of 10240 depending only on the operand draw, while the affine row at the same shape moves about 39%, and in that low-amplitude regime a single input is a coin toss. The probe now short-circuits on the first divergence, so the added cost lands only on the passing case at worker startup.
-
MTP is gated on a measured block-vs-chain exactness probe rather than a static check (#1186). Whether a
T = Kverify block is byte-identical toKsingle-token steps depends on which MLX kernel each quantized projection dispatches atM = KversusM = 1, which varies by GPU generation, quantization mode, operand size and block width, and cannot be predicted from the checkpoint. The gate measures it on the loaded model and fails closed;MLXCEL_MTP_ALLOW_INEXACT=1overrides it loudly. -
mlxcel videouses-fps_modeinstead of the removed-vsync, and the tests no longer hide that (#1183). -
A partial or unpinned Muse Glimmer checkpoint skips its tests instead of panicking (#1172, #1179), and the loading-side pinned guard is gated on
MLXCEL_REQUIRE_PINNED_CHECKPOINTS.
v0.5.1 - 2026-08-15
- A history-boundary snapshot taken during prefill makes multi-turn prompt-cache reuse work on snapshot-only families (#1143). Every model reporting
supports_snapshot_reuse()could reuse a prompt cache only through an exact-prefix match against a stored token vector, and the vector donated at end of generation isprompt + generated, which fails to prefix the next turn for three independent reasons that all live past the history boundary: templates append generation-prompt-only scaffolds, templates drop the<think>block when re-rendering an assistant turn as history, and a sampled token sequence is not the canonical tokenization of its own text. The server now takes a second snapshot during prefill, keyed by the tokenization of theadd_generation_prompt = falserender clipped to the longest common prefix it shares with the live prompt, which is a prefix of every follow-up turn by construction. Onqwen3.5-0.8b-4bit, turn 2 goes from 0 to 150 cached tokens of 189 and turn 3 from 0 to 184 of 214; allama-3.2-1b-instructdense-KV control is identical in every cell. Total tokens forwarded are unchanged; the cost is one extra graph launch plus a model-state copy on the foreground prefill of every qualifying request, soMLXCEL_DISABLE_BOUNDARY_SNAPSHOT=1restores the previous behavior for a deployment serving only single-turn traffic. Greedy output is not bit-exact across the split, for the same near-tie reason--prefill-chunk-sizeand any prompt-cache hit already move it. - The next turn's history prefix is warmed in the background (#1144). The previous assistant reply still landed on the foreground prefill of every turn. After a healthy completion the server now renders the expected history prefix and, when it has nothing else to do, restores the conversation's snapshot and prefills only the delta. On
qwen3.5-0.8b-4bitthis takes turn 2 from 150 to 194 cached tokens of 227 and drops uncached tokens 57%. Warm-ups are dispatched only from the idle tick with an empty active batch, an empty prefill queue, and no parked chunked prefill, verified by counter: 12 requests over 2.6s of continuous load ran 0 warm-ups and skipped 10, and the 2 runs landed after the load stopped. The target vector is the head two probe renders agree on, which is what keeps a warm-up from superseding a working snapshot with one the next turn cannot match. Tool-calling turns are skipped.MLXCEL_DISABLE_CACHE_WARMUP=1turns it off. - A newer snapshot supersedes its own ancestor within a conversation, and the snapshot budget is operator-controlled (#1146). A single 31B-class conversation snapshot runs 300-370 MB against a fixed 512 MiB budget with no override, so the second turn's insert LRU-evicted the first turn's snapshot instead of replacing it and two concurrent conversations evicted each other.
insert_snapshotnow removes every stored snapshot whose token vector is a strict prefix of the incoming one, within the same session and the same model/lora/template/multimodal bucket, and it runs before the new entry's bytes are accounted so an extension can be admitted where its ancestor's bytes were the obstacle. ANonesession key never triggers it and two session keys never touch each other.--prompt-cache-snapshot-capacity-bytes,--prompt-cache-snapshot-max-entries, and--prompt-cache-snapshot-ttlare on both binaries withMLXCEL_*fallbacks, and/v1/cache/statsreportssnapshot_supersedesseparately fromsnapshot_evictions_lruso deterministic in-session replacement is distinguishable from real budget pressure. Snapshot cost tracks model width rather than prompt length, so the budget scales with concurrent conversations, not with turns. The default stays at 512 MiB: deriving it from a per-family state-size formula for the thirteensupports_snapshot_reuse()families is left as follow-up work, because getting it wrong silently mis-sizes the store. - Gemma 4 restores a snapshot at the longest common prefix instead of requiring an exact one (#1145). Exact-prefix matching is stricter than rotating-attention hardware demands: the "cannot truncate" constraint holds only after a sliding layer's ring has wrapped. Following the upstream
mlx-lmcan_trim_prompt_cacheprecedent, a Gemma 4 snapshot that shares a long prefix but diverges before its own end is now adopted at that prefix, with both linearity failure modes excluded (a wrapped ring, and the oversized temporary buffer an over-window prefill leaves for one step). An exact-prefix candidate always wins, a refusal stays the classifiedsnapshot_divergedreject, and every recurrent family is unchanged, which is pinned by a negative control rather than assumed. Validated ongemma-4-e2b-it-4bit, where a diverging turn adopted 66 tokens of a 168-token stored entry. /v1/cache/statsand/metricsclassify a snapshot that diverges from the request it was looked up for (#1147). A candidate in the request's own session bucket that was not a prefix of it returned a bareNone, so a structural multi-turn miss was indistinguishable from an empty store and diagnosing one meant detokenizing by hand. Thesnapshot_divergedreject reason now carries the divergence geometry aslast_reject_context_lenandlast_reject_entry_len, with areason="snapshot_diverged"series on/metrics. A cold store and a foreign session bucket emit nothing.-m/--modelaccepts--revision <REV>ongenerate,run,serve,inspectandmlxcel-server(#1113), matchingmlxcel download --revision. Previously a pinned revision could be fetched but not then run by repo-id: the resolver always resolved againstmain. The flag is honoured only where it can be honoured correctly, which is a deliberate limit rather than an omission. The HuggingFace cache probe is revision-aware and answers normally, and a miss fetches the requested revision. The legacy./models/<name>directory and the mlxcel store are keyed on<owner>/<name>with no revision component, so for a revision-qualified request they are skipped rather than allowed to answer with an unknown revision, and a request whose store directory is already occupied is refused with an explanation instead of being silently answered with whatever is on disk. That last case is not hypothetical: the downloader treats same-named non-zero files as "already present" and skips the fetch, which is also whymlxcel download --revisioncan silently return the wrong revision today. Use--models-dirto give each revision its own root.--revisionalongside an existing local path is an error, since a local directory is used exactly as given. Revision-namespacing the store would lift these restrictions but changes an on-disk layout shared withlist,rmanddownload, so it is left as follow-up work.- Checkpoints that quantize
q_proj/k_proj/v_projat different bit widths now load on every family that shares the fused QKV projection (#1090).mlx_lm'smixed_4_8predicate raises selected tensors to 8 bits while the rest of a model stays at 4, and the loader concatenated the three packed planes along one axis and inferred a single width fromq_proj, so such a layer died inside MLX'sconcatenateinstead of loading. A layer whose planes cannot be concatenated now keeps them separate, each in exactly the layout the checkpoint stored it in: nothing is dequantized and nothing is requantized, so the values are the checkpoint's and the extra memory cost is zero. This reached all 16 families using this loader, including Llama, Mistral, Qwen2/3, Gemma v1 through v4, Cohere2, StarCoder2, InternLM3 and Jamba. Validated on amixed_4_8Llama-3.2-1B checkpoint, which greedy-decodes byte-identically to the uniform 4-bit baseline. The decision is made on the packed shapes, which is whatconcatenateactually constrains, rather than on the reconciled bit width and group size, which can alias two different packings onto one pair.
- LocateAnything no longer dequantizes its mixed-precision attention layers at load (#1090). The per-family workaround added in #1070 turned 18 of the released 3B checkpoint's 36 layers' q/k/v planes into dense bf16, about 190 MB, so the fused projection could concatenate them. Those planes now stay packed and the model loads through the shared path. Helium's pre-flight weight validator no longer rejects a
mixed_4_8attention block either; it stopped comparing the packed width across q/k/v, which scales with the bit width, and still checks each plane's logical input width againsthidden_size.
mlxcel detectreturned boxes that matched no page content (#1089). The cause was the host readback inpredictor, not the coordinate mapping the issue suspected:pixel_valuesis cast to f32 but the first conv against a bf16 weight settles the graph back into bf16, so RT-DETRv2'spred_logitsandpred_boxescome out bf16 for the shipped checkpoints while the readback parsed the raw buffer as 4-byte f32. That fused each adjacent pair of bf16 values into one bogus float and returned half the elements, and the decode then indexedquery * num_labelsandquery * 4into buffers half the advertised size, misaligning every query, label, and box association without erroring. Outputs are now read at their actual dtype. The aspect-ratio dependence the issue reported was a coincidence of which garbage indices survived; the same collapse reproduces on a square page. Three module doc comments that asserted the whole graph ran in f32, which is what let the mismatch sit unnoticed, now describe what actually happens.- An explicit
--kv-cache-budgetthat resolves to zero KV blocks now names what consumed it (#1091). The paged decode v2 workspace reserve is charged to the requested budget before the remainder is divided into blocks (#899), and that reserve is device-derived rather than bounded by what the operator asked for:device_target_ctas()is 512 on every non-Metal host, which puts it at about 16 MiB for the common 8-kv-head, 128-head-dim geometry. A byte budget below that resolves to zero blocks and the pool is left unbounded, which is the intended behavior and is unchanged, but the warning blamed model size and available memory, so an operator whose--kv-cache-budget 8MiBsilently did nothing had no way to see why. The warning now reports the requested budget, the reserve, and the smallest budget that would mint one block.--kv-cache-budget auto, the default, keeps its previous message, since reaching zero there really does mean the model leaves no room for KV and pointing at a 16 MiB reserve would send the operator after the wrong knob. No budget resolves to a different block count than before. - Server token streams change at
temperature <= 0when DRY is enabled with sequence breakers. A per-requestdry_sequence_breakersvalue was dropped by the greedy branch ofbuild_sampling_configand replaced with an empty vector (#1102). DRY is not gated on temperature, and the breakers are the backward match's termination condition, so the match ran past the intended boundary and the penalty came out at or above what the request asked for. Output for those requests changes, toward the requested configuration. Requests that leavedry_multiplierat its0.0default, or that set no breakers, are byte-identical to before. The CLI is unaffected: it has no way to set breakers (#1108). --dry-sequence-breakerreaches the sampler instead of being parsed and discarded (#1103). The value flowed from the CLI intoServerStartupConfigand stopped there: there was no tokenization step, no server-side default forbuild_server_generate_optionsto fall back on, and no/propsentry, so an operator got a clean startup and DRY matching that ran straight through the breakers they configured, with nothing to reveal the flag was inert. The failure was silent in both directions, since running DRY without breakers makes the penalty stronger than configured rather than weaker. Breaker strings are now tokenized at startup and used as the default for any request that does not send its owndry_sequence_breakers;/propsreports the resolved token IDs. Two behavior changes for existing deployments that already pass the flag: generation output changes for requests that omit the field (the flag now does what it says), and a breaker that does not encode to exactly one token for the loaded model fails startup with a message naming it, rather than being dropped. The escapes\n,\t,\rand\\are interpreted, so the"\n"in the flag's own help text works as written, and the tokenizer's prepend normalizer is discounted, so a breaker resolves to the token the operator meant on SentencePiece-derived checkpoints (Mixtral, Phi-3, MiniCPM, LLaVA and others) rather than failing startup or silently resolving a neighbouring token.- Four server flag spellings that worked on only one of the two binaries now work on both (#1109).
mlxcel serveaccepts--paralleland--predict;mlxcel-serveraccepts--n-paralleland--adapter. The parallel-slots pair was the worst case:--n-parallelworked only onmlxcel serveand--parallelonly onmlxcel-server, so a command line copied between them failed to parse even though both flags read the sameLLAMA_ARG_N_PARALLELenvironment variable. The DRY sequence breakers now use the llama-server spelling--dry-sequence-breakeras the primary name on both binaries; the plural--dry-sequence-breakersthatmlxcel servepreviously required is kept as an alias on both, so no command line that worked before stops working. - The "not a model" error names all three forms
-m/--modelaccepts, not two (#1114). It described a local path and anowner/namerepo-id, omitting the bare name that resolves against$MLXCEL_DEFAULT_ORG(defaultmlx-community), which the README puts in the quick start. That omission was worst exactly where the error fires: a bare name containing a character outside[A-Za-z0-9._-]is what falls through to it, so a user who typedmlxcel run "Qwen3 4B"was told to type a fullowner/name, more work than fixing the typo and keeping the bare name. The message now names all three forms and states the character class, matching the siblingMLXCEL_DEFAULT_ORGerror on the same path, and keeps themlx-community/Qwen3-4B-4bitexample.
v0.5.0 - 2026-08-12
- Meta Muse Glimmer (
muse_glimmer) vision-language support (#1101). The 52-layer mixed-cache decoder and the 50-layer vision and fusion path drive single-image and multi-image generation through both the CLI and the continuous-batching server, with expanded prompt accounting and fail-closed unsupported modes. The pinned Muse chat template, reasoning strengths, and bounded ATEM parsing and replay are wired across the Chat Completions, Responses, and Anthropic-compatible routes, including streaming cleanup, so reasoning is routed toreasoning_contentrather than leaking into the answer. Qualified on a real NVIDIA GB10 against the pinned bf16 checkpoint: coherent text, grounded single-image and multi-image output, a 2204-token prompt crossing the 2048-token sliding-window boundary, ATEM tool call and result replay, and scheduler image concurrency at--parallel 1and--parallel 2. mlx-community/Muse-Glimmer-30B-4bitloads through the same path (#1116), preserving the canonical bf16 path and the dense vision tower. mlx-vlm weight roots are normalized, the root quantization contract is inherited into the text configuration, and text plus vision-fusion projections load through quantization-aware layers. Warm text qualification on GB10 reached 12.43 prefill and 13.34 decode tok/s, about 3.1x the recorded 4.25 tok/s bf16 baseline on the same box, and the image path inserted 64 vision tokens and decoded at 13.15 tok/s. Checkpoint-format support is fail-closed to the pinned affine-Q4 layout: conflicting root and nested quantization contracts, unsupported modes, invalid group-size and bit combinations, alias collisions, orphan sidecars, missing affine biases, global-scale sidecars, and quantized vision-tower tensors are all rejected before kernel selection, identically on the VLM and text-only paths. One-shot CLI generation now reuses the server ATEM channel parser, soto=selfreasoning and structural tokens stay hidden by default whileto=usercontent remains visible.- Florence-2 (
florence2) encoder-decoder vision-language support, landed across the five sub-issues of epic #850: the reusable BART seq2seq engine and text core (#1060), the DaViT vision backbone (#1063), vision-language fusion and full weight loading (#1064), the processor with the fifteen task markers and<loc_N>coordinate parsing (#1069), and CLI integration with real-checkpoint validation (#1071). Task answers parse back into boxes, quadrilaterals, and polygons in original-image pixels. Nine upstream defects are reproduced deliberately and pinned by tests rather than silently corrected, including thepost_process_generationdocstring that documentsimage_sizeas height by width where every call site unpacks it as width by height. - Quantized Florence-2 checkpoints load at 3, 4, 6, and 8 bits, both
base-ftandlarge-ft(#1082). The port is pinned against upstream mlx-vlm running the same 4-bit checkpoint rather than against bf16, because comparing against bf16 can only measure lossiness: relative RMS agrees to 5.8e-4 and cosine to 2.6e-4 at every stage, and the greedy ids match exactly.image_projection, the cosine temporal buffer, the conv stack, and the normalization weights are consumed dense, and a checkpoint that packs one of them is still refused at load with the offending tensor named. mlxcel-serverserves Florence-2 through a dedicated single-stream seq2seq worker (#1083), replacing the startup refusal.message.contentis byte-identical to the CLI answer and the parsed coordinates arrive as JSON in themessage.florence2_resultextension field. Task-prompt input is validated at the request boundary for all seven input-taking modes, and images decode under the configuredImageInputLimits. Serving is one request at a time; no concurrent-throughput property is claimed.- LocateAnything (
locateanything) grounding VLM support (#1070): a MoonViT tower reused from Kimi-VL, an MLP connector, and a Qwen2 decoder that emits<ref>/<box>markers and the 1001 coordinate tokens through plain autoregressive decode. Two real-checkpoint gaps are handled that the Python reference does not show: the released conversion ismixed_4_8, which the shared fused-QKV loader cannot concatenate, so affected layers are dequantized rather than requantized onto a grid they do not land on; and the checkpoint ships notokenizer.json, so a Qwen2 slow-tokenizer reconstruction is added as a last resort, gated ontokenizer_class. - Falcon-OCR (
falcon_ocr) early-fusion document OCR support (#1075), the first family in the tree with no vision tower at all: each 16x16 RGB patch is flattened through one linear projector straight into the token stream, and a single 22-layer decoder reads image and text together under a mask that is bidirectional inside every image block and causal everywhere else.mlxcel generate --layout-detections <FILE>runs the layout-aware second stage over detections in the shapemlxcel detect --format jsonprints. Layout detection itself is not included; the boxes are an input. - Jina VLM (
jvlm) support (#1076): a 27-layer SigLIP-so400m-class tower feeding a Molmo-style connector into a Qwen2-class decoder on an OLMo tensor layout. The MLX conversion drops the checkpoint's chat template, and under the generic fallback the model answers17when asked for the capital of France, so a built-in template is consulted after every declared source comes up empty. make bump-version VERSION=x.y.zrewrites every version-tracking manifest and syncsCargo.lockin one step, andmake verify-versionsderives the member list from[workspace] membersat runtime instead of from prose, so it cannot go stale (#1056). Adding a sixth member fails the check until someone records whether it tracks the root version.make verify-kernel-dtype-keysand a CI job require everytemplate_argsinitialiser in a file that also contains acuda_kernel(call to name at least one input dtype (#1059). There is deliberately no allowlist: Metal-only launchers are excluded by the absence of that call, so adding a CUDA port to one brings it under the check automatically.- Ant Group Ling / Bailing MoE (
bailing_moe) text model support (#838). - Apple OpenELM (
openelm) text model support (#839). - TeleAI TeleChat3 (
telechat3) text model support (#841). - Databricks DBRX (
dbrx) text model support (#835). - Ling / Ring linear-attention MoE (
bailing_moe_linear) text model support (#840). - Phixtral (
phi-msftsparse MoE) text model support (#844). - Arcee AFMoE (
afmoe) text model support (#845). - Kuaishou Klear (
klear) text model support (#846). - Fused paged-attention decode v2, with a CSR page table, a cross-CTA split-KV partial kernel, and a variable-length merge kernel (#898). v1 splits the KV inside one threadgroup, so one CTA serves one
(batch, query head)pair and a long context adds no parallelism; v2 splits across CTAs, so parallelism isnum_chunks * kv_heads * q_groupsand grows with context. The CUDA bodies are structural transliterations of the Metal ones and have never been compiled or run. - Batched paged decode is routed through the v2 kernel in the server (#899). On M1 Ultra the switch is gated by measured floors rather than applied unconditionally: batch 1 loses at 1024 tokens of context (0.91x) and wins at 4096 (1.08x median), so single-request launches need 4096 visible tokens, while batch 4 and batch 8 win at 1024 tokens per request (1.41x and 1.47x), so batched launches need 512 per request.
MLXCEL_PAGED_V2_MIN_KV_TOKENSandMLXCEL_PAGED_V2_MIN_KV_TOKENS_PER_REQUESTmove the floors;MLXCEL_PAGED_ATTENTION_NATIVE=0pins the previous gather path. - Fused sparse-attention decode by page indirection over the v2 kernel (#904). A sparse selection becomes a
page_size = 1page table, so no gathered copy is materialized. MiniMax-M3 block-sparse decode is routed onto it above a sparsity gate (default 8x,MLXCEL_SPARSE_PAGED_MIN_SPARSITY), which is where the measurement turns: at MiniMax-M3 geometry the fused path runs 0.67x at 2x sparsity and 0.77x at 4x, then 1.22x at 8x, 1.17x at 16x, and 2.06x at 32x. DeepSeek Sparse Attention is not routed onto it; the reason and the kernel generalization it needs are written up indocs/sparse-paged-decode.md. - Cascade (shared-prefix) decode: a whole-page prompt prefix shared by several sequences in a decode batch is attended once for the subgroup and merged into each member's suffix state instead of being re-read per sequence (#903). Default off (
MLXCEL_CASCADE_ATTENTION); correctness is verified, throughput is not yet measured. - Fused residual-add RMSNorm and fused RoPE + KV-append decode kernels, both default on with
MLXCEL_FUSED_ADD_RMSNORM=0andMLXCEL_FUSED_ROPE_APPEND=0as kill switches (#905). - Shape-bucketed kernel autotuner (
mlxcel_core::autotune) and a cold-last-level-cache microbenchmark harness (mlxcel_core::bench_rotation) (#906). The v1 paged-decodeNumSplitslaunch shape is the first consumer, validated on Apple Silicon; the two CUDA knobs are wired but unvalidated. - Matrix-absorbed MLA decode over a compressed-latent KV cache (
mlxcel_core::mla), wired todeepseek_v2behindMLXCEL_MLA_ABSORBED(default off) (#907). - Sorting-free top-p sampling by dual-pivot rejection kernels on Metal and CUDA (#901). Where top-p is active, the fused sampler now replaces the
argsort+cumsumnucleus filter and the trailingrandom::categoricalwith one softmax and one custom kernel that resolves the filtered support by rejection sampling on a shrinking probability interval, with no sort anywhere. Measured 1.28x to 2.35x on M1 Ultra across vocab {32K, 64K, 152K} and batch {1, 4, 8}. One launch covers the whole batch, and the kernel takes per-row{top_k, top_p, min_p}so rows with different values need no second launch. Routing is deliberately restricted to what measured faster: top-k alone, min-p alone and top-k with min-p keep the stock chain (the kernel measured 0.31x to 0.97x there, because those configurations make the chain run no sort), and top-k with top-p routes only up to vocab 32768 (1.27x to 1.64x at 32K, 0.71x to 0.83x at 152K). Declines are announced once at INFO with the numbers behind them. The routed path evaluates nothing, so it does not collapse the software pipeline that both decode drivers run; the kernel's convergence flags are checked without waiting, on a later call.MLXCEL_SAMPLING_REJECTION=0restores the previous chain everywhere. - Sampling dispatch outcomes are announced at INFO, once per distinct outcome kind (#901). Every
fused_samplebranch (greedy argmax, the #900 Gumbel-max kernel, the #901 rejection kernel, the kill switch, an unsupported backend, and the convergence-cap fallback) records what it did and why, so which sampling path a run took can be read off a normal server log instead of inferred from a benchmark. Cap-overflow events are also a reachable counter,mlxcel_core::rejection_cap_overflow_rows(). - Softmax-free Gumbel-max categorical sampling kernel on Metal and CUDA (#900). On the no-filter stochastic sampling path (
temperature > 0with no top-k, top-p, or min-p) the fused sampler now adds i.i.d. Gumbel noise tologits / temperatureand takes the argmax, which draws exactly from the same softmax categorical distribution without the normalization pass over the 32K-152K-entry vocabulary. One launch covers the whole batch.MLXCEL_SAMPLING_GUMBEL=0restores the previousrandom::categoricalpath. - Opt-in acceptance-optimal rejection sampling for the classic speculative path, with the proof, regression guards, and closed-form acceptance diagnostic (
MLXCEL_SPECULATIVE_STOCHASTIC_ACCEPT=1, default off) (#902). MLXCEL_EXTRA_CA_CERTSaccepts a path to a PEM bundle, added to the default TLS trust roots of both HTTP clients, so mlxcel can fetch checkpoints from behind a TLS-inspecting corporate proxy (#912).MLXCEL_MIXED_STEPprototype and ADR 0005, which records why model-level ragged mixing and kernel-level mixing on MLX were rejected (#908). The issue's premise was inverted: tick alternation never stalls decode, because a chunked prefill does not interleave at all, it waits for the batch to drain.- The
modelsize/modeltype/archlabel taxonomy is documented (#1038).
- Chunked GLA prefill is the
bailing_moe_lineardefault (#1062). #1039 shipped both evaluations of the recurrence and left the promotion to a measurement; the measurement lands the opposite way from what the opt-in assumed. Onmlx-community/Ring-mini-linear-2.0-4bitand an M1 Ultra, chunked wins perplexity in every configuration measured, by 1.50% at a 128-token window rising to 13.75% at 512 across 32 windows, because the intra-chunk sum lands in a matmul accumulator instead of compounding in a bf16 running state. Prefill is 2.06x to 2.39x faster from about 512 to about 8192 prompt tokens, and decode is unchanged by construction. The cost is real and is stated rather than buried: a checkpoint decoded here no longer matches mlx-lm token for token, since upstream implements the sequential path.MLXCEL_BAILING_LINEAR_CHUNKED_PREFILL=0restores it. - The pinned Rust toolchain moves from 1.93.1 (2026-02-11) to 1.97.1 (2026-07-14), and the
dtolnay/rust-toolchaintag inci.ymltracks it as the comment there requires (#1066). The workflows that install@stableare unaffected and were never building at a different version: that action runsrustup defaultand never exportsRUSTUP_TOOLCHAIN, sorust-toolchain.tomloverrode it per directory and every cargo invocation in the tree already resolved to the pin.cargo fmtproduces no diff at the new version, so the bump reformats nothing, but six new clippy lints fire under-D warningsand are fixed here:question_markinmemory_estimate.rsandsanitize.rs,collapsible_matchinchat_request.rs,for_kv_mapandunnecessary_castin two test modules, andunneeded_wildcard_patterninpipeline_remote_real_models.rs. All six are mechanical and behavior-preserving. - The Debian and Launchpad packaging tree is removed (#1095). It was unshippable, and release preparation carried it as a dead path.
- Fused decode-MoE byte-identity is documented as checkpoint-dependent rather than as a general property, which it never was (#1061). Measured on an M1 Ultra over 96 calls per checkpoint, the fused kernel sits at 1.65e-3 normalized RMS from an all-f32 ground truth while the
gather_qmmfallback sits at 1.02e-2, so the fused kernel is closer to truth in 96 of 96 calls on bothKlear-46B-A2.5B-Instruct4-bit andqwen3-30b-a3b4-bit, median 6.2x. The disagreement between the two paths is mostly the reference's own distance from truth.docs/adding-models.mdgains the guidance to setMLXCEL_FUSED_MOE=0when reference-diffing a MoE port, since the kernel engages only at decode and makes prefill look exact. - Florence-2
large-ftdegenerate output is recorded as an upstream checkpoint and conversion-family issue with a tracker link (Blaizzy/mlx-vlm#1840), not an mlxcel loader defect (#1097). The same run of BOS reproduces in mlx-vlm at both bf16 and 4-bit;base-ftstays the documented working baseline. TECHNICAL_REPORTS/opts into git tracking through the.keep-reportsmarker the report workflow already defines (#1080), after the reports for the #1053 / #1054 / #1045 / #1040 chain (#1077) and the #847 / #848 / #849 VLM port chain (#1081) landed force-added by hand. The sixty-nine reports predating that are deliberately left untracked.- Top-p sampling streams differ at equal seeds, and top-k + top-p changes support slightly. The rejection sampler (#901) consumes the shared MLX random key sequence differently from
random::categorical, so a fixed seed no longer reproduces a token stream recorded before this release on the paths it is routed to (top-p active). Configurations that stay on the stock chain are bit-identical to before. The truncated support is unchanged for top-k alone, top-p alone, min-p alone, top-k+min-p and top-p+min-p (support equality against theargpartitionmask including ties is a committed test), and frequencies inside the support pass chi-square against the renormalised truncated distribution. When top-k AND top-p are both active the support does change: the stock chain renormalises over the top-k set before applying top-p, so its mass target istop_p * Z_kwithZ_kthe top-k mass, while the kernel applies both tests to the untruncated distribution and therefore keeps a superset. Greedy decoding (temperature == 0ortop_k == 1) is byte-identical. To reproduce a stream recorded before this release, or to restore the renormalised top-k+top-p semantics exactly, setMLXCEL_SAMPLING_REJECTION=0. - Sampled token streams differ at equal seeds. The Gumbel-max sampler (#900) consumes the shared MLX random key sequence differently from
random::categorical, so a fixed seed no longer reproduces a token stream recorded before this release on the no-filter stochastic path. The sampling distribution is unchanged (verified by chi-square goodness of fit against exact softmax probabilities on peaked, flat, bimodal, and-inf-masked logits at temperatures 0.5, 1.0, and 1.5), and greedy decoding (temperature == 0) is byte-identical. Runs are still fully reproducible going forward: the same seed on the same backend gives the same stream. To reproduce a stream recorded before this release, setMLXCEL_SAMPLING_GUMBEL=0. - The pinned MLX C++ commit moves to
2c46b953, and three in-tree overlays are re-derived against it (#1042). - The MLX pin has one source of truth. It was duplicated across
CMakeLists.txt,build.rs, andrelease.yml, where a mismatch produced stale build artifacts and CI breakage; the three now read one value (#1047). - Loop detection is less trigger-happy.
LOOP_DETECTION_RECOMMENDEDmoves frommin_count = 4to12, and the Gemma 4 family default-on is narrowed to the requests where the collapse actually happens (#967). At the old threshold a markdown table alignment row (| :--- | :--- | :--- |) tripped the detector at its fourth column, so a correct answer stopped mid-table and the client saw a normal stop with no error. - The paged block-pool slab size is a per-pool value instead of a constant 32. The server sizes it from the configuration it already reserved KV memory for, clamped by the per-layer share of the paged block budget;
MLXCEL_PAGED_SLAB_BLOCKS=0pins the old default (#899). - The quality gate covers all workspace members (#1007). The workspace root is itself the
mlxcelpackage, so the previous barecargo testandcargo clippyresolved to-p mlxceland never builtmlxcel-core,mlxcel-surgery, ormlxcel-xla: 1754 tests, 1354 of themmlxcel-core's, plus the whole test-target lint surface of those members.make verify-testandmake verify-clippynow pass--workspace. - Tests build under a new
[profile.test-fast]rather than[profile.release](#1000). It inherits from release and keepsopt-level = 3, so the optimized MLX numerics the suite depends on are unchanged, but drops fat LTO andcodegen-units = 1, which were making the nightly spend its entire 180-minute budget in codegen and linking without ever starting a test.release.ymlstill builds and links under[profile.release]. An unfinished nightly run is also reported instead of silently skipped: a timeout was recorded ascancelled, which skipped the "Report a red main" step. - The CUDA merge gate is runnable and serialized (#1048).
- The
rust-toolchainpin is restored and excluded from dependabot bumps (#952). - Dependency updates:
dtolnay/rust-toolchain1.93.1 to 1.100.0,actions/setup-python6 to 7, and minor and patch bumps across 10 packages (#936).
- CUDA JIT launches read their buffers through the wrong pointer type when one process ran a kernel at two input dtypes. MLX's Metal backend folds input dtypes into the JIT cache key; its CUDA backend names a kernel
"custom_kernel_" + name + template_arguments_hash(template_args)and stops there, while generating every buffer parameter's type from the runtime dtype. A launch whose template args are all ints therefore hashed to one name for every dtype, so the first dtype to compile won for the life of the process and every later call returned numbers unrelated to its inputs, with nothing thrown. Fixed for the paged decode v2 partial and merge kernels (#1058) and then for the rest of the class: the v1 paged attention kernel,sampling.cpp, andsampling_rejection.cpp(#1059). The sampler was affected in production and not only in tests, since it admits f32, f16, and bf16 logits and its launch shape depends only on batch and vocabulary. Metal was never affected. mlx::core::dequantizereturned wrong values whenever itsbiasesinput was not row-contiguous, so the FFI shim now forces contiguity (#1078). MLX bindswandscaleson the shared compute encoder and only then callsensure_row_contiguousonbiases; for a strided input that enqueues a copy kernel on the same encoder, which rebinds the first two buffers to its own operands, and the dequantize dispatch that follows decodes the raw bias bytes as quantized weights. The window opened with the MLX pin bump in #1046 and was two days wide.gemma4_unified,mistral4, andqwen3_5_moeall hand strided quantized triplets downstream; the guard sits at the boundary, so it covers every caller.- Two Molmo v1 seams that corrupt image-conditioned generation (#1099). A flat
Molmo-7B-D-0924config with norope_implnow defaults to the checkpoint's effective LLaMA rotate-half layout instead of MLX traditional RoPE, and image preprocessing pads raw black before normalization and preserves fractional coverage inimage_masksinstead of thresholding them to booleans. The pre-fix build reproduced the reported garbage byte for byte on a 640x480 COCO image; the fixed build describes the scene correctly through both the CLI and the server. - Queue admission, image cardinality, and usage accounting on the dedicated single-stream workers (#1098). DiffusionGemma, LLaDA-2 MoE, and Florence-2 now take atomic RAII queue reservations before streaming SSE opens, on the Chat, Responses, Anthropic,
/v1/completions, and/completionroutes alike, so a request losing the final race returns HTTP 503 at the route boundary instead of mid-stream. A declared image count that disagrees with the resolved one is rejected during shared request preparation with a named error, and the rejection provably does not poison the worker's next request. Florence-2 usage reports the actual fused BART encoder length rather than a stand-in. - The Gemma 4 server reasoning splitter consumes the full
<|channel>thoughtopener, soreasoning_contentno longer starts with a leakedthoughtchannel argument (#1094). The visible answer path is unchanged. mllama_parity::sub_max_real_tiles_keep_the_legacy_real_rows_byte_identicalno longer demands exact f32 equality (#1065). Selecting 1 real tile of 4 makes the vision encoder reduce over a different extent than the all-tiles path, and f32 addition is not associative, so the equivalence that holds in exact arithmetic never implied bitwise equality. On Apple M5 Max the difference is 5.9604645e-8 (2^-24) against a largest output element of 1.1521907, which is 0.5 ULP; a real row-selection error would surface seven orders larger. The assertion moves to a named 1e-6 bound. This is the same defect #953 fixed for the sibling assertion insrc/models/mllama/text.rs, which missed this file. The two chunked-SDPA assertions inlayers.rsnow also report the measured divergence instead of only naming the chunk size.- Two test suites no longer flake on a loaded runner. The autotune profiler ordering tests run on an internal deterministic timer seam instead of host
thread::sleepaccuracy, leaving production profiling on realInstanttiming (#1096), and thetp_e2escaling-efficiency assertions are recomputed from the throughputs the analysis recorded and bounded by an analytical ceiling rather than by a hand-picked 1.1 (#1057). - Four model families ran a fully bidirectional prefill. Generation calls
forwardwithmask == Noneand expects the model to build its own causal mask;deepseek_v2(#991) and theninternlm3,hunyuan, andgemma2(#999) passedNonethrough to attention instead, so every prompt token attended to every later one. Output stayed fluent, which is why it survived: a short prompt cannot expose it.gemma2was the mitigated case (its sliding window bounded the leak); PaliGemma 2 was checked and is unaffected. - Speculative decoding lost about half its throughput to a draft KV cache rewind arithmetic error (#994). Aggregate decode goes from 41.8 tok/s and a 3.09x run-to-run spread to a 1.06x spread.
- Quantized Jamba MoE experts are built through the shared
SwitchGLU(#974). The family-local expert construction was broken for every Jamba MoE checkpoint, dense ones included, not just quantized ones. The router moves toUnifiedLinearso a checkpoint that ships 8-bit routers under a 4-bit default loads. deepseek::ModelArgsdropped the nestedquantizationblock fromconfig.json, so the values reaching the expert loaders were not the ones the checkpoint declared (#975).QuantizedMultiLinearinfers its quantization mode instead of assuming affine, and validates the bias plane (#1028).- The declared
quantization.modestring is bounded by an allowlist at load. An unparseable mode reached MLX verbatim and a block-float one was silently reinterpreted as affine, both aborting at the first forward instead of failing at load (#973). - Quantization parameters are bounded in the family-local MoE expert loaders (#958) and rejected at load when MLX would abort on them (#929).
- The MLA
kv_b_projsanitizers refuse a scales-without-biases plane, in wording standardized across all five implementations (#1026), andmamba/mamba2treat a scales-without-biases embedding as non-quantized rather than misreading it (#976). - GLM4 MoE Lite loads its own layout:
kv_b_projis decomposed into the per-headembed_q/unembed_outpair (#1029). rope_traditionalfromconfig.jsonis honored on the shared Llama path (#931).topk_groupis bounds-checked insidegroup_mask_scores(#947).- KV-cache estimation recognizes the
n_layer/n_embdandmulti_queryconfig spellings (#927). - LoRA adapter weight fusion is Conv1D-layout aware (#925).
- Per-expert tensors are stacked from borrows instead of copies, removing one
Copygraph node per per-expert tensor: 5376 of them forling-lite-1.5(#948). - A parked chunked prefill's wait is bounded by a fairness grant, cutting worst-case admission latency from 50.5s to 23.8s at the default
--prefill-grant-intervalof 16, at 1.60x mean stream ITL during admission (#1011). - Grammar-only requests are excluded from loop detection (#977), and generation stops when the structured matcher completes (#978).
- Disaggregated router chat guards are enforced (#979), and client token budgets are clamped (#980).
- CUDA: the broken RMSNorm overlay is dropped and DeepSeek-V2 graph capture is restored (#831). The upstream MLX CUDA small-axis attribution is corrected in the docs (#830), and the MLX CUDA graph-cache lifetime-miss abort is reported upstream (#821).
- CUDA: the fused RoPE kernel calls libdevice
cosf/sinfinstead of the inaccurate variants (#1049). - XLA: the terminating EOS token is no longer emitted as output (#963), the Qwen2-VL prefill export is gated on the feature that calls it (PR #961), and the Qwen2-VL XLA loader contract is pinned with all three outcomes documented (#966).
- Test-harness fixes: a second
mlxcel-coretest binary sharing the GPU now fails loudly instead of aborting the run (#1008), the fused MoE GeGLU parity bound is derived from the reference's measured jitter (#964), the mllama ragged test tolerates f32 reassociation and gained a nightly gate (#939), the autotune rep-scaling test no longer pins an exact ceiling (PR #990), a dead-callsite race behind the drafter log assertions is removed (#1023), and the test harness honorsCARGO_TARGET_DIR(PR #996). - Homebrew tap formula stanzas are edited by name instead of line position (PR #949).
- The dead
AGENTS.mdlinks inCONTRIBUTING.mdare replaced (PR #1014).
v0.4.3 - 2026-07-27
- GPT-2 text model support (#924).
- GPT-BigCode text model support (#926).
- GPT-NeoX text model support (#928).
- Kyutai Helium text model support (#930).
- Gemma3n text runtime and dense PLE prefill on the OpenXLA backend (#892).
- Gemma3n audio reference path (#883).
- Token-exact Phi4MM audio reference path (#887), and Phi4MM audio with per-slot adapters on OpenXLA (#914).
- LLaVA vision execution through IREE (#913), with an end-to-end reference-architecture test (#897).
- Qwen2-VL OpenXLA vision path (#915).
- Sparse DeepStack prefill on OpenXLA (#893).
- Multimodal RoPE position state on OpenXLA (#894).
- Image requests admitted in the CLI and continuous-batch serving (#895).
- Bounded audio preprocessing and serving plumbing on OpenXLA (#896).
- Prefill embeddings entry point (#879), and IREE sessions seeded from prepared embeddings (#888).
- Parameterized static context capacity on OpenXLA (#880).
--show-reasoningCLI flag to print the reasoning channel that is otherwise hidden (#889).- OpenXLA operator numeric contracts and a bounded numeric-oracle probe harness covering dense matmul, affine Q4 dequant, prefix scan, and core ops (#934, #937, #938, #940, #941, #942, #943, #944).
- Intra-MoE phase profiling for nemotron-h (#832).
- The Gemma 4 reasoning channel (
<|channel>thought ... <channel|>) is hidden from CLI output by default. Pass--show-reasoningto print it. Serverreasoning_contentbehavior is unchanged (#889). - The IREE compiler and runtime are pinned to 3.12.0rc20260721 on both CUDA and macOS, fetched from the official GitHub release and verified by sha256 (#882).
- VLM host prefill preprocessing was refactored to owned buffers (#881).
- OpenXLA diagnostic local-task thread count is bounded (#945).
- Dependency updates: minor and patch bumps across 11 packages (#828).
- 4-bit CUDA decode was non-deterministic at temperature 0 and could emit a stray token. The
qmm_sm80quantized-matmul kernel reused shared memory still being written by in-flightcp.asynccopies; it now drains outstanding copies before the epilogue store, so greedy decode is byte-identical run to run on every 4-bit model (#910). - The fused decode-MoE kernel rounded each of the K per-expert partials to bf16 before summing, which corrupted long multi-turn Gemma 4 output. Partials are now accumulated in f32 and rounded once (#886).
- Gemma 4 chunked-prefill continuation dropped the sliding-window attention mask when the caller mask was shorter than the returned keys, collapsing output to reserved
<unused>tokens under concurrent load. The mask is now sized to the keys each attention family returns (#891). - The Gemma 4 reasoning channel and its chain-of-thought no longer leak into CLI output as raw tokens (#889).
v0.4.2 - 2026-07-20
- MiniMax-M3 text model: a hybrid dense/MoE architecture with block-sparse attention (#799).
- MiniMax-M3-VL multimodal support (#800).
- Unlimited-OCR: long-document OCR on the DeepSeek-OCR stack with a per-layer ring sliding decode cache that keeps the full prefill and rotates only the most recent decode window (#801).
- XTC (Exclude Top Choices) sampling, read and applied end to end on the OpenAI-compatible chat, completions, and responses routes, with range validation (#802).
thinking_modechat-template kwarg is injected when the template references the identifier and thinking is enabled (#811).- Per-reason prompt-cache reject metrics and an APC trace log (#810).
- Monotonic OOM backstop and a persistent OOM record for decode benchmark sweeps (#808).
- DeepSeek-V2-Lite generated repeated tokens on GB10 CUDA. An upstream MLX 0.32.1 RMSNorm kernel regression is overlaid with its last-good version, and CUDA graph capture is disabled for the DeepSeek-V2 family (as it already is for Gemma 4), so output is coherent again (#829).
- Gemma 4 audio: the CLI rendered the
<|audio|>placeholder before the prompt text, which flipped the 12B unified model from transcription into answering the perceived content on acoustically hard clips. The placeholder now follows the prompt text (#798). - Requests with no effective input (empty or whitespace-only prompts and messages) were dispatched to the model; they now return a 400 before dispatch on
/v1/chat/completions,/v1/completions, and/v1/messages(#803, #813, #814). - Long-lived speculative serving on CUDA hit a fatal MLX "Cache thrashing" abort once the graph cache filled. CUDA builds raise the
MLX_CUDA_GRAPH_CACHE_SIZEdefault so the abort no longer fires (#818). - An MLX evaluation throw in the batch decode loop aborted the whole worker and dropped every in-flight request; it now fails the affected request instead (#825).
- An empty paged-state length list from a not-yet-populated sequence returned an Err that spammed a "Failed to sync paged state" warning; it is treated as a benign no-op (#826).
- The speculative slice slot rotates across waiting requests instead of always going to the window head (#816).
- Effective-input text check is allocation-free (#815).
- Added a fast test profile to cut edit-test iteration time (#812).
v0.4.1 - 2026-07-16
- Server tool-call parsers for more model families: Kimi K2 sectioned format (#783), the pythonic
[func(arg=value)]format (#786), function-calling Gemmastart_function_call(#791), MiniMax-M3 namespaced XML (#793), and GLM-4.7 and LongCatarg_key/arg_valuegrammar (#794). - Step-3 (
step3p7) model port (#781). - Command MoE (Cohere2 MoE) model port (#761).
- Kimi-VL 3D MoonViT video support: image and video patch embedding (#762).
- Gemma 4 per-request image soft-token budget. Set it with
--image-soft-tokenson the CLI or withdetail(low/high/auto) or themax_soft_tokensextension on a serverimage_urlcontent part; off-ladder values are rejected rather than clamped (#787).
- Gemma 4 E-series (
e2b/e4b) audio transcription was garbled. The Conformer mel front-end (fft_overdrive, log floor, unfold size, frame mask) and the per-layer ClippableLinear clamp bounds now match the reference, so E-series transcription is correct again (#796). - Pixtral and Mistral 3 force-resized every image to a square and dropped the row structure. They now preserve aspect ratio and emit
[IMG_BREAK]between rows and[IMG_END]at the end, with CLIP normalization (#792). - DiffusionGemma tool-call and channel markers were registered as special tokens, so skip-special decode stripped them and the tool-call parser never saw them. They are demoted to non-special at load (#789).
- Qwen3.5 gated the RMSNorm
+1.0shift on the presence of MTP weights, which could shift an already-converted checkpoint a second time. The shift is now gated on the conv1d weight layout alone (#784). - Server tool-call parsing for the bracketed Mistral format (#785).
- Dense
--max-kv-sizetrim dropped the attention-sink prefix; it is now pinned (#759). - Needless borrow on the
marchflag in themlxcel-corebuild script (#795).
- Generate and publish a CycloneDX SBOM on release (#758).
- Drop the broken
log-levelinput from the cargo-deny action step (#790).
- Bump the minor-and-patch group with 5 updates (#780).
v0.4.0 - 2026-07-12
- ComputeBackend seam for the forward-execution engine. A
ComputeBackendabstraction at the model-load boundary lets a future non-MLX engine hostLanguageModel::forwardwithout routing through the MLX bridge. The existing MLX path moves behindMlxBackendas a behavior-preserving refactor (temp-0 output is byte-identical before and after). Under default features the selection folds to the single MLX backend at compile time with no runtime dispatch on the hot path; a default-offexperimental-backendfeature reserves the plug-in slot. No non-MLX kernels are implemented (#338). - Experimental OpenXLA / IREE compiler backend (issue #449, opt-in, default off). A second forward-execution engine built on a Rust-native StableHLO emitter and the IREE runtime, selectable with
MLXCEL_BACKEND=xlabehind thexla-backend/xla-ireebuild features. Apple Silicon and CUDA shipping binaries compile none of it. The track landed end to end:- Bring-up: export-route and Rust-native StableHLO emitter spikes for Llama-3.2-1B (#453, #455, #457, #451), int4 dequant-in-graph spike (#454), GPU decode throughput on GB10 (#456), and the backend crate/seam scaffold (#458).
- Runtime: Rust→IREE FFI proven on aarch64 via the prebuilt dist and wired into
mlxcel-xlaand the CLI (#459, #460), executing on CUDA (GB10) via a source-built IREE runtime (#461) and on macOS with a Metal HAL device defaulted on Apple Silicon (#506, #508), with device-index-aware device/stream bindings (#505). - Loading and precision: emit graphs from
config.jsonat load (#480), load sharded and f16/f32 checkpoints (#484), dequantize MLX 4/8-bit checkpoints in the loader (#490), f16/bf16 precision modes for the emitter with a per-device default and an accuracy gate (#553, #557), int8/int4 packed weight quantization (#568), and f16-resident weights for a fusion-free bandwidth win (#577). - Architecture packs: Qwen2 (plain RoPE + QKV bias) (#480 line), Gemma2 sliding-window local/global attention (#555), qk-norm and Gemma-family dense pack (#558), Seed-OSS/MiMo/InternLM3/ExaOne and parallel-block/norm-variant dense packs (#560, #561), the MoE FFN graph primitive (router + top-k dispatch) with Qwen3-MoE and OLMoE (#559, #563).
- Serving: uniform-B batched decode graph (#462), ragged continuous-batching decode graph (#466), continuous-batching scheduler (#468) productized into a serving engine (#469) and served through the batching engine (#470), plus sampling (#471), history penalties (#479), stop strings (#478), untied LM heads (#482), and
/metrics(#485). Gemma2 serves single-sequence and batched on this path (#491). - Guards: fail fast on
MLXCEL_XLA_PRECISION=bf16andMLXCEL_XLA_QUANT=packedfor Metal targets (#615, #617).
- Vision-language models. Qwen3-Omni MoE thinker (Qwen3-VL-MoE + audio tower) and talker + code2wav speech output (#664, #677), Llama 3.2 Vision (
mllama) cross-attention VLM (#596), GLM-4V (sectioned MRoPE) and GLM-4V MoE (half-split MRoPE) (#594, #598), Hunyuan-VL (ViT + perceive merger + XD-RoPE) (#663), ERNIE-4.5 MoE VL (DFNRope ViT + modality-split MoE + 3D MRoPE) (#662), DeepSeek-VL2 (#660), Kimi-VL / Kimi-VL 2.5 (MoonViT encoder) (#597), FastVLM (FastViTHD + Qwen2) (#661), Moondream2 (Moondream3 ViT + dense Phi) (#599), Idefics2 (SigLIP + perceiver resampler + Mistral) (#639), SmolVLM (SigLIP + pixel-shuffle + SmolLM2, also loads Idefics3) (#593, #606), LFM2-VL (packed-patch SigLIP2 + pixel-unshuffle + LFM2 hybrid) (#645), Granite Vision (SigLIP multi-tap + AnyRes) and Granite 4 Vision (window-QFormer + Granite-4 hybrid) (#647, #649). - OCR models. DeepSeek-OCR and DeepSeek-OCR 2 (SAM + Qwen2 query resampler), including the SAM + CLIP encoder hooks (#651, #655, #656), dots.ocr (
dots_vit+ Qwen2) (#657), GLM-OCR (ViT + GLM-4) (#646), PaddleOCR-VL (NaViT + ERNIE-4.5 MRoPE) (#595). - Diffusion LLM. LLaDA-2 MoE masked-diffusion LLM (
llada2_moe) (#659). - Text architectures and attention. DeepSeek-V3.2 / GLM-MoE DSA lightning indexer (#583), phi3-small blocksparse attention (#581), qwen3-next pipeline-parallel stage support (#580).
- MTP speculative burst reuses an adopted APC prompt-cache prefix (#591).
enable_thinkingaliased to the barethinkingflag fordeepseek_v32(#579).- The default output-token cap follows llama.cpp:
-1resolves to the context window (#477). - Detect an incomplete download snapshot and re-fetch on load (#604), formalized below under Fixed.
- Serving-throughput defaults enable the batching machinery out of the box.
mlxcel-serverandmlxcel servenow default to--parallel 4(batched decode of up to 4 concurrent sequences, clamped to 1 for SSM / hybrid / mixed-cache families that cannot batch) and--max-batch-prefill 4(batched prefill for families that support it), and add--no-prompt-cacheas a clean opt-out for the already-default-on prompt-prefix cache. The batched-decode default is paired with a default--kv-cache-budget automemory guard so the #122 paged block-budget admission bounds KV for the concurrent batch and returns backpressure instead of an OOM abort; the guard is inert on the dense decode backend and can be disabled with--kv-cache-budget none(or0). On Apple M1 Ultra (meta-llama-3.1-8b-instruct-4bit), 4 concurrent clients get 1.90x the single-client aggregate throughput and ~17x lower time-to-first-token under load, with single-client throughput unchanged.--parallel 1,--no-batch,--max-batch-prefill 1,--no-prompt-cache, and--kv-cache-budget nonerestore the previous single-client behavior. Migration note: an explicit small--ctx-sizeis now divided across 4 slots, so a value that gives fewer than 512 tokens per slot fails startup with a clear error (raise--ctx-sizeor lower--parallel);--ctx-size 0, the default, uses the model window per slot and is unaffected. GB10 numbers (including the >= 2.5x-at-4-clients target) are pending a CUDA measurement session; seedocs/benchmark_results/serving-throughput-defaults-m1u-2026-07-09.md(#628). - The CLI prefill path now defaults to chunked, and rotating-cache chunked prefill was unbroken in the same change (#679).
- The CLI routes through an MLX inference-session seam, a behavior-preserving refactor that gives the OpenXLA path a symmetric entry point (#450).
- Both drafter flag spellings are accepted on
serveandserver(#602).
- Cap the batched-prefill transient memory. With
--max-batch-prefill 4now the default (#628), the server's padded batched-prefill path engaged out of the box forsupports_batched_prefill()families and, for mixed-length prompts, ran a single unchunked[B, padded_len]forward that materialized a stacked[B, L, L]FP32 attention mask, anO(B*L^2)transient that ignored--prefill-chunk-size(four concurrent 8k prompts built a ~1 GiB mask and could abort the server on OOM, an availability edge the--kv-cache-budgetguard does not model). The drained batched window is now bounded by total padded tokens via the new--max-batch-prefill-tokensflag (andMLXCEL_MAX_BATCH_PREFILL_TOKENS): a cohort ofB >= 2rows padded toLkeepsB*Lwithin the budget, so the mask stays within~2*budget^2bytes; rows past the budget spill to the next tick and prefill via the chunked single-sequence path, and a head prompt too long to batch skips the batched path entirely. The default budget is derived (2 * max_batch_prefill * prefill_chunk_size, the shipped2 * 4 * 512 = 4096; the 2x headroom keeps a full batch of slightly-over-chunk-sized prompts in one window), bounding the FP32 mask to about 34 MiB while keeping short-prompt concurrency batching unchanged;0disables the cap for the pre-#715 unbounded behavior (#715). - CUDA / GB10 kernel parity. Native paged-attention decode kernel ported to CUDA (#731) with an adaptive selector for it (#709); fused SSM decode kernel ported to CUDA (#727); MoE prefill collapse fixed via sorted grouped GEMM (#726);
sm_120/121qmm CTA tile (M=128) for Blackwell prefill (#723); single-dtype decode graph to eliminate per-tokenAsType(#732); small-M quantized matmul amortized with a multirow qmv path (#740);sdpa_vectordecode kernels extended to head_dim 256/288 via an MLX overlay (#681); backend-aware default for the fused decode-MoE Dff threshold (#643);MLXCEL_CACHE_LIMITadded and the periodic decode cache-clear disabled on CUDA (#696); chunked parallel prefill for gated-delta on non-Metal backends (#590). - Speculative decoding. Tick-cooperative speculative decoding removes the burst head-of-line block (#745); the adaptive MTP policy is settled from measured round cost (#742); CUDA pairing matrix and burst HOL observability added (#733).
- Server decode loop. Pipeline the scheduler decode with lookahead
async_eval(#729); incremental detokenization and streaming-overhead cuts (#713). - NVFP4 quantization (Blackwell). Direct-transcode ModelOpt NVFP4 triplets to MLX native (#697), default Metal NVFP4 to native transcode (#705, #706), opt-in native repack override for non-CUDA builds (#699); gemma4 folds NVFP4 global scales into the fused MLP kernel (#701) and cuts decode-path primitive count on CUDA (#680, #682).
- Vision.
mllamabuilds cross-attention states from real tiles only (#622) and caches cross-attention K/V across decode steps (#621); PaddleOCR-VL batches vision attention segments (#700). - Record the CUDA fused-MoE Dff cap provenance and its decline test (#711); long-prompt prefill ladder plus serving telemetry benches (#641).
- Interrupted model downloads are detected and re-fetched instead of failing at load. An interrupted
mlxcel run <repo-id>(alsomlxcel serveandmlxcel-serverauto-download) previously reused the partial snapshot and died with a bareWeight not found. The load/resolve path now verifies the full weight set against the snapshot's ownmodel.safetensors.index.json(every shard present and non-zero), not justconfig.jsonpresence, so a partial snapshot is resumed through the shared downloader (re-fetching only the missing files, with a forced clean re-download as a fallback) before the model loads. Repackaged mlx-community quants whose stale full-precision index no longer matches the on-disk files are still reused without a re-fetch (#465). - CUDA. Restore
lfm2-350m-8bitdecode via an elementwise short conv (#751); correct the Int8 KV per-token scale (#717); chunk long-prompt qmm launches to avoidgridDim > 65535and int overflow (#652); bound long-prompt prefill memory for flash-ineligible models (#676); repack ModelOpt NVFP4 to the native MLX layout (#692). - Gemma 4. Bind the gemma-4 NVFP4 vision tower so image input works (#750); honor per-module MLP quant overrides (#690) and diagnose malformed ones (#695); disable CUDA graphs for the decode graph-race collapse (#688, #689); render the thinking-off closed channel to fix chat pad-collapse (#686, #687).
- DeepSeek.
deepseek_v32causal-mask fallback so maskless prefill is not bidirectional (#667); deepseek-v3 restores the last layer, causal prefill, and f16 clip (#618) and corrects the MoE reshape for 3D forward input (#614). - VLM loading against real checkpoints. moondream2 weight/contract pairing and EOS/prompt resolution (#616, #609); kimi-vl nullable text-config field (#608); paddleocr-vl
language_model.*/visual.*key remap (#607); smolvlm idefics3 checkpoint detection (#606); mllama vision-tower key mapping (#610); glm4-moe fuses separateswitch_mlpgate/up experts at load (#620); DeepStack and MRoPE-section loops capture the functionalslice_updatereturn (#666). - Qwen. qwen3.5-moe sanitizes toward the gate/up/down expert names
SparseMoeBlockloads and stacks per-expert weights (#671, #587); qwen3-next real checkpoints load and run (#588) with per-sequence (SequenceId) cache isolation (#601). - Tool calls. Parse GPT-OSS Harmony tool calls into structured calls (#658); strip the leading namespace from tool-call names before filtering (#586).
- Server / routing. Emit usage on the disaggregated
/v1/chat/completionsresponses (#707); reject model-owned paged families from the prefill handoff (#716). - longcat n-gram raw slice uses a
-1stop on axis 0 (#589); offloaddownload_repoto a dedicated thread when a Tokio runtime is active (#668); validate the external quant scheme and surface unclosed thinking (#605); load bf16 affine quant scales in the OpenXLA weight loader (#569); repair threemlxcel-coretests after the MLX pin bump (#644).
- Finalize the per-backend
MLXCEL_FUSED_QK_NORMdefault decision: CUDA (GB10) was measured and is also slower than the graph path, so the fused QK-norm decode path stays opt-in (default off) on every backend;docs/environment-variables.mdupdated to drop the CUDA-pending rationale and record the determinism nuance (#355). - Document all cargo build features and the XLA backend env vars (#747).
- OpenXLA design record: ADR 0004 compute-backend seam + StableHLO/MLIR direction (#447), Phase 1 outcome (#452), performance table and transferable-precision decision (#562), multimodal/VLM track scope (#567), SSM/hybrid/recurrent track scope (#565), low-precision validation on Metal + oracle dumper (#611), packed-int8 fusion spike findings (#578).
- Benchmarks: refresh M5 Max (#756) and M1 Ultra (#754) for 0.4.0-rc.1 (cooldown 30), M1 Ultra full refresh post VLM-port (#673), GB10 full sweep (2026-07-12), attribute the GB10 decode drops via a post-reboot re-measurement (#757), and link the GB10 findings and decode drops to issues #748, #749, #755.
- Speculative: cross-check Gemma 4 MTP acceptance on M5 Max (#737) and correct the GB10 MTP regression framing to the small-M qmv kernel gap (#739).
- CUDA/Blackwell batched-decode does not amortize (#724); re-validate the CUDA fused-MoE Dff cap on MLX 0.32.1 (#721); retire pooled paged-attention decode to a library-only API (#720); record the Metal NVFP4 native benchmark (#702); document Idefics3 support routing to the SmolVLM runtime (#640).
- Version bumped to
0.4.0-rc.1. - Bump the
mlxcel-coreandmlxcel-surgerymember crates to Rust edition 2024 and align their versions to the root crate, per the release-versioning rule (#445, #272). - MLX pin bumped to 0.32.1 (#703, #704) and earlier to
e9463bbwith the CUDA overlays rebased (#625, #642). - Reproducible IREE runtime toolchain via make targets (#576); requantization script and perplexity harness (#683, #685).
- Refactors: share the per-layer attention core across emitter graph kinds (#554) and the seq MLP across serve graphs (#492); share the short-conv decode helper and audit L=1 conv dispatch across SSM/hybrid decode paths (#753).
- Tests: reusable per-architecture XLA validation harness (#556); backend-conditional wired-limit expectation (#743); preserve MoE expert stack ordering.
- Microbench: qmm GEMV effective-bandwidth harness and GB10 results (#684).
- CI: bump
actions/checkout6 to 7 (#473),actions/cache5 to 6 (#472),actions/setup-python5 to 6 (#474); dependency group updates (#483, #669).
v0.3.3 - 2026-06-25
- Multi-node disaggregated routing. The server drives multi-node disaggregated prefill/decode routing with worker health checks and failover (#388), and the router serves
/v1/completionsalongside the chat and responses endpoints (#386). - Mellum 2 hybrid-attention MoE text model (#397).
- Video input for Gemma 4 Unified (
gemma4_unified) (#400). - Phase 1 Python client package over the server (#411).
- MTP speculative decode wired into offline
generate(#385). - Env-gated sparse-V skip-rate counter to measure KV sparsity (#377, #379).
- N-gram loop detection that breaks degenerate repetition loops at decode, on by default for the Gemma 4 family (#433).
- Nemotron-H Nano Omni audio input wired into server chat audio (#443).
- Fused single-launch xIELU Metal kernel for Apertus, on by default after M5 Max validation (#414, #417). Apertus and Seed-OSS decode were profiled and the xIELU op trimmed (#399).
- Wire MiniMax to the fused decode-MoE kernel (#390).
- Bound the audio request queue and add a per-request timeout (#381).
- Sliding-window prefill beyond the window corrected across models; the gemma3/gemma4 sliding-prefill mask was hoisted to a shared helper (#405, #412, #415).
- HTTP 422 from
/v1/messagesfor Claude Code >= 2.1.156 (#380). Claude Code interleaves{"role":"system", ...}turns inside themessagesarray as mid-conversation reminders. The missingSystemvariant inAnthropicRolecausedserde_jsonto reject those requests before any generation. A newfold_system_messagestranslator pass now relocates mid-conversation system turns into the adjacent user turn (or the head system block) so the text reaches the model under any chat template, including head-only templates (Qwen, Llama 3) that silently drop non-head system messages. - OLMoE scores the MoE router with full softmax then gather, not top-k softmax (#391).
- Preserve the assistant
reasoningfield across turns (#394). - Add BitNet to
FAMILY_ORDERsofamily_order_is_exhaustivepasses (#404). - Router: harden
/router/statsdisclosure and decode_target trust (#393), and use the worker's authoritative token count for usage (#392). - Make audio-path MLX ops fallible at the FFI boundary (#384), and make audio synthesis panic-safe in release via panic=unwind with an explicit core-thread abort (#383).
- Prefill attention masks sized from the live window, not the monotonic offset. Multi-token prefill causal and sliding-window masks are now sized from the cache's live length (
offset - live_start), so a--max-kv-sizetrim_frontcannot produce a mask wider than the K/V the cache returns. Applied across dense-cache sliding-window models (#418), the general dense path (#420), mistral4/nemotron_nas/qwen-vl (#422), and gemma3/gemma4/exaone_moe (#431, a defensive consistency fix). Byte-identical on the untrimmed path. - Double-transpose crash on mlx-community conv checkpoints (Gemma 4 audio, phi4mm patch-embed, nemotron audio, RT-DETRv2). Several weight-sanitizer functions transposed conv weights from PyTorch
[out, in, kH, kW]to MLX channel-last[out, kH, kW, in]unconditionally. Pre-converted mlx-community checkpoints already store these weights in channel-last order, so the unconditional transpose double-converted them and produced a corrupted shape. The confirmed crash: loadingmlx-community/gemma-4-e4b-it-qat-4bitturned the audio subsample conv weight[128, 3, 3, 1]into[128, 3, 1, 3], which MLX conv2d rejected because the input C_in=1 did not match the weight C_in=3. All four affected sanitizers now check the tensor shape before transposing:conv2d_weight_is_channel_last(already-MLX[out, kH, kW, in]skips; PyTorch[out, in, kH, kW]transposes) andconv1d_weight_is_channel_last(depthwise-only; MLX[out, kW, 1]skips; PyTorch[out, 1, kW]transposes). Both predicates are idempotent. Resolves #428. - Conv shape faults no longer abort the server. conv1d/conv2d are fallible at the FFI boundary (#434) and the nemotron omni audio-encoder convs route through the same fallible path (#439), so a bad conv shape returns an error instead of aborting the process.
- Gemma 4 audio placed in the user turn. The CLI resamples audio to 16 kHz and emits the
<|audio|>marker inside the user turn (#438), and the server emits its<|audio|>block inside the user turn (#440). - mistral4 loading and MoE routing. Mistral3-VLM mistral4 (MLA) text backbones route to the Mistral4 loader (#423/#424), and mistral4 MoE tokens are flattened to 2D before SwitchGLU routing (#425/#426).
- Attribute mlx-audio alongside mlx-vlm in README and NOTICE.
- Record the #370 fused-V attempt regression and keep Turbo4Asym on dequant-SDPA (#378).
- Update dependencies to latest compatible versions (#406).
- Platform-aware release with an explicit
release-cudaMakefile target. - Bump actions/checkout from 6 to 7 (#395).
- Fix clippy
useless_vec/identity_oplints in the nemotron audio encoder test (#441).
v0.3.2 - 2026-06-20
- Whisper speech-to-text on
/v1/audio/transcriptionsand/v1/audio/translations(#371), and Kokoro-82M text-to-speech with an iSTFTNet vocoder on/v1/audio/speech(#374), served through new audio request and response plumbing on the/v1/audio/*surface (#368). reasoning_contenton non-streaming chat completions, splitting thinking-model output into a separate field that matches the streaming path (#359).- Warn at startup when a CPU-only build runs on a host that has an NVIDIA GPU (#372).
- Hardware-gated
MLX_MAX_OPS_PER_BUFFERdecode default. Pre-M5 Apple Silicon (M1 to M4) gets a higher command-buffer op cap, raising steady-state decode by about 8 to 12% (gemma3n e2b 82.7 to 92.5 tok/s); M5 keeps the default with no change (#360). - Turbo4Asym decode rerouted through dequant-then-SDPA, lifting it from about 0.14x to 0.40x of fp16 with byte-exact output instead of the slow sparse-V path (#369).
- Fuse the batched decode sampler into a single
[B]dispatch (#339), add incremental per-sequence penalty-state caches (#344), and split batched prefill into compatible cohorts (#346). - Adaptive B=1 MTP enable or decline policy chosen from per-model profiling (#348).
- Generalize the fused QKV+RMSNorm+RoPE path to standard RMSNorm, opt-in behind
MLXCEL_FUSED_QK_NORM(#341). --recommend-quantnow suggests a Turbo KV-cache mode per model family and context range, advisory and opt-in only (#343).
- Correct an f16/bf16 logprobs crash and corruption where 2-byte scores were read as 4 bytes (#340).
- Suppress gemma4_unified multimodal placeholder tokens that leaked into generated output (#351).
- Reseed the RNG per row at the batched-prefill first-token sample, so a batched request's first token no longer depends on sibling rows (#356).
- Align the Turbo KV
--recommend-quantadvisor, thebench_kv_cache.shgates, anddocs/turbo-kv-cache.mdwith the measured four-model decode sweep, and add ADR 0002 on why the split Turbo decode does not reproduce the upstream sparse-V speedup (#376). - Record the GB10/CUDA fused QK-norm decode result (#357) and Gemma3n decode profiles on M5 Max (#358, #345).
v0.3.1 - 2026-06-17
- Fused decode-MoE kernel ported to CUDA. The fused single-token MoE decode path was Metal-only in 0.3.0; this implements it on CUDA, so Linux/CUDA GPUs get the same fast path with byte-identical greedy output. Measured gains run from about 10% to 55%, up to 1.55x on qwen3-moe (#319).
- Wired six more MoE families to the fused decode-MoE kernel: qwen2_moe (#308), LFM2 (#309), qwen3_vl_moe (#310), Mixtral (#311), Phi-3.5-MoE (#312), and OLMoE (#314). The kernel self-gates by expert size (
MLXCEL_FUSED_MOE_MAX_DFF, default 4096), so large-expert models such as Mixtral 8x7B and Phi-3.5-MoE keep the gather path with no regression. - BitNet BitLinear ternary matmul ported to CUDA, so BitNet b1.58 models run on CUDA GPUs (#322).
- Load non-affine quantized VLM weights with the correct quant mode and group size. The loader detects the quant mode from the absence of biases and infers
group_sizefrom tensor shape, so non-affine VLM checkpoints such as minicpm-v mxfp4 load instead of failing (#334). - OLMoE applies
q_norm/k_normbefore the head reshape, matching the reference attention order (#317). - Report load/run out-of-memory as
SKIP:oomrather thanFAIL:bench(#298).
- Add an
MLXCEL_CXX_MARCHoverride and pin the x86_64 CUDA release asset tox86-64-v3, so prebuilt CUDA binaries run on a wider range of hosts (#208).
- First Linux/CUDA (NVIDIA GB10) full benchmark sweep for 0.3.1: 136 of 147 text models pass with no code-level failures (#320, #321, #323, #324, #335, #336).
- Record fused decode-MoE gains for the newly wired MoE families (#337).
- Refresh the README performance tables (#300).
v0.3.0 - 2026-06-15
- Nine new model families. BitNet b1.58 (1.58-bit ternary weights, #252), IBM Granite dense (#254) and GraniteMoeHybrid (Mamba2 plus attention hybrid, #259), LFM2 and LFM2-MoE (#255), Falcon-H1 (Mamba2 plus attention parallel hybrid, #256), PLaMo 2 (Mamba plus attention hybrid, #257) with PlamoTokenizer support (#264), Apertus (xIELU, QK-norm, llama3 RoPE scaling, #260), ByteDance Seed-OSS (#261), and dots.llm1 MoE (#263).
- Linux x86_64 and aarch64 CUDA release builds with bundled CCCL headers, so the CUDA artifacts run on nodes that do not have the build-machine CCCL path (#262).
- Configurable allowed-origins for server CORS, replacing the any-origin default when set (#253).
- Fused decode-MoE Metal kernel is now on by default (
MLXCEL_FUSED_MOE, set to0to disable). It speeds up single-token MoE decode across families, with the GeGLU path giving about 13% on gemma4 (#285). mlxcel runwith no model argument now defaults tomlx-community/gemma-4-e2b-it-4bit(wasLlama-3.2-3B-Instruct-4bit): a smaller checkpoint that downloads faster and runs in less memory.
- Two-kernel fused decode-MoE that beats
gather_qmm, staged across the kernel foundation and the expert decode kernel (#274, #275, #276). Extended to 6-bit and mixed-bit experts for dots.llm1 (#278), wired to qwen3-next / Qwen 3.5 / 3.6 (#279), and given a GeGLU variant for gemma4 (#281); the squared-ReLU kernel stays behind a dedicated flag (#280). - Gate the Mamba2 and nemotron_h per-mixer eval to M5 Max so SSM-hybrid decode is not slowed on other Apple Silicon (#266, #271).
- CCCL header resolution at runtime now handles relative invocations and nodes without the build-machine path, and a persistent PTX kernel cache reuses JIT-compiled kernels across runs (#270).
- Quantized models now stay bf16, fixing a 33-41% M1 Ultra decode regression on bf16-scale checkpoints (qwen3, nemotron, gpt-oss, solar, and others). The blanket bf16-to-f16 quant-scale promotion added with Apertus had created a bf16-activation by f16-scale mismatch in
quantized_matmul/gather_qmm(#290). - Infer per-tensor quantization bits for embeddings, so mixed-precision exports that store the embedding at a different bit width than the top-level config load instead of aborting in dequant. For example diffusiongemma stores its embedding at 8-bit under a 4-bit default (#292).
- Refreshed the M1 Ultra and M5 Max benchmark results for the 0.3.0 sweep (#295).
- Split
mlx_cxx_bridge.cppinto domain-specific translation units (#277). - Bumped the minor-and-patch dependency group (#288).
v0.2.1 - 2026-06-13
- Exact-prefix prompt-cache snapshots now cover model-owned recurrent and mixed-cache families. Mamba, Mamba2, Jamba, Nemotron-H, Qwen 3.5 / 3.6 text, MoE, and VLM wrappers can donate and restore same-session whole-prefix state instead of falling back to cold prefill (#241).
- Gemma 4 text, VLM, and Unified wrappers now donate and restore exact-prefix prompt-cache snapshots. The snapshots preserve model-owned standard and rotating cache state; real
gemma-4-26b-a4b-it-4bitsmoke validation inserted a 10,568,520-byte snapshot with no oversized rejection (#243).
- CLI help and user docs now describe the v0.2.x server option surface consistently across
mlxcel serveandmlxcel-server, including disaggregated peer roles, VLM prefix-cache environment settings, paged KV budget settings, and Gemma 4 snapshot-cache support.
v0.2.0 - 2026-06-13
- Unified paged KV cache is now live in the batching server (epic #116). Prefix reuse and paged block storage now operate together: a concurrent shared prefix is stored once with reference counting and copy-on-write, so a second request that shares a prefix adopts the existing blocks and re-prefills only its divergent suffix. The radix prompt cache and the paged block pool were unified into one store, the scheduler backs paged sequences with the shared pool, and pool-backed decode is byte-identical to the previous dense path across qwen3 and llama3 (single, batched, and prefix-share cases) (#152, #167, #168).
- Disaggregated serving: prefill, decode, and router roles split across processes over TCP.
mlxcel-server --node-role {prefill,decode,router}with--serving-bind,--prefill-peers, and--decode-peersruns a pipeline where a model-free router fronts HTTP, hands the prompt to a prefill node, streams continuation tokens from a decode node, and merges them back to the client. A 3-process run is byte-identical to a single hybrid node. KV block contents serialize across the node handoff (#185, #187, #188, #189, #190, #191, #192, #193). - DiffusionGemma block-diffusion model (#217): text generation (#218), image input (#219), and
mlxcel-serverserving (#220). The backbone reuses the existing Gemma 4 26B-A4B path; the new pieces are the dual-mode forward, self-conditioning, and the canvas diffusion engine. Temperature-0 output is byte-identical across the MLX bump. - Qwen3-Coder XML tool-call parsing, so Qwen3-Coder function calls are extracted from the model's XML emission and surfaced as OpenAI
tool_calls(#206). --kv-cache-budget <BYTES|auto>flag (envMLXCEL_KV_CACHE_BUDGET) caps the paged KV block pool. The scheduler admits a paged prefill only when blocks are available, evicting cold cached prefixes (then preempting) to make room, and rejects or requeues otherwise. Opt-in: the pool stays unbounded by default (#174, #175, #176). Paged block-pool usage is exposed atGET /v1/cache/statsand on/metrics(#178).- Architecture-aware KV-cache memory estimation for
mlxcel inspectand the--estimate-memorypreflight (#172). Sliding-window, MLA, hybrid, and pure-SSM models now estimate KV bytes from their real attention shape instead of a flat formula that was off by about 100x for Gemma, DeepSeek, and Mamba. A separate activation term accounts for the chunked-prefill working set on top of weights and allocator overhead (#173). - Opt-in VLM prompt-prefix cache sharing for multi-turn same-image conversations, behind
--enable-vlm-prefix-cache. A follow-up turn that keeps the same image adopts the prior turn's prefix and prefills only the new text, verified byte-identical to a cold prefill on qwen2-vl-2b (#182, #184). - Fused paged-attention decode Metal kernel (split-K flash-decoding), built and numerically correct but gated off because it does not beat MLX gather-then-SDPA at long context on Apple Silicon. Enable with
MLXCEL_PAGED_ATTENTION_NATIVE(#181).
- Automatic Prefix Caching is now enabled by default. Requests that share a prompt prefix with a cached entry reuse the cached blocks, and the output is unchanged (#233).
- The prompt-prefix KV cache now serves the Anthropic
/v1/messagesand OpenAI Responses/v1/responsesendpoints, not just/v1/chat/completionsand/v1/completions(#240). - The B=1 MTP speculative-burst default is now chosen per hardware. M1 Ultra measurements showed batch-capable MTP targets (such as Gemma 4 31B) regress at B=1 (0.75x to 0.96x), while the same targets gain on M5 (1.2x to 1.4x); the discriminator is GPU generation, not memory bandwidth. Batch-capable targets now default on only on M5-class hardware with a neural accelerator; non-batchable targets stay always-on.
MLXCEL_ENABLE_MTP_B1overrides either way (#216). - Partially matched paged prefixes are now adopted instead of declined, so a request that shares a leading block run with a cached entry but diverges later reuses the matched blocks (#230). Paged adoption is non-consuming: it clones and pins the shared blocks rather than moving them, so the donor entry stays cacheable (#232).
- Vendored MLX bumped to upstream main (2026-06-11) and the steel GEMM overlay retired now that the fix is upstream (#223).
- Chunked slab storage for the paged pool, so it grows in fixed-size slabs instead of one monolithic tensor (#237).
- Presize the paged pool to the prefill span and eval grown slabs eagerly to avoid mid-decode allocation stalls (#229).
- Stream decode continuation tokens one frame at a time from the disaggregated decode role instead of buffering the full continuation (#214).
- Hardened the ragged B>1 MTP batching masks and verify tail so variable-length prompts in one burst keep greedy parity (#202).
- Per-row position holes broke B>1 batched MTP greedy parity after divergent accepts. When rows in a batched MTP burst accepted different draft-token counts, the surviving K/V is now compacted to each row's accepted end with per-row RoPE and a precise mask, so a divergent round no longer shifts later rows off their true positions (#211).
- Guard the empty-batch paged-decode fallbacks against a
drain(..1)panic, and use absolute block indexing in append, trim, restore, and serde validation so alogical_start > 0write addresses the correct block (#215). - Support chunked-prefill prompts in the disaggregated serving handoff, driving start and continue-chunked to completion with a 1M-token admission cap and pool release on extract error (#213).
- Apply the chat stream filter to disaggregated router output so reasoning-content splitting and structural-token cleanup match the single-node path (#212).
- Finish a chunked prefill when the first chunk already reaches the prompt end (#179).
- Release paged KV block pins on prompt-cache evict or decline, including a pre-existing leak that left the origin allocation pinned at reference count 1 (#170).
- Account real paged pool bytes in the prompt-cache ledger and
/v1/cache/statsinstead of a nominal placeholder (#231). - Enforce the pack3 size contracts in release builds so a mis-sized packed buffer fails fast instead of corrupting silently (#236).
- Render assistant
tool_calls.argumentsas a JSON object rather than a string on multi-turn requests (#210). - Render the request's
toolsinto the prompt so templates that inspect the tool list receive the real definitions (#207). - Expand bare model names to the default org in the
downloadsubcommand, matching the other-mconsumers (#177).
- Hardened the paged KV handoff deserialization boundary: capped the frame size, anchored the block geometry, checked per-layer consistency, and rejected empty sequences, so a malformed handoff payload from a peer cannot drive an out-of-bounds read or an unbounded allocation. A restore that fails partway now releases the blocks it already took instead of leaking them (#186).
- New
docs/CONTINUOUS_BATCHING.mdcovering continuous batching, paged decode, and the disaggregated prefill/decode/router topology, plus an expanded unified-cache section indocs/turbo-kv-cache.md(#194).
- Extended the paged KV cache scheduler and prefix-share parity suites to llama3 alongside qwen3, all byte-identical (#169).
- Added hybrid-SSM cache carve-out tests and multimodal-digest plumbing so SSM and VLM families stay correctly excluded from or included in block sharing (#182).
- Recorded upstream attribution for ported third-party code (#238).
- Bumped the minor-and-patch dependency group with 3 updates (#180).
v0.1.4 - 2026-06-05
- Gemma 4 Unified (
gemma4_unified) multimodal architecture (#153, closes #151). - Gemma 4 Unified MTP speculative drafter (
gemma4_unified_assistant) (#157, closes #158). The Gemma 4 Unified decode target now routes through the existing MTP speculative burst dispatch, reusing the MTP drafter and round loop unchanged. The drafter's pre/post projections load through the quantization-awareUnifiedLinear, so a 4-bit assistant (e.g.gemma-4-12B-it-assistant-4bit) no longer crashes at forward time with a matmul shape mismatch. Ongemma-4-12b-it-4bitplus the 4-bit assistant, temperature-0 output is byte-identical to classic decode at about 1.87x decode speedup (39 to 74 tok/s). - Variable-length prompts in B>1 batched MTP bursts, behind the new
MLXCEL_ENABLE_MTP_BATCH_RAGGEDopt-in (subordinate toMLXCEL_ENABLE_MTP_BATCH) (#162, closes #161). Rows of different prompt lengths join one burst via per-row left-padding plus a windowed left-padding causal mask; greedy parity holds because every token in a row is shifted by the same constant left-padding offset. Eligibility is limited tomax_prompt_len <= sliding_window; out-of-regime windows fall back to per-row B=1 service. Off by default (measured 0.94x to 1.13x on the 31B), so the production path is byte-for-byte unchanged. - Unified paged KV cache (epic #116), Phase 0: decode-time page-gather microbench and ADR 0001, which selects the
[num_blocks, block_size, n_kv_heads, head_dim]pool layout (about 2.1x faster on gather-then-SDPA than the head-split layout) and the gather-then-SDPA strategy (#145, closes #117). - Unified paged KV cache, Phase 1: physical block-pool K/V tensor storage in
PagedBlockPool, lazily allocated per layer withwrite_block/gather_visibleprimitives (#148, closes #118). - Unified paged KV cache, Phase 2: pooled paged-decode read path over real, possibly fragmented block tables, bit-identical to the dense fallback over 200 steps (#149, closes #119).
- Unified paged KV cache, Phase 3: paged prefill writer with shared-prefix copy-on-write, so a suffix write after a shared prefix allocates only the divergent blocks (#150, closes #120). These four phases are additive machinery exercised by tests; the live decode path stays byte-for-byte unchanged until the scheduler wiring lands.
- B=1 MTP speculative decoding now runs by default for every MTP target, including batch-capable ones such as Gemma 4 31B (#159, closes #158). Previously batch-capable targets declined singleton MTP unless
MLXCEL_ENABLE_MTP_B1=1was set, a calibration from an earlier "B=1 is slower" measurement. M5 Max measurement shows B=1 MTP is profitable with byte-identical output at temperature 0: about 1.2x to 1.4x on the 31B plus bf16 assistant, and about 1.87x on the 12B Unified pair. Opt out withMLXCEL_ENABLE_MTP_B1=0.
- Quantized fused MoE experts in the
gemma4_unifiedloader are now split correctly. The fused-expert split insanitize_gemma4_unified_weightsonly matched the bare non-quantized.weight, so a quantized MoE checkpoint's.weight/.scales/.biaseslegs fell through unsplit andswitch_gluconstruction could not find its per-projection quantized parts. The split now matches each quantized component leg and slices it on the output (doubled-FFN) axis at the same half boundary, with a dequantize-equivalence test proving no group straddling (#156).
- Recorded the measured Gemma 4 31B B>1 batched MTP numbers and aligned the related code comments (#160).
- Bumped the
minor-and-patchdependency group:uuid1.23.1 to 1.23.2 andhyper1.9.0 to 1.10.1 (#147). - Added the local
/notes/scratch directory to.gitignore.
v0.1.3 - 2026-05-30
- BREAKING:
mlxcel listnow lists local downloaded models by default; the supported-architecture catalog moved to the newmlxcel archverb. Previously baremlxcel listprinted the architecture catalog and the local store inventory was gated behindmlxcel list --local, which inverted the bare-verb convention of every comparable tool (ollama list,docker images,pip list,brew listall show the local inventory with no flag) and contradicted mlxcel's own store-centricrun/download/rmverbs. Nowmlxcel list(and itslsalias) enumerates downloaded models from the global store with repo-id, on-disk size, and path, mirroringollama list;mlxcel list --models-dir <PATH>applies to that listing. The catalog is reachable viamlxcel arch(aliasmlxcel supported), byte-identical to the prior bare-listoutput. Themlxcel list --localflag is removed outright: clap rejects--localas an unknown argument, and the empty-store hint now points users tomlxcel archfor the catalog. This is pre-1.0 (v0.1.x) with no deprecation cycle because--localhad not seen real-world use, so carrying a hidden flag plus a deprecation shim was not warranted. Migration: usemlxcel archfor the catalog, and drop--localfrom anymlxcel list --localinvocation (the bare form now does the same thing) (#138). mlxcel listdefault table redesigned: columns are now NAME / SIZE / MODIFIED. The absolute PATH column is no longer shown by default; pass-v/--verboseto restore it. A relative MODIFIED time column is derived from the snapshot directory mtime and renders as human-friendly durations ("just now", "2 days ago", "3 weeks ago", or "-" when the mtime is unavailable). The compact header contracts$HOMEto~and dims secondary columns on a TTY (respectingNO_COLOR). New output modes:--jsonemits a stable[{repo_id, size_bytes, path, modified}]array (modified is Unix epoch seconds or null) suitable for scripting;-q/--quietprints one repo-id per line for pipe-friendly use withxargsandmlxcel rm;--sort name|size|modifiedcontrols ordering (default: name).--jsonand-qare never styled and are mutually exclusive;-vis incompatible with both (#141).
- Security: chat-template rendering is now bounded to prevent a denial-of-service from untrusted model templates. Model-supplied chat templates render through minijinja both per request and at model-load time (the
supports_toolsprobe). Rendering was previously unbounded, so a pathological template (for example deeply nested or effectively unboundedforloops) could consume unbounded CPU and memory. The fix enables minijinja'sfuelfeature and caps each render at 50M VM instructions in the sharedconfigure_environment, covering every render path. Exhaustion surfaces as a cleanOutOfFuelerror throughResult(the load-time probe degrades to a string heuristic) and never panics. The cap is generous: real templates run well under about 1M instructions (audited across 91 templates and 267 scenarios with 0 failures), while an unbounded loop is bounded to a fraction of a second. This is RCE-safe and matters most for multi-tenant deployments where untrusted parties can cause arbitrary models to load (#129, PR #139). - Base-model warning no longer presents
-itas a universal instruction-tuned naming convention. The warningmlxcel run/mlxcel generateprints when a model ships no chat template (added in PR #134) recommended trying a variant "named with an-itsuffix", but-itis the Gemma convention. For other families the advice was wrong: Llama and Qwen2.5 instruction-tuned checkpoints use-Instruct, and Qwen3 / Qwen3.5 use the plain repo name (with-Basemarking the non-instruct variant), so a user runningQwen3.5-0.8B-Basewas pointed at a non-existent-itrepo instead of being told to drop-Base. The advice now names the per-family conventions (Gemma-it; Llama / Qwen2.5-Instruct; Qwen3 / Qwen3.5 plain name vs.-Base). Base-model detection is unchanged: it keys on chat-template absence, never on the model name.
v0.1.2 - 2026-05-29
- Chat fallback for models without a
chat_templateno longer collapses into echo loops. Whentokenizer_config.jsonships nochat_templatefield and there is nochat_template.jinja,render_promptpreviously calledconcat_plaintext, which is bare content-only concatenation with no role markers. Base / non-instruction-tuned models, being completion models, then took the most natural continuation of an unstructured prompt and parroted the user's last turn indefinitely (the symptom reported in #133). The implicit "no template found" path now uses a genericUser: ... Assistant: ...pseudo-template viaconcat_userassistant_fallback, with a trailingAssistant:cue (no newline) that nudges the model to produce an assistant turn next instead of completing its own prompt with anotherUser:line. Theprocessor.is_none()warning still fires and still names base-model behavior as the cause; the recommendation to try the-itHub counterpart is unchanged.--no-chat-templatekeeps its existing raw concatenation semantics and remains the offlinemlxcel generate --no-chat-templateparallel for completion-style usage. Template-render failure inside the chat-template path now falls back to the structured form as well, rather than raw concat, since by then the user is already in chat mode. Unknown roles such astoolare preserved verbatim with the sameRole:pattern instead of silently merging into the prior turn (#133, PR #136).
v0.1.1 - 2026-05-28
chat_template.jinjais now downloaded alongside the rest of the model snapshot. The downloader allow-list insrc/downloader/filters.rs::is_wanted_fileonly accepted exact-namechat_template(no extension) plus the broader*.json/*.safetensors/*.tiktoken/*.model/ constrained*.txtallowances, but the actual HuggingFace convention ischat_template.jinja. The file was being filtered out at download time, leavingChatTemplateProcessor::from_model_path'schat_template.jinjafallback dead and forcing the REPL into the raw-text path for any model that ships its template as a separate Jinja file (e.g.mlx-community/gemma-4-e4b-it-4bit).is_wanted_filenow also accepts*.jinjafiles; theis_safe_relative_pathandis_explicitly_deniedguards still run before the allow-list so no new attack surface is opened (#132, PR #134).mlxcel runwarning for models without a chat template is now actionable: it states that the model is likely a base / non-instruction-tuned model, that chat replies will be incoherent or repetitive, suggests trying an-it(instruction-tuned) variant on the Hub (e.g. forgemma-4-e4b-4bit, trygemma-4-e4b-it-4bit), and explains how to proceed silently (--no-chat-template) or with one-shot completion (mlxcel generate -p <prompt>). The explicit--no-chat-templatepath remains completely silent (no regression) (#132, PR #134).
- GB10 (NVIDIA Grace Blackwell) doc refreshed to the 2026-05-28 full sweep on mlxcel 0.1.0 with MLX pin
84961223and the warm same-process harness (--cooldown 0). Adds the recoveredinternvl3-1bandmolmo-7btext rows and three VLM image-path entries (qwen2-vl-2b,qwen2-vl-2b-4bit,qwen3-vl-30b-a3b). The cross-hardware decode table inmodel_tests.mdnow reflects the canonical state of each per-hardware doc: GB10 2026-05-28, M1 Ultra 2026-05-28, M5 Max 2026-05-27 (all on mlxcel 0.1.0, same MLX pin, same same-process harness). The "vs 2026-05-19" delta framing is dropped so the doc reads as a current-state snapshot, and thePartial (⚠️)status is collapsed intoPass (✅)because the partial-token information already lives in the Notes column. Updated GB10 Overall Status counts: 101 text pass / 8 fail, 38 VLM image-path pass / 0 fail (#131).
- macOS release binaries are now notarized. The release workflow submits signed
mlxcelandmlxcel-serverto Apple's notary service viarcodesign notary-submit --waitso Gatekeeper no longer blocks first launch with "developer cannot be verified". Stapling is skipped because bare Mach-O executables do not support stapling, andspctl --assessruns as a soft warn-only check since the notary ticket may still be propagating. Paired withrcodesign verifyafter signing to catch a broken signature before shipping,set -euo pipefailon the prepare-cert and code-sign steps so a failure on the first binary does not silently fall through to the second, surfacedopenssl pkcs12stderr on extraction failure, up-front validation ofAPPLE_CERTIFICATE/APPLE_CERTIFICATE_PASSWORD/AC_API_*secrets,chmod 600on the materialized PEM and API key files, and an always-run cleanup that scrubssigning.pem,original.p12,AuthKey.p8,ac-key.json, and the notarization zip from$RUNNER_TEMPso self-hosted runners no longer carry an unencrypted Developer ID private key across jobs. - Per-target
workflow_dispatchfilter on the release workflow (targets:all/macos/linux). Re-uploading a single platform's artifact to an existing release (for example retrofitting notarized macOS binaries onto a release that was cut before notarization landed) no longer rebuilds and replaces the other platforms' bit-different (timestamp-driven) zips, so any sha256 pinned by a downstream consumer remains valid. Release events still build everything; the filter is dispatch-only. Modeled after the per-familytargetsfilter inall-smi's release workflow. actions/checkoutref pinned to the target release tag in both the macOS and Linux CUDA jobs. The ref is resolved asgithub.event.release.tag_nameon release events,github.event.inputs.release_tagonworkflow_dispatch, otherwisegithub.sha. Without an explicit ref,actions/checkoutwould grab the dispatched ref (which ismainforworkflow_dispatch), so re-dispatching a build for an older tag would silently usemainHEAD's source instead of the tag's source. The workflow YAML itself still runs from the dispatched ref so a CI-only fix can be applied onmainand replayed against an old tag without rebuilding from newer sources, matchingall-smi's self-healing release pattern.
v0.1.0 - 2026-05-28
mlxcel run <repo-id-or-path>subcommand (#102, epic #92). Capstone of the unified download + run epic, mirroringollama run/ mlx-lm ergonomics. With no-p,runenters the interactive chat REPL via the sharedrun_chatentry point; with-p, it produces output byte-identical to the equivalentmlxcel generate -m <model> -p <prompt>throughrun_generate_once. With no model argument,runfalls back tomlx-community/Llama-3.2-3B-Instruct-4bit, matchingmlx_lm.generate/mlx_lm.chat'sDEFAULT_MODEL. The model is a positional argument somlxcel run <repo-id>reads likeollama run, and the repo-id auto-downloads through the shared resolver on first use. Sampling/generation/TurboQuant KV-cache flags are shared withgeneratevia clap argument groups; advanced groups not exposed byrun(tensor/pipeline parallel, speculative, lang-bias, surgery) are lowered to clap defaults, pinned by a drift-guard test.- Interactive multi-turn chat REPL (#101).
mlxcel generatewithout-p/--promptnow enters a chat loop that streams the assistant reply token-by-token, preserves conversation context across turns by re-rendering the full transcript through the chat template each turn, and supports/bye,/clear,/?(alias/help), and ollama-style"""multiline input blocks. The REPL forks no generation code: it reusesresolve_model_source,MlxcelTokenizer,ChatTemplateProcessor,build_sampling_config,CxxGenerator::generate_streaming, and the server's byte-fallback-safeStreamingDecodeState. Factored as a publicrun_chat(ChatOptions)entry point so the newmlxcel runverb dispatches into it. The end-of-turn flush also re-emits any UTF-8 suffix the streaming detokenizer held back mid-stream, so the displayed reply is complete instead of byte-truncated. - Local model management.
mlxcel list --localenumerates downloaded snapshots under${MLXCEL_CACHE_DIR:-$HOME/.cache/mlxcel}/models/with repo-id, on-disk size, and absolute path, recognizing both<owner>/<name>and bare<name>store layouts and gating onconfig.jsonso partial directories are skipped. The baremlxcel list(architecture summary) is unchanged.mlxcel rm <repo-id>deletes from the mlxcel store and reports freed size; it prompts on a TTY, refuses on a non-TTY without--yes, contains deletion tostore_root()/models/(path-sanitized bymodel_dirand re-asserted beforeremove_dir_all), and treats HF-cache-only models as read-only with explicit guidance instead of silently deleting (#99). - Repo-id-aware
-m/--modelacrossgenerate,serve,inspect,mlxcel-server, andrun(#100, #92).-mnow accepts either a local path or a HuggingFaceowner/namerepo-id with a locked resolution precedence: existing on-disk path used verbatim (byte-identical to the pre-#100 local-path behavior, even when the path looks likeowner/name); otherwise a repo-id matching^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$is resolved as legacy per-CWD./models/<basename>then HuggingFace cache snapshot (read-only reuse) then mlxcel global store then auto-download into the store. The legacy and store reuse branches gate on a presentconfig.jsonso a half-written or unrelated directory is treated as a miss rather than handed to a model loader that would then fail. Each subcommand resolves its-mvalue once at the top of the handler, leaving downstream.modelconsumers unchanged.generatereorders surgery YAML validation ahead of the resolver so--surgery <bad.yaml>still fails fast without a network download. - Global model store and HuggingFace cache read-reuse (#98, epic #92 foundation). The default download destination moves from per-CWD
models/<basename>to${MLXCEL_CACHE_DIR:-$HOME/.cache/mlxcel}/models/<owner>/<name>, namespaced so two repos with the same name across owners do not collide.download_repo()short-circuits to an existing snapshot under$HF_HUB_CACHE/$HF_HOME/~/.cache/huggingface/hubwhen no--local-diris pinned and--forceis off; mlxcel never writes into the HF content-addressed layout. Branch / tag revisions are resolved via therefs/<rev>pointer to the snapshot SHA; raw commit hashes resolve directly. Newmlxcel_core::cache_root()is the single source of truth forMLXCEL_CACHE_DIR/ home-dir resolution, shared with the tokenizer language-analysis disk cache. MLXCEL_MODELS_DIRenvironment variable and uniform--models-dirmodel-store override (#108). Precedence: inline--models-dir>MLXCEL_MODELS_DIR>${MLXCEL_CACHE_DIR:-$HOME/.cache/mlxcel}/models. Wired throughdownload,generate,serve,inspect,run,list, andrm. Closes #107.- Bare model names default to the
mlx-communityorg (#113). A value with no slash (e.g.Qwen3-4B-4bit) now resolves asmlx-community/<name>rather than erroring immediately. The expansion runs inresolve_model_source_with_overrideas a new step in the locked precedence (after existing-path andowner/namerepo-id, before the error arm), so pre-existing behavior for all currently-valid inputs is byte-identical. The default org is overridable viaMLXCEL_DEFAULT_ORG; unset or whitespace-only values fall back tomlx-community, and an invalid org (e.g. containing a slash) is caught up front and produces a clear error without any network request. Help text and the README quick start document the bare-name shortcut.
mlxcel-serverlegacy startup-m(epic #92 hardening) now uses the same repo-id resolver and--models-dirstore override asmlxcel serve, including the safetensors-only presence check. Preserves byte-identical behavior for existing local paths and aligns docs/tests with the resolver-backed path.- Video resource caps are now injected via a
VideoLimitsstruct resolved once at the boundary, instead ofapply_probe_capsandsplit_png_streamreadingMLXCEL_VIDEO_MAX_PIXELS,MLXCEL_VIDEO_MAX_DURATION_SEC, andMLXCEL_VIDEO_MAX_PNG_FRAME_BYTESfromstd::envdeep in the decode path. The previous design leaked test-mutated env values into concurrently running tests under the threadedcargo testrunner (the load-video fd-vs-path parity test intermittently saw a leakedMLXCEL_VIDEO_MAX_DURATION_SEC=2and failed withDurationTooLong { seconds: 4.0, max_seconds: 2.0 }) and was unsound because concurrent setenv/getenv is a libc data race.load_video_source/load_video/load_videoskeep their signatures and internally callVideoLimits::from_env(), so production callers are unchanged; new_with_limitsvariants accept an injectedVideoLimitsfor tests (#104). - Pipeline runtime tests now bind
127.0.0.1:0directly throughTcpTransportand resolve the real port vialocal_addr(), removing the release-then-rebind window in the previousreserve_bind_addresshelper. Eliminates the intermittent "stub stage startup channel dropped" failure that happened when another concurrently-running test grabbed the freed ephemeral port between release and re-bind. Verified by 16 consecutivecargo test -p mlxcel --lib distributed::pipeline::runtimeruns with 0 failures (#106). require_secure_endpoint_refuses_plaintext_with_tokentest now acquiresenv_lock()at the start, matching the contract its sibling opt-out tests already follow. Without the lock, when a sibling test setMLXCEL_ALLOW_INSECURE_ENDPOINT="1"underenv_lock, the refusal test could observe that value mid-window and take the opt-out path, returningOkinstead of the expectedErrunder the threaded test runner. Removes the concurrent setenv/getenv libc data race the previous test exposed (#111).
- README "Run a model" section now leads with the
mlxcel runone-liner instead of the verbose explicit-download flow, and collapses the core verbs (generate,serve,inspect,--estimate-memory) into a single one-line-comment block. Store-root precedence,-mresolution rules,--local-dir/--models-dir,MLXCEL_DEFAULT_ORG, and the memory preflight env vars now live behind a single link todocs/environment-variables.md(which already documents them) and a compressed model-management paragraph. Net: README drops about 58 lines with no loss of documented behavior (#114). - M5 Max detailed table, README headline version, and the benchmark report method table refreshed to the 2026-05-27 full-sweep state on mlxcel 0.1.0.
internvl3-1bnow passes in both text (661 tok/s) and VLM (601 tok/s, ahead of mlx-vlm's 529 tok/s), raising the M5 Max pass count to 94.paligemma2-3b-6bittext decode is 168.83 tok/s andqwen3-vl-30b-a3b-4bitVLM is 56.38 tok/s over a 45-token sample. Aggregate parity statistics and the M1 Ultra / GB10 columns remain on the 2026-05-19 baseline campaign that was not re-run (#115). - M5 Max decode aggregates recomputed against the unchanged
mlx-lm/mlx-vlmbaselines (baselines unchanged, only mlxcel-derived ratios recomputed). Text decode average 98% to 99% (median 99%), 62 of 66 at >=90% parity; the benchmark report headline is corrected from the stale 58 of 66. VLM decode average 101% to 102%, median 100% to 101%, 22 comparable pairs (was 20), 18 of 22 at >=90% parity (coverage counts reflect internvl3). README decode tables add Gemma 2 2B (100%), Phi-3.5-mini (98%), Jamba (98%), and InternVL3 1B (114% vs mlx-vlm) (#127). - 2026-05-28 M1 Ultra full sweep mirrored into the public benchmark docs (mlxcel 0.1.0;
mlx-lm/mlx-vlmbaselines unchanged). README M1 Ultra column refreshed across both representative decode tables (Phi-3.5-vision M5 VLM row corrected to its post- value, 169 tok/s, 106%); benchmark report headline updated to text 74 pairs / 99% median / 64 of 74 at >=90%, VLM 18 pairs / 98% median / 12 of 18 at >=90%;model_tests_m1ultra.mdper-model refresh with aggregate blocks (text avg 97% / median 99%; VLM avg 101% / median 98%) andinternvl3now passing on M1. The cross-hardware quick table stays a labeled 2026-05-19 same-version snapshot (#130). - Reword the v0.0.31 #86 entry to match the GitHub release note. The fix landed at the batch scheduler level: a burst of concurrent VLM requests overwrote a single shared
per_layer_inputscell before prefill consumed it, reading the wrong sequence's tensor. The earlier wording described the symptom as a per-layer input shape issue, which understated the concurrency cause.
v0.0.31 - 2026-05-27
- MiniCPM-V 4.6 VLM architecture, including hardened image grid handling (#82, #83).
- RT-DETRv2 object detection model exposed through the new
mlxcel detectsubcommand (#80). - Anthropic-style
/v1/messagesAPI endpoint on the server for Messages API clients (#74).
- Chat message
contentthat is missing or explicitlynull, such as assistant tool-call turns, is now tolerated instead of being rejected with an HTTP 422, restoring multi-turn tool loops for OpenAI-compatible clients (#91). - Gemma 3n VLM
per_layer_inputsis now keyed per sequence id, so a burst of concurrent VLM requests in the batch scheduler can no longer race on a single shared cell and read the wrong sequence's tensor (#86). - Qwen3.5 MTP speculative decoding uses per-position verify attention so the draft and verify passes stay in parity (#78).
- Batched quantized KV caches now apply the correct mask offset (#76).
- Document the
MLXCEL_CAPTURE_DECODEenvironment variable and clarify the memory headroom wording (#72).
- Pin the Rust toolchain to 1.93.1 for reproducible builds (#87, #90).
- Bump the
minor-and-patchdependency group:serde_json1.0.149 to 1.0.150 andminijinja2.19.0 to 2.20.0 (#84). - Exclude the root
modelssymlink (#88) and AI assistant temporary directories from.gitignore.
v0.0.30 - 2026-05-23
- Unified pre-load memory estimator (epic #52).
mlxcel inspectis a new read-only subcommand that prints a byte-level breakdown of model weights, KV cache, and runtime headroom against available unified memory without loading any tensors.--estimate-memoryonmlxcel generateandmlxcel serveruns the same estimator as a preflight and aborts when the model will not fit;--force(alias--no-memory-check) overrides the abort,MLXCEL_MEMORY_LIMIT=NGBtightens the available figure to a soft cap, and the runtime headroom factor defaults to1.20x(#67). - Exact weight footprint from the safetensors header. The estimator parses the safetensors header to derive real per-dtype byte counts without materializing tensors (#64).
- KV cache memory estimator with 256-token rounding that matches the runtime's pre-allocation steps (#65).
- MLX runtime memory API bindings that expose the active, peak, and limit byte counters through FFI (#66).
- Molmo v1 (molmo-7b) VLM architecture (#41).
- InternVL (internvl_chat) VLM architecture (#37).
- Server parallel context sizing:
--ctx-sizeis now treated as a total context budget shared across active request slots, matching llama.cpp server semantics.--parallel N --ctx-size Cyields an effective per-slot window offloor(C / N); explicit--max-batch-sizevalues share the same budget,--no-batchkeeps a single full-context slot,/slotsreports the per-slot window, startup rejects per-slot windows below 512 tokens, and memory preflight uses the same sizing model (#57).
- Gemma 3n bf16 decode reduces AltUp/MLP graph overhead (#60) and improves M5 decode bandwidth with pretransposed weights (#62).
- Phi-3.5 SuScaledRoPE decode speedup (#42).
- Gemma dense GeGLU aligned with the mlx-lm reference for faster decode (#43).
- Jamba hybrid decode speedup (#44).
- CLI boolean cache flags are now validated, and CLI flags correctly take precedence over their environment-variable equivalents (#70).
- Prompt cache radix trie is now iterative, preventing a stack overflow on deep prompt prefixes (#63).
- Gemma 3n gates the bf16 fused decode path off the M5 Neural Accelerator so output stays correct on that hardware (#61).
- CUDA Hopper builds append the
90aarchitecture suffix for auto-detect and fallback builds (#51). - VLM server image decoding is hardened to skip invalid entries instead of failing the whole request (#50).
- Qwen2-VL image placeholder is expanded to the full grid count (#39).
- Tighten memory estimator preflight coverage so the abort path is exercised across
generateandserve.
- Refresh the M1 Ultra and README decode benchmark figures for the Molmo / Phi-3.5 / Gemma / Jamba / InternVL work (#45, #46).
- Correct the M5 Max baichuan-m1-14b decode comparison and flag the qwen3-0.6b gap (#49).
- Drop change-cause notes from the result tables to keep them current-state only (#47, #48).
- Add a qwen2.5-vl-3b-4bit warmup regression guard (#38).
v0.0.29 - 2026-05-20
mlxcel-bench-decodesame-process benchmark harness. Loads a model once, runs the warmup pass, resets model and cache state, then runs the measured pass in the same process. This mirrors the Pythonstream_generatetiming far more closely than two coldmlxcel generateinvocations, especially for prefill.scripts/bench_decode.shnow drives this binary, andscripts/bench_mlxlm.pyprovides the matching mlx-lm / mlx-vlm baseline sweep (#36).
- Model-owned VLM fallback state reset. Single-row CLI and benchmark generations, which do not carry a
SequenceId, reused stale fallback caches between runs. A newLanguageModel::reset_runtime_state()hook, invoked fromCxxGenerator::reset_with_model, clears the model-owned fallback slot in lockstep with the generator-owned cache vector for Gemma 3, Gemma 4, Llama 4, and Qwen 3.5 Next (#34). - Gemma 3n VLM padded prefill alignment. Gemma 3n VLM prefill aborted with a
[broadcast_shapes]mismatch (for example(1,288,256)vs(1,273,256)) when the projected per-layer-inputs tensor diverged from the tile-padded token stream. The per-layer tensor is now aligned to the embeddings sequence length (pad with zeros when shorter, slice when longer, leave untouched when equal) before the per-layer blend (#36).
- Reorganize benchmark result reports and refresh the Apple Silicon and M5 Max VLM benchmark tables (#33).
- Clarify the README benchmark phases, the model surgery advantage, and the Qwen3.5-0.8B-4bit quickstart (#32).
v0.0.28 - 2026-05-19
- Gemma3n bf16 prefill path. Materialize full-precision bf16 casts and preserve the Gemma3n language MLP bf16 path; gemma3n-e4b-bf16 decode improves from 11.16 to 38.81 tok/s on M5 Max while output stays coherent (#28).
- Qwen3-VL text-only decode. Fuse Q/K/V into a single
FusedQKVLinearand add aforward_text_onlyfast path on Attention / DecoderLayer / Model that skips MRoPE position-id computation and visual-state propagation when no image is present. Qwen3-VL-30B-A3B-Instruct-4bit decode 56.00 to 146.29 tok/s (2.61x, 99% of mlx-lm parity); Qwen3-VL-32B-Instruct-4bit decode 18.79 to 27.33 tok/s (1.45x, 94% of mlx-lm parity). Image-in-prompt and DeepStack paths are unchanged (#29). - GatedDeltaNet fast RMSNorm. Replace the expanded
square/mean_axis/sqrt/divide/multiplyQ/K and gated-output RMSNorm graphs in Qwen3.5, Qwen3-Next, and KimiLinear withmlx::fast::rms_normkernel calls via a new sharedscaled_fast_rms_norm_no_weighthelper. qwen3.5-0.8b-4bit decode 425.16 to 535.43 tok/s on M5 Max (+25.9%, 96% of mlx-lm parity); the Qwen3.5 speculative-decoding verify pass is updated alongside prefill / decode so draft / verify dtype agreement is preserved (#30).
- Rewrite
mlxcel listoutput to drive from theModelTyperegistry: fixes stale count, missing VLM family, missing ~30 model types, and removes broken docs link (#27).
- Refresh README performance snapshot and
docs/benchmarks.mdto match the latest M5 Max sweep.
v0.0.27 - 2026-05-16
- End-to-end speculative decoding for Gemma 4 MTP and Qwen 3.5 DFlash drafter families. New
Draftertrait +DrafterKindenum +model_typeauto-detection. Ported drafter components:MaskedEmbedderfor Gemma 4 E2B / E4B, drafter masks (bidirectional full + sliding-window) andnormalize_batched_shared_kv_stateshelper,Gemma4AssistantDraftModel(4-layer drafter + pre/post projections), andDFlashDraftModel(5-layer drafter +DFlashAttention+DFlashKVCache). Target-side hooks: Gemma 4return_hidden/return_shared_kv/rollback_speculative_cachefor MTP; Qwen 3.5return_hidden+capture_layer_ids+ GDN-awarerollback_speculative_cachefor DFlash. Round loops: DFlash single-batch,MtpGeneratorsingle-batch, batched DFlash with continuous batching + GDN-aware rollback, and batched MTP with continuous batching + left-padding normalization. Greedy-parity + perf benchmark scaffolding undersrc/bin/speculative_bench.rsand real-model byte-equality end-to-end tests. - Server speculative dispatch. Speculative dispatch resolution and
MtpTargetadapters wired into the inference server; the assistant model paths now plug into the realMaskedEmbedderandmake_drafter_masks; speculative dispatch is wired into the scheduler via per-request B=1 bursts and a B>1 batched path viaMtpBatchedGenerator/DFlashBatchedGenerator. Per-request properties propagated through the speculative-burst path: cancellation propagation throughMtpGenerator/DFlashGenerator,token_historythreading through the speculative-burst first sample, logprobs support, thinking-budget enforcement, and prompt-cache donate symmetric with the classic path and into the B>1 batched arm. - CLI:
--draft-kind {dflash,mtp}and--draft-block-sizeflags on bothmlxcelandmlxcel-server. - OpenAI Responses API (Phase 1) at
/v1/responsesfor both binaries. New modulesresponses_store,responses_translator,conversation_store,streaming_responses,routes/responses, and request/response/stream type modules undertypes/responses_*. Implements conversation store with shared-LRU semantics,response.created/response.in_progress/response.completedSSE event stream, reasoning-trace forwarding, response cancellation, and four new CLI flags. User guide atdocs/responses-api.md. - APC block-level partial cache adoption in the scheduler. When Automatic Prefix Caching is enabled, a request whose prompt shares the first N blocks with a cached entry but diverges at block N+1 now reuses blocks 0..N and re-prefills only from the divergence boundary — rather than cold-prefilling the entire prompt. Three components:
DetachedKVCache::trim_toandDetachedCacheSet::truncate_toinmlxcel-coreperform per-layer KV tensor slicing on the detached handle (mirroringKVCache::trimsemantics, covering FP16/INT8/Turbo4/Turbo4Delegated sidecars); thePromptCacheStorelookup relaxes the legacy "stored prefix must be fully contained in request" gate when APC is on, routing the actual common-prefix depth through the existingapc_consistent_prefix_lenblock-hash discriminator; andScheduler::try_adopt_cached_prefixcallstruncate_to(matched_len)before adoption when the lookup returns a sub-entry-length match. APC-off retains the earlier behaviour bit-exactly. Wall-clock bench procedure on Apple Silicon documented indocs/apc-partial-adoption-bench.md. - Nemotron H Nano Omni audio modality. Ports the Parakeet/Conformer sound encoder (
NemotronOmniSoundEncoder), mel-spectrogram feature extractor (NemotronOmniFeatureExtractor), and audio projector (NemotronOmniSoundProjection) from the upstreammlx-vlmPython reference. The encoder implements depthwise/pointwise Conv2D subsampling, Transformer-XL relative positional encoding, multi-head self-attention with per-head u/v bias terms, a GLU+BatchNorm convolution module, and half-weight feed-forward blocks. Audio weights are loaded conditionally whensound_configis present inconfig.json; the loader applies the upstreamsanitize_audio_weightstranspose pass before population. The VLM runtime path (generate_vlm) accepts--audio <wav>, runs the feature extractor and encoder, merges the resulting token embeddings atsound_context_token_idslots, and interleaves them with vision tokens when both modalities are present. Bring-up procedure for Apple Silicon engineers documented indocs/nemotron-h-nano-omni-audio-bringup.md. mlxcel download/mlxcel-server downloadprogress bars. Newsrc/downloader/progress.rsmodule provides terminal-aware suppression (should_show_progress), aMultiProgressfactory, and 6 suppression unit tests. The downloader streams files viareqwestto aNamedTempFileand atomically renames into place; outerstream_fileand innerstream_to_tempfileare split so the progress bar covers the network read and the rename is observable.- Server
--max-kv-sizeflag matching llama-server, plus a tightened chat-completion response envelope. - Tokenizer support for multi-token think and tool-call sequences so chat templates that emit
<think>/<tool_call>across multiple BPE tokens stream and parse correctly.
StreamFilterextended to handle multi-token markers and reset state when a partial marker is broken by a non-marker token.- Speculative drafter epic follow-ups hardened post-merge — covers misc invariants surfaced by integration testing against the real
z-lab/Qwen3.5-4B-DFlashcheckpoint and themlx-community/gemma-4-*drafter variants. - README and speculative decoding guidance refreshed to match the current code paths and the latest M1 Ultra / M5 Max benchmarks.
- Qwen 3.5 DFlash greedy-argmax decode-path optimization that drops the per-decode-step copy and an unnecessary argmax temporary, restoring decode tok/s on Qwen 3.5 32B / 9B DFlash configurations.
- Avoid slow Gemma 4 MTP singleton bursts — the speculative-burst path now correctly short-circuits to the classic path when the batch size collapses to 1 with no draft tokens accepted, eliminating a per-step over-evaluation regression introduced by the initial dispatch wiring.
- DFlash drafter lazy-bind for the upstream
z-lab/Qwen3.5-4B-DFlashcheckpoint —Drafter::bindwas previously not called on the DFlash family, causing an internal cache mis-binding on the first speculative burst. The drafter now performs lazy-bind on first use, matching the MTP path. - Enable DFlash for Qwen 3.5 VLM text requests — pure-text generations against a Qwen 3.5 VLM checkpoint can now resolve a DFlash drafter when one is available, instead of silently falling back to the classic path.
- Speculative-rollback safety: validate trimmable cache and reserve the last token in prefill so a rolled-back speculative burst always lands on a valid sampling boundary.
- Prompt cache RadixTrie:
pop_prefixesnow uses correct immediate-prefix semantics. - MiniMax M2 parallel tool calling parser correctly emits one
ChatToolCallper parallel call instead of merging them into a single call. - Server tool-call buffering: preserve token positions when buffering parallel tool calls; skip the tool→normal transition when
tool_call_endis empty so streaming continues correctly for templates without an explicit close marker. video_urlallowlist TOCTOU race closed by passing the resolvedOwnedFdto ffmpeg via/dev/fd/Ninstead of re-opening the path inside the subprocess. Symlink swaps betweenmetadataand the subsequent open now cannot mis-route the subprocess. Audit hardening.- Gemma 4: skip
k_proj/v_proj/k_normweight load for KV-shared layers — the previous load step would error out on real Gemma 4 E2B / E4B checkpoints that omit these tensors per KV-shared design. - Nemotron-H: default
time_step_limitto(0.0, +inf)regardless oftime_step_min/time_step_maxto match the upstream mlx-lmtime_step_limitbehaviour even when only one of the bounds is supplied. gated_deltamasked Metal kernel variants: zero-inity[dv_idx]when the mask is false.- Tests: add
max_kv_sizefield to theServeArgstest fixture. - Address upstream-sync review follow-ups carried over from the v0.0.26 sync cycle.
- Downloader security hardening post-plaintext
HF_ENDPOINT+ token warning (M1), client connect / read timeouts (M2), URL path percent-encoding for?/#/%in filenames (L1),O_NOFOLLOWtempfile creation (L2),Result-based token-ASCII handling instead ofexpect("token must be ASCII")(L3), stale.mlxcel-partial.*cleanup at download start (L5), and parallel HEAD requests bounded bybuffer_unordered(8)(L6). - Closed the residual TOCTOU window in the
video_urlallowlist resolver. The dominant canonicalise → ffmpeg-open race was already closed by passing anOwnedFdto ffmpeg via/dev/fd/N. This change hardens the narrowed metadata→open gap by opening every allowlisted video file withO_NOFOLLOW: a symlink swap that occurs between themetadatacall and theopensyscall now returnsELOOPinstead of silently following the swapped-in link. Subprocesses continue to receive/dev/fd/N(never the path), so they cannot be misdirected post-open regardless. A startup warning is now also emitted on non-Unix targets whenMLXCEL_VIDEO_DIR_ALLOWLISTis set, becauseO_NOFOLLOWand fd-passing are unavailable on those platforms.
- Bump GitHub Actions to Node 24 runtime to clear the Node 20 deprecation warning surfaced on the macOS runners.
- Replace
.map_or(false, ...)with.is_some_and(...)in tokenizer call sites (clippy 1.93 lint clean-up).
v0.0.26 - 2026-05-10
- TurboQuant KV cache. New 3–4 bit KV cache compression family built on a Walsh–Hadamard transform op and a Lloyd-Max PolarQuant codebook generator ported to Rust. Four KV cache modes wired through
KVCacheMode:Turbo4symmetric with per-model allowlist,Turbo4AsymFp16-K + Turbo4-V,Turbo3Asym3-bit Fp16-K + Turbo3-V, andTurbo4Delegatedwith a FP16 hot tail + packed turbo cold body. TurboQuant +RotatingKVCacheintegration covers sliding-window attention (B9). Sparse-V dequant scaffolding, Boundary-V layer protection that keeps the first/last layer at FP16, and a packed-awarePagedKvLayoutround out the runtime. Quality gates: wikitext-2 PPL + NIAH harness, full 283K-token test split fixture, per-model PPL/NIAH results committed, and VLM B3 quality gates with image-token kurtosis. Speed gate matrix runner with M1 Ultra and M5 Max readings. User guide and validated config matrix published. - Server flag parity for KV quantization.
--cache-type-kand--cache-type-vflags acceptf16,q8_0,q4_0, etc. matching llama-server semantics, with TurboQuant modes exposed asmlxcel_turbo*variants. KV cache quantization extended to continuous batching and a unified--kv-cache-modeflag layout shared acrossmlxcel,mlxcel-server, andmlxcel download. - Automatic Prefix Caching (APC) with hash blocks. Hash-keyed block-table prefix reuse on top of v0.0.25's cross-sequence prompt-prefix KV cache, enabling shared physical blocks across requests with the same hashed prefix without per-request token-prefix matching cost.
- OpenAI-compatible
response_format: {"type": "json_schema", ...}structured-output support for/v1/chat/completionsand/v1/completions. Constrained decoding viallguidance(the same backend used by upstream mlx-vlm PR #1047) ensures every emitted token keeps the partial output conforming to the supplied schema. Per-request schema validation enforces a 64 KiB size cap, a 32-level nesting depth limit, and a 64-entry$refcount limit so an adversarial schema cannot exhaust CPU or memory during grammar compilation. The tokenizer environment is cached by SHA-256 fingerprint so consecutive requests to the same model share the build cost (~1–2 s for a 150k-vocab tokenizer). Reusable per-sequencemask_bufandbias_bufallocations eliminate per-tokenVecallocation on the hot decode path. The legacyjson_objectmode is rejected with a clean 400 in this MVP;json_schemawith a well-formed schema is the supported path. Supported on HuggingFace BPE tokenizers; SentencePiece and Tiktoken backends return a cleanUnsupportedTokenizererror. Verbose llguidance internals are never surfaced in public error messages — they are routed to server-side tracing only. mlxcel download/mlxcel-server downloadsubcommand to fetch HuggingFace model repository snapshots without Python tooling. Useshf-hubwith an allow-list file filter (SafeTensors, tokenizer, and config files only), cache-hit detection, and formatted per-file progress output. Supports--local-dir,--revision,--token, and--force. Default destination mirrors themodels/<repo-basename>convention from AGENTS.md./healthendpoint now includescontext_size(the configured--ctx-sizevalue;0means model default) andtool_call_parser("mlxcel"when the chat template exposes thetoolsvariable,nullotherwise). Both fields are present once a model is loaded;context_sizeis absent while loading,tool_call_parserserializes asnullduring startup so monitoring clients can distinguish "template has no tool support" from "model not yet loaded". The tool-support heuristic is extracted into a sharedtemplate_mentions_tools()helper used by both the health route and the existingcompute_supports_toolsfallback path.- Paged scheduler dispatch on
PagedKvLayout::cache_modeso the scheduler routes batches into the matching paged decode kernel for each KV cache mode. - Video input infrastructure for VLMs. Gemma 4 video support with the new VLM video input pipeline; ffmpeg-backed frame extraction with single-pass extraction and a
Dropguard for cleanup of temporary frame files;video_urlcontent blocks wired through/v1/chat/completions; content-preservation tests covering the frame extraction path. - New models. Youtu-VL vision-language model; Nemotron H Nano Omni vision plus follow-up correctness/validation hardening.
- Multi-task M1 Ultra benchmark refresh to 2026-05-08 and full M1 Ultra column resync in
benchmarks-by-hardware.md.
- MLX upstream pin bumped twice. First from the v0.0.25 baseline (
5d7e96cd) to v0.32.0 /c9aa5605, then forward to84961223covering 3 PRs: #3443 splits the CUDAqmm_naive/qmm_sm80kernel bodies into newqmm_naive.cuh/qmm_sm80.cuhheaders without changing the public ABI consumed by mlxcel'spatches/mlx/backend/cuda/quantized/qmm/qmm.h; #3463 routes the CPU JIT preamble throughJitCompiler::get_preamble()and renames the prebuilt symbol fromget_kernel_preambletoget_prebuilt_preamble(mlxcel does not call either directly); #3475 fixes contiguity-flag accuracy inAsStridedby computingdata_sizefrom the actually-occupied stride range. Three-location pin update applied tosrc/lib/mlx-cpp/CMakeLists.txt,src/lib/mlxcel-core/build.rs, and.github/workflows/release.ymlperCLAUDE.md. Fused Metal kernel launchers insrc/lib/mlx-cpp/turbo/re-validated against both bumps:mlx::core::fast::metal_kernel,mlx::core::full,mlx::core::Shape,mlx::core::float32,mlx::core::int32, andmetal::fast::expsymbols unchanged. - Refactor: unified TurboQuant KV-cache CLI flags across
mlxcel,mlxcel-server, andmlxcel downloadso all binaries accept the same--kv-cache-mode/--cache-type-{k,v}syntax. - mlx-lm version reference in docs bumped from 0.31.2 to 0.31.3. The
bridge-overhead-microbenchreference at v0.31.2 is preserved because it pins the MLX C++ runtime, not the mlx-lm Python package.
- Sparse-V kernel: fused per-thread Metal kernel that skips the full SDPA pass when sparse-V dequant predicts zero contribution; precomputed kernel rescale to drop per-token threadgroup barriers.
- Turbo4Delegated decode hot path: unified K storage to drop the per-step K concat; cold-V dequant cache across decode steps followed by a cold-V dequant Metal kernel that retires the FP16 memo; steel-attention-envelope fused SDPA kernel with parallelized Pass 1 softmax; delegated FP16 predecode compaction and lazy delegated FP16 sidecars; compressed fold moved before decode.
- Compressed dequant-SDPA paths for TurboQuant decode.
- Server hot-path: thread-local generation stream and uniform-batch RoPE collapse to remove per-request allocation in the steady-state batching loop.
- TurboQuant continuous batching: correct batch cache offset merging when batches with different cache offsets are joined or split; Turbo3 split-flag, documentation alignment, and an
ENV_LOCKrace in concurrent process startup. - Vision / VLM mixed batching: per-sequence MRoPE alignment for mixed VL+text batches; per-sequence
per_layer_inputsfor Gemma 4 E2B/E4B VLM; mixed-length batching support for Gemma 4; relaxed cached-position shape check in Qwen VL chunked prefill; Qwen3.5-MoE batch-size validation on cachedposition_idsreuse. - Streaming and sampling: correct streamed detokenization for byte-fallback tokens that previously leaked raw byte fragments to the client; top-p filter correctness for batched logits; token queue timeout handling during long prefills so clients no longer see spurious 408s on slow first-token paths;
StreamFilterextended to cover Hermes-style<tool_call>/</tool_call>and Mistral Nemo[TOOL_CALLS]markers, which previously leaked raw markup intodelta.contentduring streaming. Partial-marker buffering at token boundaries correctly holds back prefixes (e.g.<tool_) until the full tag can be confirmed, then releases them todelta.contentif they turn out not to be a boundary. Gemma 4<|tool_call>suppression is unaffected; the delimiter table ordering ensures the Gemma 4 pipe-delimited form wins the tiebreak over the Hermes plain form. - Models: Gemma3-4B attention SIGABRT from a sliding-window mask
T_kmismatch on long-context prompts; preserve Qwen2 fused QKV bias when it is present in the checkpoint; test fixture swap to Qwen2.5-1.5B base variant for the B3 quality gate; harden post-merge review findings on the Nemotron-H Nano Omni vision PR.
- Path-traversal defense in the downloader:
is_safe_relative_pathpre-filters each sibling filename returned by the HuggingFace API (rejects absolute paths,..components, backslash separators, and empty components). A secondary canonicalizedstarts_withguard on the resolved destination path is applied before writing each file. Download target files are written to a temporary path and atomically renamed into place, preventing partial writes from leaving corrupt files in the output directory (fixes C1 and H1 from security review). - Structured-output schema limits (64 KiB serialized size, 32 nesting depth, 64
$refcount) and tightenedllguidanceparser caps (max_grammar_size: 100 000,max_lexer_states: 50 000) applied before grammar compilation so an adversarial client cannot use the schema endpoint as a CPU/memory exhaustion vector. Schema content is never echoed in public error messages.
v0.0.25 - 2026-04-24
- Cross-sequence prompt-prefix KV cache. New
KVCache::trim/detach/adoptAPI enables adopting a previously-cached prefix on the next request. Backed byPromptCacheStore, an in-process LRU keyed by tokenized prompt prefix, plus a longest common token-prefix matcher (PrefixMatcher) for fast lookup. Paged KV cache gains block-table prefix reuse so adopted prefixes share physical blocks. Scheduler integration prefills only the unmatched suffix on cache hits. Wired into the server via--prompt-cache-size,--prompt-cache-min-tokens, and matchingLLAMA_ARG_*env vars; multimodal/vision-aware cache key (MultimodalDigest) prevents cross-modality collisions. OpenAI-compatiblecached_tokensis reported in/v1/chat/completionsresponses, mirrored to Prometheus counters, and verified by a multi-turn E2E test plus a prefill-latency benchmark. Design rationale and operator guide added to docs. - Language-bias steering (Axis B, Phase 1). New
lang_analyzermodule with a Unicode script classifier (B2) andTokenLanguageIndexbuilder that scans the tokenizer vocabulary, partitions tokens by script, and persists the result to disk for fast warm starts (B3, B4). Sampling primitiveTokenBiasMap+apply_token_biasis wired throughLangBiasSetwithConservative/Strictpolicies (B5), exposed via CLI flags and a YAML config (B6),LLAMA_ARG_LANG_BIASenv var inmlxcel-server(B7),LangBiasConfiginjection into the generator pipelines (B8), tracing fields and Prometheus counters (B9), byte-fragment CJK classification via UTF-8 start-byte analysis so byte-level BPE tokenizers correctly attribute fragments, byte-level reverse map for token decoding, and integration tests for the steering matrix (B10). User guide and Quickstart published (B11). thinking_token_budgetsampling parameter for the Qwen3 family — caps tokens emitted between<think>/</think>markers without disabling streaming.preserve_thinkingchat-template hook for Qwen 3.6 so multi-turn conversations retain prior<think>blocks instead of stripping them on subsequent turns.StreamFilterextended to recognize Qwen-style<think>/</think>token boundaries during streaming and route the segment intoreasoning_content.thinking_budget_tokensextended to Gemma 4.feat(benchmarks): bridge-overhead microbench tool measuring per-op cost of the Rust cxx bridge against Python nanobind across MLX primitives, with a published baseline and reproduction steps.feat(ci): multi-stage pipeline-parallel smoke job activated using a Qwen3-0.6B fixture so PR runs catch PP regressions.- Per-layer + per-sub-op decode profiling for Gemma 4, plus a Gemma 4 perf harness with the 2026-04-22 baseline used to drive the parity work below.
- Prompt cache prefix isolation — sequences whose prompts share a non-trivial prefix no longer leak adopted KV state across each other after detach/adopt.
MultimodalDigestpropagated to allPromptCacheKeycallers after the + merge so vision-aware cache lookups stay collision-free.- Gemma 4
enable_thinking=falseno longer triggers degenerate output, andreasoning_contentnow streams correctly whenenable_thinking=true. - Tool-only assistant turns now emit
content: nullinstead of""to match the OpenAI Chat Completions schema. chat-template: support flattenedextra_bodyand pseudo-user tool responses so OpenAI-style tool flows render correctly under HF-style templates.lang-analyzer: decode tokens using the byte-level reverse map instead of the textual tokenizer view, so byte-level BPE (Qwen, Llama) tokens are classified by their actual code-point payload.ci: unblock Pipeline Parallel CI on Ubuntu by installing LAPACK and treating clippy-D warningsconsistently.vision: read Gemma 4 encoderhidden_sizefrom afterinput_projso the multimodal projector wires the correct dimension on encoders that include a learned input projection.- Bumped
ccto 1.2.60 to silence the BSDarprobe warning surfaced by recentcc-rsreleases.
- Gemma 4 mlx-lm decode parity pass (closes the remaining gap on 26B / 31B / e2b):
- Router RMS norm fused with top-k-then-softmax to remove a separate normalization pass.
- SwitchGeGLU gate / up / geglu / down fused into a single
mlx::core::compilewindow. - Metal-trace-driven attention / RoPE / per-layer chain fusion.
- Compiled Gemma 4 SwitchGeGLU decode path enabled.
- Single-query causal masks skipped in decode.
- BF16 decode graph aligned with mlx-lm.
- Proportional RoPE aligned with mlx-lm (no rotated-only normalization).
- SwitchGLU projection order matched to mlx-lm.
- QKV projection shape matched to mlx-lm.
- Router top-k aligned with mlx-lm.
- Load and MoE decode paths tuned.
- Redundant residual copies in the decoder layer dropped.
- SwitchGeGLU
expand_dimscollapsed and a MoE inner profiler added.
Qwen 3.5: SSM decode masks aligned with mlx-lm; benchmark artifacts cleanup.MLX: upstream pin upgraded to v0.31.2; in-tree SDPA and steel-attention overlays dropped now that upstream covers them. Three-location update (src/lib/mlx-cpp/CMakeLists.txt,src/lib/mlxcel-core/build.rs,.github/workflows/release.yml) per CLAUDE.md.CUDA: QMM patches updated for the new upstreamlhs/rhs_indicessignatures.deploy: SIGTERM the runningmlxcel-serverafter binary copy so the respawned supervisor picks up the new binary.style:cargo fmtswept across server modules to land previously-unformatted blocks.
v0.0.24 - 2026-04-18
- Zero-config multi-machine pipeline-parallel bring-up:
mlxcel-server --pp-auto Ndeclares pipeline depth; peers register via--cluster-peersseeds or opt-in mDNS discovery (--cluster-discovery=mdns). Newsrc/distributed/cluster_init.rsowns deterministic stage assignment, port allocation, and byte-identical TOML emission consumed by the existing manual-TOML runtime path. - RDMA-aware transport backend with transparent TCP fallback. Negotiates
io_uringregistered buffers on Linux andkqueuebatched send on macOS, emits exactly one structured log line on fallback, and preserves theArc<dyn Transport>abstraction used byactivation_transfer.rs. Newrdma_capabilities.rs,rdma_transport.rs, andbench_activation.rsharness. - 2D
(pp_stage, tp_rank)mesh composing PP with TP for Llama-70B-class topologies. AddsNodeRole::PipelineTensorParallel, validation for exactpp_size × tp_sizecoverage with unique coordinates, registry helpers,TrafficClassrouting (TpCollective/PpActivation), and grid-coherent KV admission (coordinated_2d_admission) (addresses). - Byte-accurate pipeline auto-partition with adjacency constraints.
ModelProfilegains per-layer byte weights plus layer-adjacency constraints so the balancer refuses to cut MoE expert layers or Gemma 4 KV-shared source/consumer pairs. Drops the hand-specified--pp-layersrequirement for MoE and gemma-4-e2b-it-4bit. Extracted intopartition_balance,partition_profile, andpartition_qualitymodules. - Elastic pipeline-parallel repartitioning behind
--enable-elastic-pp.RepartitionCoordinatordrivesIdle → Draining → Rebalancing → Resuming → Idleand emitsRepartitionEventto a transport-agnostic sink without a full cluster restart. CLI flags:--elastic-pp-drain-timeout,--elastic-pp-pressure-fraction,--elastic-pp-cool-down. - Per-stage LoRA adapter composition across pipeline ranks via the existing
--adapterflag. Each stage loads only the adapter tensors inside its layer range through a new filtered safetensors loader (load_safetensors_filtered), fuses them in place with the samefuse_lora_weights_intoprimitive that backs the non-PP path, and unchanged-family guards (ensure_no_adapter) prevent silent drops. Llama family implements composition; parity integration test asserts bit-equality with the single-process adapter run. - Stage-executor coverage for five new families: Mistral dense, Mixtral 8x7B MoE, DeepSeek V3 (MLA + routed MoE with MTP-trailer strip), Llama 4 Scout text-only tower, and Mamba-family hybrids Jamba and Nemotron-H.
StageFamilyenum plussupported_families()surfaces per-family capability on the server startup log. - Pipeline-parallel observability:
/metricsendpoint renders per-stage utilization, rolling bubble ratio, activation-transfer latency histograms (p50/p95/p99 per stage pair), and KV admission rejection counters labeled by stage and reason.--metrics-port,--debug-pp-trace <PATH>(chrome-tracing JSON), andAdmissionDiagnosticreplace opaque 500s on rejection. Grafana dashboard JSON atdocs_internal/performance/pipeline-dashboard.json. - Multi-host pipeline-parallel regression CI harness at
.github/workflows/pipeline-parallel-ci.yml:two-host-logicalon GitHub runners (path-filtered, intended as required status) plusthree-host-real-modelgated by theci:pp-three-hostPR label or manual dispatch. Shares shell entry points with local reproduction (refs). VisionFeatureCacheLRU for multi-turn VLM image feature reuse, wired through Gemma 4 VLM, Qwen2.5-VL, and Qwen3-VL via_with_cachevariants. Cache keys are filesystem paths or SHA-256 digests of inline payloads. New--vision-cache-size NCLI flag (default 20, 0 disables) (matches).- Null/empty-cache safety guards in the batch scheduler. Pure-text requests with zero tokenized prompt tokens are rejected before admission (VLM image/audio injection paths unaffected);
execute_decode_stepandexecute_batched_decodeno-op on emptyseq_ids. Mirrors the upstream mlx-lm BatchKVCache extend/filter/merge null guards.
- Auto-detect per-layer quantization bit overrides in
UnifiedLinear::from_weights_with_modeandFusedQKVLinear::from_weights_separate_with_mode. Newinfer_quantization_bits()verifies the MLX invariantpacked_in * 32 == bits * num_groups * group_sizeand infers the actual bit width from tensor shapes when the caller-supplied bits disagree. Enables qwen3.6-35b-a3b-4bit, which stores router-gate and shared-expert-gate at 8-bit while the rest of the model is 4-bit. - Use additive f32 attention mask (0.0 attended, f32::MIN masked) in
prepare_inputs_for_multimodalinstead of the previous multiplicative INT32 0/1 mask.mx.fast.scaled_dot_product_attentiontreats non-bool masks as additive bias on pre-softmax scores, so the old form silently leaked padding tokens into the attention distribution wheneverattention_maskcontained a zero. - Mirror conditional
embed_scaletoTensorParallelGemma4Model::forward_impl. Previously,multiply_scalarwas applied unconditionally afterembed_tokens, double-scaling text embeddings and incorrectly scaling image/audio features from VLM callers. Moved into theNonearm only, matchingGemma4TextModel::forward. Added regression test asserting TP/non-TP logits match for bothinput_embeddingsandinput_idspaths. - Wrap every
cache.conv_state = Some(slice_axis(...))assignment inmlxcel_core::contiguous(&tail, false)across mamba, mamba2, nemotron-h, and jamba, plus the two NemotronH fused-kernel paths.slice_axis()returns a lazy MLXSlicegraph node that retains the sourcepadded_inputas a live input, causing per-step memory growth proportional to sequence length. 50-step shape-plateau regression test added per model. - Apply RMS norm BEFORE
embedding_projectionon the encoder-side dim inGemma4 Multimodal Embedder(was previously AFTER, on the text-sidehidden_size). Mirrors upstream mlx-vlm. Renamed fieldpost_projection_norm→pre_projection_norm. BREAKING for pre-fix VLM checkpoints: re-downloadmlx-community/gemma-4-*-it-4bitto obtain the post-rename weights. - Apply
sqrt(hidden_size)embed_scaleto text embeddings inGemma4VLModel::get_input_embeddings_with_audioBEFORE merging vision/audio features, and make the scalar multiply inGemma4TextModel::forwardconditional oninput_embeddingsbeingNone. Vision/audio features are already in language-model embedding space; double-scaling them degraded multimodal generation quality. - Implement proportional RoPE for Gemma 4 full-attention layers. Real Gemma 4 checkpoints declare
rope_type="proportional"on full-attention andrope_type="default"on sliding-attention layers; the previous implementation silently droppedrope_typeand normalized by the rotated-only slice instead of the fullhead_dim. Newmlxcel_core::rope_proportionalmodule withcompute_proportional_rope_freqsandapply_proportional_ropematching the upstream slice/concat/fast_rope/re-splice pipeline. For head_dim=256, partial_rotary_factor=0.25, the two formulations differ by a factor-of-4 exponent shift. - Gemma 4 audio feature extractor: drop
+0.5phase shift in Hann window so it uses the periodic formw(i) = 0.5 - 0.5·cos(2π·i/N)matching HuggingFace Gemma 4. Prependframe_length/2 (160)zero samples before frame extraction for semicausal convention (first frame centered at t=0). Usetotal_leninnum_framescalculation and correctframe_size_for_unfoldto useframe_length+1only for non-HTK preemphasis. Restores the correct 100 frames for 1s 16 kHz audio with 10 ms hop. - Ensure
conv_inputcache slice is contiguous in GatedDeltaNet forward paths (Qwen 3.5, Kimi Linear, Qwen 3 Next).mlxcel_core::slice()callsmlx::core::slice()which creates a graph node holding source reference — withoutcontiguous(), every cached entry holds the fullconv_inputbuffer, preventing freeing and causing per-step memory growth proportional to sequence length. 50-step regression test added. - Default NemotronH
time_step_limitto(time_step_min.unwrap_or(0.0), time_step_max.unwrap_or(+inf))unconditionally when absent. Changed from(f32, f32)toOption<(f32, f32)>so absent configs are distinguishable from explicit(0.0, +inf)sentinels. Matches upstream mlx-lm behavior.
- Replace Gemma 4
ScaledLinearwrapper withUnifiedLineardirectly across bothGemma4TextModelandGemma4StageModel(tensor-parallel path). Newper_layer_projection_scale: f32field stores(hidden_size as f32).powf(-0.5)and is applied explicitly inproject_per_layer_inputs()after the linear forward pass, preserving bit-identical math.
v0.0.23 - 2026-04-15
- Render chat templates that use Python-style dict/string methods. Extends minijinja's
unknown_method_callbackwith shims for.get,.items,.keys,.values,.strip,.lstrip,.rstrip,.startswith,.endswith,.split,.rsplit,.replace,.join,.upper,.lower,.title,.capitalize,.casefold,.swapcase,.find,.count,.is{digit,alpha,alnum,space,upper,lower}. Previously rendering silently fell back toto_prompt()'sUser: ... Assistant:format, and instruction-tuned models echoedAssistant:in a loop. - Pass
toolsas an empty iterable (notNone) so{% if tools is iterable and tools | length > 0 %}guards work under minijinja. Fixes Qwen 3 Next, Nemotron-H, and Nemotron-NAS tool-free rendering. - Strip HuggingFace
transformers'{% generation %}/{% endgeneration %}extension markers during template preprocessing so SmolLM 3 parses cleanly. - Apply the Gemma 4 structural-token stream filter and non-streaming cleanup unconditionally, not only when tool parsing is enabled, so plain chat responses no longer leak
<|channel>,<channel|>,<turn|>,<|turn>,<|tool_call>, or<tool_call|>markers into content. - Extend
clean_content_markerswith<|channel>/<channel|>/<|tool_call>/<tool_call|>so stray closing tags that Gemma 4 occasionally emits in non-thinking mode are stripped even without a matching open tag.
test_all_local_model_templates_renderignored-by-default audit that renders every locally-available model against three canonical scenarios (simple user, system + user, multi-turn with<think>blocks). Current result: 85 models checked, 249/249 scenarios pass, 0 failures, 6 intentional templateraise_exceptionrejections categorized separately.
- Clean up pre-existing
cargo clippy --release -p mlxcel --libwarnings (7 → 0): replaceunwrap()afteris_some()checks indistributed/config.rs, bind the MoE router viaif letchain in the Gemma 4 TP path (distributed/tensor_parallel/llama_runtime.rs), collapse two character-identical QKV shard branches, auto-elide'alifetimes, collapse athunderbolt_transport.rsnestedif, replace a manual% != 0withis_multiple_of()in NVFP4 sanitize, and drop a now-redundantcache.as_deref_mut()+mutannotation in Qwen 3.5GatedDeltaNet::forward. - Clean up pre-existing webpage
pnpm lint/tsc --noEmiterrors (20 / 4 warnings → 0 / 0): replace framer-motion wrapperany-typed props withHTMLMotionProps,let→constindownloads.tsx, swap<img>fornext/image's<Image />on the local Lablup logo, and rewriteuse-os.tsto avoid synchronously setting state insideuseEffectwith properNavigatorUAData/WebGLDebugInfoExtensiontypings.
v0.0.22 - 2026-04-13
- Pipeline stage executor framework with per-family executors
- Gemma 3 pipeline stage executor
- Gemma 4 pipeline stage executor
- Qwen3 pipeline stage executor
- Qwen3.5 pipeline stage executor
- GLM4-family pipeline stage executors
- GLM MoE DSA pipeline stage executor
- gpt-oss pipeline stage executor
- In-process pipeline stage worker loop
- CLI pipeline generate path
- Server pipeline runtime integration
- Pipeline transport lifecycle controls
- TCP-backed remote pipeline stages
- Thunderbolt transport backend for remote pipeline parallelism
- Multi-machine validation for remote pipeline parallelism
- bench_decode
--cooldownand--big-cooldownfor M5 Max thermal management - M5 Max benchmark refresh for 2026-04-13 (97 models, 88 pass; 8 multimodal models restored)
- Tolerate stale
model.safetensors.index.jsonin mlx-community repackaged quants (gemma3-4b, gemma3n-e2b/e4b, llama-4-scout-17b, mistral-small-3.1, molmo2) - Tolerate partial
text_config(nonum_hidden_layers) in single-rank tensor-parallel planning (LLaVA-1.5, LLaVA-Next-Mistral) - Prevent Gemma 4 special tokens from leaking into streaming content deltas
- Complete remote pipeline lifecycle recovery
- bench_decode single-model runs no longer truncate the day's full-suite CSV
- Log lazy pipeline peer reconnects
- Generalize stage executor backends and remove legacy stage executor file
- Transport-capable pipeline runtime seam
- Pipeline server smoke validation
- Pipeline rollout real-model coverage
- Remote pipeline usage examples
- Remote pipeline rollout workflow
- Refreshed M5 Max benchmark documentation with measurement-variance analysis
- Recorded issue execution workflow
v0.0.21 - 2026-04-12
- Paged KV cache substrate with batch scheduler integration
- Native paged decode kernel paths for rotating and chunked caches
- Paged compatibility for windowed caches
- Default paged decode for supported server workers
- Paged KV transfer observability
- NVFP4 load-time dequantization for Gemma 4 nvfp4 checkpoints
- F8_E4M3 / F8_E5M2 safetensors loading for nvfp4 checkpoints
- Paged decode rollout benchmark matrix and eligibility tracking
- Unify model-owned sequence state with backend seam
- Vectorize batched decode positional metadata
- CI: auto-promote pre-release to full release after successful builds
- Skip Teams notification when webhook URL secret is not configured
v0.0.20 - 2026-04-10
- In-process tensor parallel runtime for Llama
- Tensor parallel support for Qwen2, Qwen3, and Qwen3.5 text models
- Gemma 3 tensor-parallel runtime with tp4 parity stabilization
- Gemma 4 tensor-parallel support
- Dense TP support for ERNIE 4.5 and Hunyuan v1 models
- Server batching support for tensor parallel runtimes
- Tensor-parallel config wiring into CLI and server entrypoints
- Qwen 3.5 tensor-parallel parity on large CUDA models
- Expand tp4 parity coverage to larger models and server end-to-end tests
v0.0.19 - 2026-04-10
- Improved sharded/multi-file safetensors loading robustness
- Teams release notification via Power Automate webhook
- Ensure input contiguity in QuantizedMatmul for MLA models on CUDA
- Skip models exceeding system memory in bench script
- Increase CUDA warmup timeout and add JIT preheat to bench script
v0.0.18 - 2026-04-08
- GatherQMM CUDA implementation via upstream MLX upgrade to b98831ad
- SM80 and naive QMM dispatch paths for non-Hopper CUDA GPUs
- Gemma 4 CUDA support: all 7 variants (e2b, e4b, 26b, 31b in 4bit/8bit)
- Qwen 3.5 CUDA support: 27b-4bit, 9b-bf16, 35b-MoE-4bit
- Mixed-type bf16/float JIT compilation failures in CUDA binary_ops.cuh
- Remove stale NO_GPU(BlockMaskedMM) override that conflicted with upstream implementation
- Gemma 3-4b and Gemma 3n (e2b, e4b) recovered on CUDA via binary_ops fix
- Upgrade MLX C++ upstream from 6a9a121d to b98831ad
- Replace custom gather_qmv.cu with upstream integrated qmv.cu
- Sync CUDA quantized.cpp with upstream SM80/naive dispatch paths
- GB10 CUDA: 14 models recovered from FAIL, 24 models improved >10%
- mamba2-1.3b +180%, minicpm-2b +131%, llama-3.1-8b +130%, hunyuan-dense +125%, llama-3.2-1b +115%
v0.0.17 - 2026-04-06
- Resolve broadcast crash in Gemma 4 chunked prefill with undersized attention mask
v0.0.16 - 2026-04-05
- Audio input support for server chat completions endpoint
- Gemma 4 audio encoder and audio-language model support
- Metal 4 fused attention path
- OpenAI-compatible tool calling support
- M5 GPU acceleration experiments
- M5 Neural Accelerator rollout research
- Unify attention dispatch for Metal 4 path
- Propagate client disconnection to BatchScheduler to prevent orphaned sequences
- Harden tool calling with input limits, parser improvements, and format handlers
- Remove eval() calls from qwen3_moe forward hot path
- Resolve Gemma SDPA crash on M1 by reducing threadgroup memory for head_dim=256
- Update compiled.cpp patch for upstream MLX API change
- Add str.split() support in chat template for Gemma 4 multi-turn
v0.0.15 - 2026-04-03
- Gemma 4 text and VLM model support
- User-facing warning when loading full-precision bf16 models
- Download webpage with Next.js static site (EN/KO i18n)
- Extend bf16→f16 weight conversion to all Apple Silicon generations
- Audit f32 upcasts and optimize MoE gate sigmoid for fp16 co-issue
- Improve Metal 4 fused attention scaffolding with research documentation
- Reuse cached MLX source for faster rebuilds
v0.0.14 - 2026-04-03
- Logprobs support for chat completions and completions endpoints
- Runtime Apple Silicon generation detection for hardware-specific optimizations
- Prefill tile alignment for M5 Neural Accelerator
- Batched speculative decode verification for NA utilization
- Batched prefill in server mode
- Layer pipelining with strategic async_eval
- Metal 4 fused attention kernel scaffolding
- KV cache INT8 quantization for memory savings
- INT8 quantization optimization for M5 Neural Accelerator
- Multimodal chat template support for VLM image token placement
- Apple Silicon precision hardware guide documentation
- Centralize bf16→f16 weight conversion in shared VLM loading path
- Skip bf16→f16 conversion for quantized models (restores +20% throughput)
- Add compiled gelu_topk kernel matching Python mlx-lm
@mx.compilepattern - Expand QKV projection fusion to GQA models
- Expand compiled MLP fusion to non-quantized models
- Fuse Q/K/V projections in Gemma v1 attention for faster decode
- Refactor AGENTS.md into focused reference docs (313→75 lines)
- Auto-convert bf16 weights to f16 on M5 for Metal JIT compatibility
- Skip add_special_tokens when prompt already contains BOS token (double-BOS fix)
- Prevent NemotronH all-
<unk>output on M5 Max by avoiding mixed float32/float16 ops - Prevent Nemotron-H/NAS GPU hang and state corruption on M5 Max
- Trim NemotronH internal caches after padded prefill to prevent GPU hang
- Fix PhiMoE expert activation from GeGLU to SwiGLU
- Fix matmul outside compile boundary in FP MLP to fix output corruption
- Replace gelu_approx power(x,3) with erf-based GELU to fix NaN in vision encoder
- Guard multimodal chat template to avoid garbled output on text-only VLMs
- Skip compiled FP MLP for bfloat16 models
- Patch MLX compiled kernel JIT to cast mixed bfloat16/float operands
- Patch MLX Metal kernels for macOS 26.4 compatibility
- Correct M5 Max benchmark results affected by GPU cascade corruption
v0.0.13 - 2026-03-31
- Mistral4 MLA (Multi-head Latent Attention) language model support
- Molmo-Point VLM model support
- NemotronSuper model support (upstream mlx-lm sync)
sync-upstreamClaude Code command for tracking mlx-lm/mlx-vlm changes
- Fuse GatedDeltaNet decode step with
mlx::core::compilefor improved throughput - Apply MRoPE and position ID optimizations to Qwen3-VL-MoE
- Fast-path single-token decode position IDs in Qwen3-VL
- Vectorize Qwen3-VL interleaved MRoPE with
take_along_axis - Optimize VLM vision encoding and sampling pipeline
- Use SDPA for NemotronH attention, boosting decode throughput 59%
- Improve SSM/Mamba2 numerical precision with float32 dt computation
- Improve GatedDelta numerical precision with float32 state
- Resolve Mamba/NemotronNAS output corruption with softplus overflow and fused norm grouping
- Guard Qwen3.5 GatedDeltaNet state batch dimension mismatches
- Use
h.shapeinstead ofinputs.shapefor Ministral3 attn_scale - Document scalar offset invariant for Llama4 BatchKVCache compatibility
- Correct model_tests.md table placement and dedup nemotron entries
v0.0.12 - 2026-03-26
- Compiled C++ operations using
mlx::core::compile(shapeless=true)for small model throughput:compiled_gelu/compiled_gelu_approx: fused GELU activation kernelscompiled_geglu_activation: fused GELU-gated activation (gelu(gate) * x)compiled_softcap: fused softcap (tanh(x/cap)*cap) for Gemma2compiled_softcap_sdpa: entire attention path with softcap fused into single compiled graphcompiled_softcap_sdpa_gqa: fused GQA + softcap SDPA variantcompiled_clip_residual: fused float16-safe residual addition for Gemma3compiled_gelu_mlp_forward: full GELU MLP as single compiled graph
UnifiedLinear::quantized_weight()accessor for compiled MLP kernel dispatch- Distributed inference framework: node discovery, cluster configuration, tensor/pipeline parallelism, disaggregated serving
- Comprehensive mkdocs documentation site (EN/KO) with PDF export
- Project-specific Claude Code commands and skills
- Gemma3: fused SDPA, pre-computed GemmaRMSNorm, skip decode masks, Gemma3 1B reaches 94% of Python mlx-lm
- Gemma2: uses
compiled_softcap_sdpa_gqawith internal GQA head expansion - StarCoder2: uses
compiled_geluactivation - Phi3: pre-compute SuScaledRoPE scale array at load time
- Hoist env var checks out of generation hot loop
- Incremental token history and cached EOS in BatchScheduler
- Use MLX native
load_safetensors()for faster weight loading - Optimize model loading with batched synchronization
- OpenAI API streaming response format compatibility
- Guard compiled MLP/MoE paths against non-standard quantization params (
group_size != 64orbits != 4)
v0.0.11 - 2026-03-18
- Compiled kernel fusion for
relu_squaredandsiluactivation functions - Compiled kernel fusion for MoE gate and
compute_dtoperations - Fused SSM Metal kernel for Mamba2 single-token decode
- Compiled MoE gate function for NemotronH
- Fused MoE forward function for NemotronH
- Fused Mamba2 mixer forward for NemotronH
- NemotronH full-forward C++ decode path (experimental, disabled)
MLXCEL_FORCE_SYNCdebug flag for pipelining analysisMLXCEL_PROFILE_PIPELINEfor precise build/wait timing- Per-block and build/eval profiling for NemotronH
- Auto-cast SDPA mask to Q dtype, preventing mask type errors across models
- Load float16 weights natively on Metal (was converting to float32)
- Eliminate float32 type promotion across all models
- Prevent float32 type promotion in NemotronH hidden states
- Add affine fast-path for quantized_matmul (omit mode parameter)
- Correct mlx-lm benchmark baselines and update nemotron/mamba results
- Optimize Mamba single-token decode path and remove unnecessary copies
v0.0.10 - 2026-03-17
- ExaOne4: Cast causal mask to bfloat16 to match model weights dtype (MLX SDPA requires mask type to promote to output type)
- StableLM: Read
eos_token_idfrom config.json instead of hardcoding 0, fixing premature 1-token generation
- Add static mode string pool for quantized ops to avoid per-call heap allocation in C++ bridge hot path
v0.0.9 - 2026-03-17
- GptOss MoE model with sinks SDPA support
- MXFP4/NVFP4/MXFP8 quantization mode support across FFI bridge and model layers
- GPT-OSS benchmark results to model test documentation
- Set wired memory limit to
gpu_max_memory_sizeby default
- Re-benchmark all models after wired limit fix
v0.0.8 - 2026-03-17
- Support explicit
head_dimconfig field in Qwen3-VL, Qwen2-VL, and Qwen2-MoE models, fixes Qwen3-VL-32B crash wherehead_dim(128) != hidden_size/num_heads(80) - Switch macOS CI runner to macos-15 for Xcode 16+ C++20 ranges support
- Add CUDA release pipeline and refresh benchmark report with MoE results
v0.0.7 - 2026-03-16
- GatherMM/GatherQMM for MoE model support on CUDA (#34)
- CUDA bf16 support: type promotion table patching, mixed-precision binary kernels, normalization ops, reduce accumulation with fp32 precision, native bf16 array creation in bridge layer (#42-#46)
- CUDA bf16 validation scripts and documentation (#47)
- CUDA GB10 benchmark results for 57 models
- GB10 vs M1 Ultra benchmark comparison report
--batch-sizeand--ubatch-sizeas llama-server compatible aliases (#32)- Debian packaging, man pages, and optimized release profile
- CUDA build guide and build troubleshooting documentation (#33)
- CUDA qmv shared-memory optimization with block.sync() fix
- CUDA dtype and fp16 bridge fixes
- C++ bridge build: removed
-flto, upgraded to C++20 - C++ bridge LTO enabled only on macOS
- Bumped MLX to v0.31.1, GPU backend now shown in runtime display
- CUDA qmv kernel optimized with shared memory x-broadcast and
__restrict__ - Phase 19 CUDA optimization report and final benchmarks
v0.0.6 - 2026-03-14
- Continuous batching with iteration-level BatchScheduler for concurrent request handling
- Request lifecycle types and sequence state machine for batch management
- Per-sequence KV cache isolation and CachePool for independent request processing
- Tensor-batched decode forward pass for efficient multi-sequence generation
- Preemptive scheduling and chunked prefill for better latency and throughput
- HTTP server integration with batch scheduler and concurrency support
- Explicit
forward_batched()for Qwen3 with split-attention support - Continuous batching benchmarks and observability instrumentation
- Feature gate for batching to preserve CLI single-request path
- Scheduling policy now admits queued requests to grow batch beyond initial size
- Added continuous batching development guide and benchmark comparison documentation
- Benchmark results for 84 models with scheduler fix improvements
v0.0.5 - 2026-03-11
- Phi4-SigLIP vision-language model support with NaFlex-style patch processor and SigLIP2 vision tower
- Phi4MM vision-language model support with SigLIP + HD transform + AvgPool2d pipeline
- MiniCPM-o vision-language model support with SigLIP + Perceiver-style resampler
- Moondream3 vision-language model support with packed int4 dequantization and BOS-prefix prompting
- Runtime LoRA support on Linear layers with
Cell<bool>active toggle for on-the-fly application after_prefill()dispatch through LoadedModel enum and LanguageModel trait- Server support for data URIs, file URLs, bare local paths, and http(s) image fetches
- Phi4MM VLM: add SuScaledRoPE (longrope) to Phi3 attention for correct positional encoding
- Phi4MM VLM: fix image token placement in prompt (insert after
<|user|>tag, not before entire prompt) - Phi4MM VLM: use runtime LoRA instead of weight fusion, matching Python PEFT behavior
- MiniCPM-o VLM: switch text backbone from Qwen3-VL (MRoPE) to standard Qwen3 (standard RoPE)
- MiniCPM-o VLM: add automatic Qwen3-style chat template wrapping for models without chat_template
- Moondream3 VLM: fix RoPE layout (NeoX-style halves), attention mask dtype, and vision tiling
- Moondream3 VLM: use exact GELU for tau scaling and MoE GeGLU matching Python F.gelu
- Synced mlx-vlm upstream Qwen-VL: fused-SDPA head-dim padding in shared Qwen3-VL vision encoder
- Refactored server image extraction into async edge helpers with multi-format support
v0.0.4 - 2026-03-10
- Tiktoken BPE tokenizer support for models using
.tiktokenvocabulary files (HunYuan MoE 13B) - Quality gate entry point script (
scripts/run_quality_gate.sh) with--include-serial-helpersand--fullmodes - Comprehensive model validation: 71/74 local models pass (95.9%)
- Solar Open 100B-4bit config parsing: add serde defaults for
n_group/topk_groupin GLM4 MoE config - GatedDeltaNet
RMSNormGated: promote SwiGLU gate path to float32 before restoring hidden-state dtype (upstream mlx-lm parity for Qwen3Next/Qwen3.5) - Step3p5 sliding-window layers now use
RotatingKVCacheinstead of plainKVCache - Suppress deprecated-copy warning in mlxcel-core build for MLX v0.31.0
- Converged model registration: centralized config-backed text model registration in
src/model_metadata.rs - Split mlxcel-core internals into focused modules:
cache.rs,ops.rs,dtype.rs,sampling.rs,generation_policy.rs,streams.rs - Extracted large-model helper hotspots:
gemma3n_helpers.rs,llama4_helpers.rs,qwen3_next_helpers.rs - Split
LoadedModelcapabilities intoloaded_model_capabilities.rswithVlmRuntimeRef - Separated model detection (
detection.rs) and sanitization (sanitize.rs) helpers - Unified model loading descriptors with
StaticModelDescriptorandmodel_load_policy() - Normalized server startup edge inputs into
cli_input.rs - Removed unsafe
Send/Syncauto traits fromModelProvider - Strengthened vision merge contracts with dedicated tests
- Refreshed architecture, control-plane guide, and model addition documentation
v0.0.3 - 2026-03-10
- Streaming UTF-8 corruption for multi-byte characters (e.g., Korean, CJK) caused by byte-level BPE token boundaries
- Default
max_tokensincreased from 512 to 4096 so thinking models produce complete responses - Release archive now includes
mlx.metallibfor Metal GPU acceleration
v0.0.2 - 2026-03-10
- Solar Open 100B INT4 model support with GPTQ conversion
- MiniMax-M2 MoE model support
- GPU wired memory limit now opt-in via
MLXCEL_WIRED_LIMITenvironment variable - Llama4 vision encoder now uses UnifiedLinear to support quantized weights
- Molmo2 VLM inherits quantization config correctly; stale examples updated
- PaliGemma2 VLM no longer produces pad/EOS tokens instead of correct output
- Qwen3.5 VLM loader variants corrected
- Resolved all clippy warnings in vision and loading modules
- Major codebase refactoring: modularized server, CLI, loader, and multimodal paths
- Extracted loader modules into
src/loading/directory (SigLIP, Pixtral, Gemma, LLaVA, Qwen VLM loaders) - Moved CLI command handlers under
src/commands/ - Grouped execution policy helpers under
src/execution/ - Grouped multimodal helpers under
src/multimodal/ - Split server into config, state, streaming, and media helper modules
- Centralized LoadedModel embedding dispatch and reduced accessor boilerplate
- Shared sampling config assembly across CLI and server
- Refined model detection helpers with added guide
- Refreshed architecture and vision documentation
v0.0.1 - 2026-03-07
Initial public release of mlxcel.
- 59+ model architectures: Transformers, MoE, SSM/RNN, and Hybrid models
- Vision-Language Model support: Gemma 3, LLaVA, Llama 4, Qwen2-VL, Qwen2.5-VL, Qwen3-VL, Pixtral, Phi-3.5 Vision, and more
- OpenAI-compatible HTTP server with SSE streaming
mlxcel-serverstandalone binary as llama-server drop-in replacement- LoRA adapter loading and fusion at runtime
- Speculative decoding with draft models
- Advanced sampling: Top-P, Top-K, Min-P, XTC, DRY penalty, repetition/frequency/presence penalties
- Chat template support via Jinja2 (minijinja)
- Unix domain socket support for server mode
- EOS token detection from generation_config.json
- SentencePiece tokenizer support
- Linux + CUDA backend support (CUDA 12.0+, cuDNN 9+)
- Direct MLX C++ bindings via cxx FFI (zero Python dependencies)
- Pre-allocated KV cache with slice_update for O(1) per-token performance
- Sliding window and rotating KV cache support
- UnifiedLinear layer supporting both quantized and non-quantized models
- GitHub Actions release workflow for macOS ARM64
- Profile mode for prefill/decode timing analysis