Thanks for the crate — the vendored MLX with checksums and the offline build are exactly what I wanted from a Rust MLX stack, and safemlx-lm built cleanly first try on two accounts (3 m 3x s each) with Xcode 26.6 / Metal toolchain 17F109.
I measured it against mlx-lm on the same model, same snapshot, same machine, and the decode path looks like it is recomputing the whole context every step. Reporting in case it is a known gap for gemma-4, or a misuse on my side.
Environment
• Mac mini M4, 16 GB, macOS 26.5.2, Xcode 26.6 (clang 21.0.0), Metal toolchain 17F109 (metal version 32023.883), cmake 4.4.3, cargo 1.98.0
• safemlx-sys 0.2.3 / safemlx 0.3.1 / safemlx-lm 0.4.1 from crates.io, cargo build --release --locked -j 4, default features
• Model: mlx-community/gemma-4-e4b-it-4bit, snapshot 475b9088d29754a3379866cf5aeb6b41acd313c2
• Comparison: mlx-lm 0.31.3 on mlx 0.31.2, same snapshot, python -m mlx_lm.generate --temp 0 --max-tokens 128
• Both runs: greedy, 128 generated tokens, two other idle model servers resident (~4.6 GB free); pausing them changes wall time by ~2 s, so this is not memory pressure
safemlx-lm — examples/qwen35_moe_bench.rs pointed at the gemma-4 snapshot (it loads through LoadedModel::load; profiling off)
$ ./target/release/examples/qwen35_moe_bench <snapshot> 128 prefill_128
load_s=2.512
warmup_s=0.469 case,prompt_tokens,prefill_s,generated_tokens,decode_s,decode_tok_s,first_id,last_id
prefill_128,152,0.489611,128,85.149649,1.491,107,106
$ ./target/release/examples/qwen35_moe_bench <snapshot> 128 prefill_512
load_s=2.239
warmup_s=0.458
prefill_512,529,1.665251,128,241.580618,0.526,107,106
So: prefill ~310 tok/s (fine), but decode 1.49 tok/s at a 152-token prompt and 0.53 tok/s at 529 tokens. Per generated token that is 0.66 s and 1.89 s — almost exactly the measured prefill time of the whole prompt in each case, which is what made me suspect the cache is not being extended for this architecture. The gemma4_e4b_probe example shows the same rate (90 tokens in ~27 s including a
2.5 s load), and it uses the same generate_input_with_cache path.
mlx-lm on the same snapshot
Prompt: 160 tokens, 253.398 tokens-per-sec
Generation: 128 tokens, 36.350 tokens-per-sec
Peak memory: 4.448 GB
36 tok/s is ~110 GB/s of weight traffic, close to the M4's 120 GB/s bus, so mlx-lm is at the bandwidth wall; safemlx-lm is at ~4 % of it.
Questions
- Is incremental KV-cache decode expected to work for the gemma-4 loader in 0.4.1, or is the per-step recompute known?
- Is there a flag or a different generation entry point I should be using for gemma-4 (the bench example is the Qwen3.5-MoE one; I used it only because it is the one that prints prefill/decode separately)?
Happy to re-run any diagnostic build on this machine and post the numbers.
Thanks for the crate — the vendored MLX with checksums and the offline build are exactly what I wanted from a Rust MLX stack, and safemlx-lm built cleanly first try on two accounts (3 m 3x s each) with Xcode 26.6 / Metal toolchain 17F109.
I measured it against mlx-lm on the same model, same snapshot, same machine, and the decode path looks like it is recomputing the whole context every step. Reporting in case it is a known gap for gemma-4, or a misuse on my side.
Environment
• Mac mini M4, 16 GB, macOS 26.5.2, Xcode 26.6 (clang 21.0.0), Metal toolchain 17F109 (metal version 32023.883), cmake 4.4.3, cargo 1.98.0
• safemlx-sys 0.2.3 / safemlx 0.3.1 / safemlx-lm 0.4.1 from crates.io, cargo build --release --locked -j 4, default features
• Model: mlx-community/gemma-4-e4b-it-4bit, snapshot 475b9088d29754a3379866cf5aeb6b41acd313c2
• Comparison: mlx-lm 0.31.3 on mlx 0.31.2, same snapshot, python -m mlx_lm.generate --temp 0 --max-tokens 128
• Both runs: greedy, 128 generated tokens, two other idle model servers resident (~4.6 GB free); pausing them changes wall time by ~2 s, so this is not memory pressure
safemlx-lm — examples/qwen35_moe_bench.rs pointed at the gemma-4 snapshot (it loads through LoadedModel::load; profiling off)
prefill_128,152,0.489611,128,85.149649,1.491,107,106
So: prefill ~310 tok/s (fine), but decode 1.49 tok/s at a 152-token prompt and 0.53 tok/s at 529 tokens. Per generated token that is 0.66 s and 1.89 s — almost exactly the measured prefill time of the whole prompt in each case, which is what made me suspect the cache is not being extended for this architecture. The gemma4_e4b_probe example shows the same rate (90 tokens in ~27 s including a
2.5 s load), and it uses the same generate_input_with_cache path.
mlx-lm on the same snapshot
36 tok/s is ~110 GB/s of weight traffic, close to the M4's 120 GB/s bus, so mlx-lm is at the bandwidth wall; safemlx-lm is at ~4 % of it.
Questions
Happy to re-run any diagnostic build on this machine and post the numbers.