rMLX 0.2.4
Vision, KV, and embedding-lookup bug-fix batch for Qwen3-VL and Gemma 4, plus a
/metrics/cache recording/docs fix and a Homebrew bottle build+publish flow.
Highlights: Qwen3-VL large images now work end to end (KV sized from --max-ctx;
the O(seq²) embedding lookup that tripped the Metal GPU watchdog is gone), and
Gemma 4 image grounding is fixed by placing image tokens inside the user turn. No
breaking changes.
Added
- Homebrew bottle build+publish flow.
scripts/release/build_bottle.sh+
make bottledrivebrew bottleagainst an installed keg, rename the local
bottle to the GitHub-Release asset name, and emit the ready-to-paste
bottle doblock; documented as a release-time step indocs/RELEASING.md.
The committed formula stays source-build until a real bottle is uploaded, so
existing tap installs are unaffected. (#143, #139)
Fixed
/metrics/cacheTTFT empty for non-streaming completions. Both
non-streaming paths (generate_blocking, OpenAI + Anthropic) measured TTFT
but never pushed it into the in-memoryttft_storering — only the streaming
path did, sottftstayed[]for non-streaming traffic. The ring is now
written on both paths.docs/SERVER.mdis realigned to the endpoint's actual
shape (models[],itl,tokens_in/out), dropping the never-emitted
prompt_cache/last_itlkeys. (#142, #141)- Gemma 4 image grounding (degenerate / image-independent output). The
per-image token block was spliced after BOS but before the user-turn opener,
leaving the image outside the user message; the model then ignored it. Image
blocks are now spliced inside the (final) user turn via a shared
splice_image_block, matching the HF/mlx-vlm placeholder substitution. Fixes
the reported e4b QAT-fp4 degeneration (the soft tokens were correct all along)
and a latent flakiness that affected all Gemma 4 image requests; Qwen3-VL is
unified onto the same path. (#144, #140) - Qwen3-VL ignored
--max-ctx; large images failed with aslice_update
broadcast. The image and text generate paths built KV with the bare 4096
default and never bracketed prefill, so any prompt over 4096 tokens (a large
image tiles to thousands of soft tokens) overran the fixed buffer. Both paths
now size the KV ring from the effective--max-ctxand chunk the prefill;
an over-cap prompt returns a cleancontext_overflowinstead of the broadcast
panic. (#145, #138) - Qwen3-VL large images hit the Metal GPU watchdog. The quantized embedding
lookup used an O(seq²)eye(seq) @ widentity-matmul on CPU (plus a GPU↔CPU
round-trip); embedding the whole augmented prompt for a large image produced a
single command buffer that overran the ~10 s watchdog. Replaced with on-device
take + dequantize(O(seq)); added query-tiled ViT attention as a faithful
defense for very large single images. (#147, #146) - Qwen3.6 (
qwen3_5_moe) embedding lookup carried the same O(seq²)
eye(seq) @ w-on-CPU trick (plus anunsafeblock); ported to the same
on-devicetake + dequantize. Numerically faithful, removes a per-step CPU
round-trip. (#149, #148)
Performance
- Qwen3-VL: large images (e.g. 2560×2560 → 6400 soft tokens) now complete
end-to-end instead of aborting the process at the Metal GPU watchdog. (#145, #147)
Tested
- New CI-gated tests: image-token placement (in-turn, last-turn, multi-image,
after-BOS fallback), ViT attention tiling equals a single SDPA, and
qwen3_5_moeembed_lookup numeric equivalence across both dtype arms (the
prior coverage was#[ignore]+ env-gated). Real-model proofs across Qwen3-VL
(KV + large-image), Gemma 4 e4b QAT-fp4 vision, and Qwen3.6 (decode-TPS
same-session A/B: no regression).