Step-by-step walkthrough for setting up lumen-rs from a clean clone
and exercising every validated path. Companion to the top-level
README — the README is the reference; this doc is the
copy-paste recipe.
| Hardware | Apple Silicon (M1/M2/M3/M4) |
| OS | macOS 14+ |
| Rust | rustup with stable 1.85+ (edition 2024) |
| Python | 3.10+ (only needed for fixture download or mlx-lm parity) |
| Disk | ~50 GB for full Gemma 4 + Qwen3.6 checkpoints + target/ |
| Memory | Embedding ≈ 1 GB · Gemma 4 26B-A4B 4-bit ≈ 22 GB |
Cargo.toml references a sibling candle/ directory carrying our
single-line clear_kv_cache patch (see DEPENDENCIES.md).
mkdir -p ~/your-projects && cd ~/your-projects/
# This repo
git clone https://github.com/rabbitson87/lumen-rs.git
# Sibling candle checkout — must be at ../candle/ relative to lumen-rs/
git clone https://github.com/huggingface/candle.git
# Apply the `pub fn clear_kv_cache` patch (see DEPENDENCIES.md for the diff)Final layout:
your-projects/
├── candle/ (patched fork)
└── lumen-rs/ (this repo)
The default build covers the embedding endpoint + lumen-server.
cd ~/your-projects/lumen-rs
cargo build --release # embedding only
cargo build --release --features mlx-native # + Gemma 4 chatAdd Qwen3.6 (opt-in) when you also want the Candle-based MoE backend:
cargo build --release -p lumen-serverThe first build downloads dependencies and compiles ~250 crates — allow 5–10 minutes on a clean machine. Subsequent builds are incremental.
cargo test -p lumen-mlx
# expected: 7 passed; 0 failedlumen-rs does not bundle any weights. Set env vars to local
checkpoint directories.
huggingface-cli download \
mlx-community/Qwen3-Embedding-0.6B-8bit-mlx \
--local-dir ~/models/qwen3-embedding-0.6b-8bit
export EMBEDDING_MODEL_ID=~/models/qwen3-embedding-0.6b-8bithuggingface-cli download \
mlx-community/gemma-4-26b-a4b-mlx-4bit \
--local-dir ~/models/gemma-4-26b-a4b-mlx-4bit
export MODEL_ID=~/models/gemma-4-26b-a4b-mlx-4bitOn 24 GB machines, use the 3-bit variant
(mlx-community/gemma-4-26b-a4b-mlx-3bit, ~16 GB resident).
huggingface-cli download \
mlx-community/Qwen3.6-35B-A3B-mxfp4 \
--local-dir ~/models/qwen3.6-35b-a3b-mxfp4
export LUMEN_QWEN35_SHARDS=~/models/qwen3.6-35b-a3b-mxfp4Loads model, embeds 3 KR/EN phrases, prints timings + cosines.
cargo run --release --features mlx-native -p lumen-mlx --example embedding_parityExpected (M3 Max):
[smoke] embed b=3 over 5 iters: median=19.35ms min=18.72ms max=22.35ms
[smoke] OK: semantic ordering preserved
cargo run --release --features mlx-native -p lumen-mlx --example embedding_parityExpected:
[quality] P@1 = 0.960 P@3 = 0.880 MRR = 0.980
# Cooperative simdgroup (default):
cargo run --release --features mlx-native -p lumen-mlx --example embedding_parity
# Naive 1-thread/output (forced):
LUMEN_EMBEDDING_BATCH_ROWS=1 \
cargo run --release --features mlx-native -p lumen-mlx --example embedding_parityYou should see ~19 ms vs ~35 ms — proof the kernel optimization preserves quality (identical cosine ordering) while halving latency.
cargo run --release --features mlx-native \
-p lumen-mlx --example bench_gemma4_native_e2e -- \
PROMPT_LEN=4096 STEPS=32 WARMUP=8Expected (M3 Max): ~18.8 ms/step decode with the custom flash-attn primitive (vs ~19.9 ms with mlx default sdpa).
MODEL_ID=mlx-community/Qwen3.6-35B-A3B-mxfp4 \
PROMPT_LEN=2048 STEPS=32 WARMUP=8 \
cargo run --release --features mlx-native \
-p lumen-mlx --example bench_mlx_e2eExpected (M3 Max): ~14.85 ms/step p50, 67.3 tok/s.
EMBEDDING_MODEL_ID=~/models/qwen3-embedding-0.6b-8bit \
MODEL_ID=~/models/gemma-4-26b-a4b-mlx-4bit \
cargo run --release --features mlx-native --bin lumen-serverDefault listen address: 127.0.0.1:8080. Override with HOST / PORT.
EMBEDDING_MODEL_ID=~/models/qwen3-embedding-0.6b-8bit \
cargo run --release --bin lumen-server/v1/chat/completions returns 503 when no chat model is configured.
EMBEDDING_MODEL_ID=~/models/qwen3-embedding-0.6b-8bit \
MODEL_ID=~/models/gemma-4-26b-a4b-mlx-4bit \
cargo run --release --bin lumen-server# Embeddings
curl -s localhost:8080/v1/embeddings \
-H 'content-type: application/json' \
-d '{"model":"qwen3-embedding-0.6b","input":["hello","안녕"]}' \
| jq '.data[0].embedding | length' # → 1024
# Chat
curl -s localhost:8080/v1/chat/completions \
-H 'content-type: application/json' \
-d '{
"model": "gemma-4-26b-a4b",
"messages": [{"role":"user","content":"Capital of South Korea?"}],
"max_tokens": 32
}' | jq -r '.choices[0].message.content'Gemma 4 and Qwen 3.6 both ship an image tower, but neither is loaded unless you ask for it — it costs ~1.1 GB (Gemma 4) or ~0.9 GB (Qwen 3.6) on top of the text weights. Restart the server with:
LUMEN_VISION=1 \
LUMEN_VISION_MAX_SOFT_TOKENS=140 \
MODEL_ID=~/models/mlx-community--gemma-4-26b-a4b-it-4bit \
cargo run --release --features mlx-native --bin lumen-serverLUMEN_VISION_MAX_SOFT_TOKENS=140 halves the patch grid versus the
checkpoint default of 280, which keeps peak memory near the text-only
footprint on a 36 GB machine.
Qwen 3.6 is the same flag, with a token budget instead of a soft-token
one (LUMEN_VISION_MAX_IMAGE_TOKENS, default 1024):
LUMEN_VISION=1 \
LUMEN_VISION_MAX_IMAGE_TOKENS=512 \
MODEL_ID=~/models/Qwen3.6-27B-MTPLX-Speed \
cargo run --release --features mlx-native --bin lumen-serverEither way, the request looks the same:
B64=$(base64 -i some-image.png)
curl -s localhost:8080/v1/chat/completions \
-H 'content-type: application/json' \
-d "{
\"model\": \"gemma-4-26b-a4b\",
\"max_tokens\": 128,
\"messages\": [{\"role\": \"user\", \"content\": [
{\"type\": \"text\", \"text\": \"Describe this image in one sentence.\"},
{\"type\": \"image_url\", \"image_url\": {\"url\": \"data:image/png;base64,$B64\"}}
]}]
}" | jq -r '.choices[0].message.content'Only data: URLs are accepted — the server will not fetch remote ones.
If the reply ignores the image, check the startup log: checkpoints that
were requantized without their vision_tower.* weights print a warning
and leave image input disabled. There is also a CLI path that skips HTTP
entirely:
LUMEN_VISION=1 MODEL_ID=~/models/mlx-community--gemma-4-26b-a4b-it-4bit \
cargo run --release --features mlx-native -p lumen-mlx \
--example gemma4_vision_describe -- some-image.png "What is this?"| Env | Default | Effect |
|---|---|---|
LUMEN_EMBEDDING_BATCH_ROWS=1 |
off | Force naive 8-bit GEMM (embedding path) |
LUMEN_GEMMA4_CUSTOM_FLASH_ATTN=0 |
on | Disable custom flash-attn primitive |
LUMEN_GEMMA4_PREFILL_SYNC=0 |
on | Skip explicit eval-sync after prefill |
LUMEN_MLX_BACKEND=native|pyo3|subprocess |
native | Picks the mlx runner. native is the Apple-silicon-optimized mlx-rs path |
The Qwen 3.6 fusion opt-outs that used to be listed here are gone, and were
gone from the code long before they were removed from this table: seven
LUMEN_DISABLE_*_FUSION variables that went with the Candle backend in
7eacd3a. Anyone who followed this table set them and got silence.
Their MLX-era replacements are registered flags, so they are in
env-flags.md — which is generated from the source and
verified by cargo xtask flags --check, rather than maintained by hand like
this table. Prefer that file; it cannot drift.
cargo xtask flags --check now also fails when any committed doc names an
env var the source does not define, which is what surfaced these.
See the README's Troubleshooting section for the standard pitfalls (missing candle fork, missing model env vars, slow embedding latency, OOM on Gemma 4). The rest of this section documents issues specific to the validation workflow.
The mean step-latency reported by the bench is sensitive to thermal
state. After a hot run, the mean can drift +50 % even though the
algorithmic comparison is unchanged. Use p50 (printed alongside
mean) as the apples-to-apples metric; let the machine cool for 60 s
between A/B runs.
The fixtures dataset is under the hsng95 namespace by default. If you
clone this repo and want to publish your own fixture builds, replace
(DEFAULT_REPO) and use a token with Write + Create repos scope.
Fine-grained tokens need both checks; classic "Write" tokens cover
both implicitly.
The default build compiles MLX from source (cmake + the Metal shader
compiler), which dominates the first build. Use cargo build --release
with CARGO_BUILD_JOBS
matched to your performance cores to avoid scheduler thrash on small
M-series machines.
- The TurboQuant codec (
lumen-core) is hardware-agnostic — a CUDA backend would only need a new dispatcher. - PagedAttention is not on the roadmap. It was measured before being
built and would reclaim under 1% of process memory on this workload;
the scaffolding crate was deleted.
docs/maintainer-workflow.md§9 has the numbers. The remaining memory lever is bf16 KV storage; prefill chunking already ships (LUMEN_QWEN35_PREFILL_CHUNK, default 2048) and §9 gives its memory-versus-latency exchange rate. - Spec-decode + MTP draft heads are partially implemented for the
Qwen3.6 path; see
LUMEN_SPEC=mtpandLUMEN_QWEN35_HF_ORIGINAL.
For day-to-day questions, the source-level docstrings under
crates/*/src/ are the authoritative answer — most non-obvious
behavior is annotated where it lives.