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 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
228fn 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
310fn 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
356fn 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
404fn 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
448fn 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
489fn 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
546fn 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
599fn 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 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
800fn 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
987fn 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
1038fn 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
1146fn 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}