Qwen3.8 Flash-Next is a 125B MoE with 6B active per token and a 51B-parameter n-gram embedding table. Its REAP-288 4-bit MLX build, sh0wie/Qwen3.8-Flash-Next-REAP-288-MLX-4bit, arrives at about 68 GB resident. Stock mlx-vlm maps everything, so this machine swaps. The transformer weights are ~39 GB. The table is the problem.
The measured result, before the story: 41.7 GB peak MX memory against ~68 GB stock, 32.2 tok/s decode against the 28 tok/s in-memory reference, ~29.5 GB max process RSS, and bit-exact output versus the in-memory QuantizedEmbedding path. All numbers are local runs on this box.
The table nobody needed hot
The n-gram table is a hashed embedding at layer 2 of the decoder. Sixteen n-gram heads hash the last two or three tokens, and each head fetches one row. Sixteen rows of 160 dimensions feed the 2560-wide residual stream. The table is split across 128 shard files, each 2,500,012 rows by 160 dims, affine 4-bit with group size 32, about 100 bytes per row.
The model card is direct about this design: the table does not need to be resident, and the card's own "39 GB mode" serves it from NVMe with a row-granular disk-read patch. That patch is the model author's and has not been upstreamed, so I reimplemented it. Each token touches sixteen rows, roughly 1.6 KB of reads; keeping 51B parameters hot for that is waste.
The memory move
How 68 GB became 41.7 GB
Leave it on NVMe
A patched mlx_vlm/models/qwen4_exp/ngram_disk.py reimplements the card's 39 GB mode.
- Gather path. A shared per-file numpy memmap reads the sixteen rows;
mx.dequantizeexpands them.MLX_VLM_NGRAM_DISK_CACHE_ROWSdefaults to 131,072 rows for repeats. One memmap per shard file: per-tensor memmaps exhaust the default 256-descriptor limit. - Opt-in.
MLX_VLM_NGRAM_ON_DISK=1; fitting models keep the old path. - Correctness gate. Synthetic and real shard gathers are bit-exact with QuantizedEmbedding, covered by pytest before serving.
Disk-backed mmap, dequantize, and cache changed the result: 32.2 tok/s versus the 28 tok/s in-memory reference, 41.7 GB peak MX memory, and ~29.5 GB process RSS. The working set falls from ~68 GB to ~42 GB. NVMe reads for sixteen short rows cost less than compressed-memory pressure.
One memory owner per box
Making the loader work was only half. A launchd sidecar on :8080 first served the patched module as a dedicated Studio provider. Then a headless job loaded a second 36 GB model in another server. No process owned total memory; the guard aborted, the other server crashed, and the dashboard still said "No resident model" while 41.6 GB was gone. Two servers that each believe they own the box will meet in the swap file.
Current state: REAP-288 runs in oMLX on :8000 with thinkingFormat: qwen-chat-template. The sidecar stays retired. One memory owner per box.
Honest edges
- 262k context is config-verified, not stress-tested; no prefill beyond 32k has run. The memory guard is the backstop.
- REAP is not pinned. Another model evicts it; the next call pays a ~14 s reload.
- oMLX throughput is ~20-29 tok/s without the MTP drafter. The model card reports speculative decoding as roughly break-even on M4-class machines and 1.5-2.6x on M5-class; this box is an M4 Max, so I will measure before counting the drafter as a win.
- Reasoning effort is partial. The template accepts
xhigh(default),medium, andlow;highreturns a 500.
The transferable part
Find the tensor that breaks residency. Here it was the 51B n-gram table, with sixteen rows touched per token. For sparse access, memmap from NVMe, gather by row, dequantize on read, keep one mmap per file, and bound the cache. Prove bit-exactness against the resident path before serving.
A model that does not fit can run on the box when the access pattern, not the whole tensor, determines residency.