rlx-cpu
CPU backend for RLX — SIMD kernels, BLAS dispatch, persistent thread pool, arena executor.
Two execution paths share the same Thunk types:
- Direct execution (single
matchover&Thunkat line ~1280 onward inthunk.rs) — hot path, zero closure overhead. Performance-critical ops (Attention, FusedAttnBlock, matmul) live here. - Closure-based (
Box<dyn Fn(*mut u8)>per thunk; line ~780 onward inthunk.rs) — older, used for ops where dispatch overhead doesn't matter, and for some unit tests.
Keep both paths in sync when changing a Thunk variant.
Features
- NEON / AVX2 + FMA SIMD kernels for softmax, layer norm, RMS norm,
GELU / SiLU / RoPE, fused matmul-bias-act, and vForce-style
vmath(vvexpf/vvtanhf/vvrecf/vvlogf/vvsqrtf/vvrsqrtf, plus*_fastSIMD). Exp/tanh activation paths use SIMD fast math by default; setRLX_VMATH_ACCURATE=1for Accelerate/libm. The same host API is re-exported from Metal / CUDA / ROCm / wgpu / Vulkan / oneAPI / MLX /rlx-gpu-hostfor staging; GPU unary kernels cover exp/tanh/recip on-device. - BLAS dispatch via Apple Accelerate (default on macOS) or OpenBLAS / MKL via Cargo features.
- LAPACK bindings (
dgesv,dpotrf,dgeqrf,dgesvd,dsyevd,dtrsm) forOp::DenseSolveand the downstreamrlx-linalgcrate. - Work-stealing thread pool with
par_for(total, grain, &|off, cnt| …)primitive. - Reverse-mode AD support: thunks for every backward op
rlx_opt::autodiffemits. - FFT —
Op::Fftfor F32 / F64 / C64 (2N real-block or native C64). In-place Cooley-Tukey radix-2 for pow-2 (radix-4 for pure powers of four,RLX_FFT_RADIX4=0to disable); naive DFT for small composite N (≤16); Bluestein for other non-pow-2. Independent batch rows run rayon-parallel above a work threshold (RLX_FFT_CPU_PARALLEL=0to force serial). Host entry shared with GPU fallbacks.
What's here
thunk.rs(the bulk) — Thunk enum + lowering fromOp+ both execution paths. The two giant match functions (compile_thunks_with_rng,execute_thunks) are now compact dispatchers routing to ~200 per-opcompile_<op>/exec_<op>fns (the latter#[inline(always)], verified perf-neutral against thetests/bench_execute_hotloop.rsgate).executor.rs— alternate non-thunk executor used by old paths and some unit tests.kernels.rs— NEON intrinsics: softmax, layer norm, RMSNorm, matmul inner loops.blas.rs— Accelerate / MKL dispatch. SGEMM variants for different alignment regimes.naive.rs— reference scalar implementations. Used by tests for parity and as a fallback.pool.rs— work-stealing thread pool.arena.rs— buffer planning interface (the actual byte buffer comes from rlx-runtime).autotune.rs—Tick-based search overRuntimeConfig. Userlx_ir::Tickfor sub-ms timing.cost.rs/config.rs— model selection + runtime knobs (par_threshold, sdpa_seq_threshold, attn_mask_neg_inf, ...).
Cargo features
| feature | what it links |
|---|---|
blas (default) |
platform CBLAS via FFI |
blas-accelerate |
Apple Accelerate |
blas-mkl |
Intel MKL |
blas-openblas |
OpenBLAS |
portable-gemm |
no-BLAS builds: m ≥ 8 GEMMs via the gemm crate's blocked kernels |
int8-gemv |
x86-64 AVX2 int8 decode GEMVs for Q4_K/Q5_K/Q6_K/Q8_0/Q4_0/Q4_1 (int8-rounded activation, as llama.cpp) |
With --no-default-features, a portable scalar gemm is linked instead
— slow, but useful on hosts without a system BLAS.
Install
[]
= "0.1"
Or via rlx's cpu feature.
Build / test
ISA portability gate
One binary has to run on the AVX-512 server or M4 that built it and on
an Atom box or a Raspberry Pi, so every above-baseline instruction must
sit inside a function reached through a runtime CPU-feature check.
cargo build enforces none of that and the failure mode is a bare
Illegal instruction on hardware you don't own, so
tools/isa_portability.py checks both halves:
scan attributes every above-baseline instruction to its enclosing symbol
and fails on any that isn't runtime-gated. "Above baseline" is per-target,
not per-arch: sdot is baseline on aarch64-apple-darwin (apple-m1, v8.5)
and a finding on aarch64-unknown-linux-gnu (ARMv8.0-A), so the check is
judged against the baseline of the target the binary was built for.
--baseline overrides that to ask the cross-target question ("would this
survive a Cortex-A53?"), and the report says so when you do. It also catches
the inverse problem: a -C target-cpu=native artifact smears above-baseline
code across ordinary symbols (Op::clone included) where no runtime
dispatch can save it.
atom and arm run the suite for real. Docker --platform linux/amd64
supplies an x86-64 Linux toolchain and qemu-user-static -cpu Denverton
emulates a Goldmont Atom that traps AVX (Snowridge = Tremont, Haswell =
control); arm uses linux/arm64 — fully native on Apple Silicon — with
-cpu cortex-a53, which has neither DotProd nor FP16 arithmetic.
Gotchas
Thunk::Attentioncarriesmask_kind: MaskKind(plan #20). Custom readsmaskslice, others synthesize viaapply_synthetic_mask. Both execution paths handle this — keep them in sync.RuntimeConfig::global()is read once per thunk closure. If you need per-call config, pass it through the thunk fields, not via global.cfg.sdpa_seq_thresholdcontrols the NEON-vs-BLAS attention crossover. The NEON path skips dispatch for batch=1 / short seq.- Thunk-level fusion runs after compile_thunks (line ~990) — it rewrites Q/K/V → Narrow×3 → [Rope×2] → Attention → out_proj sequences into a single FusedAttnBlock. Fragile pattern matching.
License
MIT OR Apache-2.0.