Thermite
Thermite, the generic ISA-portable Rust SIMD library, and its thermite-* companions (special, dual/autodiff, compensated/double-double, sort, sdf, geometry, ffi). Load when writing/reviewing/debugging code using thermite/thermite-* crates - GenericVector/NumericVector/FloatVector bounds, the policy math library, masks, dispatch, slice iteration, sorting, or composite vector types (Dual/Compensated). ALWAYS load before modifying Thermite's OWN source (register op, backend, math kernel, polyfill, macro, trait) and read references/development.md first. Triggers - "write a SIMD kernel", "generic over vector types", "use thermite", "FloatVector bound", "autodiff with Dual", "compensated arithmetic", "SDF", "SIMD sort"/"sort by key", thermite build/test errors, edits under crates/thermite*/src.From its SKILL.md
npx -y skills add raygon-renderer/thermite --skill thermiteAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
SKILL.md
15.7 KB, as published. Nobody here has run it
Thermite
Pure-Rust SIMD abstraction. Write a function once over a trait hierarchy
(GenericVector -> NumericVector -> FloatVector + math traits); it compiles to
optimal code for every backend (SSE2, SSE4.2, AVX2, WASM SIMD128, NEON, scalar,
experimental AVX-512 + SPIR-V) at every lane count. The same function also runs
on composite types -- Dual (autodiff), Compensated (double-double) -- with
no changes, because they implement the same traits.
This skill is for crates that depend on Thermite. Sub-file paths like
crates/thermite/src/... point into the Thermite source (authoritative impl), not
your project. If prose ever disagrees with the code, trust the code and fix the skill.
Step 1: Read the reference for your task
Loading this skill loaded only this file; the rules live in references/ and nothing
loads them for you. Match the request below and make your next tool call a Read of that
file (both, if two match) before writing code. Every sub-file exists because a
SKILL.md-only attempt compiled, passed tests, and was wrong or slow anyway.
| Request involves | Read |
|---|---|
any edit under crates/thermite/src (op, backend, math fn, polyfill, macro, trait) | development.md (routes onward) |
a new fn/kernel over *Vector bounds | generic-programming.md, then performance.md sec 0 |
| "optimize" / FMA / divisions / op count / asm of an existing kernel | optimization-pass.md |
accuracy, ulp, policy tiers _p::<P>(), a new math fn | math.md, performance.md sec 7 |
Dual, Compensated, Complex, or code generic over a type that might be one | composite-types.md |
masks, _c/_m/_z, select vs masked ops | masks.md, vector-api.md |
slices, alignment, #[dispatch], dispatch_dyn! | slices-and-dispatch.md |
| a type error you do not understand | architecture.md, trait-hierarchy.md |
| the full method list for a trait | vector-api.md |
thermite-special / -sort / -sdf / -geometry / -ffi | special.md / sort.md / sdf.md / geometry.md / ffi.md |
You may not know which to read at first. Read one the moment any of these happen:
you are about to guess at a method name or signature; a compile error names a
trait you did not write; you are reaching for #[dispatch], a masked variant, or
a policy tier; you have written 30+ lines without opening one.
Add to a project
On crates.io at 0.2.0. Add thermite = "0.2.0" (its proc-macro crate
thermite-macros comes along automatically), plus only the companions you use -
thermite-special, -dual, -complex, -compensated, -sort, -sdf,
-geometry. See the sub-file list for what each provides. Every companion pulls
in thermite transitively, but list it anyway when you name its types (you
usually do), and keep all of them on the same version.
Toolchain: stable Rust (MSRV 1.95, edition 2024). The nightly feature
unlocks nightly-only paths (smarter const splat, wasm64 SIMD, SPIR-V) and then
requires nightly (compile_error!s otherwise). Only FFI mandates nightly -- see
references/ffi.md. Crate is no_std by default.
Feature flags (thermite)
Defaults: document_registers, bitvec, avx2-f16c, avx2-pclmul.
| Feature | Default | Effect |
|---|---|---|
std | off | Enable std (fmt in panics, num-traits/std). |
bitvec | on | bitvec mask integration. |
avx2-f16c | on | Assume f16c w/ AVX2 (all AVX2 CPUs have it); enables f16 conv, no runtime check. x86. |
avx2-pclmul | on | Assume pclmulqdq w/ AVX2; CLMUL 2D-Morton fast path on u64 lanes. x86. |
partial-ord | off | PartialOrd for Vector (only when all lanes share order). |
strict_ieee754 | off | Spec-exact denormals/NaN/min-max; implies preserve_denormals. Much slower. |
preserve_denormals | off | Keep denormals (required for strict IEEE-754); slower on denormal-heavy data. |
ignore_denormals | off | Flush denormals by default. Also compiles the subnormal machinery out of the emulated FMA (~20% faster non-FMA mul_add; subnormal-scale results become faithful, normal-range stays bit-exact). Ignored under preserve_denormals/strict_ieee754. |
disable_fast_fma | REMOVED | Historical. The emulated FMA is now correctly rounded (bit-identical to hardware FMA) unconditionally, so there was nothing left to trade. |
algebraic-scalar | off | Scalar (1-lane) backend uses LLVM algebraic_* ops so loops written against it can autovectorize. Costs exact cancellation: thermite-compensated rejects it at compile time. strict_ieee754 overrides it. Needs nightly until 1.98. |
disable_dispatch | off | Replace runtime dispatch with #[inline(always)]. Bloats/slows unless all inlines. Advanced. |
nightly | off | Nightly-only paths (requires nightly compiler). |
wasm | off | wasm32/wasm64 SIMD128 backend. |
avx512-tier1..3 | off | UNDER CONSTRUCTION. Select which AVX-512 tier the x86-v4 backend compiles to (one per build, highest requested wins; tier 1 = Skylake-SP F+CD+BW+DQ+VL floor, no KNL tier). No registers yet, so today they compile the module skeleton only; AVX-512 CPUs run the x86-v3 (AVX2) backend, which dispatch maps X86V4 onto. |
spirv | off | HARD COMPILE ERROR in released versions. Incomplete: no impl Simd (so no vector type aliases, not a dispatch target), f32/i32/u32 only, nothing off target_arch = "spirv". Needs a git dep plus RUSTFLAGS='--cfg thermite_unstable_spirv'. |
No neon feature: NEON/AdvSIMD is mandatory in AArch64, so the backend is
gated on target_arch = "aarch64" and is always compiled there -- nothing to
opt into, and --features neon is now an error. (Contrast wasm, which stays
opt-in because SIMD128 is a proposal the engine may not enable.)
Strict-numerics std app: features = ["std"]. Fully no_std:
default-features = false + re-enable what you need.
The thesis, in one example
One generic fn run 4 ways -- scalar, native SIMD, Dual, Compensated. The fn is
never edited; the type you instantiate decides plain SIMD vs derivative vs precision.
use thermite::prelude::*;
use thermite::math::TranscendentalMath; // math trait names need explicit import to NAME in bounds
use thermite_dual::AutoDiff;
use thermite_compensated::Compensated;
// Written ONCE over trait bounds - no backend, lane count, or element named.
fn gaussian<V: FloatVector + TranscendentalMath>(x: V) -> V { (-(x * x)).exp() }
type V = Vector<f64>;
fn main() {
let s = gaussian(0.5_f64.as_vector()).extract::<0>(); // 0.7788007830714049 (scalar)
// as_vector: bare f64 -> 1-lane V
let simd = thermite::dispatch_dyn!(for<S> || -> f32 { // runtime-dispatched to best ISA
gaussian(f32xN::splat(0.5)).extract::<0>()
}); // ~0.77880079 (f32)
let r = gaussian.ad([V::splat(0.5)]); // value AND gradient
let value = r.re.extract::<0>(); // 0.7788007830714049
let dydx = r.dual[0].extract::<0>(); // -0.7788... (= -2x e^{-x^2})
let hi = gaussian(Compensated::<V>::new(V::splat(0.5))) // double-double
.value().extract::<0>(); // 0.7788007830714049
}
Constrain on traits, never a concrete backend/width -- that is the whole point.
Layer discipline: user code targets the *Vector traits ONLY. The *Register
layer (R::op(storage)) is backend-implementation machinery -- never call it
from user or generic code (its semantics can even differ from the vector layer,
e.g. bitandnot operand order). Concrete Vector<R> types are a last resort
too: legitimate mainly as the scalar 1-lane seeds (Vector<f32>/Vector<f64>)
and at dispatch_dyn! boundaries; anything reusable stays generic over bounds.
See references/generic-programming.md (read
first) and references/composite-types.md.
Rule zero: #[thermite::dispatch] + #[inline(always)]
The single highest-impact rule in Thermite code, and invisible to the type
system. rustc will not inline a #[target_feature] fn into a caller lacking
those features, and every core::arch intrinsic is one. A generic-over-S
body with no #[dispatch] above it (and no #[dispatch] ancestor inlining it)
is compiled featureless: every intrinsic stays out-of-line, a call per single
instruction, no scheduling or regalloc across ops. Compiles, correct,
catastrophically slow. Two attributes fix it:
#[thermite::dispatch(S)](or(Self)/(TypeName)) on every SIMD entry point -- fn,impl,traitormod. Emits a#[target_feature]trampoline per backend plus a const-folded ISA match; the only way the body gets per-ISA codegen at all.#[inline(always)]on every helper called from inside a dispatched body. Features propagate into a callee only if it is inlined; a non-inlined helper is featureless and hits the same soup one level down. Plain#[inline]is a hint the optimizer declines in exactly the big bodies that matter.
The pairing is cheap, not bloat: the trampoline itself carries the features, so
the dispatched fn need not inline (one out-of-line copy per backend) while the
interior inlines aggressively. Exception: don't #[dispatch] one-line leaf
helpers -- it only blocks inlining; keep them #[inline(always)].
#[inline(always)] // interior: must inline to keep features
fn step<V: FloatVector>(v: V) -> V { v.mul_adde(v, v) }
#[thermite::dispatch(S)] // boundary: per-backend target_feature
pub fn kernel<S: FloatSimd<f32>>(data: &mut [f32]) {
let (_, chunks, _) = data.try_aligned_simd_iter_mut::<Vector<S::fxN>>();
for v in chunks { *v = step(*v); }
}
let _ = thermite::dispatch_dyn!(kernel(&mut data)); // runtime ISA selection
Details: references/performance.md sec 0, references/slices-and-dispatch.md.
Quick reference
use thermite::prelude::*; // Vector, Mask, *Vector traits, SimdSlice
use thermite::math::{TranscendentalMath, RealMath, CoreMath, SpatialMath}; // to NAME math traits in bounds
a + b a - b a * b a / b -a // NumericVector / SignedVector
a & b a | b a ^ b !a // BitwiseVector
a << n a >> n // BitshiftVector (>> is LOGICAL; use srai/sra/srav for arithmetic)
let m = a.cmp_lt(b); m.select(a, b) // PartialOrdVector -> Mask, branchless select
v.sqrt() v.exp() v.sin() a.mul_adde(b, c) // FloatVector + math + estimating-FMA
v.sqrt_c(mask) a.add_c(mask, b) // masked variants: mask is the FIRST arg
v.compress_z(m) / v.expand_m(src, m) // stream compaction and its exact inverse
v.prefix_sum() v.count_conflicts() // inclusive lane scan; duplicate-lane ranks
v.group_by_value(valid) // divergent packet -> uniform sub-packets
v.total_order() // FloatVectorWithBits: NaN-safe integer sort keys
thermite_sort::sort::<i32x8>(&mut keys) // thermite-sort: see references/sort.md
Build/test as part of your own crate -- normal cargo build/cargo test, no
separate library step.
Gotchas (full list in sub-files)
- Missing
#[thermite::dispatch]/#[inline(always)]is the #1 silent perf bug -- no compile error, no test failure, but the kernel degenerates to acallper intrinsic. See Rule zero above; check it first when a kernel underperforms. - Bare
f32/f64don't implFloatVector. Wrap:x.as_vector()(Elementmethod, in the prelude) /Vector::<f64>::splat(x)/Vector(x), or useScalarMathscalar_-prefixed methods (x.scalar_sin()). - Masked variants take the mask FIRST:
a.add_c(mask, b),v.sqrt_c(mask),a.add_m(src, mask, b)(merge:srcthenmask). Oldadd_c(b, mask)order is wrong. - Math trait names are
used anonymously by the prelude (as _): methods work, but to write<V: TranscendentalMath>you mustuse thermite::math::TranscendentalMath;. - Prefer
mul_adde(estimating FMA) overmul_addfor speed. On non-FMA backendsmul_addlowers to a vectorized emulated FMA that is correctly rounded -- bit-identical to a true hardware FMA for every input (Boldo-Melquiond round-to-odd,fmadd_ro/fmadd_widen_roinbackend/generic/polyfills/math.rs), unconditionally -- no feature changes this, and it never goes scalar or touches libm. On wasm, a one-time canary detects whether the engine's relaxed madd is a true FMA and then uses that single instruction instead (bit-identical either way). Somul_addis a full accuracy guarantee everywhere; gate onHAS_TRUE_FMAonly to avoid the emulation cost (~2-4x a plain multiply-add; free on fusing wasm engines). >>is LOGICAL even on signed vectors. Usesrai/sra/sravfor sign-filling shifts.bitandnotdiffers by layer:a.bitandnot(b)onVector/Mask=a & !b, but the register layerR::bitandnot(lhs, rhs)=!lhs & rhs(x86 convention) -- the vector impls swap operands when delegating.V::loadis an ALIGNED load. Loading a table from a plainBox/Vecfaults nondeterministically; useload_unalignedor an aligned container.- Masked access does not license going out of bounds.
store_masked(and any masked-load idiom) is UB in Rust if the full-width access extends past the allocation, even on ISAs that guarantee the masked-off lanes never fault. A ragged tail therefore cannot be a masked access atlen - LANES + k. Fill a zeroed vector's lanes directly instead, or use the slice iterators, which never form the out-of-bounds access. thermite-sort: instantiate it at a NATIVE register width (i32x8on AVX2,i32x4on SSE4.2/NEON), never anArrayRegistercomposite - a two-chunk composite sorts ~30% slower than the width it is built from, becausecompressand the merge swizzles do not scale across sub-registers. See references/sort.md.- Two special functions still
todo!()-panic, both on composites:Dual::trigamma(its derivative needs the tetragammapsi_2), and six functions on the doubly-nestedComplex<Compensated<..>>(tgamma/lgamma/digamma/trigamma/lambert_w/Faddeeva). Everything onVector<R>,Dual,ComplexandCompensatedalone is implemented for both f32 and f64. Greptodo!before relying on a special function.