Skip to main content

mlx_native/
kernel_registry.rs

1//! [`KernelRegistry`] — lazy compilation and caching of Metal compute pipelines.
2//!
3//! MSL shader source is embedded at compile time via `include_str!`.  On first
4//! access, the source is compiled into a Metal library, the named function is
5//! extracted, and a `ComputePipelineState` is created and cached.  Subsequent
6//! calls return the cached pipeline.
7//!
8//! ## Precompiled `.metallib` fast path
9//!
10//! `build.rs` runs `xcrun metal -O3` on every `.metal` file under
11//! `src/shaders/` and links the results into a single `default.metallib`
12//! placed in `OUT_DIR`.  We embed the bytes via `include_bytes!`.
13//!
14//! When `MLX_PRECOMPILED_METALLIB=1` is set, `get_pipeline` and
15//! `get_pipeline_with_constants` first try to resolve the kernel function
16//! against this precompiled library; if found, build the pipeline from it
17//! (saves Apple's runtime source-compile pass).  On any failure (function
18//! missing, empty embedded blob, load error) the code transparently falls
19//! back to the original source-compile path — byte-identical behavior.
20//!
21//! Default-ON; precompiled gives ~+6% on gemma4 Q-sliding decode (M5 Max).
22
23use std::collections::HashMap;
24use std::sync::OnceLock;
25
26use metal::{ComputePipelineDescriptor, ComputePipelineState, FunctionConstantValues, MTLDataType};
27
28use crate::error::{MlxError, Result};
29
30/// Bytes of the precompiled `default.metallib` produced by `build.rs` from
31/// every `src/shaders/*.metal` file.  Empty when `MLX_NATIVE_SKIP_METALLIB`
32/// was set at build time or xcrun was unavailable.
33const EMBEDDED_METALLIB: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/default.metallib"));
34
35/// Returns `true` when the precompiled `.metallib` fast path is enabled
36/// (default-ON).  Set `MLX_PRECOMPILED_METALLIB=0` (or `false`, `off`)
37/// to opt out — useful for diagnosing kernel-compile regressions or
38/// A/B benching.
39fn precompiled_enabled() -> bool {
40    static FLAG: OnceLock<bool> = OnceLock::new();
41    *FLAG.get_or_init(|| {
42        match std::env::var("MLX_PRECOMPILED_METALLIB").as_deref() {
43            Ok("0") | Ok("false") | Ok("off") => false,
44            _ => true,
45        }
46    })
47}
48
49/// Returns `true` when the precompiled `.metallib` is consulted for
50/// `get_pipeline_with_constants` (FCV-specialized) kernels.  Inherits
51/// the master gate [`precompiled_enabled`]; both must be ON for the
52/// FCV path to use precompiled.  Default-ON.
53fn precompiled_fcv_enabled() -> bool {
54    static FLAG: OnceLock<bool> = OnceLock::new();
55    *FLAG.get_or_init(|| {
56        match std::env::var("MLX_PRECOMPILED_METALLIB_FCV").as_deref() {
57            Ok("0") | Ok("false") | Ok("off") => false,
58            _ => true,
59        }
60    })
61}
62
63// MTLDataType numeric values (from metal-rs argument.rs, confirmed in Apple Metal spec):
64//   Int  = 29
65//   Bool = 53
66// These are used when calling set_constant_value_at_index so the Metal runtime
67// knows how wide each constant value is.
68
69/// Registry that lazily compiles and caches Metal compute pipelines from
70/// embedded MSL source.
71///
72/// # Usage
73///
74/// ```ignore
75/// let mut registry = KernelRegistry::new();
76/// let pipeline = registry.get_pipeline("elementwise_add", device.metal_device())?;
77/// encoder.encode(&pipeline, &buffers, grid, tg);
78/// ```
79///
80/// # Thread Safety
81///
82/// `KernelRegistry` is **not** `Sync` by default (it uses `&mut self` for
83/// `get_pipeline` to allow mutable cache insertion).  If you need concurrent
84/// access, wrap it in a `Mutex` or use one registry per thread.
85pub struct KernelRegistry {
86    /// Cached pipelines keyed by kernel function name.
87    cache: HashMap<String, ComputePipelineState>,
88    /// MSL source text keyed by kernel function name.
89    ///
90    /// Populated at construction time with all embedded shader sources.
91    sources: HashMap<String, &'static str>,
92    /// Precompiled `default.metallib` (lazy).
93    ///
94    /// `None` initially.  On first `get_pipeline*` call under
95    /// `MLX_PRECOMPILED_METALLIB=1`, populated via
96    /// `device.new_library_with_data(EMBEDDED_METALLIB)` — or set to a
97    /// sentinel "load-failed" marker (still `None`) so we don't retry.
98    /// Set to `Some(library)` on success.
99    ///
100    /// Lazily filled because we need a `&metal::DeviceRef` to load it
101    /// and `KernelRegistry::new()` does not have one.
102    precompiled_lib: Option<metal::Library>,
103    /// Whether we've already attempted to load the precompiled library.
104    /// Prevents repeated load attempts on failure.
105    precompiled_load_attempted: bool,
106}
107
108impl KernelRegistry {
109    /// Create a new registry with all embedded shader sources pre-registered.
110    ///
111    /// No compilation happens here — shaders are compiled lazily on first use.
112    pub fn new() -> Self {
113        let mut sources = HashMap::new();
114
115        // Register embedded shader sources.
116        sources.insert(
117            "placeholder".into(),
118            include_str!("shaders/placeholder.metal"),
119        );
120        sources.insert(
121            "quantized_matmul".into(),
122            include_str!("shaders/quantized_matmul.metal"),
123        );
124        sources.insert(
125            "quantized_matmul_simd".into(),
126            include_str!("shaders/quantized_matmul.metal"),
127        );
128        sources.insert(
129            "quantized_matmul_simd_bf16".into(),
130            include_str!("shaders/quantized_matmul.metal"),
131        );
132        sources.insert(
133            "quantized_matmul_simd_bf16_expert".into(),
134            include_str!("shaders/quantized_matmul.metal"),
135        );
136
137        // GGML block-format quantized mat-vec kernels (ADR-006 Phase 3)
138        let ggml_src: &'static str =
139            include_str!("shaders/quantized_matmul_ggml.metal");
140        sources.insert("kernel_mul_mv_q4_0_f32".into(), ggml_src);
141        sources.insert("kernel_mul_mv_q8_0_f32".into(), ggml_src);
142        // ADR-028 iter-368: peer-style NSG=4 NR=2 variant (128 threads/TG).
143        sources.insert("kernel_mul_mv_q8_0_f32_nr2".into(), ggml_src);
144        sources.insert("kernel_mul_mv_q6_K_f32".into(), ggml_src);
145        // ADR-028 iter-309 — q6_K mat-vec with nr0=2 + cached yl[16]
146        // (peer-pattern port of llama.cpp's `kernel_mul_mv_q6_K_f32_impl`
147        // with N_R0_Q6_K=2; 4 rows/TG vs baseline's 2).  Env-gated via
148        // `HF2Q_Q6K_MV_NR2=1` in the dispatcher.
149        sources.insert("kernel_mul_mv_q6_K_f32_nr2".into(), ggml_src);
150        // ADR-040 §0.21c — q6_K column-amortizing mat-vec (mN). Reads each
151        // weight block once and reuses its dequant across R1 ∈ {2..8} src1
152        // columns (batched-decode m axis). BIT-IDENTICAL to plain mv (literal
153        // sums[4]/sc/dall/simd_sum clone); gated via HF2Q_DECODE_MVN.
154        for r1 in 2..=8 {
155            sources.insert(format!("kernel_mul_mv_q6_K_f32_mN_r1_{r1}"), ggml_src);
156        }
157        // ADR-022 Phase 1 — Q5_1 / IQ4_NL dense mat-vec.
158        sources.insert("kernel_mul_mv_q5_1_f32".into(), ggml_src);
159        sources.insert("kernel_mul_mv_iq4_nl_f32".into(), ggml_src);
160        // ADR-013 P7 — Q4_K dense decode mat-vec (port of llama.cpp's
161        // kernel_mul_mv_q4_K_f32 at ggml-metal.metal:7715-7821).
162        sources.insert("kernel_mul_mv_q4_K_f32".into(), ggml_src);
163        // ADR-022 Phase 2 — Q5_K dense mv kernel.
164        sources.insert("kernel_mul_mv_q5_K_f32".into(), ggml_src);
165
166        // GGML block-format quantized matrix-matrix kernels
167        // (ADR-011 Phase 3 Wave P3a: port of llama.cpp's kernel_mul_mm_<q>_f32).
168        // Used at prefill m > 8 to reuse each weight tile across a 32-row
169        // block via threadgroup-staged simdgroup MMA, instead of re-reading
170        // every block per prompt-token as the mv kernel does.
171        let ggml_mm_src: &'static str =
172            include_str!("shaders/quantized_matmul_mm.metal");
173        sources.insert("kernel_mul_mm_q4_0_f32".into(), ggml_mm_src);
174        sources.insert("kernel_mul_mm_q8_0_f32".into(), ggml_mm_src);
175        sources.insert("kernel_mul_mm_q6_K_f32".into(), ggml_mm_src);
176        // ADR-022 Phase 1 — dense Q5_1 / IQ4_NL mm.
177        sources.insert("kernel_mul_mm_q5_1_f32".into(), ggml_mm_src);
178        sources.insert("kernel_mul_mm_iq4_nl_f32".into(), ggml_mm_src);
179        // ADR-022 Phase 2 — dense Q5_K mm.
180        sources.insert("kernel_mul_mm_q5_K_f32".into(), ggml_mm_src);
181        // ADR-022 Phase 3 — dense Q4_K mm.
182        sources.insert("kernel_mul_mm_q4_K_f32".into(), ggml_mm_src);
183
184        // GGML block-format quantized matrix-matrix kernels — tensor API
185        // variant (ADR-011 Phase 3 Wave P3b-tensor: port of llama.cpp's
186        // kernel_mul_mm_impl `#ifdef GGML_METAL_HAS_TENSOR` branch).
187        // Uses Apple's MetalPerformancePrimitives `tensor_ops::matmul2d`
188        // primitive which on M3+ dispatches to hardware tensor cores for
189        // 2-3x the effective FLOP throughput vs the simdgroup MMA path.
190        // Only compiled on devices where the tensor API is available; the
191        // kernel_registry's runtime-probe (see MlxDevice::has_tensor) gates
192        // compilation so non-tensor devices transparently fall back to the
193        // non-tensor `kernel_mul_mm_<q>_f32` kernels.
194        let ggml_mm_tensor_src: &'static str =
195            include_str!("shaders/quantized_matmul_mm_tensor.metal");
196        sources.insert("kernel_mul_mm_q4_0_tensor_f32".into(), ggml_mm_tensor_src);
197        sources.insert("kernel_mul_mm_q4_0_tensor_bf16_perm021".into(), ggml_mm_tensor_src);
198        sources.insert("kernel_mul_mm_q6_K_tensor_bf16_perm021".into(), ggml_mm_tensor_src);
199        sources.insert("kernel_mul_mm_q8_0_tensor_f32".into(), ggml_mm_tensor_src);
200        sources.insert("kernel_mul_mm_q6_K_tensor_f32".into(), ggml_mm_tensor_src);
201        // ADR-022 Phase 1 — Q5_1 / IQ4_NL tensor mm.
202        sources.insert("kernel_mul_mm_q5_1_tensor_f32".into(), ggml_mm_tensor_src);
203        sources.insert("kernel_mul_mm_iq4_nl_tensor_f32".into(), ggml_mm_tensor_src);
204        // ADR-022 Phase 2 — Q5_K tensor mm.
205        sources.insert("kernel_mul_mm_q5_K_tensor_f32".into(), ggml_mm_tensor_src);
206        // ADR-022 Phase 3 — Q4_K tensor mm + Q8_0 perm021.
207        sources.insert("kernel_mul_mm_q4_K_tensor_f32".into(), ggml_mm_tensor_src);
208        sources.insert("kernel_mul_mm_q8_0_tensor_bf16_perm021".into(), ggml_mm_tensor_src);
209        // ADR-029 iter-30 H29-speed — F16-weight V2 large-tile mm.
210        // Same source file as the V2 quantized variants; reads F16 weight
211        // directly from device memory (no per-call dequant).  Used when
212        // MlxQWeight.f16_shadow is populated and m > MM_ROUTING_THRESHOLD.
213        sources.insert("hf2q_mul_mm_tensor_v2_f16".into(), ggml_mm_tensor_src);
214        // ADR-029 iter-36 H28-D — F16-weight perm021 mm for O-projection.
215        // Same source file; reads F16 weight from MlxQWeight.f16_shadow when
216        // populated, bypassing the per-call quantized dequant.  B-stage
217        // (bfloat permuted [n_heads, seq_len, head_dim] input) is byte-
218        // identical to the quantized variant.
219        sources.insert("kernel_mul_mm_f16_tensor_bf16_perm021".into(), ggml_mm_tensor_src);
220        // ADR-029 iter-23 H28-A — V2 large-tile tensor mm (NRA=64 M, NRB=128 N).
221        // Same source file as V1 tensor mm; distinct kernel host names so the
222        // dispatcher can pick V1 vs V2 at runtime via HF2Q_LARGE_TILE_MM.
223        sources.insert("kernel_mul_mm_q4_0_tensor_v2_f32".into(), ggml_mm_tensor_src);
224        sources.insert("kernel_mul_mm_q8_0_tensor_v2_f32".into(), ggml_mm_tensor_src);
225        sources.insert("kernel_mul_mm_q6_K_tensor_v2_f32".into(), ggml_mm_tensor_src);
226        sources.insert("kernel_mul_mm_q5_1_tensor_v2_f32".into(), ggml_mm_tensor_src);
227        sources.insert("kernel_mul_mm_iq4_nl_tensor_v2_f32".into(), ggml_mm_tensor_src);
228        sources.insert("kernel_mul_mm_q5_K_tensor_v2_f32".into(), ggml_mm_tensor_src);
229        sources.insert("kernel_mul_mm_q4_K_tensor_v2_f32".into(), ggml_mm_tensor_src);
230        // ADR-029 iter-28 H29 — whole-tensor dequant from block_q → F16.
231        // Used at model load to materialize an F16 shadow of attn/dense MLP
232        // weights so the runtime dispatch can use kernel_mul_mm_f16_f32_*
233        // (peer's gemma4 pattern).  Trades ~1 GB resident memory for 2-3×
234        // faster per-call dense matmul at prefill.
235        let dequant_to_f16_src: &'static str =
236            include_str!("shaders/dequant_to_f16.metal");
237        sources.insert("hf2q_dequant_q4_0_to_f16".into(), dequant_to_f16_src);
238        sources.insert("hf2q_dequant_q8_0_to_f16".into(), dequant_to_f16_src);
239        sources.insert("hf2q_dequant_q5_1_to_f16".into(), dequant_to_f16_src);
240        sources.insert("hf2q_dequant_iq4_nl_to_f16".into(), dequant_to_f16_src);
241        sources.insert("hf2q_dequant_q4_K_to_f16".into(), dequant_to_f16_src);
242        sources.insert("hf2q_dequant_q5_K_to_f16".into(), dequant_to_f16_src);
243        sources.insert("hf2q_dequant_q6_K_to_f16".into(), dequant_to_f16_src);
244
245        // ADR-022 Phase 1 P1.7 — Q5_1 / IQ4_NL mul_mv_ext r1 family.
246        // Eight instantiations (2 types × 4 r1ptg widths). Each PSO is
247        // additionally specialized at PSO-compile time with FC_mul_mv_nsg
248        // (function_constant 600) and FC_mul_mv_nxpsg (function_constant 601).
249        let mul_mv_ext_src: &'static str = include_str!("shaders/mul_mv_ext.metal");
250        sources.insert("kernel_mul_mv_ext_q5_1_f32_r1_2".into(), mul_mv_ext_src);
251        sources.insert("kernel_mul_mv_ext_q5_1_f32_r1_3".into(), mul_mv_ext_src);
252        sources.insert("kernel_mul_mv_ext_q5_1_f32_r1_4".into(), mul_mv_ext_src);
253        sources.insert("kernel_mul_mv_ext_q5_1_f32_r1_5".into(), mul_mv_ext_src);
254        sources.insert("kernel_mul_mv_ext_iq4_nl_f32_r1_2".into(), mul_mv_ext_src);
255        sources.insert("kernel_mul_mv_ext_iq4_nl_f32_r1_3".into(), mul_mv_ext_src);
256        sources.insert("kernel_mul_mv_ext_iq4_nl_f32_r1_4".into(), mul_mv_ext_src);
257        sources.insert("kernel_mul_mv_ext_iq4_nl_f32_r1_5".into(), mul_mv_ext_src);
258        // ADR-022 Phase 4 — Q4_0 / Q8_0 / Q4_K / Q5_K / Q6_K mv_ext.
259        // 5 types × 4 r1ptg widths = 20 instantiations.
260        for r1 in [2, 3, 4, 5].iter() {
261            for ty in ["q4_0", "q8_0", "q4_K", "q5_K", "q6_K"].iter() {
262                let name = format!("kernel_mul_mv_ext_{ty}_f32_r1_{r1}");
263                sources.insert(name, mul_mv_ext_src);
264            }
265        }
266
267        // Dense bf16×f32 → f32 tensor-API matmul (non-flash-attention
268        // prefill Q@K^T and scores@V, modeled on llama.cpp's
269        // kernel_mul_mm_bf16_f32 with the GGML_METAL_HAS_TENSOR branch
270        // active).  Tile geometry and write-back identical to the
271        // quantized tensor kernel; only the A-stage copy (bfloat →
272        // bfloat, no dequantize) differs.
273        let dense_mm_bf16_tensor_src: &'static str =
274            include_str!("shaders/dense_mm_bf16_tensor.metal");
275        sources.insert("hf2q_dense_mm_bf16_f32_tensor".into(), dense_mm_bf16_tensor_src);
276        // ADR-029 iter-80 H60: V2 large-tile variant (NRA=64, NRB=128).
277        // Same source file (`dense_mm_bf16_tensor.metal`) — second host_name
278        // entry resolves to the V2 kernel appended at the bottom of that
279        // file. Picked at dispatch time when HF2Q_LARGE_TILE_MM=1.
280        sources.insert("hf2q_dense_mm_bf16_f32_tensor_v2".into(), dense_mm_bf16_tensor_src);
281
282        // Dense f32×f32 → f32 tensor-API matmul (F32-everywhere
283        // sibling of dense_mm_bf16_tensor).  Used by hf2q's ADR-005
284        // iter-118 BF16-vs-F32 ViT attention A/B diagnostic to remove
285        // the BF16 K-stage cast as a confounding variable.  Port of
286        // llama.cpp's kernel_mul_mm_f32_f32 specialization
287        // (ggml-metal.metal:10098) on the GGML_METAL_HAS_TENSOR
288        // branch.  Same tile geometry (NR0=64 NR1=32 NK=32) but
289        // float-everywhere shmem staging.
290        let dense_mm_f32_f32_tensor_src: &'static str =
291            include_str!("shaders/dense_mm_f32_f32.metal");
292        sources.insert("hf2q_dense_mm_f32_f32_tensor".into(), dense_mm_f32_f32_tensor_src);
293
294        // Dense f16×f32 → f32 tensor-API matmul (F16-staging sibling
295        // of dense_mm_bf16_tensor).  Used by hf2q's ADR-005 Phase 2c
296        // iter-128 gemma4v ViT precision-parity path: every mmproj
297        // weight is stored as F16 in GGUF, peer's `kernel_mul_mm_f16_f32`
298        // (`ggml-metal.metal:10099`) stages BOTH A and B as `half` in
299        // shmem and computes on `simdgroup_half8x8`.  Matches peer
300        // per-element rounding budget exactly (10-bit mantissa vs
301        // BF16's 7-bit), closing the 1.16x/block cascade compound that
302        // iter-127 numerically bisected to BF16 staging.  Same tile
303        // geometry as the BF16 sibling (NR0=64 NR1=32 NK=32, 8 KB
304        // shmem) — half and bfloat share 16-bit storage.
305        let dense_mm_f16_tensor_src: &'static str =
306            include_str!("shaders/dense_mm_f16_tensor.metal");
307        sources.insert("hf2q_dense_mm_f16_f32_tensor".into(), dense_mm_f16_tensor_src);
308
309        // Dense bf16×f32 → f32 GEMV (matrix-vector multiply) — optimized
310        // for M=1 single-token decode.  Port of llama.cpp's
311        // kernel_mul_mv_bf16_f32_4 (bfloat4-vectorized GEMV kernel).
312        // Used in apply_linear_projection_f32 when seq_len=1 and the
313        // weight matrix is BF16, replacing the MM kernel (~2× faster for
314        // M=1 due to better memory bandwidth utilization per thread).
315        let dense_gemv_bf16_src: &'static str =
316            include_str!("shaders/dense_gemv_bf16.metal");
317        sources.insert("hf2q_dense_gemv_bf16_f32_4".into(), dense_gemv_bf16_src);
318
319        // Fused scale-mask-softmax for the non-flash-attention prefill
320        // path.  One row-local threadgroup per (head, query) pair
321        // replaces three separate dispatches (scale, mask-add, softmax);
322        // reads a bf16 mask (-INF at masked positions, matching
323        // flash_attn_prefill_mask.metal) that is shared across heads.
324        let scale_mask_softmax_src: &'static str =
325            include_str!("shaders/scale_mask_softmax.metal");
326        sources.insert("scale_mask_softmax_f32".into(), scale_mask_softmax_src);
327        // ADR-029 iter-93 H71: float4-vectorized variant for peer parity
328        // with kernel_soft_max_f32_4. Same source file; v4 host_name resolves
329        // to the second kernel appended at the bottom of scale_mask_softmax.metal.
330        sources.insert("scale_mask_softmax_f32_v4".into(), scale_mask_softmax_src);
331
332        // Expert-routed (MoE) quantized matmul kernel (Story 2.1)
333        sources.insert(
334            "quantized_matmul_id".into(),
335            include_str!("shaders/quantized_matmul_id.metal"),
336        );
337
338        // Expert-routed (MoE) GGML block-format quantized matmul kernels
339        let ggml_id_src: &'static str =
340            include_str!("shaders/quantized_matmul_id_ggml.metal");
341        sources.insert("kernel_mul_mv_id_q4_0_f32".into(), ggml_id_src);
342        sources.insert("kernel_mul_mv_id_q8_0_f32".into(), ggml_id_src);
343        // ADR-013 P7 — Q4_K MoE expert-routed mat-vec (port of
344        // llama.cpp's kernel_mul_mv_id_q4_K_f32 at ggml-metal.metal:10349).
345        sources.insert("kernel_mul_mv_id_q4_K_f32".into(), ggml_id_src);
346        sources.insert("kernel_mul_mv_id_q5_K_f32".into(), ggml_id_src);
347        sources.insert("kernel_mul_mv_id_q6_K_f32".into(), ggml_id_src);
348        // ADR-028 iter-321 — q6_K _id with nr0=2 + cached yl[16]
349        // (peer-pattern port mirroring iter-309's non-_id variant).
350        // Env-gated via HF2Q_Q6K_ID_MV_NR2=1 in dispatch_id_mv.
351        sources.insert("kernel_mul_mv_id_q6_K_f32_nr2".into(), ggml_id_src);
352        // ADR-029 iter-6 — q8_0 _id with nr0=2 + nsg=4 cross-SG reduce
353        // (peer-pattern port; peer N_R0_Q8_0=2 + N_SG_Q8_0=4 in
354        //  /opt/llama.cpp/ggml/src/ggml-metal/ggml-metal-impl.h:27,40).
355        // Env-gated via HF2Q_Q8_0_ID_MV_NR2=1 in dispatch_id_mv.
356        sources.insert("kernel_mul_mv_id_q8_0_f32_nr2".into(), ggml_id_src);
357        // ADR-022 Phase 1 — Q5_1 / IQ4_NL MoE expert-routed mat-vec.
358        sources.insert("kernel_mul_mv_id_q5_1_f32".into(), ggml_id_src);
359        sources.insert("kernel_mul_mv_id_iq4_nl_f32".into(), ggml_id_src);
360        // Fused-SwiGLU mv_id variants (ADR-012 §Optimize / Task #15):
361        // computes y[r][n] = sum_k(dequant(W[expert][n][k]) * silu(gate[r][k]) * up[r][k])
362        // in one dispatch — replaces silu_mul + expert_down sequence.
363        sources.insert("kernel_mul_mv_id_q4_0_f32_swiglu".into(), ggml_id_src);
364
365        // Expert-routed (MoE) GGML block-format QUANTIZED MATRIX-MATRIX kernels
366        // (ADR-011 Phase 3 Wave P3a: port of llama.cpp's
367        // `kernel_mul_mm_id_map0_ne20_N` + `kernel_mul_mm_id_<q>_f32`).
368        // Two-stage dispatch: map0 regroups the token-to-expert table into
369        // per-expert routed-token lists, then mm_id stages a 64x32 expert
370        // weight tile into threadgroup shmem and reuses it across a 32-row
371        // block of that expert's routed tokens.
372        let ggml_id_mm_src: &'static str =
373            include_str!("shaders/quantized_matmul_id_mm.metal");
374        sources.insert("kernel_mul_mm_id_map0_ne20_1".into(), ggml_id_mm_src);
375        sources.insert("kernel_mul_mm_id_map0_ne20_8".into(), ggml_id_mm_src);
376        sources.insert("kernel_mul_mm_id_q4_0_f32".into(), ggml_id_mm_src);
377        sources.insert("kernel_mul_mm_id_q8_0_f32".into(), ggml_id_mm_src);
378        sources.insert("kernel_mul_mm_id_q6_K_f32".into(), ggml_id_mm_src);
379        // ADR-013 P16 — Q4_K mm_id (port of llama.cpp ggml-metal.metal:10169).
380        sources.insert("kernel_mul_mm_id_q4_K_f32".into(), ggml_id_mm_src);
381        // ADR-022 Phase 1 P1.6 — Q5_1 / IQ4_NL mm_id template instantiations.
382        sources.insert("kernel_mul_mm_id_q5_1_f32".into(), ggml_id_mm_src);
383        sources.insert("kernel_mul_mm_id_iq4_nl_f32".into(), ggml_id_mm_src);
384        // ADR-022 Phase 2 — Q5_K mm_id template instantiation.
385        sources.insert("kernel_mul_mm_id_q5_K_f32".into(), ggml_id_mm_src);
386
387        // ADR-033 §Pi Task #20 / ADR-034 §93 — fused MoE gate+up+silu_mul
388        // mm_id kernel for Q6_K. Replaces 3 dispatches (gate_mm_id, up_mm_id,
389        // silu_mul_id) with 1 fused dispatch per MoE FFN per layer. Closes
390        // hf2q-vs-llama.cpp prefill gap at production Qwen MoE shapes.
391        let fused_q6_k_mm_id_src: &'static str =
392            include_str!("shaders/fused_gate_up_silu_mm_id_q6_K.metal");
393        sources.insert("kernel_fused_gate_up_silu_mm_id_q6_K_f32".into(), fused_q6_k_mm_id_src);
394
395        // MoE-routed quantized matrix-matrix kernels — tensor API variant
396        // (ADR-011 Phase 3 Wave P3b-tensor).  Uses the MPP tensor_ops
397        // matmul2d primitive for hardware-tensor-core MMA on M3+.  Only
398        // the mm_id kernel is ported — map0 is a short pre-pass (not
399        // matmul) and continues to use the simdgroup version.
400        let ggml_id_mm_tensor_src: &'static str =
401            include_str!("shaders/quantized_matmul_id_mm_tensor.metal");
402        sources.insert("kernel_mul_mm_id_q4_0_tensor_f32".into(), ggml_id_mm_tensor_src);
403        sources.insert("kernel_mul_mm_id_q8_0_tensor_f32".into(), ggml_id_mm_tensor_src);
404        sources.insert("kernel_mul_mm_id_q6_K_tensor_f32".into(), ggml_id_mm_tensor_src);
405        // ADR-013 P16 — Q4_K tensor-API mm_id.
406        sources.insert("kernel_mul_mm_id_q4_K_tensor_f32".into(), ggml_id_mm_tensor_src);
407        // ADR-022 Phase 1 P1.6 — Q5_1 / IQ4_NL tensor-API mm_id.
408        sources.insert("kernel_mul_mm_id_q5_1_tensor_f32".into(), ggml_id_mm_tensor_src);
409        sources.insert("kernel_mul_mm_id_iq4_nl_tensor_f32".into(), ggml_id_mm_tensor_src);
410        // ADR-022 Phase 2 — Q5_K tensor-API mm_id.
411        sources.insert("kernel_mul_mm_id_q5_K_tensor_f32".into(), ggml_id_mm_tensor_src);
412
413        // Embedding kernels (Story 1.5)
414        let embedding_src: &'static str = include_str!("shaders/embedding.metal");
415        sources.insert("embedding_gather_4bit".into(), embedding_src);
416        sources.insert("embedding_gather_6bit".into(), embedding_src);
417
418        // MoE gate kernel (Story 1.5)
419        let moe_gate_src: &'static str = include_str!("shaders/moe_gate.metal");
420        sources.insert("moe_gate".into(), moe_gate_src);
421
422        // MoE dispatch kernels (Story 1.5)
423        let moe_dispatch_src: &'static str = include_str!("shaders/moe_dispatch.metal");
424        sources.insert("fused_gelu_mul".into(), moe_dispatch_src);
425        sources.insert("moe_swiglu_fused".into(), moe_dispatch_src);
426        sources.insert("moe_swiglu_batch".into(), moe_dispatch_src);
427        sources.insert("moe_swiglu_seq".into(), moe_dispatch_src);
428        sources.insert("moe_accumulate".into(), moe_dispatch_src);
429        sources.insert("moe_weighted_sum".into(), moe_dispatch_src);
430        sources.insert("moe_weighted_sum_seq".into(), moe_dispatch_src);
431        sources.insert("zero_buffer".into(), moe_dispatch_src);
432        sources.insert("naive_matvec_f32".into(), moe_dispatch_src);
433        sources.insert("moe_gather_topk_weights".into(), moe_dispatch_src);
434        // bf16 variants (Phase 2 bf16 activation path)
435        sources.insert("fused_gelu_mul_bf16".into(), moe_dispatch_src);
436        sources.insert("moe_swiglu_seq_bf16".into(), moe_dispatch_src);
437        sources.insert("moe_weighted_sum_seq_bf16_input".into(), moe_dispatch_src);
438
439        // ADR-033 §Pi next-iter arc — two-pass MoE mm_id (iter A: map0).
440        // Pre-pass that sorts tokens by expert assignment before the main
441        // mm_id kernel. Ported from llama.cpp's kernel_mul_mm_id_map0; one
442        // template specialization per supported ne20 (n_expert_used).
443        let moe_mm_id_map0_src: &'static str =
444            include_str!("shaders/moe_mm_id_map0.metal");
445        sources.insert("moe_mm_id_map0_ne20_1".into(),  moe_mm_id_map0_src);
446        sources.insert("moe_mm_id_map0_ne20_2".into(),  moe_mm_id_map0_src);
447        sources.insert("moe_mm_id_map0_ne20_4".into(),  moe_mm_id_map0_src);
448        sources.insert("moe_mm_id_map0_ne20_5".into(),  moe_mm_id_map0_src);
449        sources.insert("moe_mm_id_map0_ne20_6".into(),  moe_mm_id_map0_src);
450        sources.insert("moe_mm_id_map0_ne20_8".into(),  moe_mm_id_map0_src);
451        sources.insert("moe_mm_id_map0_ne20_10".into(), moe_mm_id_map0_src);
452        sources.insert("moe_mm_id_map0_ne20_16".into(), moe_mm_id_map0_src);
453        sources.insert("moe_mm_id_map0_ne20_22".into(), moe_mm_id_map0_src);
454
455        // ADR-033 §Pi iter B-1 — main mm_id kernel skeleton (Q4_0). Body
456        // pending iter B-2 (simdgroup matmul + Q4_0 dequant chain). NOT
457        // registered: the shader currently writes zeros for routed tiles, so
458        // registering it would expose a known-wrong pipeline (and prewarm_all
459        // would compile it). The shader file is retained for iter B-2; re-add
460        // the source insert once the real matmul body lands.
461        //   let moe_mm_id_q4_0_src = include_str!("shaders/moe_mm_id_q4_0.metal");
462        //   sources.insert("moe_mm_id_q4_0_f32_skeleton".into(), moe_mm_id_q4_0_src);
463        // ADR-020 iter-11h-e3a: backward kernels for moe_weighted_sum_seq.
464        sources.insert(
465            "moe_weighted_sum_seq_backward_outputs_f32".into(),
466            moe_dispatch_src,
467        );
468        sources.insert(
469            "moe_weighted_sum_seq_backward_weights_f32".into(),
470            moe_dispatch_src,
471        );
472        // ADR-020 iter-11h-e3b: fused backward kernel for moe_swiglu_seq.
473        sources.insert(
474            "moe_swiglu_seq_backward_f32".into(),
475            moe_dispatch_src,
476        );
477
478        // Batched KV cache copy kernels
479        let kv_cache_src: &'static str = include_str!("shaders/kv_cache_copy.metal");
480        sources.insert("kv_cache_copy_batch_f32".into(), kv_cache_src);
481        sources.insert("kv_cache_copy_batch_f32_to_f16".into(), kv_cache_src);
482        // ADR-040 M4 — batched multi-seq F16-K copy (grid.z = N queries)
483        sources.insert("kv_cache_copy_batch_f32_to_f16_batched".into(), kv_cache_src);
484        sources.insert("kv_cache_copy_seq_f32".into(), kv_cache_src);
485        sources.insert("kv_cache_copy_seq_f32_to_f16".into(), kv_cache_src);
486        // Wave P4.11 — fused K+V copy variants
487        sources.insert("kv_cache_copy_seq_f32_kv_dual".into(), kv_cache_src);
488        sources.insert("kv_cache_copy_seq_f32_to_f16_kv_dual".into(), kv_cache_src);
489        // ADR-028 iter-145 — fused single-position K+V copy variants (decode shape)
490        sources.insert("kv_cache_copy_batch_f32_kv_dual".into(), kv_cache_src);
491        sources.insert("kv_cache_copy_batch_f32_to_f16_kv_dual".into(), kv_cache_src);
492        // bf16-source KV cache copy (Phase 2 bf16 activation path)
493        sources.insert("kv_cache_copy_seq_bf16".into(), kv_cache_src);
494        // ADR-030 iter-95 — bit-exact BF16→BF16 head-major cache copy for
495        // Option A xlen verify (avoids F16 round-trip precision drift).
496        sources.insert("kv_cache_copy_seq_bf16_to_bf16_head_major".into(), kv_cache_src);
497
498        // Elementwise and transpose kernels (Story 1.5)
499        let elementwise_src: &'static str = include_str!("shaders/elementwise.metal");
500        sources.insert("elementwise_add_f32".into(), elementwise_src);
501        sources.insert("elementwise_add_f16".into(), elementwise_src);
502        sources.insert("elementwise_mul_f32".into(), elementwise_src);
503        sources.insert("elementwise_mul_f16".into(), elementwise_src);
504        sources.insert("elementwise_add_bf16".into(), elementwise_src);
505        sources.insert("elementwise_mul_bf16".into(), elementwise_src);
506        sources.insert("cast_f16_to_f32".into(), elementwise_src);
507        sources.insert("cast_f32_to_f16".into(), elementwise_src);
508        sources.insert("cast_bf16_to_f32".into(), elementwise_src);
509        sources.insert("cast_f32_to_bf16".into(), elementwise_src);
510        sources.insert("cast_bf16_to_f16".into(), elementwise_src);
511        sources.insert("cast_f16_to_bf16".into(), elementwise_src);
512        sources.insert("scalar_mul_bf16".into(), elementwise_src);
513        sources.insert("scalar_mul_f32".into(), elementwise_src);
514        sources.insert("embedding_gather_scale_f32".into(), elementwise_src);
515        sources.insert("embedding_gather_scale_batch_f32".into(), elementwise_src);
516        sources.insert("permute_021_bf16".into(), elementwise_src);
517        sources.insert("transpose_last2_bf16".into(), elementwise_src);
518        sources.insert("transpose_last2_f16".into(), elementwise_src);
519        sources.insert("permute_021_f32".into(), elementwise_src);
520        sources.insert("permute_021_bf16_to_f32".into(), elementwise_src);
521        sources.insert("permute_021_f32_to_f16".into(), elementwise_src);
522        sources.insert("transpose_2d_f32".into(), elementwise_src);
523        sources.insert("transpose_2d_f16".into(), elementwise_src);
524
525        // Attention kernels (Story 1.3)
526        let sdpa_src: &'static str = include_str!("shaders/sdpa.metal");
527        sources.insert("sdpa".into(), sdpa_src);
528        sources.insert("sdpa_bf16".into(), sdpa_src);
529        let sdpa_sliding_src: &'static str = include_str!("shaders/sdpa_sliding.metal");
530        sources.insert("sdpa_sliding".into(), sdpa_sliding_src);
531        sources.insert("sdpa_sliding_bf16".into(), sdpa_sliding_src);
532
533        // Flash-attention tiled prefill kernel (ADR-011 Phase 1).
534        // Ten entry points; all backed by the same shader source.
535        // Pipelines are compiled with function constants via
536        // `get_pipeline_with_bool_constants` — not `get_pipeline`.
537        let flash_attn_prefill_src: &'static str =
538            include_str!("shaders/flash_attn_prefill.metal");
539        // D=256 variants (BQ=32, BK=16, WM=4, WN=1 — 128 threads/threadgroup)
540        sources.insert(
541            "steel_attention_float32_bq32_bk16_bd256_wm4_wn1_maskfloat32".into(),
542            flash_attn_prefill_src,
543        );
544        sources.insert(
545            "steel_attention_float32_bq32_bk16_bd256_wm4_wn1_maskbool_".into(),
546            flash_attn_prefill_src,
547        );
548        sources.insert(
549            "steel_attention_bfloat16_bq32_bk16_bd256_wm4_wn1_maskbfloat16".into(),
550            flash_attn_prefill_src,
551        );
552        sources.insert(
553            "steel_attention_bfloat16_bq32_bk16_bd256_wm4_wn1_maskbool_".into(),
554            flash_attn_prefill_src,
555        );
556        sources.insert(
557            "steel_attention_float16_bq32_bk16_bd256_wm4_wn1_maskfloat16".into(),
558            flash_attn_prefill_src,
559        );
560        sources.insert(
561            "steel_attention_float16_bq32_bk16_bd256_wm4_wn1_maskbool_".into(),
562            flash_attn_prefill_src,
563        );
564        // D=512 variants (BQ=8, BK=8, WM=1, WN=1 — 32 threads/threadgroup)
565        // NOTE: f32 at D=512 is NOT instantiated — threadgroup memory exceeds
566        // the 32 KB Metal limit (candle sdpa.rs:86-94).
567        sources.insert(
568            "steel_attention_bfloat16_bq8_bk8_bd512_wm1_wn1_maskbfloat16".into(),
569            flash_attn_prefill_src,
570        );
571        sources.insert(
572            "steel_attention_bfloat16_bq8_bk8_bd512_wm1_wn1_maskbool_".into(),
573            flash_attn_prefill_src,
574        );
575        sources.insert(
576            "steel_attention_float16_bq8_bk8_bd512_wm1_wn1_maskfloat16".into(),
577            flash_attn_prefill_src,
578        );
579        sources.insert(
580            "steel_attention_float16_bq8_bk8_bd512_wm1_wn1_maskbool_".into(),
581            flash_attn_prefill_src,
582        );
583
584        // Flash attention vector kernels — SIMD-vectorized decode-path SDPA
585        // (ported from llama.cpp flash_attn_ext_vec)
586        let flash_attn_vec_src: &'static str =
587            include_str!("shaders/flash_attn_vec.metal");
588        sources.insert("flash_attn_vec_dk256".into(), flash_attn_vec_src);
589        sources.insert("flash_attn_vec_dk512".into(), flash_attn_vec_src);
590        sources.insert("flash_attn_vec_reduce_dk128".into(), flash_attn_vec_src);
591        sources.insert("flash_attn_vec_reduce_dk256".into(), flash_attn_vec_src);
592        sources.insert("flash_attn_vec_reduce_dk512".into(), flash_attn_vec_src);
593        // F16 KV variants (Phase 4a)
594        sources.insert("flash_attn_vec_f16kv_dk256".into(), flash_attn_vec_src);
595        sources.insert("flash_attn_vec_f16kv_dk512".into(), flash_attn_vec_src);
596
597        // ADR-037 Phase E1.1 (2026-05-22) — tree-attention kernel for
598        // EAGLE-3 + dynamic tree speculative decoding. Variant of
599        // flash_attn_vec consuming an explicit per-(query, kv_pos) mask
600        // buffer instead of implicit causal. Reduce pass reuses
601        // flash_attn_vec_reduce_* (identical output layout).
602        let tree_attention_src: &'static str =
603            include_str!("shaders/tree_attention.metal");
604        sources.insert("tree_attention_dk128".into(), tree_attention_src);
605        sources.insert("tree_attention_dk256".into(), tree_attention_src);
606        sources.insert("tree_attention_dk512".into(), tree_attention_src);
607        sources.insert("tree_attention_f16kv_dk128".into(), tree_attention_src);
608        sources.insert("tree_attention_f16kv_dk256".into(), tree_attention_src);
609        sources.insert("tree_attention_f16kv_dk512".into(), tree_attention_src);
610
611        // RoPE, normalization, activation kernels (Story 1.4)
612        let rope_src: &'static str = include_str!("shaders/rope.metal");
613        sources.insert("rope_f32".into(), rope_src);
614        sources.insert("rope_f16".into(), rope_src);
615        sources.insert("rope_bf16".into(), rope_src);
616        sources.insert("rope_neox_bf16".into(), rope_src);
617        sources.insert("rope_neox_f32".into(), rope_src);
618        let rms_norm_src: &'static str = include_str!("shaders/rms_norm.metal");
619        sources.insert("rms_norm_f32".into(), rms_norm_src);
620        // ADR-028 iter-310 — float4 + simd_sum variants (peer-pattern,
621        // ported from llama.cpp kernel_rms_norm_fuse_impl<float4, 1>).
622        // Env-gated via HF2Q_RMS_NORM_V2=1 in the dispatchers.
623        sources.insert("rms_norm_f32_v2".into(), rms_norm_src);
624        sources.insert("rms_norm_no_scale_f32_v2".into(), rms_norm_src);
625        sources.insert("rms_norm_f16".into(), rms_norm_src);
626        sources.insert("rms_norm_bf16".into(), rms_norm_src);
627        sources.insert("rms_norm_no_scale_bf16".into(), rms_norm_src);
628        sources.insert("rms_norm_no_scale_f32".into(), rms_norm_src);
629        sources.insert("rms_norm_no_scale_f32_dual".into(), rms_norm_src);
630        sources.insert("rms_norm_f32_triple".into(), rms_norm_src);
631        sources.insert("fused_post_attn_triple_norm_f32".into(), rms_norm_src);
632        // ADR-028 iter-370: V2 (float4 + simd_sum) variant of triple_norm.
633        sources.insert("fused_post_attn_triple_norm_f32_v2".into(), rms_norm_src);
634        // ADR-028 iter-217: fused post-FF norm 2 + end-of-layer FINAL
635        // (combines 2 sequential fused_norm_add dispatches into 1 kernel).
636        sources.insert("fused_post_ff_norm2_endlayer_f32".into(), rms_norm_src);
637        // ADR-028 iter-362: V2 (float4 + simd_sum) variant of the above.
638        // Same math, 75% fewer barriers per dispatch (4 vs 16 at tg=256).
639        sources.insert("fused_post_ff_norm2_endlayer_f32_v2".into(), rms_norm_src);
640        // ADR-028 iter-367: V2 fusion of moe_weighted_sum INTO Path A end-of-layer.
641        // Eliminates 1 dispatch + moe_accum round-trip from gemma4 decode default.
642        sources.insert("fused_moe_wsum_post_ff_norm2_endlayer_f32_v2".into(), rms_norm_src);
643        sources.insert("rms_norm_no_scale_f32_dual_perm".into(), rms_norm_src);
644        // Fused RMS norm + elementwise multiply kernels (Phase 4e.2)
645        sources.insert("rms_norm_mul_f32".into(), rms_norm_src);
646        sources.insert("rms_norm_mul_f16".into(), rms_norm_src);
647        sources.insert("rms_norm_mul_bf16".into(), rms_norm_src);
648        // L2 norm kernels (ADR-013 Decision 3 — Gated DeltaNet Q/K norm)
649        let l2_norm_src: &'static str = include_str!("shaders/l2_norm.metal");
650        sources.insert("l2_norm_f32".into(), l2_norm_src);
651        sources.insert("l2_norm_f16".into(), l2_norm_src);
652        sources.insert("l2_norm_bf16".into(), l2_norm_src);
653        // ADR-015 iter59a — fused L2 norm + scalar multiply (DN q-path).
654        sources.insert("l2_norm_scale_f32".into(), l2_norm_src);
655        // Cumulative-sum kernels (ADR-013 Decision 4 — DeltaNet decay-mask base)
656        let cumsum_src: &'static str = include_str!("shaders/cumsum.metal");
657        sources.insert("cumsum_f32".into(), cumsum_src);
658        sources.insert("cumsum_bf16".into(), cumsum_src);
659        // SSM conv kernels (ADR-013 Decision 7 — DeltaNet 1D causal conv + SiLU)
660        let ssm_conv_src: &'static str = include_str!("shaders/ssm_conv.metal");
661        sources.insert("ssm_conv_forward_f32".into(), ssm_conv_src);
662        sources.insert("ssm_conv_forward_bf16".into(), ssm_conv_src);
663        sources.insert("ssm_conv_state_update_f32".into(), ssm_conv_src);
664        sources.insert("ssm_conv_state_update_bf16".into(), ssm_conv_src);
665        // Tri-solve kernels (ADR-013 Decision 5 — chunked DeltaNet debug path)
666        let tri_solve_src: &'static str = include_str!("shaders/tri_solve.metal");
667        sources.insert("tri_solve_lower_unit_f32".into(), tri_solve_src);
668        sources.insert("tri_solve_lower_unit_bf16".into(), tri_solve_src);
669        // Rope-multi kernels (ADR-013 Decision 10 — IMROPE for Qwen3.5)
670        let rope_multi_src: &'static str = include_str!("shaders/rope_multi.metal");
671        sources.insert("rope_multi_f32".into(), rope_multi_src);
672        sources.insert("rope_multi_bf16".into(), rope_multi_src);
673        // Gated DeltaNet fused kernel (ADR-013 Decision 6 — centerpiece)
674        let gdn_src: &'static str = include_str!("shaders/gated_delta_net.metal");
675        sources.insert("gated_delta_net_f32".into(), gdn_src);
676        // ADR-015 iter56 — decode-only `simd_sum` variant. Three NSG-templated
677        // host names share the same source; selection is by D_k via
678        // `dispatch_gated_delta_net_decode`. Drop-in for the fused kernel
679        // above when n_tokens=1.
680        let gdn_decode_src: &'static str =
681            include_str!("shaders/gated_delta_net_decode.metal");
682        sources.insert("gated_delta_net_decode_f32_1".into(), gdn_decode_src);
683        sources.insert("gated_delta_net_decode_f32_2".into(), gdn_decode_src);
684        sources.insert("gated_delta_net_decode_f32_4".into(), gdn_decode_src);
685        // Wave 5b — chunk-parallel inter-chunk state-recurrence kernel
686        // (the one new kernel in the chunk-parallel pipeline; spec source:
687        // arXiv 2412.06464 §4 + FLA chunk_delta_h.py:43-298).
688        let gdn_chunk_src: &'static str =
689            include_str!("shaders/gated_delta_net_chunk.metal");
690        sources.insert(
691            "gated_delta_net_chunk_inter_state_bf16".into(),
692            gdn_chunk_src,
693        );
694        // ADR-033 §Pi Task #25 iter 19 — K=256 native variant. Same algorithm
695        // as gated_delta_net_chunk_inter_state_bf16 but with compile-time
696        // 32-tile MMA loops (vs 16 for K=128). Required for Qwen3.6's
697        // head_dim=256 chunk-scan path support. Per the K=128 kernel's
698        // documented constraint at gated_delta_net_chunk.metal:441, runtime-K
699        // bounds defeat MMA scheduling — this separate kernel keeps K=256
700        // compile-time-known, avoiding the 3.15× regression.
701        let gdn_chunk_k256_src: &'static str =
702            include_str!("shaders/gated_delta_net_chunk_k256.metal");
703        sources.insert(
704            "gated_delta_net_chunk_inter_state_bf16_k256".into(),
705            gdn_chunk_k256_src,
706        );
707        // ADR-033 §Pi Task #25 iter 20 — K=256 native chunk_o variant.
708        // Sister kernel to iter 19's inter_state_k256. Bumped from K=128's
709        // num_k_tiles=16 to num_k_tiles=32; bo_acc/bA_acc accumulators are
710        // V/BT-indexed (not K-indexed) so they keep their original sizes.
711        let gdn_chunk_o_k256_src: &'static str =
712            include_str!("shaders/gated_delta_net_chunk_o_k256.metal");
713        sources.insert(
714            "gated_delta_net_chunk_o_bf16_k256".into(),
715            gdn_chunk_o_k256_src,
716        );
717        // Wave 5b.1 iter 2 — chunk_scaled_dot_kkt kernel (input-side of
718        // the chunk pipeline; spec source: FLA chunk_scaled_dot_kkt.py:36-99).
719        let gdn_kkt_src: &'static str =
720            include_str!("shaders/gated_delta_net_kkt.metal");
721        sources.insert("gated_delta_net_kkt_bf16".into(), gdn_kkt_src);
722        // Wave 5b.1 iter 2 — recompute_w_u_fwd kernel (applies post-solve A
723        // to (β·v) and (β·k·exp(g)) to produce w and u; spec source: FLA
724        // wy_fast.py:29-117).
725        let gdn_recompute_wu_src: &'static str =
726            include_str!("shaders/gated_delta_net_recompute_wu.metal");
727        sources.insert(
728            "gated_delta_net_recompute_wu_bf16".into(),
729            gdn_recompute_wu_src,
730        );
731        // Wave 5b.1 iter 3 — chunk_fwd_o kernel (per-chunk output: closes
732        // the chunk pipeline; spec source: FLA chunk_o.py:42-138).
733        let gdn_chunk_o_src: &'static str =
734            include_str!("shaders/gated_delta_net_chunk_o.metal");
735        sources.insert("gated_delta_net_chunk_o_bf16".into(), gdn_chunk_o_src);
736        // Wave 5b.1 iter 4 — orchestrator helper kernels:
737        //   chunk_local_cumsum_g_f32      — per-chunk prefix sum on g [B, T, H]
738        //   chunk_tri_solve_invert_f32    — per-chunk-block (I + A_strict)^-1
739        //                                   on FLA's [B, T, H, BT] layout.
740        let chunk_local_cumsum_g_src: &'static str =
741            include_str!("shaders/chunk_local_cumsum_g.metal");
742        sources.insert(
743            "chunk_local_cumsum_g_f32".into(),
744            chunk_local_cumsum_g_src,
745        );
746        let chunk_tri_solve_invert_src: &'static str =
747            include_str!("shaders/chunk_gated_delta_rule_tri_solve_invert.metal");
748        sources.insert(
749            "chunk_tri_solve_invert_f32".into(),
750            chunk_tri_solve_invert_src,
751        );
752        // Sigmoid-gated elementwise multiply (ADR-013 Decision 9 — full-attn output gate)
753        let sigmoid_mul_src: &'static str = include_str!("shaders/sigmoid_mul.metal");
754        sources.insert("sigmoid_mul_f32".into(), sigmoid_mul_src);
755        sources.insert("sigmoid_mul_bf16".into(), sigmoid_mul_src);
756        let silu_mul_src: &'static str = include_str!("shaders/silu_mul.metal");
757        sources.insert("silu_mul_f32".into(), silu_mul_src);
758        // ADR-033 §Pi Task #25 iter 16 — K-bank slice copy for K=256 → 2×K=128
759        // bank-split chunk-scan path (Qwen3.6 head_dim=256 support).
760        let bank_slice_bf16_src: &'static str =
761            include_str!("shaders/bank_slice_bf16.metal");
762        sources.insert("bank_slice_bf16".into(), bank_slice_bf16_src);
763        // ADR-033 §Pi Task #25 iter 17 — F32 variants (for h0 input and
764        // final_state output) + concat (inverse of slice, for assembling
765        // the K=256 final_state from per-bank K=128 outputs). Same source
766        // file — multiple kernels share the BankSliceParams struct.
767        sources.insert("bank_slice_f32".into(), bank_slice_bf16_src);
768        sources.insert("bank_concat_f32".into(), bank_slice_bf16_src);
769        // ADR-034 task #93 — fused gate_proj + up_proj + silu_mul Q8_0.
770        let fused_gate_up_silu_q8_0_src: &'static str =
771            include_str!("shaders/fused_gate_up_silu_q8_0.metal");
772        sources.insert(
773            "kernel_fused_gate_up_silu_q8_0_f32".into(),
774            fused_gate_up_silu_q8_0_src,
775        );
776        // ADR-034 task #94 — fused dual Q4_0 projection (FA Q/K/V/gate fuse).
777        let fused_dual_proj_q4_0_src: &'static str =
778            include_str!("shaders/fused_dual_proj_q4_0.metal");
779        sources.insert(
780            "kernel_fused_dual_proj_q4_0_f32".into(),
781            fused_dual_proj_q4_0_src,
782        );
783        // ADR-034 task #93 cont. 24 — fused gate+up+silu_mul Q4_K (broader quant coverage).
784        #[allow(non_snake_case)]
785        let fused_gate_up_silu_q4_K_src: &'static str =
786            include_str!("shaders/fused_gate_up_silu_q4_K.metal");
787        sources.insert(
788            "kernel_fused_gate_up_silu_q4_K_f32".into(),
789            fused_gate_up_silu_q4_K_src,
790        );
791        // ADR-034 task #93 cont. 26 — fused gate+up+silu_mul IQ4_NL.
792        let fused_gate_up_silu_iq4_nl_src: &'static str =
793            include_str!("shaders/fused_gate_up_silu_iq4_nl.metal");
794        sources.insert(
795            "kernel_fused_gate_up_silu_iq4_nl_f32".into(),
796            fused_gate_up_silu_iq4_nl_src,
797        );
798        // ADR-034 task #93 cont. 27 — fused gate+up+silu_mul Q5_K.
799        #[allow(non_snake_case)]
800        let fused_gate_up_silu_q5_K_src: &'static str =
801            include_str!("shaders/fused_gate_up_silu_q5_K.metal");
802        sources.insert(
803            "kernel_fused_gate_up_silu_q5_K_f32".into(),
804            fused_gate_up_silu_q5_K_src,
805        );
806        // ADR-034 task #93 cont. 28 — fused gate+up+silu_mul Q6_K.
807        #[allow(non_snake_case)]
808        let fused_gate_up_silu_q6_K_src: &'static str =
809            include_str!("shaders/fused_gate_up_silu_q6_K.metal");
810        sources.insert(
811            "kernel_fused_gate_up_silu_q6_K_f32".into(),
812            fused_gate_up_silu_q6_K_src,
813        );
814        let compute_g_beta_src: &'static str = include_str!("shaders/compute_g_beta.metal");
815        sources.insert("compute_g_beta_f32".into(), compute_g_beta_src);
816        let ssm_norm_gate_src: &'static str = include_str!("shaders/ssm_norm_gate.metal");
817        sources.insert("ssm_norm_gate_f32".into(), ssm_norm_gate_src);
818        let gelu_src: &'static str = include_str!("shaders/gelu.metal");
819        sources.insert("gelu_f32".into(), gelu_src);
820        sources.insert("gelu_f16".into(), gelu_src);
821        sources.insert("gelu_bf16".into(), gelu_src);
822        let softmax_src: &'static str = include_str!("shaders/softmax.metal");
823        sources.insert("softmax_f32".into(), softmax_src);
824        sources.insert("softmax_f16".into(), softmax_src);
825        sources.insert("softmax_bf16".into(), softmax_src);
826        let softmax_backward_src: &'static str =
827            include_str!("shaders/softmax_backward.metal");
828        sources.insert("softmax_backward_f32".into(), softmax_backward_src);
829        let log_elementwise_src: &'static str =
830            include_str!("shaders/log_elementwise.metal");
831        sources.insert("log_f32".into(), log_elementwise_src);
832        sources.insert("log_backward_f32".into(), log_elementwise_src);
833        let row_sum_src: &'static str = include_str!("shaders/row_sum.metal");
834        sources.insert("row_sum_f32".into(), row_sum_src);
835        sources.insert("row_sum_backward_f32".into(), row_sum_src);
836        // ADR-020 iter-10a: GGUF-legacy quantize-dequantize round-trip kernels
837        // (Q4_0 + Q8_0).  Used by hf2q's dynamic_quant Track 1 to produce
838        // W_low / W_high for the gradient-Taylor sensitivity formula.
839        let qdq_legacy_src: &'static str = include_str!("shaders/qdq_legacy.metal");
840        sources.insert("qdq_q4_0_f32".into(), qdq_legacy_src);
841        sources.insert("qdq_q8_0_f32".into(), qdq_legacy_src);
842        // ADR-020 iter-10b: RMSNorm reverse-mode autograd kernels.
843        // r_inv helper is reused by both backward kernels; dx and dw cover
844        // the full backward identity for `y = x * rsqrt(mean(x²) + eps) * w`.
845        let rms_norm_backward_src: &'static str =
846            include_str!("shaders/rms_norm_backward.metal");
847        sources.insert(
848            "rms_norm_compute_rms_inv_f32".into(),
849            rms_norm_backward_src,
850        );
851        sources.insert("rms_norm_backward_dx_f32".into(), rms_norm_backward_src);
852        sources.insert("rms_norm_backward_dw_f32".into(), rms_norm_backward_src);
853        // ADR-020 iter-11a: 2-D row-major slice + concat-by-column kernels.
854        // Used by hf2q's multi-head SDPA on GpuTape (slice Q/K/V into
855        // per-head views, run per-head SDPA, concat per-head contexts
856        // back to full attention output).
857        let slice_concat_2d_src: &'static str =
858            include_str!("shaders/slice_concat_2d.metal");
859        sources.insert("slice_2d_cols_f32".into(), slice_concat_2d_src);
860        sources.insert("copy_2d_cols_into_f32".into(), slice_concat_2d_src);
861        // ADR-020 iter-11b: SiLU forward + backward kernels for GpuTape
862        // SwiGLU FFN composition.
863        let silu_backward_src: &'static str =
864            include_str!("shaders/silu_backward.metal");
865        sources.insert("silu_f32".into(), silu_backward_src);
866        sources.insert("silu_backward_f32".into(), silu_backward_src);
867        // ADR-020 iter-11d: FP32 embedding lookup + scatter-add backward.
868        let embedding_autograd_src: &'static str =
869            include_str!("shaders/embedding_autograd.metal");
870        sources.insert("embedding_lookup_f32".into(), embedding_autograd_src);
871        sources.insert(
872            "embedding_scatter_add_f32".into(),
873            embedding_autograd_src,
874        );
875        // ADR-020 iter-13a: Adam optimizer step kernel for Track 2
876        // DWQ-proper training loop.
877        let adam_update_src: &'static str =
878            include_str!("shaders/adam_update.metal");
879        sources.insert("adam_update_f32".into(), adam_update_src);
880        // ADR-020 iter-13b: differentiable affine qdq kernels for the
881        // DWQ-proper training loop.  Init + forward + backward (scales,
882        // biases) — q_int is FROZEN, scales+biases learnable.
883        let qdq_affine_src: &'static str =
884            include_str!("shaders/qdq_affine.metal");
885        sources.insert("qdq_affine_init_f32".into(), qdq_affine_src);
886        sources.insert("qdq_affine_forward_f32".into(), qdq_affine_src);
887        sources.insert(
888            "qdq_affine_backward_scales_f32".into(),
889            qdq_affine_src,
890        );
891        sources.insert(
892            "qdq_affine_backward_biases_f32".into(),
893            qdq_affine_src,
894        );
895        // ADR-020 iter-15: fused affine quantized matmul for DWQ inference.
896        // Per-element kernel; one thread per (m, n) output element.
897        // Tiled + simdgroup-MMA variant lands in iter-15b.
898        let qmm_affine_src: &'static str =
899            include_str!("shaders/qmm_affine.metal");
900        sources.insert("qmm_affine_t_f32".into(), qmm_affine_src);
901        // ADR-020 iter-15b: tiled variant — 16x16 thread block with
902        // cooperative-load X/W tiles in threadgroup-shared memory for
903        // 2-5x speedup over the per-element kernel.
904        let qmm_affine_tiled_src: &'static str =
905            include_str!("shaders/qmm_affine_tiled.metal");
906        sources.insert(
907            "qmm_affine_t_f32_tiled".into(),
908            qmm_affine_tiled_src,
909        );
910        // ADR-020 iter-15c: simdgroup-MMA variant — uses Apple GPU
911        // hardware `simdgroup_matrix<float, 8, 8>` MMA for the inner
912        // reduction.  Per-tile algorithmic 8× over scalar tiled, lands
913        // as ~3-4× wall after launch / load amortization.
914        let qmm_affine_simd_src: &'static str =
915            include_str!("shaders/qmm_affine_simd.metal");
916        sources.insert(
917            "qmm_affine_t_f32_simd".into(),
918            qmm_affine_simd_src,
919        );
920        // ADR-020 iter-15c-2: 4-simdgroup-per-TG variant — 32×32
921        // output tile, 4 simdgroups arranged as 2×2 grid each owning
922        // a 16×16 sub-tile = 4 simdgroup_matrix accumulators.  Same
923        // math as 15c-1, fuller warp-pool exploitation.
924        let qmm_affine_simd4_src: &'static str =
925            include_str!("shaders/qmm_affine_simd4.metal");
926        sources.insert(
927            "qmm_affine_t_f32_simd4".into(),
928            qmm_affine_simd4_src,
929        );
930        // ADR-020 iter-15c-2b: gs=64 variant (mlx-lm dynamic_quant
931        // canonical default).  Same 4-simdgroup geometry, BK=64
932        // instead of 32 (= 8 sub-K-tiles per K-step instead of 4).
933        let qmm_affine_simd4_gs64_src: &'static str =
934            include_str!("shaders/qmm_affine_simd4_gs64.metal");
935        sources.insert(
936            "qmm_affine_t_f32_simd4_gs64".into(),
937            qmm_affine_simd4_gs64_src,
938        );
939        // ADR-020 AC#5 Iter A: packed-U32 dense affine matmul (bits=4,
940        // gs=32) — production decode/prefill kernel for serving DWQ
941        // safetensors directly without a load-time unpack pass.
942        let qmm_affine_t_packed_simd4_b4_src: &'static str =
943            include_str!("shaders/qmm_affine_t_packed_simd4_b4.metal");
944        sources.insert(
945            "qmm_affine_t_packed_simd4_b4".into(),
946            qmm_affine_t_packed_simd4_b4_src,
947        );
948        // ADR-020 iter-11h-b: training-mode causal depthwise 1D
949        // convolution (forward + backward dx + backward dw).  Used by
950        // GpuTape autograd for differentiable Qwen3.5MoE forward
951        // (GatedDeltaNet's conv1d step).
952        let conv1d_dwc_src: &'static str =
953            include_str!("shaders/conv1d_depthwise_causal.metal");
954        sources.insert(
955            "conv1d_depthwise_causal_forward_f32".into(),
956            conv1d_dwc_src,
957        );
958        sources.insert(
959            "conv1d_depthwise_causal_backward_dx_f32".into(),
960            conv1d_dwc_src,
961        );
962        sources.insert(
963            "conv1d_depthwise_causal_backward_dw_f32".into(),
964            conv1d_dwc_src,
965        );
966        // ADR-020 iter-11h-c1: elementwise exp forward + backward.
967        // Building block for GatedDeltaNet's alpha = exp(-g) state-decay.
968        let exp_src: &'static str =
969            include_str!("shaders/exp_elementwise.metal");
970        sources.insert("exp_f32".into(), exp_src);
971        sources.insert("exp_backward_f32".into(), exp_src);
972        // ADR-020 iter-11h-c2: vector outer product (forward + dlhs +
973        // drhs).  Building block for gated_delta_update's
974        // outer(delta, k) state-update term.
975        let outer_src: &'static str =
976            include_str!("shaders/outer_product.metal");
977        sources.insert("outer_product_f32".into(), outer_src);
978        sources.insert("outer_product_backward_lhs_f32".into(), outer_src);
979        sources.insert("outer_product_backward_rhs_f32".into(), outer_src);
980        // ADR-020 iter-11h-e1: take_along_axis (gather) + scatter-backward.
981        // Building block for MoE router on GpuTape.
982        let taa_src: &'static str =
983            include_str!("shaders/take_along_axis.metal");
984        sources.insert("take_along_axis_f32".into(), taa_src);
985        sources.insert("take_along_axis_backward_f32".into(), taa_src);
986        // ADR-020 iter-11h-misc-1: elementwise divide forward + backward.
987        let div_src: &'static str =
988            include_str!("shaders/divide_elementwise.metal");
989        sources.insert("divide_f32".into(), div_src);
990        sources.insert("divide_backward_f32".into(), div_src);
991        // ADR-020 iter-11h-misc-3: elementwise sqrt forward + backward.
992        let sqrt_src: &'static str =
993            include_str!("shaders/sqrt_elementwise.metal");
994        sources.insert("sqrt_f32".into(), sqrt_src);
995        sources.insert("sqrt_backward_f32".into(), sqrt_src);
996        let softcap_src: &'static str = include_str!("shaders/softcap.metal");
997        sources.insert("softcap_f32".into(), softcap_src);
998        sources.insert("softcap_f16".into(), softcap_src);
999        sources.insert("softcap_bf16".into(), softcap_src);
1000
1001        // Fused norm-add kernels — Gemma4 post-attention / post-FFN ordering:
1002        //   normed = rms_norm(input, weight, eps);  output = residual + normed
1003        let fused_norm_add_src: &'static str =
1004            include_str!("shaders/fused_norm_add_bf16.metal");
1005        sources.insert("fused_norm_add_bf16".into(), fused_norm_add_src);
1006        sources.insert("fused_norm_add_no_weight_bf16".into(), fused_norm_add_src);
1007
1008        // Fused head-norm + RoPE f32 kernel — replaces separate rms_norm + rope_neox_f32
1009        let fused_hnr_f32_src: &'static str =
1010            include_str!("shaders/fused_head_norm_rope_f32.metal");
1011        sources.insert("fused_head_norm_rope_f32".into(), fused_hnr_f32_src);
1012        // ADR-028 iter-337 — float4 + simd_sum Phase 1 variant.  Phases
1013        // 2-4 byte-identical to v1; race-fix barrier preserved.  Env-gated
1014        // via HF2Q_FUSED_HEAD_NORM_ROPE_V2 (default ON, opt-out via =0).
1015        sources.insert("fused_head_norm_rope_f32_v2".into(), fused_hnr_f32_src);
1016
1017        // Fused head-norm + RoPE bf16 kernels (single-token + batch prefill)
1018        // Both entry points live in the same .metal file.
1019        let fused_hnr_bf16_src: &'static str =
1020            include_str!("shaders/fused_head_norm_rope_bf16.metal");
1021        sources.insert("fused_head_norm_rope_bf16".into(), fused_hnr_bf16_src);
1022        sources.insert("fused_head_norm_rope_batch_bf16".into(), fused_hnr_bf16_src);
1023
1024        // Fused norm-add f32 kernels — post-attention / post-FFN / end-of-layer
1025        let fused_norm_add_f32_src: &'static str =
1026            include_str!("shaders/fused_norm_add_f32.metal");
1027        sources.insert("fused_norm_add_f32".into(), fused_norm_add_f32_src);
1028        // ADR-028 iter-331 — float4 + simd_sum variant (peer-pattern,
1029        // ported from llama.cpp kernel_rms_norm_fuse_impl<float4, 3>).
1030        // Env-gated via HF2Q_FUSED_NORM_ADD_V2=1 in the dispatcher
1031        // (default ON since iter-331; opt-out via =0/false/off).
1032        sources.insert("fused_norm_add_f32_v2".into(), fused_norm_add_f32_src);
1033        sources.insert("fused_residual_norm_f32".into(), fused_norm_add_f32_src);
1034        sources.insert("fused_residual_norm_scalar_f32".into(), fused_norm_add_f32_src);
1035        sources.insert("fused_moe_routing_f32".into(), fused_norm_add_f32_src);
1036        // ADR-028 iter-363: V2 (simd_max + simd_sum) variant of MoE routing.
1037        sources.insert("fused_moe_routing_f32_v2".into(), fused_norm_add_f32_src);
1038        // ADR-029 iter-175 Step 1i: V3 = V2 + parallel SG-tournament top-K
1039        // (replaces V2's single-thread serial scan for k = 0..top_k).
1040        sources.insert("fused_moe_routing_f32_v3".into(), fused_norm_add_f32_src);
1041        sources.insert("fused_moe_routing_batch_f32".into(), fused_norm_add_f32_src);
1042        // ADR-029 iter-175 Step 1j: batched-prefill V3 (same parallel
1043        // SG-tournament top-K as fused_moe_routing_f32_v3, applied per-token
1044        // within each TG of the batched dispatch).
1045        sources.insert("fused_moe_routing_batch_f32_v3".into(), fused_norm_add_f32_src);
1046        sources.insert("fused_norm_add_scalar_f32".into(), fused_norm_add_f32_src);
1047        sources.insert("fused_moe_wsum_norm_add_f32".into(), fused_norm_add_f32_src);
1048        sources.insert("fused_moe_wsum_dnorm_add_f32".into(), fused_norm_add_f32_src);
1049
1050        // Argsort kernel (Story 2.3) — MoE top-K routing
1051        let argsort_src: &'static str = include_str!("shaders/argsort.metal");
1052        sources.insert("argsort_desc_f32".into(), argsort_src);
1053
1054        // Gather / index_select kernel (Story 2.4)
1055        let gather_src: &'static str = include_str!("shaders/gather.metal");
1056        sources.insert("gather_f32".into(), gather_src);
1057
1058        // F32 KV cache copy kernel (Session merge S1+S2)
1059        let kv_cache_copy_src: &'static str =
1060            include_str!("shaders/kv_cache_copy.metal");
1061        sources.insert("kv_cache_copy".into(), kv_cache_copy_src);
1062        sources.insert("kv_cache_copy_f32".into(), kv_cache_copy_src);
1063
1064        // Strided copy kernel (Story 2.5)
1065        let copy_src: &'static str = include_str!("shaders/copy.metal");
1066        sources.insert("strided_copy_f32".into(), copy_src);
1067        sources.insert("offset_copy_f32".into(), copy_src);
1068
1069        // Fused-QKV split kernel (ADR-005 W-5b.18 — replaces hf2q CPU
1070        // download → triple-loop split → 3× upload round-trip in
1071        // gpu_delta_net::layer_qkv_deinterleave).
1072        let qkv_split_src: &'static str = include_str!("shaders/qkv_split.metal");
1073        sources.insert("qkv_split_f32".into(), qkv_split_src);
1074
1075        // Tiled-GQA broadcast kernel (ADR-005 W-5b.19 — replaces hf2q CPU
1076        // tiled-replicate at gpu_delta_net::apply_gated_delta_net_chunk
1077        // GQA pre-expansion, ~497 ms / 10.4 ms-per-layer at PP4106).
1078        let repeat_tiled_src: &'static str =
1079            include_str!("shaders/repeat_tiled.metal");
1080        sources.insert("repeat_tiled_f32".into(), repeat_tiled_src);
1081
1082        // Dense F16 GEMM kernel (Story 2.6) — lm_head projection
1083        let dense_gemm_src: &'static str = include_str!("shaders/dense_gemm.metal");
1084        sources.insert("dense_gemm_f16".into(), dense_gemm_src);
1085        sources.insert("dense_matvec_f16".into(), dense_gemm_src);
1086        sources.insert("dense_matvec_f16w_f32io".into(), dense_gemm_src);
1087        // BF16-weight mat-vec: BF16 weights × F32 input → F32 output (decode lm_head)
1088        sources.insert("dense_matvec_bf16w_f32io".into(), dense_gemm_src);
1089        // Pure F32 mat-vec: F32 weights × F32 input → F32 output (decode lm_head)
1090        sources.insert("dense_matvec_f32".into(), dense_gemm_src);
1091
1092        // Standalone FWHT for TurboQuant pre/post-rotation (SIMD shuffle, zero barriers)
1093        let fwht_src: &'static str = include_str!("shaders/fwht_standalone.metal");
1094        sources.insert("fwht_standalone_f32_d256".into(), fwht_src);
1095        sources.insert("fwht_standalone_f32_d512".into(), fwht_src);
1096        // ADR-007 iter-14 D1 SRHT variants: sign pre-mult (for Q) + sign undo (for output)
1097        sources.insert("fwht_sign_premult_f32_d256".into(), fwht_src);
1098        sources.insert("fwht_sign_premult_f32_d512".into(), fwht_src);
1099        sources.insert("fwht_sign_undo_f32_d256".into(), fwht_src);
1100        sources.insert("fwht_sign_undo_f32_d512".into(), fwht_src);
1101
1102        // Fast Hadamard quantize (SIMD shuffle, zero barriers)
1103        let hq_fast_src: &'static str = include_str!("shaders/hadamard_quantize_kv_fast.metal");
1104        sources.insert("hadamard_quantize_kv_fast_d256".into(), hq_fast_src);
1105        sources.insert("hadamard_quantize_kv_fast_d512".into(), hq_fast_src);
1106        // ADR-028 iter-485 (Phase 7d / H4): fused K+V single-position 4-bit encoder.
1107        sources.insert("hadamard_quantize_kv_fast_dual_d256".into(), hq_fast_src);
1108        sources.insert("hadamard_quantize_kv_fast_dual_d512".into(), hq_fast_src);
1109        // Track B (iter-21): higher-bit (5/6-bit) quantize kernels (byte-packed)
1110        sources.insert("hadamard_quantize_kv_hb_d256".into(), hq_fast_src);
1111        sources.insert("hadamard_quantize_kv_hb_d512".into(), hq_fast_src);
1112        // ADR-040 M4 — batched multi-seq FWHT-V quantize (grid.y = N queries)
1113        sources.insert("hadamard_quantize_kv_hb_batched_d256".into(), hq_fast_src);
1114        sources.insert("hadamard_quantize_kv_hb_batched_d512".into(), hq_fast_src);
1115        // ADR-028 iter-148: fused K+V single-position HB encoder
1116        sources.insert("hadamard_quantize_kv_hb_dual_d256".into(), hq_fast_src);
1117        sources.insert("hadamard_quantize_kv_hb_dual_d512".into(), hq_fast_src);
1118        // ADR-028 Phase 10e.5 (iter-351): no-FWHT V quantize for hybrid path.
1119        // Same byte-packed Lloyd-Max codebook output, but skips the Hadamard
1120        // rotation so dequant in SDPA recovers raw V (no FWHT-undo needed).
1121        sources.insert("kv_quantize_v_no_fwht_d256".into(), hq_fast_src);
1122        sources.insert("kv_quantize_v_no_fwht_d512".into(), hq_fast_src);
1123        // ADR-028 Phase 10c.5 (iter-354): fused F16-K-copy + V-no-FWHT-encode.
1124        // Saves 30 KV-write dispatches/decode-token at gemma4 30L by combining
1125        // the per-layer K-cast and V-encode into a single dispatch (Z-dim).
1126        sources.insert("kv_copy_kf16_quantize_v_no_fwht_d256".into(), hq_fast_src);
1127        sources.insert("kv_copy_kf16_quantize_v_no_fwht_d512".into(), hq_fast_src);
1128
1129        // iter-20 Leg F: TQ KV dequantize kernel (nibbles+norms → F32)
1130        let tq_dq_src: &'static str = include_str!("shaders/tq_dequantize_kv.metal");
1131        sources.insert("tq_dequantize_kv".into(), tq_dq_src);
1132        // Track B (iter-21): higher-bit dequantize kernel (byte-packed indices)
1133        sources.insert("tq_dequantize_hb_kv".into(), tq_dq_src);
1134        // ADR-027 Phase B iter-30 (hf2q sub-sub-iter 23c-β.1): sequence-batch
1135        // dequant variant. Same MSL source; new kernel entry point
1136        // `tq_dequantize_hb_kv_seq` reads positions [start_pos..start_pos+n_tokens)
1137        // in one dispatch (one threadgroup per (kv_head, position)). Unblocks
1138        // hf2q's TQ-aware prefill SDPA path (current per-position kernel
1139        // requires cur_len separate dispatches).
1140        sources.insert("tq_dequantize_hb_kv_seq".into(), tq_dq_src);
1141
1142        // iter-24: native higher-bit (5/6/8-bit) TQ SDPA kernel (byte-packed K/V)
1143        let tq_hb_src: &'static str = include_str!("shaders/flash_attn_vec_tq_hb.metal");
1144        sources.insert("flash_attn_vec_tq_hb_dk256".into(), tq_hb_src);
1145        sources.insert("flash_attn_vec_tq_hb_dk512".into(), tq_hb_src);
1146        // ADR-040 M-SPEED-LC — batched multi-seq TQ-HB decode flash (same source file).
1147        sources.insert("flash_attn_vec_tq_hb_batched_dk256".into(), tq_hb_src);
1148        sources.insert("flash_attn_vec_tq_hb_batched_dk512".into(), tq_hb_src);
1149
1150        // ADR-028 §iter-485 (Phase 7d H3): fused TQ-HB reduce + FWHT-sign-undo.
1151        // Combines flash_attn_vec_reduce + fwht_sign_undo_f32 into a single
1152        // dispatch, saving 1 dispatch + 1 forced barrier per layer per decode
1153        // token. Gated by env flag `HF2Q_TQ_HB_OUT_FUSED=1` in forward_mlx.rs.
1154        let reduce_undo_src: &'static str = include_str!("shaders/flash_attn_vec_reduce_tq_hb_undo.metal");
1155        sources.insert("flash_attn_vec_reduce_tq_hb_undo_dk256".into(), reduce_undo_src);
1156        sources.insert("flash_attn_vec_reduce_tq_hb_undo_dk512".into(), reduce_undo_src);
1157
1158        // ADR-028 Phase 10d (iter-349): hybrid F16-K + TQ-HB-V SDPA kernel.
1159        // Same V-side codebook as flash_attn_vec_tq_hb (5/6/8-bit Lloyd-Max);
1160        // K-side reads F16 dense — peer-equivalent layout, no codebook lookup.
1161        let hybrid_src: &'static str = include_str!("shaders/flash_attn_vec_hybrid.metal");
1162        sources.insert("flash_attn_vec_hybrid_dk256".into(), hybrid_src);
1163        sources.insert("flash_attn_vec_hybrid_dk512".into(), hybrid_src);
1164        // ADR-040 M4 — batched multi-seq decode flash (same source file).
1165        sources.insert("flash_attn_vec_hybrid_batched_dk256".into(), hybrid_src);
1166        sources.insert("flash_attn_vec_hybrid_batched_dk512".into(), hybrid_src);
1167
1168        // ADR-029: verbatim llama.cpp peer port.
1169        // F16-K + F16-V, DK=DV=256, NWG=1, NSG=1, NE=1. No function constants — baked.
1170        let peer_port_src: &'static str = include_str!("shaders/flash_attn_vec_peer_port_f16.metal");
1171        sources.insert("flash_attn_vec_peer_port_f16_dk256_dv256".into(), peer_port_src);
1172
1173        // ADR-029 iter-134: peer reduce kernel (verbatim port of ggml-metal.metal 7235-7275).
1174        // Pairs with the NWG=32 vec kernel to match peer's actual runtime dispatch.
1175        let peer_port_reduce_src: &'static str =
1176            include_str!("shaders/flash_attn_vec_peer_port_f16_reduce.metal");
1177        sources.insert(
1178            "flash_attn_vec_peer_port_f16_reduce_dv256_nwg32".into(),
1179            peer_port_reduce_src,
1180        );
1181
1182        // ADR-029 iter-135: NWG=32 variant of the verbatim peer port. Same body as
1183        // flash_attn_vec_peer_port_f16.metal with NWG=1→32. Pairs with iter-134 reduce kernel.
1184        let peer_port_nwg32_src: &'static str =
1185            include_str!("shaders/flash_attn_vec_peer_port_f16_nwg32.metal");
1186        sources.insert(
1187            "flash_attn_vec_peer_port_f16_nwg32_dk256_dv256".into(),
1188            peer_port_nwg32_src,
1189        );
1190
1191        // GPU sampling kernels — eliminate logits readback (Phase 6)
1192        let argmax_src: &'static str = include_str!("shaders/argmax.metal");
1193        sources.insert("argmax_f32".into(), argmax_src);
1194        // ADR-040 §26 iter-M — GPU first-max argmax + threshold candidate collect.
1195        let gpu_sample_src: &'static str =
1196            include_str!("shaders/gpu_sample_argmax_candidates.metal");
1197        sources.insert("gpu_sample_argmax_candidates".into(), gpu_sample_src);
1198        let softmax_sample_src: &'static str =
1199            include_str!("shaders/softmax_sample.metal");
1200        sources.insert("softmax_sample_f32".into(), softmax_sample_src);
1201        // Top-K kernel for Q8 rerank: avoids full-logits readback.
1202        let top_k_src: &'static str = include_str!("shaders/top_k.metal");
1203        sources.insert("top_k_f32".into(), top_k_src);
1204
1205        // MoE GPU routing + weighted reduce (ADR-013 P13.3 perf).
1206        // Replaces CPU softmax+topk round-trip and CPU weighted accumulate.
1207        let moe_stk_src: &'static str =
1208            include_str!("shaders/moe_softmax_topk.metal");
1209        sources.insert("moe_softmax_topk_f32".into(), moe_stk_src);
1210        let moe_wr_src: &'static str =
1211            include_str!("shaders/moe_weighted_reduce.metal");
1212        sources.insert("moe_weighted_reduce_f32".into(), moe_wr_src);
1213        let sdpa_decode_src: &'static str =
1214            include_str!("shaders/sdpa_decode.metal");
1215        sources.insert("sdpa_decode".into(), sdpa_decode_src);
1216
1217        Self {
1218            cache: HashMap::new(),
1219            sources,
1220            precompiled_lib: None,
1221            precompiled_load_attempted: false,
1222        }
1223    }
1224
1225    /// Try to obtain the precompiled `default.metallib` Library, loading it
1226    /// lazily on first call.  Returns `None` when:
1227    /// - `MLX_PRECOMPILED_METALLIB` is unset (default)
1228    /// - The embedded blob is empty (build.rs skipped metallib build)
1229    /// - `device.new_library_with_data` failed previously
1230    /// - The previous load attempt already failed (no retry)
1231    fn try_precompiled_lib(
1232        &mut self,
1233        device: &metal::DeviceRef,
1234    ) -> Option<&metal::LibraryRef> {
1235        if !precompiled_enabled() {
1236            return None;
1237        }
1238        if !self.precompiled_load_attempted {
1239            self.precompiled_load_attempted = true;
1240            if EMBEDDED_METALLIB.is_empty() {
1241                return None;
1242            }
1243            // Apple's `newLibraryWithData:` expects a dispatch_data_t.
1244            // metal-rs wraps this via `new_library_with_data` which takes
1245            // a `&[u8]`.
1246            match device.new_library_with_data(EMBEDDED_METALLIB) {
1247                Ok(lib) => self.precompiled_lib = Some(lib),
1248                Err(_) => self.precompiled_lib = None,
1249            }
1250        }
1251        self.precompiled_lib.as_deref()
1252    }
1253
1254    /// Register a shader source at runtime (useful for testing and dynamic
1255    /// kernel generation).
1256    pub fn register_source(&mut self, name: impl Into<String>, source: &'static str) {
1257        let name = name.into();
1258        // Invalidate any cached pipeline for this name since the source changed.
1259        self.cache.remove(&name);
1260        self.sources.insert(name, source);
1261    }
1262
1263    /// ADR-033 §Pi Task #20 iter 11 (2026-05-23) — eagerly compile a list
1264    /// of kernel pipelines to move first-call JIT/PSO-creation cost out of
1265    /// the prefill hot path and into the model-load window.
1266    ///
1267    /// The profiler showed that on Qwen3.6 35B-A3B MoE prefill at seq=553,
1268    /// the FIRST FA layer + FIRST FFN layer take ~40ms each (vs ~14µs
1269    /// warm) — that's 80ms of the 221ms prefill, dominated by Metal
1270    /// pipeline state creation. Pre-creating these pipelines at load time
1271    /// (when 3.3s is already being spent on model parse + upload) is a
1272    /// strict perf win for measured prefill throughput.
1273    ///
1274    /// Best-effort: silently skips kernels that aren't registered (e.g.,
1275    /// list contains a kernel name for an arch this build doesn't use).
1276    /// Logs at debug level on failure to keep load-path quiet.
1277    ///
1278    /// Returns the count of pipelines successfully prewarmed.
1279    pub fn prewarm_pipelines(
1280        &mut self,
1281        device: &metal::DeviceRef,
1282        names: &[&str],
1283    ) -> usize {
1284        let mut warmed = 0_usize;
1285        for name in names {
1286            // Skip if already cached.
1287            if self.cache.contains_key(*name) {
1288                warmed += 1;
1289                continue;
1290            }
1291            // Skip if no source registered for this name.
1292            if !self.sources.contains_key(*name) {
1293                continue;
1294            }
1295            // Best-effort: ignore errors so one broken kernel doesn't
1296            // poison the whole prewarm pass.
1297            if self.get_pipeline(name, device).is_ok() {
1298                warmed += 1;
1299            }
1300        }
1301        warmed
1302    }
1303
1304    /// ADR-033 §Pi Task #20 iter 12 (2026-05-23) — prewarm pipelines that
1305    /// require `[[function_constant]]` specialization. Each entry is
1306    /// `(name, &[(constant_index, bool_value)])`. Mirrors
1307    /// `prewarm_pipelines` but routes through
1308    /// `get_pipeline_with_bool_constants` so kernels declaring
1309    /// `function_constant` decls without defaults can be safely
1310    /// prewarmed.
1311    ///
1312    /// Use case: hot-path kernels like `flash_attn_prefill_bf16_d256`
1313    /// (uses bool constants 200/201/300/301/303 for align/mask/causal/blk
1314    /// flags) cannot be safely prewarmed without specialization — Metal
1315    /// `validateWithDevice:` asserts and aborts the process. Provide
1316    /// the constants production uses and prewarming becomes safe.
1317    ///
1318    /// Returns count warmed.
1319    pub fn prewarm_pipelines_with_bool_constants(
1320        &mut self,
1321        device: &metal::DeviceRef,
1322        entries: &[(&str, &[(usize, bool)])],
1323    ) -> usize {
1324        let mut warmed = 0_usize;
1325        for (name, bool_constants) in entries {
1326            if !self.sources.contains_key(*name) {
1327                continue;
1328            }
1329            if self
1330                .get_pipeline_with_bool_constants(name, device, bool_constants)
1331                .is_ok()
1332            {
1333                warmed += 1;
1334            }
1335        }
1336        warmed
1337    }
1338
1339    /// ADR-033 §Pi Task #20 iter 11 (2026-05-23) — prewarm every registered
1340    /// kernel source. Useful when the exact set of needed kernels is hard
1341    /// to enumerate (e.g., serving paths that span multiple arches).
1342    /// Total cost is bounded by the number of registered kernels times
1343    /// the per-pipeline PSO creation cost (~5-15ms typical on M-series).
1344    ///
1345    /// Returns (warmed, skipped) counts.
1346    pub fn prewarm_all(&mut self, device: &metal::DeviceRef) -> (usize, usize) {
1347        let names: Vec<String> = self.sources.keys().cloned().collect();
1348        let mut warmed = 0_usize;
1349        let mut skipped = 0_usize;
1350        for name in &names {
1351            if self.cache.contains_key(name) {
1352                warmed += 1;
1353                continue;
1354            }
1355            if self.get_pipeline(name, device).is_ok() {
1356                warmed += 1;
1357            } else {
1358                skipped += 1;
1359            }
1360        }
1361        (warmed, skipped)
1362    }
1363
1364    /// Get a compiled compute pipeline for the named kernel function.
1365    ///
1366    /// On first call for a given name, this compiles the MSL source into a
1367    /// Metal library, extracts the named function, and creates a
1368    /// `ComputePipelineState`.  Subsequent calls return the cached pipeline.
1369    ///
1370    /// # Errors
1371    ///
1372    /// * `MlxError::KernelNotFound` — no source registered for this name.
1373    /// * `MlxError::ShaderCompilationError` — MSL compilation or pipeline
1374    ///   creation failed.
1375    pub fn get_pipeline(
1376        &mut self,
1377        name: &str,
1378        device: &metal::DeviceRef,
1379    ) -> Result<&ComputePipelineState> {
1380        if !self.cache.contains_key(name) {
1381            // ADR-029 iter-175 Step 1l: precompiled .metallib fast path.
1382            // When MLX_PRECOMPILED_METALLIB=1 AND the kernel exists in the
1383            // embedded library, use it.  Otherwise fall through to runtime
1384            // source compile.  Empirically ~+5.89% faster on q6_K matvec
1385            // (iter 1k bench).
1386            let precompiled_function = self
1387                .try_precompiled_lib(device)
1388                .and_then(|lib| lib.get_function(name, None).ok());
1389
1390            let function = match precompiled_function {
1391                Some(f) => f,
1392                None => {
1393                    // Slow path: compile the shader.
1394                    let source = self.sources.get(name).ok_or_else(|| {
1395                        MlxError::KernelNotFound(name.to_string())
1396                    })?;
1397
1398                    let compile_opts = metal::CompileOptions::new();
1399                    let library = device
1400                        .new_library_with_source(source, &compile_opts)
1401                        .map_err(|msg| MlxError::ShaderCompilationError {
1402                            name: name.to_string(),
1403                            message: msg,
1404                        })?;
1405
1406                    library
1407                        .get_function(name, None)
1408                        .map_err(|msg| MlxError::ShaderCompilationError {
1409                            name: name.to_string(),
1410                            message: msg,
1411                        })?
1412                }
1413            };
1414
1415            // Build the pipeline through a descriptor so we can attach a
1416            // human-readable label.  The label propagates into Instruments /
1417            // xctrace Metal System Trace as the per-pipeline identifier
1418            // (`metal-object-label` schema), giving us per-kernel attribution
1419            // instead of the generic "Compute Command 0" placeholder.
1420            //
1421            // `MTLComputePipelineState.label` is read-only after creation per
1422            // the Apple Metal spec; the only supported way to set it is via
1423            // the descriptor before pipeline creation.  ADR-015 iter9b.
1424            let descriptor = ComputePipelineDescriptor::new();
1425            descriptor.set_compute_function(Some(&function));
1426            descriptor.set_label(name);
1427            // ADR-028 iter-376: threadGroupSizeIsMultipleOfThreadExecutionWidth
1428            // hint allows the Metal compiler to skip bounds checks and use more
1429            // aggressive codegen.  Opt-in via HF2Q_PIPELINE_TG_MULT_HINT=1.
1430            // SAFETY: every dispatched threadgroup MUST be a multiple of 32 at
1431            // runtime — Apple specifies undefined behavior otherwise.  Our hot
1432            // kernels use tg_size ∈ {32, 64, 256, 1024} (all multiples of 32).
1433            if std::env::var("HF2Q_PIPELINE_TG_MULT_HINT").ok().as_deref() == Some("1") {
1434                descriptor.set_thread_group_size_is_multiple_of_thread_execution_width(true);
1435            }
1436
1437            let pipeline = device
1438                .new_compute_pipeline_state(&descriptor)
1439                .map_err(|msg| MlxError::ShaderCompilationError {
1440                    name: name.to_string(),
1441                    message: msg,
1442                })?;
1443
1444            self.cache.insert(name.to_string(), pipeline);
1445        }
1446
1447        // At this point the pipeline is guaranteed to be in the cache.
1448        // We use `ok_or_else` instead of `expect` to satisfy the no-panic policy.
1449        self.cache.get(name).ok_or_else(|| {
1450            MlxError::KernelNotFound(name.to_string())
1451        })
1452    }
1453
1454    /// Get a compiled compute pipeline for the named kernel, specialized with
1455    /// Metal function constants (both bool and i32 in one call).
1456    ///
1457    /// `bool_constants` contains `(index, value)` pairs mapping to
1458    /// `[[function_constant(index)]]` bool declarations in the MSL shader.
1459    /// `int_constants` contains `(index, value)` pairs mapping to
1460    /// `[[function_constant(index)]]` int (int32_t) declarations in the MSL
1461    /// shader.
1462    ///
1463    /// Pipelines are cached by a composite key:
1464    /// `"<name>|<index>:b<0|1>|...|<index>:i<value>|..."`.  The 'b' prefix
1465    /// marks bool entries and the 'i' prefix marks i32 entries, making the
1466    /// format unambiguous regardless of constant ordering.  Distinct
1467    /// `(name, constants)` combinations each compile to a separate pipeline;
1468    /// the slow compilation path runs at most once per unique combination.
1469    ///
1470    /// # Errors
1471    ///
1472    /// * `MlxError::KernelNotFound` — no source registered for this name.
1473    /// * `MlxError::ShaderCompilationError` — MSL compilation, function
1474    ///   specialisation, or pipeline creation failed.
1475    pub fn get_pipeline_with_constants(
1476        &mut self,
1477        name: &str,
1478        device: &metal::DeviceRef,
1479        bool_constants: &[(usize, bool)],
1480        int_constants: &[(usize, i32)],
1481    ) -> Result<&ComputePipelineState> {
1482        // Build a composite cache key so distinct constant combinations each
1483        // compile to their own pipeline.  Bool entries use the 'b' type marker
1484        // and i32 entries use 'i'; this prevents a collision between, e.g.,
1485        // bool index 5 value 1 and int index 5 value 1.
1486        let mut cache_key = name.to_string();
1487        for &(index, value) in bool_constants {
1488            cache_key.push('|');
1489            cache_key.push_str(&index.to_string());
1490            cache_key.push_str(if value { ":b1" } else { ":b0" });
1491        }
1492        for &(index, value) in int_constants {
1493            cache_key.push('|');
1494            cache_key.push_str(&index.to_string());
1495            cache_key.push(':');
1496            cache_key.push('i');
1497            cache_key.push_str(&value.to_string());
1498        }
1499
1500        if !self.cache.contains_key(&cache_key) {
1501            // Build the FunctionConstantValues object with all bool and i32
1502            // constants.  Metal's set_constant_value_at_index reads the value
1503            // through a raw pointer; the pointed-to bytes must match the size
1504            // declared in the MSL shader (1 byte for bool, 4 bytes for int).
1505            let fcv = FunctionConstantValues::new();
1506
1507            for &(index, value) in bool_constants {
1508                // MTLDataType::Bool = 53 (metal-rs argument.rs).
1509                // The Metal runtime reads it as an Objective-C BOOL (uint8_t).
1510                let v: u8 = if value { 1 } else { 0 };
1511                fcv.set_constant_value_at_index(
1512                    (&v as *const u8).cast::<std::ffi::c_void>(),
1513                    MTLDataType::Bool,
1514                    index as u64,
1515                );
1516            }
1517
1518            for &(index, value) in int_constants {
1519                // MTLDataType::Int = 29 (metal-rs argument.rs).
1520                // The Metal runtime reads 4 bytes as a signed 32-bit integer,
1521                // matching the Metal shader type `constant int`.
1522                fcv.set_constant_value_at_index(
1523                    (&value as *const i32).cast::<std::ffi::c_void>(),
1524                    MTLDataType::Int,
1525                    index as u64,
1526                );
1527            }
1528
1529            // ADR-029 iter-175 Step 1l: try precompiled .metallib first.
1530            // Step 1m: gated separately on MLX_PRECOMPILED_METALLIB_FCV=1
1531            // so we can isolate whether the integration regression at
1532            // Step 1l (tg50 95.4 → 62.1) lives in this FCV path vs the
1533            // no-FCV path in `get_pipeline`.
1534            //
1535            // get_function with FCV takes ownership of the FCV, so we
1536            // build a separate one for the precompiled probe.
1537            let precompiled_function = if precompiled_fcv_enabled() {
1538                let probe_fcv = FunctionConstantValues::new();
1539                for &(index, value) in bool_constants {
1540                    let v: u8 = if value { 1 } else { 0 };
1541                    probe_fcv.set_constant_value_at_index(
1542                        (&v as *const u8).cast::<std::ffi::c_void>(),
1543                        MTLDataType::Bool,
1544                        index as u64,
1545                    );
1546                }
1547                for &(index, value) in int_constants {
1548                    probe_fcv.set_constant_value_at_index(
1549                        (&value as *const i32).cast::<std::ffi::c_void>(),
1550                        MTLDataType::Int,
1551                        index as u64,
1552                    );
1553                }
1554                self.try_precompiled_lib(device)
1555                    .and_then(|lib| lib.get_function(name, Some(probe_fcv)).ok())
1556            } else {
1557                None
1558            };
1559
1560            let function = match precompiled_function {
1561                Some(f) => f,
1562                None => {
1563                    // Slow path: compile the shader with function constant specialisation.
1564                    let source = self.sources.get(name).ok_or_else(|| {
1565                        MlxError::KernelNotFound(name.to_string())
1566                    })?;
1567
1568                    let compile_opts = metal::CompileOptions::new();
1569                    let library = device
1570                        .new_library_with_source(source, &compile_opts)
1571                        .map_err(|msg| MlxError::ShaderCompilationError {
1572                            name: name.to_string(),
1573                            message: msg,
1574                        })?;
1575
1576                    library
1577                        .get_function(name, Some(fcv))
1578                        .map_err(|msg| MlxError::ShaderCompilationError {
1579                            name: name.to_string(),
1580                            message: msg,
1581                        })?
1582                }
1583            };
1584
1585            // Label this specialisation with the full composite cache key
1586            // (e.g. `kernel_mul_mv_q4_0_f32|0:b1|3:i32`) so xctrace Metal
1587            // System Trace shows each function-constant variant as a distinct
1588            // pipeline.  Without this, all specialisations share a generic
1589            // "Compute Command 0" identifier and we cannot attribute µs/token
1590            // to a specific (kernel, constants) combination.  ADR-015 iter9b.
1591            let descriptor = ComputePipelineDescriptor::new();
1592            descriptor.set_compute_function(Some(&function));
1593            descriptor.set_label(&cache_key);
1594            // ADR-028 iter-376: same hint as primary pipeline path.
1595            if std::env::var("HF2Q_PIPELINE_TG_MULT_HINT").ok().as_deref() == Some("1") {
1596                descriptor.set_thread_group_size_is_multiple_of_thread_execution_width(true);
1597            }
1598
1599            let pipeline = device
1600                .new_compute_pipeline_state(&descriptor)
1601                .map_err(|msg| MlxError::ShaderCompilationError {
1602                    name: name.to_string(),
1603                    message: msg,
1604                })?;
1605
1606            self.cache.insert(cache_key.clone(), pipeline);
1607        }
1608
1609        self.cache.get(&cache_key).ok_or_else(|| {
1610            MlxError::KernelNotFound(name.to_string())
1611        })
1612    }
1613
1614    /// Get a compiled compute pipeline for the named kernel, specialized with
1615    /// Metal bool function constants.
1616    ///
1617    /// The `bool_constants` slice contains `(index, value)` pairs.  Each pair
1618    /// maps to a `[[function_constant(index)]]` declaration in the MSL shader.
1619    ///
1620    /// This is a thin wrapper around [`get_pipeline_with_constants`] that
1621    /// passes an empty `int_constants` slice.  Existing callers continue to
1622    /// work without modification; the cache-key format for pure-bool pipelines
1623    /// is compatible (bool entries carry the 'b' type marker, which is the
1624    /// only format ever written by this wrapper).
1625    ///
1626    /// # Errors
1627    ///
1628    /// * `MlxError::KernelNotFound` — no source registered for this name.
1629    /// * `MlxError::ShaderCompilationError` — MSL compilation, function
1630    ///   specialisation, or pipeline creation failed.
1631    pub fn get_pipeline_with_bool_constants(
1632        &mut self,
1633        name: &str,
1634        device: &metal::DeviceRef,
1635        bool_constants: &[(usize, bool)],
1636    ) -> Result<&ComputePipelineState> {
1637        self.get_pipeline_with_constants(name, device, bool_constants, &[])
1638    }
1639
1640    /// Check if a pipeline for the given name is already compiled and cached.
1641    pub fn is_cached(&self, name: &str) -> bool {
1642        self.cache.contains_key(name)
1643    }
1644
1645    /// Number of compiled pipelines currently in the cache.
1646    pub fn cached_count(&self) -> usize {
1647        self.cache.len()
1648    }
1649
1650    /// Number of registered shader sources.
1651    pub fn source_count(&self) -> usize {
1652        self.sources.len()
1653    }
1654}
1655
1656impl Default for KernelRegistry {
1657    fn default() -> Self {
1658        Self::new()
1659    }
1660}
1661
1662#[cfg(test)]
1663mod tests {
1664    use super::*;
1665
1666    /// Minimal Metal shader that uses a single int function constant.
1667    ///
1668    /// The kernel writes the constant value N into the first element of the
1669    /// output buffer, allowing the test to verify that the Metal compiler
1670    /// actually sees distinct specialisations for N=4 and N=8.
1671    ///
1672    /// The shader is intentionally trivial — we only need it to *compile* with
1673    /// an int function constant; correctness of the kernel logic is not under
1674    /// test here.
1675    const INT_FC_TEST_SHADER: &str = r#"
1676#include <metal_stdlib>
1677using namespace metal;
1678
1679constant int test_N [[function_constant(100)]];
1680
1681kernel void int_fc_test_kernel(
1682    device int* out [[buffer(0)]],
1683    uint tid [[thread_position_in_grid]])
1684{
1685    if (tid == 0) {
1686        out[0] = test_N;
1687    }
1688}
1689"#;
1690
1691    /// Verify that `get_pipeline_with_constants` produces distinct cached
1692    /// pipelines for different i32 function-constant values, and that
1693    /// `get_pipeline_with_bool_constants` (the backward-compat wrapper) still
1694    /// works correctly with the new 'b'-prefixed cache-key format.
1695    ///
1696    /// This test requires a real Metal device and is therefore marked
1697    /// `#[ignore]` on non-Apple platforms, but runs unconditionally on macOS.
1698    #[test]
1699    fn test_int_fc_distinct_pipelines_and_bool_compat() {
1700        let device = metal::Device::system_default()
1701            .expect("no Metal device — run on Apple Silicon or x86 Mac with Metal support");
1702
1703        let mut registry = KernelRegistry::new();
1704
1705        // Register the inline test shader under a name that cannot collide with
1706        // any production kernel.
1707        registry.register_source("int_fc_test_kernel", INT_FC_TEST_SHADER);
1708
1709        // Compile with N=4.
1710        let p4_ptr = registry
1711            .get_pipeline_with_constants(
1712                "int_fc_test_kernel",
1713                &device,
1714                &[],                  // no bool constants
1715                &[(100, 4_i32)],      // int constant index 100 = 4
1716            )
1717            .expect("pipeline N=4 should compile") as *const _;
1718
1719        // Cache must now have exactly 1 entry for this kernel.
1720        // (Other production kernels may already be in cache from new(); here
1721        // we check that the N=4 key was inserted.)
1722        let count_after_n4 = registry.cached_count();
1723
1724        // Compile with N=8 — must produce a SEPARATE pipeline.
1725        let p8_ptr = registry
1726            .get_pipeline_with_constants(
1727                "int_fc_test_kernel",
1728                &device,
1729                &[],
1730                &[(100, 8_i32)],
1731            )
1732            .expect("pipeline N=8 should compile") as *const _;
1733
1734        // Cache must have grown by exactly 1.
1735        assert_eq!(
1736            registry.cached_count(),
1737            count_after_n4 + 1,
1738            "N=8 must produce a new cache entry"
1739        );
1740
1741        // The two pipelines must be distinct objects in the cache.
1742        assert_ne!(
1743            p4_ptr, p8_ptr,
1744            "N=4 and N=8 specialisations must be separate ComputePipelineState objects"
1745        );
1746
1747        // A second call with N=4 must return the SAME pipeline (cache hit, no
1748        // new compilation).
1749        let p4_again_ptr = registry
1750            .get_pipeline_with_constants(
1751                "int_fc_test_kernel",
1752                &device,
1753                &[],
1754                &[(100, 4_i32)],
1755            )
1756            .expect("pipeline N=4 cache hit should succeed") as *const _;
1757
1758        assert_eq!(
1759            registry.cached_count(),
1760            count_after_n4 + 1,
1761            "repeated N=4 call must be a cache hit, not a new entry"
1762        );
1763        assert_eq!(
1764            p4_ptr, p4_again_ptr,
1765            "repeated N=4 call must return the same pipeline pointer"
1766        );
1767
1768        // Verify backward compatibility: get_pipeline_with_bool_constants must
1769        // still route through get_pipeline_with_constants and produce a cached
1770        // pipeline without panicking.
1771        //
1772        // We register a separate bool-constant shader that does NOT use a bool
1773        // function constant (so the Metal compiler ignores missing FCs for
1774        // this trivial case) — but the call path and cache-key format are what
1775        // matter here.  We reuse the int_fc_test_kernel source; the bool FC is
1776        // simply unused by the shader (Metal allows unused FCs when the shader
1777        // declares them with `function_constant` but the value is never read).
1778        //
1779        // To avoid a Metal compiler error for an undeclared function constant,
1780        // we register a separate bare-kernel shader for the bool wrapper test.
1781        const BARE_SHADER: &str = r#"
1782#include <metal_stdlib>
1783using namespace metal;
1784kernel void bare_kernel(device int* out [[buffer(0)]], uint tid [[thread_position_in_grid]]) {
1785    if (tid == 0) { out[0] = 42; }
1786}
1787"#;
1788        registry.register_source("bare_kernel", BARE_SHADER);
1789
1790        let count_before_bool = registry.cached_count();
1791        let _bool_pipeline = registry
1792            .get_pipeline_with_bool_constants("bare_kernel", &device, &[])
1793            .expect("bool-constants wrapper with empty slice must succeed");
1794
1795        assert_eq!(
1796            registry.cached_count(),
1797            count_before_bool + 1,
1798            "bool-constants wrapper must insert one new cache entry"
1799        );
1800    }
1801
1802    /// Verify that the `MTLComputePipelineState.label` produced by
1803    /// `get_pipeline` and `get_pipeline_with_constants` actually propagates
1804    /// from the descriptor to the resulting pipeline state.
1805    ///
1806    /// This is the in-process smoke check for ADR-015 iter9b: we cannot
1807    /// reach into xctrace from Rust, but we can read back the same `label`
1808    /// property xctrace consumes via `ComputePipelineStateRef::label()`.
1809    /// If labels are missing or wrong here, the MST trace will also show
1810    /// generic identifiers — so this test gates the iter9 retry's
1811    /// per-Q4_0-kernel attribution.
1812    #[test]
1813    fn test_pipeline_labels_propagate_for_mst() {
1814        let device = metal::Device::system_default()
1815            .expect("no Metal device — run on Apple Silicon or x86 Mac with Metal support");
1816
1817        let mut registry = KernelRegistry::new();
1818
1819        // Reuse the same trivial shaders as the int-FC test.
1820        registry.register_source("int_fc_test_kernel", INT_FC_TEST_SHADER);
1821
1822        const BARE_SHADER_LABEL_TEST: &str = r#"
1823#include <metal_stdlib>
1824using namespace metal;
1825kernel void label_smoke_kernel(device int* out [[buffer(0)]], uint tid [[thread_position_in_grid]]) {
1826    if (tid == 0) { out[0] = 7; }
1827}
1828"#;
1829        registry.register_source("label_smoke_kernel", BARE_SHADER_LABEL_TEST);
1830
1831        // Plain get_pipeline path — label must equal the kernel name.
1832        // Capture as owned String so the cache borrow is released before
1833        // the next get_pipeline_with_constants call below.
1834        let plain_label = registry
1835            .get_pipeline("label_smoke_kernel", &device)
1836            .expect("plain pipeline must compile")
1837            .label()
1838            .to_string();
1839        assert_eq!(
1840            plain_label, "label_smoke_kernel",
1841            "get_pipeline must label the pipeline with the kernel name (xctrace MST attribution)"
1842        );
1843
1844        // Constants path — label must equal the composite cache key so each
1845        // function-constant variant is individually attributable in MST.
1846        // We capture the label as an owned String to release the borrow on
1847        // the cache before fetching the next specialisation.
1848        let label_v7 = registry
1849            .get_pipeline_with_constants(
1850                "int_fc_test_kernel",
1851                &device,
1852                &[],
1853                &[(100, 7_i32)],
1854            )
1855            .expect("specialised pipeline must compile")
1856            .label()
1857            .to_string();
1858        assert_eq!(
1859            label_v7, "int_fc_test_kernel|100:i7",
1860            "get_pipeline_with_constants must label with the cache_key so each \
1861             specialisation is distinct in xctrace MST"
1862        );
1863
1864        // A second specialisation must produce a different label.
1865        let label_v13 = registry
1866            .get_pipeline_with_constants(
1867                "int_fc_test_kernel",
1868                &device,
1869                &[],
1870                &[(100, 13_i32)],
1871            )
1872            .expect("second specialised pipeline must compile")
1873            .label()
1874            .to_string();
1875        assert_eq!(label_v13, "int_fc_test_kernel|100:i13");
1876        assert_ne!(
1877            label_v7, label_v13,
1878            "distinct constant values must yield distinct pipeline labels"
1879        );
1880    }
1881}