Skip to main content

qualia_core_db/wgsl_forge/emit/
msl.rs

1use std::fmt::Write;
2
3use super::GeneratedShader;
4use crate::wgsl_forge::{ForgeError, KernelSpec, Op, Schedule};
5
6pub fn emit_msl(kernel: &KernelSpec, schedule: Schedule) -> Result<GeneratedShader, ForgeError> {
7    kernel.validate()?;
8    let semantic_hash = kernel.semantic_hash()?;
9    let mut source = String::with_capacity(2048);
10
11    writeln!(
12        source,
13        "// MSL emitted for {}@{}",
14        kernel.id, kernel.semantic_version
15    )
16    .map_err(|e| ForgeError::Emission(e.to_string()))?;
17    writeln!(source, "// Semantic hash: {}", semantic_hash)
18        .map_err(|e| ForgeError::Emission(e.to_string()))?;
19    writeln!(
20        source,
21        "// Schedule: workgroup={}, items={}, vector={}",
22        schedule.workgroup_size, schedule.items_per_invocation, schedule.vector_width
23    )
24    .map_err(|e| ForgeError::Emission(e.to_string()))?;
25
26    emit_kernel_body(&mut source, kernel, schedule)?;
27
28    let source_hash = blake3::hash(source.as_bytes()).to_hex().to_string();
29    Ok(GeneratedShader {
30        kernel_id: kernel.id.clone(),
31        semantic_hash,
32        source_hash,
33        schedule,
34        source,
35    })
36}
37
38fn emit_kernel_body(
39    source: &mut String,
40    kernel: &KernelSpec,
41    schedule: Schedule,
42) -> Result<(), ForgeError> {
43    if kernel.id == "topk" {
44        return emit_topk_msl(source, kernel, schedule);
45    }
46    if kernel.id == "fused-ffn" {
47        return emit_ffn_msl(source, kernel, schedule);
48    }
49    if kernel.id == "p64-project" {
50        return emit_p64_msl(source, kernel, schedule);
51    }
52    if kernel.id == "gemm" {
53        return emit_gemm_msl(source, kernel, schedule);
54    }
55    if kernel.id == "gemv" {
56        if schedule.workgroup_size >= 32 {
57            return emit_gemv_simd_msl(source, kernel, schedule);
58        }
59        return emit_gemv_msl(source, kernel, schedule);
60    }
61    if kernel.id == "gemv-simd-matrix" {
62        return emit_gemv_simdgroup_matrix_msl(source, kernel, schedule);
63    }
64    if kernel.id == "fused-qkv-rope" {
65        return emit_fused_qkv_rope_msl(source, kernel, schedule);
66    }
67    if kernel.id == "rmsnorm" {
68        return emit_rmsnorm_msl(source, kernel, schedule);
69    }
70    if kernel.id == "sdpa-decode" {
71        return emit_sdpa_decode_msl(source, kernel, schedule);
72    }
73    if kernel.id == "ternary-gemv" {
74        return emit_ternary_gemv_msl(source, kernel, schedule);
75    }
76    if kernel.id == "fft" {
77        return emit_fft_msl(source, kernel, schedule);
78    }
79    if kernel.id == "ray-probe" {
80        return Err(ForgeError::Emission(
81            "ray-query is only emitted for the WGSL target (Metal RT uses a distinct API)"
82                .to_string(),
83        ));
84    }
85    writeln!(source, "#include <metal_stdlib>\nusing namespace metal;\n")
86        .map_err(|error| ForgeError::Emission(error.to_string()))?;
87
88    if kernel.id == "affine-f32" {
89        writeln!(
90            source,
91            r#"struct AffineParams {{
92    uint length;
93    float scale;
94    float bias;
95    uint _pad;
96}};"#
97        )
98        .map_err(|error| ForgeError::Emission(error.to_string()))?;
99    }
100    writeln!(source, "").map_err(|error| ForgeError::Emission(error.to_string()))?;
101
102    writeln!(source, "kernel void {}(", kernel.entry_point)
103        .map_err(|error| ForgeError::Emission(error.to_string()))?;
104
105    for (i, buffer) in kernel.buffers.iter().enumerate() {
106        let type_decl = match (buffer.element, buffer.access) {
107            (crate::wgsl_forge::ir::BufferElement::AffineParams, _) => "constant AffineParams&",
108            (
109                crate::wgsl_forge::ir::BufferElement::P64Words64,
110                crate::wgsl_forge::ir::BufferAccess::StorageRead,
111            ) => "device const P64Words64*",
112            (
113                crate::wgsl_forge::ir::BufferElement::P64Words64,
114                crate::wgsl_forge::ir::BufferAccess::StorageReadWrite,
115            ) => "device P64Words64*",
116            (
117                crate::wgsl_forge::ir::BufferElement::Scalar(
118                    crate::wgsl_forge::ir::ScalarType::F32,
119                ),
120                crate::wgsl_forge::ir::BufferAccess::StorageRead,
121            ) => "device const float*",
122            (
123                crate::wgsl_forge::ir::BufferElement::Scalar(
124                    crate::wgsl_forge::ir::ScalarType::F32,
125                ),
126                crate::wgsl_forge::ir::BufferAccess::StorageReadWrite,
127            ) => "device float*",
128            _ => "device float*",
129        };
130        let separator = if i < kernel.buffers.len() - 1 {
131            ","
132        } else {
133            ""
134        };
135        writeln!(
136            source,
137            "    {} {} [[buffer({})]]{}",
138            type_decl, buffer.name, buffer.binding, separator
139        )
140        .map_err(|error| ForgeError::Emission(error.to_string()))?;
141    }
142    writeln!(source, "    , uint3 gid [[thread_position_in_grid]]")
143        .map_err(|error| ForgeError::Emission(error.to_string()))?;
144    writeln!(source, ") {{").map_err(|error| ForgeError::Emission(error.to_string()))?;
145
146    writeln!(
147        source,
148        "    const uint ITEMS_PER_INVOCATION = {}u;\n    const uint VECTOR_WIDTH = {}u;",
149        schedule.items_per_invocation, schedule.vector_width
150    )
151    .map_err(|error| ForgeError::Emission(error.to_string()))?;
152
153    if kernel.id == "affine-f32" {
154        writeln!(
155            source,
156            "    for (uint item = 0; item < ITEMS_PER_INVOCATION; item++) {{"
157        )
158        .map_err(|error| ForgeError::Emission(error.to_string()))?;
159        writeln!(
160            source,
161            "        uint global_id = (gid.x * ITEMS_PER_INVOCATION + item) * VECTOR_WIDTH;"
162        )
163        .map_err(|error| ForgeError::Emission(error.to_string()))?;
164
165        if schedule.vector_width == 1 {
166            writeln!(source, "        if (global_id < params.length) {{")
167                .map_err(|error| ForgeError::Emission(error.to_string()))?;
168            emit_ops(source, &kernel.ops, "            ")?;
169            writeln!(source, "        }}")
170                .map_err(|error| ForgeError::Emission(error.to_string()))?;
171        } else {
172            // Vectorized affine (affine-f32 is the only kernel that sets vector_width>1):
173            // an unrolled fast path when the whole VECTOR_WIDTH span is in bounds, plus a
174            // bounds-checked tail for the final partial span. Correct for affine-f32 (its sole
175            // op is out = in*scale + bias). Native Metal float4 SIMD loads would be a throughput
176            // optimization (needs a Metal compiler to validate — absent on this host), not a gap.
177            writeln!(
178                source,
179                "        if (global_id + {}u < params.length) {{",
180                schedule.vector_width - 1
181            )
182            .map_err(|error| ForgeError::Emission(error.to_string()))?;
183            for index in 0..schedule.vector_width {
184                writeln!(source, "            output[global_id + {index}u] = input[global_id + {index}u] * params.scale + params.bias;").map_err(|error| ForgeError::Emission(error.to_string()))?;
185            }
186            writeln!(source, "        }} else {{")
187                .map_err(|error| ForgeError::Emission(error.to_string()))?;
188            writeln!(
189                source,
190                "            for (uint component = 0; component < VECTOR_WIDTH; component++) {{"
191            )
192            .map_err(|error| ForgeError::Emission(error.to_string()))?;
193            writeln!(source, "                uint base = global_id + component;")
194                .map_err(|error| ForgeError::Emission(error.to_string()))?;
195            writeln!(source, "                if (base < params.length) {{")
196                .map_err(|error| ForgeError::Emission(error.to_string()))?;
197            writeln!(
198                source,
199                "                    output[base] = input[base] * params.scale + params.bias;"
200            )
201            .map_err(|error| ForgeError::Emission(error.to_string()))?;
202            writeln!(source, "                }}")
203                .map_err(|error| ForgeError::Emission(error.to_string()))?;
204            writeln!(source, "            }}")
205                .map_err(|error| ForgeError::Emission(error.to_string()))?;
206            writeln!(source, "        }}")
207                .map_err(|error| ForgeError::Emission(error.to_string()))?;
208        }
209        writeln!(source, "    }}\n}}").map_err(|error| ForgeError::Emission(error.to_string()))?;
210    } else {
211        writeln!(
212            source,
213            "    for (uint item = 0; item < ITEMS_PER_INVOCATION; item++) {{"
214        )
215        .map_err(|error| ForgeError::Emission(error.to_string()))?;
216        writeln!(
217            source,
218            "        uint global_id = gid.x * ITEMS_PER_INVOCATION + item;"
219        )
220        .map_err(|error| ForgeError::Emission(error.to_string()))?;
221        emit_ops(source, &kernel.ops, "        ")?;
222        writeln!(source, "    }}\n}}").map_err(|error| ForgeError::Emission(error.to_string()))?;
223    }
224
225    Ok(())
226}
227
228/// Top-k reduction in Metal: one threadgroup per block, `k` largest values per
229/// block in descending order, using `threadgroup` shared arrays (driven by the
230/// IR) and `threadgroup_barrier`.
231fn emit_topk_msl(
232    source: &mut String,
233    kernel: &KernelSpec,
234    schedule: Schedule,
235) -> Result<(), ForgeError> {
236    let wg = schedule.workgroup_size;
237    writeln!(source, "#include <metal_stdlib>\nusing namespace metal;\n")
238        .map_err(|error| ForgeError::Emission(error.to_string()))?;
239    writeln!(
240        source,
241        "struct TopKParams {{\n    uint length;\n    uint k;\n    uint block_size;\n    uint _pad;\n}};\n"
242    )
243    .map_err(|error| ForgeError::Emission(error.to_string()))?;
244
245    writeln!(source, "kernel void {}(", kernel.entry_point)
246        .map_err(|error| ForgeError::Emission(error.to_string()))?;
247    writeln!(source, "    device const float* input [[buffer(0)]],")
248        .map_err(|error| ForgeError::Emission(error.to_string()))?;
249    writeln!(source, "    device float* output [[buffer(1)]],")
250        .map_err(|error| ForgeError::Emission(error.to_string()))?;
251    writeln!(source, "    constant TopKParams& params [[buffer(2)]],")
252        .map_err(|error| ForgeError::Emission(error.to_string()))?;
253    writeln!(source, "    uint tid [[thread_position_in_threadgroup]],")
254        .map_err(|error| ForgeError::Emission(error.to_string()))?;
255    writeln!(source, "    uint block [[threadgroup_position_in_grid]]")
256        .map_err(|error| ForgeError::Emission(error.to_string()))?;
257    writeln!(source, ") {{").map_err(|error| ForgeError::Emission(error.to_string()))?;
258
259    for shared in &kernel.shared_memory {
260        let ty = msl_scalar(shared.element);
261        writeln!(
262            source,
263            "    threadgroup {} {}[{}];",
264            ty,
265            shared.name,
266            shared.length.resolve(wg)
267        )
268        .map_err(|error| ForgeError::Emission(error.to_string()))?;
269    }
270
271    writeln!(
272        source,
273        r#"
274    uint base = block * {wg}u;
275    uint gidx = base + tid;
276    float sentinel = as_type<float>(0xff7fffffu);
277    float v = sentinel;
278    if (gidx < params.length) {{ v = input[gidx]; }}
279    s_val[tid] = v;
280    s_idx[tid] = tid;
281    threadgroup_barrier(mem_flags::mem_threadgroup);
282
283    for (uint i = 0u; i < params.k; i++) {{
284        r_val[tid] = s_val[tid];
285        r_idx[tid] = s_idx[tid];
286        threadgroup_barrier(mem_flags::mem_threadgroup);
287        for (uint stride = {wg}u / 2u; stride > 0u; stride /= 2u) {{
288            if (tid < stride) {{
289                if (r_val[tid + stride] > r_val[tid]) {{
290                    r_val[tid] = r_val[tid + stride];
291                    r_idx[tid] = r_idx[tid + stride];
292                }}
293            }}
294            threadgroup_barrier(mem_flags::mem_threadgroup);
295        }}
296        if (tid == 0u) {{
297            output[block * params.k + i] = r_val[0];
298            s_val[r_idx[0]] = sentinel;
299        }}
300        threadgroup_barrier(mem_flags::mem_threadgroup);
301    }}
302}}"#,
303        wg = wg
304    )
305    .map_err(|error| ForgeError::Emission(error.to_string()))?;
306
307    Ok(())
308}
309
310/// Fused FFN in Metal: one thread per output element (see the WGSL emitter for
311/// the math). Self-contained nested matvec + GELU + accumulate.
312fn emit_ffn_msl(
313    source: &mut String,
314    kernel: &KernelSpec,
315    schedule: Schedule,
316) -> Result<(), ForgeError> {
317    let _ = schedule;
318    writeln!(
319        source,
320        r#"#include <metal_stdlib>
321using namespace metal;
322
323struct FfnParams {{
324    uint input_size;
325    uint hidden_size;
326    uint output_size;
327    uint _pad;
328}};
329
330kernel void {entry}(
331    device const float* input [[buffer(0)]],
332    device const float* w1 [[buffer(1)]],
333    device const float* w2 [[buffer(2)]],
334    device float* output [[buffer(3)]],
335    constant FfnParams& params [[buffer(4)]],
336    uint3 gid [[thread_position_in_grid]]
337) {{
338    uint o = gid.x;
339    if (o >= params.output_size) {{ return; }}
340    float acc = 0.0f;
341    for (uint h = 0; h < params.hidden_size; h++) {{
342        float hv = 0.0f;
343        uint w1_row = h * params.input_size;
344        for (uint i = 0; i < params.input_size; i++) {{ hv += w1[w1_row + i] * input[i]; }}
345        float g = 0.5f * hv * (1.0f + tanh(0.7978845608f * (hv + 0.044715f * hv * hv * hv)));
346        acc += w2[o * params.hidden_size + h] * g;
347    }}
348    output[o] = acc;
349}}"#,
350        entry = kernel.entry_point
351    )
352    .map_err(|error| ForgeError::Emission(error.to_string()))?;
353    Ok(())
354}
355
356/// P64 descriptor projection in Metal: one thread per record. Metal device buffers
357/// carry no length, so the record count travels in a `P64Params` constant (the WGSL
358/// kernel uses `arrayLength`, HLSL uses `GetDimensions`) — same math, same bindings.
359fn emit_p64_msl(
360    source: &mut String,
361    kernel: &KernelSpec,
362    schedule: Schedule,
363) -> Result<(), ForgeError> {
364    let _ = schedule;
365    writeln!(
366        source,
367        r#"#include <metal_stdlib>
368using namespace metal;
369
370struct P64Words64 {{
371    uint4 lanes[4];
372}};
373
374struct P64Params {{
375    uint record_count;
376    uint _pad0;
377    uint _pad1;
378    uint _pad2;
379}};
380
381kernel void {entry}(
382    device const P64Words64* input [[buffer(0)]],
383    device const float* weights [[buffer(1)]],
384    device float* output [[buffer(2)]],
385    constant P64Params& params [[buffer(3)]],
386    uint3 gid [[thread_position_in_grid]]
387) {{
388    uint r = gid.x;
389    if (r >= params.record_count) {{ return; }}
390    P64Words64 rec = input[r];
391    float acc = 0.0f;
392    for (uint w = 0; w < 16u; w++) {{
393        uint word = rec.lanes[w / 4u][w % 4u];
394        acc += weights[w] * (float)word;
395    }}
396    output[r] = acc;
397}}"#,
398        entry = kernel.entry_point
399    )
400    .map_err(|error| ForgeError::Emission(error.to_string()))?;
401    Ok(())
402}
403
404/// Dense row-major GEMM in Metal: one thread per output element, same binding order,
405/// params layout and accumulation order as the certified WGSL `gemm`.
406fn emit_gemm_msl(
407    source: &mut String,
408    kernel: &KernelSpec,
409    schedule: Schedule,
410) -> Result<(), ForgeError> {
411    let _ = schedule;
412    writeln!(
413        source,
414        r#"#include <metal_stdlib>
415using namespace metal;
416
417struct GemmParams {{
418    uint m;
419    uint n;
420    uint k;
421    uint _pad;
422}};
423
424kernel void {entry}(
425    device const float* a [[buffer(0)]],
426    device const float* b [[buffer(1)]],
427    device float* c [[buffer(2)]],
428    constant GemmParams& params [[buffer(3)]],
429    uint3 gid [[thread_position_in_grid]]
430) {{
431    uint o = gid.x;
432    if (o >= params.m * params.n) {{ return; }}
433    uint row = o / params.n;
434    uint col = o % params.n;
435    float acc = 0.0f;
436    uint a_row = row * params.k;
437    for (uint kk = 0; kk < params.k; kk++) {{
438        acc += a[a_row + kk] * b[kk * params.n + col];
439    }}
440    c[o] = acc;
441}}"#,
442        entry = kernel.entry_point
443    )
444    .map_err(|error| ForgeError::Emission(error.to_string()))?;
445    Ok(())
446}
447
448/// Dense row-major GEMV in Metal: one thread per output ROW, same order as WGSL `gemv`.
449fn emit_gemv_msl(
450    source: &mut String,
451    kernel: &KernelSpec,
452    schedule: Schedule,
453) -> Result<(), ForgeError> {
454    let _ = schedule;
455    writeln!(
456        source,
457        r#"#include <metal_stdlib>
458using namespace metal;
459
460struct GemvParams {{
461    uint m;
462    uint n;
463    uint _pad0;
464    uint _pad1;
465}};
466
467kernel void {entry}(
468    device const float* a [[buffer(0)]],
469    device const float* x [[buffer(1)]],
470    device float* y [[buffer(2)]],
471    constant GemvParams& params [[buffer(3)]],
472    uint3 gid [[thread_position_in_grid]]
473) {{
474    uint i = gid.x;
475    if (i >= params.m) {{ return; }}
476    float acc = 0.0f;
477    uint a_row = i * params.n;
478    for (uint j = 0; j < params.n; j++) {{
479        acc += a[a_row + j] * x[j];
480    }}
481    y[i] = acc;
482}}"#,
483        entry = kernel.entry_point
484    )
485    .map_err(|error| ForgeError::Emission(error.to_string()))?;
486    Ok(())
487}
488
489/// SIMD-group cooperative GEMV in Metal: one SIMD group (32 lanes on Apple Silicon)
490/// per output row. Lanes split the N-dimensional dot product, then `simd_sum`
491/// reduces. This is the Apple Silicon equivalent of HLSL wave-intrinsic GEMV.
492///
493/// For tensor-core access, Apple Silicon M1+ has `simdgroup_matrix` (8×8 f16 tiles).
494/// This emitter uses the scalar SIMD-group path; the `simdgroup_matrix` path
495/// would require `metal::simdgroup_matrix` from `metal_stdlib` and is left as
496/// a future enhancement for f16-weight GEMV.
497fn emit_gemv_simd_msl(
498    source: &mut String,
499    kernel: &KernelSpec,
500    schedule: Schedule,
501) -> Result<(), ForgeError> {
502    let wg = schedule.workgroup_size;
503    let rows_per_block = (wg / 32).max(1);
504    writeln!(
505        source,
506        r#"#include <metal_stdlib>
507using namespace metal;
508
509struct GemvParams {{
510    uint m;
511    uint n;
512    uint _pad0;
513    uint _pad1;
514}};
515
516kernel void {entry}(
517    device const float* a [[buffer(0)]],
518    device const float* x [[buffer(1)]],
519    device float* y [[buffer(2)]],
520    constant GemvParams& params [[buffer(3)]],
521    uint3 gtid [[thread_position_in_threadgroup]],
522    uint3 gid  [[threadgroup_position_in_grid]]
523) {{
524    uint simd_lane = gtid.x % 32u;
525    uint simd_row  = gtid.x / 32u;
526    uint row = gid.x * {rpb}u + simd_row;
527    if (row >= params.m) return;
528
529    float acc = 0.0f;
530    uint a_row = row * params.n;
531    for (uint j = simd_lane; j < params.n; j += 32u) {{
532        acc += a[a_row + j] * x[j];
533    }}
534    acc = simd_sum(acc);
535    if (simd_lane == 0u) {{
536        y[row] = acc;
537    }}
538}}"#,
539        entry = kernel.entry_point,
540        rpb = rows_per_block,
541    )
542    .map_err(|error| ForgeError::Emission(error.to_string()))?;
543    Ok(())
544}
545
546/// BitNet-style ternary GEMV in Metal: one thread per output row. 2-bit codes,
547/// 16 per `uint` (`0->0.0, 1->+1.0, 2->-1.0, 3->0.0`), `k_words` per row. `w_packed`
548/// is `device const uint*` — the generic path wrongly typed it float.
549fn emit_ternary_gemv_msl(
550    source: &mut String,
551    kernel: &KernelSpec,
552    schedule: Schedule,
553) -> Result<(), ForgeError> {
554    let _ = schedule;
555    writeln!(
556        source,
557        r#"#include <metal_stdlib>
558using namespace metal;
559
560struct TernaryGemvParams {{
561    uint m;
562    uint k;
563    uint k_words;
564    uint _pad;
565}};
566
567kernel void {entry}(
568    device const float* x [[buffer(0)]],
569    device const uint* w_packed [[buffer(1)]],
570    device const float* scale [[buffer(2)]],
571    device float* output [[buffer(3)]],
572    constant TernaryGemvParams& params [[buffer(4)]],
573    uint3 gid [[thread_position_in_grid]]
574) {{
575    uint o = gid.x;
576    if (o >= params.m) {{ return; }}
577    float acc = 0.0f;
578    uint row_base = o * params.k_words;
579    for (uint word_idx = 0; word_idx < params.k_words; word_idx++) {{
580        uint word = w_packed[row_base + word_idx];
581        uint lane_base = word_idx * 16u;
582        for (uint lane = 0; lane < 16u; lane++) {{
583            uint i = lane_base + lane;
584            if (i >= params.k) {{ break; }}
585            uint code = (word >> (lane * 2u)) & 3u;
586            float tern = 0.0f;
587            if (code == 1u) {{ tern = 1.0f; }} else if (code == 2u) {{ tern = -1.0f; }}
588            acc += tern * x[i];
589        }}
590    }}
591    output[o] = scale[o] * acc;
592}}"#,
593        entry = kernel.entry_point
594    )
595    .map_err(|error| ForgeError::Emission(error.to_string()))?;
596    Ok(())
597}
598
599/// Forward radix-2 DIT FFT in Metal over ONE threadgroup of `N = workgroup_size`
600/// threads. Interleaved complex f32, bit-reversal load into `threadgroup` arrays,
601/// then `log2(N)` butterfly stages with `threadgroup_barrier`. Same
602/// `exp(-2*pi*i*k/m)` convention as the WGSL kernel and the CPU DFT oracle;
603/// `reverse_bits` is the Metal intrinsic (WGSL spells it `reverseBits`).
604fn emit_fft_msl(
605    source: &mut String,
606    kernel: &KernelSpec,
607    schedule: Schedule,
608) -> Result<(), ForgeError> {
609    let wg = schedule.workgroup_size;
610    writeln!(
611        source,
612        r#"#include <metal_stdlib>
613using namespace metal;
614
615struct FftParams {{
616    uint n;
617    uint log2n;
618    uint _pad0;
619    uint _pad1;
620}};
621
622kernel void {entry}(
623    device const float* input [[buffer(0)]],
624    device float* output [[buffer(1)]],
625    constant FftParams& params [[buffer(2)]],
626    uint tid [[thread_position_in_threadgroup]]
627) {{
628    threadgroup float s_re[{wg}];
629    threadgroup float s_im[{wg}];
630    uint t = tid;
631    uint n = params.n;
632    uint logn = params.log2n;
633    uint rev = reverse_bits(t) >> (32u - logn);
634    s_re[rev] = input[2u * t];
635    s_im[rev] = input[2u * t + 1u];
636    threadgroup_barrier(mem_flags::mem_threadgroup);
637    for (uint s = 0u; s < logn; s++) {{
638        uint span = 1u << s;
639        uint m = span << 1u;
640        if (t < (n >> 1u)) {{
641            uint k = t & (span - 1u);
642            uint j = ((t >> s) << (s + 1u)) + k;
643            uint jp = j + span;
644            float ang = -6.28318548f * (float)k / (float)m;
645            float wr = cos(ang);
646            float wi = sin(ang);
647            float ur = s_re[j];
648            float ui = s_im[j];
649            float vr = s_re[jp];
650            float vi = s_im[jp];
651            float tr = vr * wr - vi * wi;
652            float ti = vr * wi + vi * wr;
653            s_re[j] = ur + tr;
654            s_im[j] = ui + ti;
655            s_re[jp] = ur - tr;
656            s_im[jp] = ui - ti;
657        }}
658        threadgroup_barrier(mem_flags::mem_threadgroup);
659    }}
660    output[2u * t] = s_re[t];
661    output[2u * t + 1u] = s_im[t];
662}}"#,
663        wg = wg,
664        entry = kernel.entry_point
665    )
666    .map_err(|error| ForgeError::Emission(error.to_string()))?;
667    Ok(())
668}
669
670fn msl_scalar(element: crate::wgsl_forge::ir::ScalarType) -> &'static str {
671    use crate::wgsl_forge::ir::ScalarType;
672    match element {
673        ScalarType::F32 => "float",
674        ScalarType::U32 => "uint",
675        ScalarType::I32 => "int",
676        ScalarType::U64Words => "uint2",
677    }
678}
679
680fn emit_ops(source: &mut String, ops: &[Op], indent: &str) -> Result<(), ForgeError> {
681    for op in ops {
682        match op {
683            Op::StructLoad {
684                buffer,
685                field,
686                destination,
687            } => {
688                writeln!(source, "{indent}float {destination} = {buffer}.{field};")
689                    .map_err(|error| ForgeError::Emission(error.to_string()))?;
690            }
691            Op::Load {
692                buffer,
693                index,
694                destination,
695            } => {
696                writeln!(source, "{indent}float {destination} = {buffer}[{index}];")
697                    .map_err(|error| ForgeError::Emission(error.to_string()))?;
698            }
699            Op::Store {
700                buffer,
701                index,
702                value,
703            } => {
704                writeln!(source, "{indent}{buffer}[{index}] = {value};")
705                    .map_err(|error| ForgeError::Emission(error.to_string()))?;
706            }
707            Op::Fma {
708                a,
709                b,
710                c,
711                destination,
712            } => {
713                writeln!(source, "{indent}float {destination} = {a} * {b} + {c};")
714                    .map_err(|error| ForgeError::Emission(error.to_string()))?;
715            }
716            Op::Mul {
717                left,
718                right,
719                destination,
720            } => {
721                writeln!(source, "{indent}float {destination} = {left} * {right};")
722                    .map_err(|error| ForgeError::Emission(error.to_string()))?;
723            }
724            Op::Add {
725                left,
726                right,
727                destination,
728            } => {
729                writeln!(source, "{indent}float {destination} = {left} + {right};")
730                    .map_err(|error| ForgeError::Emission(error.to_string()))?;
731            }
732            Op::DotProduct {
733                left_buffer,
734                left_base,
735                right_buffer,
736                right_base,
737                len,
738                destination,
739            } => {
740                writeln!(source, "{indent}float {destination} = 0.0;")
741                    .map_err(|error| ForgeError::Emission(error.to_string()))?;
742                writeln!(source, "{indent}for (uint i = 0; i < {len}; i++) {{")
743                    .map_err(|error| ForgeError::Emission(error.to_string()))?;
744                writeln!(source, "{indent}    {destination} += {left_buffer}[{left_base} + i] * {right_buffer}[{right_base} + i];").map_err(|error| ForgeError::Emission(error.to_string()))?;
745                writeln!(source, "{indent}}}")
746                    .map_err(|error| ForgeError::Emission(error.to_string()))?;
747            }
748            Op::Loop {
749                induction_var,
750                start,
751                end,
752                step,
753                body,
754            } => {
755                writeln!(source, "{indent}for (uint {induction_var} = {start}; {induction_var} < {end}; {induction_var} += {step}) {{").map_err(|error| ForgeError::Emission(error.to_string()))?;
756                emit_ops(source, body, &format!("{indent}    "))?;
757                writeln!(source, "{indent}}}")
758                    .map_err(|error| ForgeError::Emission(error.to_string()))?;
759            }
760            Op::Relu {
761                operand,
762                destination,
763            } => {
764                writeln!(
765                    source,
766                    "{indent}float {destination} = max(0.0f, {operand});"
767                )
768                .map_err(|error| ForgeError::Emission(error.to_string()))?;
769            }
770            Op::Gelu {
771                operand,
772                destination,
773            } => {
774                writeln!(source, "{indent}float {destination} = 0.5f * {operand} * (1.0f + tanh(0.7978845608f * ({operand} + 0.044715f * {operand} * {operand} * {operand})));").map_err(|error| ForgeError::Emission(error.to_string()))?;
775            }
776            Op::MatrixMultiply { .. } => {
777                // No scalar MSL lowering for a dense GEMM op; fail loudly rather
778                // than silently emit nothing (tensor-core GEMM is delivered elsewhere).
779                return Err(ForgeError::Emission(
780                    "Op::MatrixMultiply has no scalar MSL lowering; use the cooperative-matrix / CUDA WMMA path".to_string(),
781                ));
782            }
783            Op::Barrier => {
784                writeln!(
785                    source,
786                    "{indent}threadgroup_barrier(mem_flags::mem_threadgroup);"
787                )
788                .map_err(|error| ForgeError::Emission(error.to_string()))?;
789            }
790            Op::Intrinsic(_) => {
791                return Err(ForgeError::Emission(
792                    "Intrinsics not implemented for MSL yet".to_string(),
793                ));
794            }
795        }
796    }
797    Ok(())
798}
799
800/// MSL fused QKV + RoPE kernel: f32 GEMV for Q, K, V projections with
801/// RoPE rotation applied to Q and K outputs. Uses `threadgroup` memory.
802///
803/// Bindings: x, Wq, Wk, Wv, yq, yk, yv, dims, rope_params.
804/// Dispatch: grid = ceil(n_q / ROWS_PER_BLOCK), block = 256.
805fn emit_fused_qkv_rope_msl(
806    source: &mut String,
807    kernel: &KernelSpec,
808    schedule: Schedule,
809) -> Result<(), ForgeError> {
810    let wg = schedule.workgroup_size.max(32);
811    writeln!(
812        source,
813        r#"#include <metal_stdlib>
814using namespace metal;
815
816#define ROWS_PER_BLOCK 16u
817
818kernel void {entry}(
819    device const float* x        [[buffer(0)]],
820    device const float* Wq       [[buffer(1)]],
821    device const float* Wk       [[buffer(2)]],
822    device const float* Wv       [[buffer(3)]],
823    device float* yq             [[buffer(4)]],
824    device float* yk             [[buffer(5)]],
825    device float* yv             [[buffer(6)]],
826    device const uint* dims      [[buffer(7)]],
827    device const uint* rope_params [[buffer(8)]],
828    uint3 gtid [[thread_position_in_threadgroup]],
829    uint3 gid  [[threadgroup_position_in_grid]]
830) {{
831    uint n_in = dims[0];
832    uint n_q = dims[1];
833    uint n_kv = dims[2];
834    uint n_head = dims[3];
835    uint head_dim = dims[4];
836    uint pos = dims[5];
837    uint row0 = gid.x * ROWS_PER_BLOCK;
838    uint t = gtid.x;
839    if (row0 >= n_q) return;
840
841    float base = as_type<float>(rope_params[0]);
842    float scale = as_type<float>(rope_params[1]);
843    float inv_scale = 1.0 / scale;
844    float inv_head_dim = 1.0 / (float)head_dim;
845
846    threadgroup float s_red[ROWS_PER_BLOCK * {wg}];
847    threadgroup float s_rope_buf[ROWS_PER_BLOCK];
848
849    // === Q projection with RoPE ===
850    float acc_q[ROWS_PER_BLOCK];
851    for (uint r = 0u; r < ROWS_PER_BLOCK; r++) acc_q[r] = 0.0;
852    for (uint j = t; j < n_in; j += {wg}) {{
853        float xv = x[j];
854        for (uint r = 0u; r < ROWS_PER_BLOCK; r++) {{
855            uint row = row0 + r;
856            if (row < n_q) acc_q[r] += Wq[row * n_in + j] * xv;
857        }}
858    }}
859    for (uint r = 0u; r < ROWS_PER_BLOCK; r++)
860        s_red[r * {wg} + t] = acc_q[r];
861    threadgroup_barrier(mem_flags::mem_threadgroup);
862    for (uint s = {wg} / 2u; s > 0u; s >>= 1u) {{
863        if (t < s) {{
864            for (uint r = 0u; r < ROWS_PER_BLOCK; r++)
865                s_red[r * {wg} + t] += s_red[r * {wg} + t + s];
866        }}
867        threadgroup_barrier(mem_flags::mem_threadgroup);
868    }}
869    if (t < ROWS_PER_BLOCK) {{
870        uint row = row0 + t;
871        if (row < n_q) {{
872            uint head = row / head_dim;
873            uint d = row % head_dim;
874            uint half = head_dim / 2u;
875            if (half > 0u && head < n_head) {{
876                float val = s_red[t * {wg}];
877                uint i = d / 2u;
878                float theta = (float)pos * inv_scale * pow(base, -2.0 * (float)i * inv_head_dim);
879                float s_val = sin(theta);
880                float c_val = cos(theta);
881                float pair_val;
882                if (d % 2u == 0u) {{
883                    pair_val = (t + 1u < ROWS_PER_BLOCK && (row0 + t + 1u) < n_q)
884                        ? s_red[(t + 1u) * {wg}] : 0.0;
885                    s_rope_buf[t] = val * c_val - pair_val * s_val;
886                }} else {{
887                    pair_val = (t >= 1u) ? s_red[(t - 1u) * {wg}] : 0.0;
888                    s_rope_buf[t] = pair_val * s_val + val * c_val;
889                }}
890            }} else {{
891                s_rope_buf[t] = s_red[t * {wg}];
892            }}
893        }}
894    }}
895    threadgroup_barrier(mem_flags::mem_threadgroup);
896    if (t < ROWS_PER_BLOCK) {{
897        uint row = row0 + t;
898        if (row < n_q) yq[row] = s_rope_buf[t];
899    }}
900    threadgroup_barrier(mem_flags::mem_threadgroup);
901
902    // === K projection with RoPE ===
903    float acc_k[ROWS_PER_BLOCK];
904    for (uint r = 0u; r < ROWS_PER_BLOCK; r++) acc_k[r] = 0.0;
905    for (uint j = t; j < n_in; j += {wg}) {{
906        float xv = x[j];
907        for (uint r = 0u; r < ROWS_PER_BLOCK; r++) {{
908            uint row = row0 + r;
909            if (row < n_kv) acc_k[r] += Wk[row * n_in + j] * xv;
910        }}
911    }}
912    for (uint r = 0u; r < ROWS_PER_BLOCK; r++)
913        s_red[r * {wg} + t] = acc_k[r];
914    threadgroup_barrier(mem_flags::mem_threadgroup);
915    for (uint s = {wg} / 2u; s > 0u; s >>= 1u) {{
916        if (t < s) {{
917            for (uint r = 0u; r < ROWS_PER_BLOCK; r++)
918                s_red[r * {wg} + t] += s_red[r * {wg} + t + s];
919        }}
920        threadgroup_barrier(mem_flags::mem_threadgroup);
921    }}
922    if (t < ROWS_PER_BLOCK) {{
923        uint row = row0 + t;
924        if (row < n_kv) {{
925            uint head = row / head_dim;
926            uint d = row % head_dim;
927            uint half = head_dim / 2u;
928            if (half > 0u && head < n_head) {{
929                float val = s_red[t * {wg}];
930                uint i = d / 2u;
931                float theta = (float)pos * inv_scale * pow(base, -2.0 * (float)i * inv_head_dim);
932                float s_val = sin(theta);
933                float c_val = cos(theta);
934                float pair_val;
935                if (d % 2u == 0u) {{
936                    pair_val = (t + 1u < ROWS_PER_BLOCK && (row0 + t + 1u) < n_kv)
937                        ? s_red[(t + 1u) * {wg}] : 0.0;
938                    s_rope_buf[t] = val * c_val - pair_val * s_val;
939                }} else {{
940                    pair_val = (t >= 1u) ? s_red[(t - 1u) * {wg}] : 0.0;
941                    s_rope_buf[t] = pair_val * s_val + val * c_val;
942                }}
943            }} else {{
944                s_rope_buf[t] = s_red[t * {wg}];
945            }}
946        }}
947    }}
948    threadgroup_barrier(mem_flags::mem_threadgroup);
949    if (t < ROWS_PER_BLOCK) {{
950        uint row = row0 + t;
951        if (row < n_kv) yk[row] = s_rope_buf[t];
952    }}
953    threadgroup_barrier(mem_flags::mem_threadgroup);
954
955    // === V projection (no RoPE) ===
956    float acc_v[ROWS_PER_BLOCK];
957    for (uint r = 0u; r < ROWS_PER_BLOCK; r++) acc_v[r] = 0.0;
958    for (uint j = t; j < n_in; j += {wg}) {{
959        float xv = x[j];
960        for (uint r = 0u; r < ROWS_PER_BLOCK; r++) {{
961            uint row = row0 + r;
962            if (row < n_kv) acc_v[r] += Wv[row * n_in + j] * xv;
963        }}
964    }}
965    for (uint r = 0u; r < ROWS_PER_BLOCK; r++)
966        s_red[r * {wg} + t] = acc_v[r];
967    threadgroup_barrier(mem_flags::mem_threadgroup);
968    for (uint s = {wg} / 2u; s > 0u; s >>= 1u) {{
969        if (t < s) {{
970            for (uint r = 0u; r < ROWS_PER_BLOCK; r++)
971                s_red[r * {wg} + t] += s_red[r * {wg} + t + s];
972        }}
973        threadgroup_barrier(mem_flags::mem_threadgroup);
974    }}
975    if (t < ROWS_PER_BLOCK) {{
976        uint row = row0 + t;
977        if (row < n_kv) yv[row] = s_red[t * {wg}];
978    }}
979}}"#,
980        wg = wg,
981        entry = kernel.entry_point,
982    )
983    .map_err(|e| ForgeError::Emission(e.to_string()))?;
984    Ok(())
985}
986
987/// MSL RMSNorm kernel: x_normed = x / sqrt(mean(x²) + eps) * weight.
988/// One threadgroup per row, tree reduction in threadgroup memory.
989///
990/// Bindings: x (f32[n]), weight (f32[n]), y (f32[n]), params (u32[2] = {n, eps_bits}).
991/// Dispatch: grid = 1, block = 256 (or n rounded up to power of 2).
992fn emit_rmsnorm_msl(
993    source: &mut String,
994    kernel: &KernelSpec,
995    schedule: Schedule,
996) -> Result<(), ForgeError> {
997    let wg = schedule.workgroup_size.max(32);
998    writeln!(
999        source,
1000        r#"#include <metal_stdlib>
1001using namespace metal;
1002
1003kernel void {entry}(
1004    device const float* x        [[buffer(0)]],
1005    device const float* weight   [[buffer(1)]],
1006    device float* y              [[buffer(2)]],
1007    device const uint* params    [[buffer(3)]],
1008    uint3 gtid [[thread_position_in_threadgroup]]
1009) {{
1010    uint n = params[0];
1011    float eps = as_type<float>(params[1]);
1012    uint t = gtid.x;
1013
1014    threadgroup float s_sum[{wg}];
1015    float val = 0.0f;
1016    if (t < n) val = x[t] * x[t];
1017    s_sum[t] = val;
1018    threadgroup_barrier(mem_flags::mem_threadgroup);
1019
1020    for (uint s = {wg} / 2u; s > 0u; s >>= 1u) {{
1021        if (t < s) s_sum[t] += s_sum[t + s];
1022        threadgroup_barrier(mem_flags::mem_threadgroup);
1023    }}
1024
1025    float rms = s_sum[0] / (float)n;
1026    float inv_rms = rsqrt(rms + eps);
1027    if (t < n) {{
1028        y[t] = x[t] * inv_rms * weight[t];
1029    }}
1030}}"#,
1031        wg = wg,
1032        entry = kernel.entry_point,
1033    )
1034    .map_err(|e| ForgeError::Emission(e.to_string()))?;
1035    Ok(())
1036}
1037
1038/// MSL SDPA decode kernel: single-token GQA causal self-attention.
1039/// One threadgroup per head, threads split over head_dim.
1040/// Uses threadgroup memory for max/sum reduction and online softmax.
1041///
1042/// Bindings: q (f32[n_head * head_dim]), kv (f32[n_kv * 2 * n_layer * head_dim]),
1043/// out (f32[n_head * head_dim]), params (u32[9]).
1044/// Dispatch: grid = n_head, block = head_dim (or 256).
1045fn emit_sdpa_decode_msl(
1046    source: &mut String,
1047    kernel: &KernelSpec,
1048    schedule: Schedule,
1049) -> Result<(), ForgeError> {
1050    let wg = schedule.workgroup_size.max(32);
1051    writeln!(
1052        source,
1053        r#"#include <metal_stdlib>
1054using namespace metal;
1055
1056kernel void {entry}(
1057    device const float* q    [[buffer(0)]],
1058    device const float* kv   [[buffer(1)]],
1059    device float* out        [[buffer(2)]],
1060    device const uint* params [[buffer(3)]],
1061    uint3 gid [[threadgroup_position_in_grid]],
1062    uint3 gtid [[thread_position_in_threadgroup]]
1063) {{
1064    uint n_head = params[0];
1065    uint n_kv = params[1];
1066    uint head_dim = params[2];
1067    uint layer = params[3];
1068    uint pos = params[4];
1069    uint max_context = params[5];
1070    uint layer_stride = params[6];
1071    uint slot_kv_elems = params[7];
1072    uint q_per_kv = params[8];
1073    float scale = 1.0f / sqrt((float)head_dim);
1074
1075    uint head = gid.x;
1076    if (head >= n_head) return;
1077    uint t = gtid.x;
1078    uint kv_head = head / q_per_kv;
1079
1080    threadgroup float s_max[1];
1081    threadgroup float s_sum[1];
1082
1083    // Phase 1: compute max attention score
1084    float max_score = -1e30f;
1085    for (uint kv_idx = 0u; kv_idx <= pos && kv_idx < max_context; kv_idx++) {{
1086        float dot = 0.0f;
1087        for (uint d = t; d < head_dim; d += {wg}) {{
1088            uint q_off = head * head_dim + d;
1089            uint k_off = kv_idx * layer_stride + layer * 2u * slot_kv_elems + kv_head * head_dim + d;
1090            dot += q[q_off] * kv[k_off];
1091        }}
1092        dot = simd_sum(dot);
1093        float score = scale * dot;
1094        max_score = max(max_score, score);
1095    }}
1096    if (t == 0u) s_max[0] = max_score;
1097    threadgroup_barrier(mem_flags::mem_threadgroup);
1098    max_score = s_max[0];
1099
1100    // Phase 2: compute softmax weights and weighted V sum
1101    float weighted_sum = 0.0f;
1102    float sum_weights = 0.0f;
1103    for (uint kv_idx = 0u; kv_idx <= pos && kv_idx < max_context; kv_idx++) {{
1104        float dot = 0.0f;
1105        for (uint d = t; d < head_dim; d += {wg}) {{
1106            uint q_off = head * head_dim + d;
1107            uint k_off = kv_idx * layer_stride + layer * 2u * slot_kv_elems + kv_head * head_dim + d;
1108            dot += q[q_off] * kv[k_off];
1109        }}
1110        dot = simd_sum(dot);
1111        float score = scale * dot;
1112        float weight = exp2(score - max_score);
1113        sum_weights += weight;
1114        for (uint d = t; d < head_dim; d += {wg}) {{
1115            uint v_off = kv_idx * layer_stride + layer * 2u * slot_kv_elems + slot_kv_elems + kv_head * head_dim + d;
1116            out[head * head_dim + d] = 0.0f; // initialized below
1117        }}
1118    }}
1119
1120    // Phase 3: normalize and write
1121    float inv_sum = 1.0f / sum_weights;
1122    for (uint d = t; d < head_dim; d += {wg}) {{
1123        float acc = 0.0f;
1124        for (uint kv_idx = 0u; kv_idx <= pos && kv_idx < max_context; kv_idx++) {{
1125            float dot = 0.0f;
1126            for (uint dd = 0u; dd < head_dim; dd++) {{
1127                uint q_off = head * head_dim + dd;
1128                uint k_off = kv_idx * layer_stride + layer * 2u * slot_kv_elems + kv_head * head_dim + dd;
1129                dot += q[q_off] * kv[k_off];
1130            }}
1131            float score = scale * dot;
1132            float weight = exp2(score - max_score) * inv_sum;
1133            uint v_off = kv_idx * layer_stride + layer * 2u * slot_kv_elems + slot_kv_elems + kv_head * head_dim + d;
1134            acc += weight * kv[v_off];
1135        }}
1136        out[head * head_dim + d] = acc;
1137    }}
1138}}"#,
1139        wg = wg,
1140        entry = kernel.entry_point,
1141    )
1142    .map_err(|e| ForgeError::Emission(e.to_string()))?;
1143    Ok(())
1144}
1145
1146/// MSL SIMD-group matrix GEMV using `metal::simdgroup_matrix` (8×8 tiles).
1147/// Apple Silicon M1+ has simdgroup matrix instructions for tensor-core access.
1148/// This emitter uses f32 weights and f32 input, with f32 accumulation via
1149/// `simdgroup_multiply_accumulate`.
1150///
1151/// Bindings: a (f32[M*N]), x (f32[N]), y (f32[M]), params (u32[4] = {m, n, _pad, _pad}).
1152/// Dispatch: grid = ceil(M / 8), block = 32 (one SIMD group per output row tile).
1153fn emit_gemv_simdgroup_matrix_msl(
1154    source: &mut String,
1155    kernel: &KernelSpec,
1156    _schedule: Schedule,
1157) -> Result<(), ForgeError> {
1158    writeln!(
1159        source,
1160        r#"#include <metal_stdlib>
1161using namespace metal;
1162
1163struct GemvParams {{
1164    uint m;
1165    uint n;
1166    uint _pad0;
1167    uint _pad1;
1168}};
1169
1170kernel void {entry}(
1171    device const float* a    [[buffer(0)]],
1172    device const float* x    [[buffer(1)]],
1173    device float* y          [[buffer(2)]],
1174    constant GemvParams& params [[buffer(3)]],
1175    uint3 gid [[threadgroup_position_in_grid]],
1176    uint3 gtid [[thread_position_in_threadgroup]]
1177) {{
1178    uint row = gid.x * 8u;
1179    if (row >= params.m) return;
1180
1181    simdgroup_matrix<float, 8, 8> sm_a;
1182    simdgroup_matrix<float, 8, 8> sm_x;
1183    simdgroup_matrix<float, 8, 8> sm_acc = make_simdgroup_matrix<float, 8, 8>(0.0f);
1184
1185    uint n_tiles = params.n / 8u;
1186    for (uint t = 0u; t < n_tiles; t++) {{
1187        for (uint i = 0u; i < 8u; i++) {{
1188            for (uint j = 0u; j < 8u; j++) {{
1189                uint r = row + i;
1190                uint c = t * 8u + j;
1191                if (r < params.m && c < params.n) {{
1192                    sm_a[i * 8u + j] = a[r * params.n + c];
1193                }} else {{
1194                    sm_a[i * 8u + j] = 0.0f;
1195                }}
1196            }}
1197        }}
1198        for (uint j = 0u; j < 8u; j++) {{
1199            uint c = t * 8u + j;
1200            sm_x[j] = (c < params.n) ? x[c] : 0.0f;
1201            for (uint i = 1u; i < 8u; i++) {{
1202                sm_x[i * 8u + j] = 0.0f;
1203            }}
1204        }}
1205        simdgroup_multiply_accumulate(sm_acc, sm_a, sm_x, sm_acc);
1206    }}
1207
1208    uint lane = gtid.x;
1209    if (lane < 8u) {{
1210        uint r = row + lane;
1211        if (r < params.m) {{
1212            y[r] = sm_acc[lane * 8u];
1213        }}
1214    }}
1215}}"#,
1216        entry = kernel.entry_point,
1217    )
1218    .map_err(|e| ForgeError::Emission(e.to_string()))?;
1219    Ok(())
1220}