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