Repository navigation
Conversation
The rayon MMM dispatcher gated threading on panel count alone, so a GEMV on a wide-mr kernel ran on one thread whatever its k: every 2048-row projection of a 1B LLM decode step is 32 panels and stayed serial. A GEMV now also threads once its multiply-accumulates reach set_threading_mac_threshold (2^21 by default, off on wasm); matrix-matrix products keep the panel gate alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Einsum codegen computed every suitable kernel and kept the heuristic's pick. It now exposes the legal candidates and a tuning key built from the op's concrete facts, and takes a tuning table's choice for that key when one is loaded (tract_core::tuning, CLI --tuning); with no table codegen is unchanged. The tuning-candidates CLI subcommand lists each matmul's key and candidates as codegen sees them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@kali editing description as I go with the experiments |
|
First results with this slice and a scripted
Every kept table was faster in 5/5 rounds. The other 13 cases (GTCRN and PP-OCRv6 small_rec everywhere, the rest of DFN3/FastEnhancer/mobilenet) found nothing better and keep main's codegen; one PP-OCR table that won only 2/5 rounds was discarded by the guard. So 7/20 faster, 13 unchanged, 0 slower. The wins come from shapes where the heuristic picks the wrong kernel family: NEON where AMX/SME is faster, and on the M4 (which offers SME, AMX and NEON) sometimes SME where AMX or NEON is faster. Tables were tuned and validated on the same model and device; the held-out evaluation from the protocol is next. Thread count is not yet part of the choice. |
Problem. Codegen picks one implementation per op with fixed heuristics, and no fixed rule wins everywhere. Measured on an M4 against main: threading a matmul is right for Llama decode GEMVs (+28–36% tg128) and wrong for FastEnhancer's small AMX convs (serial is 35–48% faster multithreaded), and a topology rule that helped some models cost others 6–25% (#2982, closed). The right choice depends on shape, kernel and machine.
Proposal. Keep today's heuristics as the default. Add an opt-in autotuning step that records, per device, which alternative each optimisable op should use, and let codegen consult it.
strategize()already computes every suitable kernel and keeps one; it returns them all instead, heuristic pick first.einsum_nk,km->mn_F32_F32_F32_b1_m288_k96_n64_aconst_nstrided. Every node with that key shares one decision, across layers and across models, so the file is a per-device cache (FFTW-wisdom style), not a per-model artefact.S), reusing theSymbolValueshints the GPU arena sizing already takes.tract … tunebuilds each optimisable op in isolation per scenario, benches its alternatives, writes or merges the entries, then checks the whole model end-to-end and drops entries that don't win.Design refinements (from review and measurements).
S=1, prefillS∈ {64, 256, 1024}); a key whose size was not tuned takes the nearest tuned size of the same op signature, else the heuristic.tuneprofiles first and screens keys by their share of the model's time.user_cache_dir()(XDG_CACHE_HOME /~/.cache,Library/Cacheson Apple):<cache>/tract/<version>/tuning/<cpu>-t<threads>.tune. The version in the path invalidates entries on upgrade, as for cubins.Evaluation protocol (acceptance criteria). Corpus: 11 noise-suppression models, PP-OCRv6 (det/rec), mobilenet/inception, LLMs at a few sequence lengths. Devices: M1, M4, x86, Cortex-A53/A55 (bench bot). Conditions: single- and multi-thread, idle and loaded. Tune on half the models, apply the shared cache to the held-out half, and report improved / neutral / regressed against the heuristic with paired statistics. Pass: no held-out regression beyond noise, gains on some models, bounded tuning time.
First measurements (M1 Pro, f32, paired whole-model method, single thread). FastEnhancer-large, GTCRN, DFN3 df_dec and PP-OCRv6 small_rec: no candidate beats the heuristic (0 of 16/17/7/25 keys); the guard keeps today's behaviour. In isolation one PP-OCR shape (960×48×96) runs 1.1–2.8× faster on AMX than the heuristic's NEON pick across thread counts, but in the model it is a 1×1 conv with a different layout taking ~0.5% of runtime, below what whole-model acceptance can see. Large f32 GEMMs: AMX wins at every thread count (no NEON crossover), and AMX+NEON co-execution has a lower ceiling than AMX on all cores. Single short runs swing ±10%, which is why acceptance needs paired confirmation.
Prior art. Op-signature keys and offline tuning with a persistent cache are the established pattern: TensorRT's timing cache (tactics timed at engine build, invalidated by GPU/TensorRT version), TVM's tuning records keyed by op workload (with pre-tuned logs shipped per target), ONNX Runtime's TunableOp (results saved or embedded in the model, guarded by device/version validators), XLA's GPU autotune cache. Serving engines add two ideas adopted above: vLLM ships tuned kernel configs per device name and uses the nearest tuned shape at run time, and TensorRT-LLM's min/opt/max optimization profiles match "scenario + hint". CPU engines (XNNPACK, OpenBLAS, oneDNN) mostly ship per-microarchitecture tables instead of autotuning, which Apple's AMX/SME/NEON mix and P/E cores make harder to get right by hand.
This PR (first slice). Behaviour-neutral plumbing, einsum only:
kernel_selection::candidates()returns the legal strategies, thestrategize()pick first;strategize()itself is unchanged.kernel_selection::tuning_key()for fully concrete shapes: expression, operand/accumulator types, batch, m, k, n, const-A, strided m/n.tract_core::tuning: a key → (kernel, packing) table, process default or thread-scoped. Codegen'sselect()takes the table's choice when it names a legal candidate, the heuristic otherwise.--tuning <file>loads a table;tuning-candidateslists each matmul's key and candidates. Keys are recorded from codegen itself rather than recomputed: by the time codegen runs, earlier rules have often swapped operands (the constant moves to A), so a key derived from the decluttered graph would never match.On FastEnhancer-large (M1): 50 matmul nodes share 16 keys, each with 23 legal candidates; one table line moves the 5 nodes sharing a key from
apple_amx_mmm_f32_32x32toarm64simd_mmm_f32_8x8_gen.Next slices: thread count as part of the choice, a
tunesubcommand that benches candidates and writes the table, scenarios and hints for symbolic dims, file versioning.Open questions.
simd128differently), browser timers are coarsened and other tabs/throttling change conditions minute to minute, so measured or on-device tuning goes stale. Proposed instead: robust defaults per build (simd128/relaxed-simd), chosen offline from a cross-engine benchmark matrix (idle and loaded) by smallest worst-case slowdown and shipped as a table; host hints (engine, threads available, page hidden) passed in by the embedding app; conservative threading. The table API takes and returns bytes, not paths, so this stays possible.Stacked on #3010 (first commit); the RFC is the second commit.
🤖 Generated with Claude Code
🍍