#[cfg(all(feature = "metal", target_os = "macos"))]
pub const MSL_GEMM_F32_SIMDGROUP: &str = r#"
#include <metal_stdlib>
using namespace metal;
// Tile / simdgroup geometry (must match dispatch_gemm_f32 in metal_dispatch.rs).
//
// output tile : F32_TM x F32_TN = 64 x 64
// simdgroups/threadgroup: F32_SIMDGROUPS = 4 (-> 128 threads)
// per-simdgroup quadrant: F32_SG_M x F32_SG_N = 32 x 32
// accumulator grid : (F32_SG_M/8) x (F32_SG_N/8) = 4 x 4 simdgroup_float8x8
// K step : F32_TK = 32 (= 4 fragments of 8)
// threadgroup memory : Ash(64*32*4) + Dsh(32*64*4) = 8 + 8 = 16 KiB (float)
constant constexpr uint F32_TM = 64u;
constant constexpr uint F32_TN = 64u;
constant constexpr uint F32_TK = 32u;
constant constexpr uint F32_SIMDGROUPS = 4u;
constant constexpr uint F32_THREADS = F32_SIMDGROUPS * 32u; // 128
constant constexpr uint F32_SG_M = 32u; // rows per simdgroup quadrant (M)
constant constexpr uint F32_SG_N = 32u; // cols per simdgroup quadrant (N)
constant constexpr uint F32_FRAG = 8u; // hardware matrix edge
constant constexpr uint F32_MFRAGS = F32_SG_M / F32_FRAG; // 4 accumulator rows
constant constexpr uint F32_NFRAGS = F32_SG_N / F32_FRAG; // 4 accumulator cols
constant constexpr uint F32_KFRAGS = F32_TK / F32_FRAG; // 4 K-fragments per K-tile
constant constexpr uint F32_AELEMS = F32_TM * F32_TK; // A floats staged per K-tile (2048)
kernel void gemm_f32_simdgroup(
device const float* weights [[buffer(0)]],
device const float* inputs [[buffer(1)]],
device float* outputs [[buffer(2)]],
constant uint& n_rows [[buffer(3)]],
constant uint& batch_size [[buffer(4)]],
constant uint& k [[buffer(5)]],
uint2 tgid [[threadgroup_position_in_grid]],
uint lid [[thread_index_in_threadgroup]],
uint sgid [[simdgroup_index_in_threadgroup]])
{
// This threadgroup's output tile origin.
const uint row_base = tgid.x * F32_TN; // first weight row (N)
const uint col_base = tgid.y * F32_TM; // first batch column (M)
// simdgroup -> 32x32 quadrant of the 64x64 tile.
// sgid in [0, F32_SIMDGROUPS) ; (sg_mi, sg_ni) in [0,2)x[0,2).
const uint sg_mi = sgid / (F32_TN / F32_SG_N); // 0..1 -> quadrant row
const uint sg_ni = sgid % (F32_TN / F32_SG_N); // 0..1 -> quadrant col
const uint sg_m0 = sg_mi * F32_SG_M; // tile-local first M row of this simdgroup
const uint sg_n0 = sg_ni * F32_SG_N; // tile-local first N col of this simdgroup
const uint k_tiles = (k + F32_TK - 1u) / F32_TK; // number of F32_TK-wide K-tiles (ceil)
// Threadgroup staging (float — the weights are already f32):
// Ash[m][kk] = input for tile row m, global k = kt*F32_TK + kk. (row-major, ld = F32_TK)
// Dsh[kk][n] = weight for tile col n, global k = kt*F32_TK + kk. (row-major, ld = F32_TN)
threadgroup float Ash[F32_TM * F32_TK]; // 64 * 32 * 4 = 8 KiB
threadgroup float Dsh[F32_TK * F32_TN]; // 32 * 64 * 4 = 8 KiB
// Per-simdgroup accumulators: a 4x4 grid of 8x8 f32 fragments (the 32x32 quadrant).
simdgroup_float8x8 acc[F32_MFRAGS][F32_NFRAGS];
for (uint mi = 0u; mi < F32_MFRAGS; mi++) {
for (uint ni = 0u; ni < F32_NFRAGS; ni++) {
acc[mi][ni] = make_filled_simdgroup_matrix<float, 8, 8>(0.0f);
}
}
// Boundary extents for this tile (out-of-range rows/cols staged as zero).
const uint valid_rows = (row_base < n_rows) ? min(F32_TN, n_rows - row_base) : 0u;
const uint valid_cols = (col_base < batch_size) ? min(F32_TM, batch_size - col_base) : 0u;
for (uint kt = 0u; kt < k_tiles; kt++) {
const uint k_off = kt * F32_TK; // global K offset of this tile
const uint k_span = (k_off < k) ? min(F32_TK, k - k_off) : 0u; // valid K elems in this tile
// -- Stage Dsh: load F32_TN weight rows x F32_TK elems, transposed.
// One thread per (weight row n) copies its F32_TK weight floats,
// scattering to Dsh[kk][n]. F32_THREADS=128 >= F32_TN=64, so threads
// [F32_TN, F32_THREADS) idle here (they participate in Ash below).
if (lid < F32_TN) {
const uint n_local = lid; // 0..F32_TN-1 -> tile-local weight col
if (n_local < valid_rows) {
const uint w_row = row_base + n_local;
const device float* wrow = weights + (ulong)w_row * (ulong)k + (ulong)k_off;
for (uint kk = 0u; kk < F32_TK; kk++) {
Dsh[kk * F32_TN + n_local] = (kk < k_span) ? wrow[kk] : 0.0f;
}
} else {
// Padded weight column: zero so out-of-range N contributes 0.
for (uint kk = 0u; kk < F32_TK; kk++) {
Dsh[kk * F32_TN + n_local] = 0.0f;
}
}
}
// -- Stage Ash: F32_TM input columns x F32_TK elems (read inputs[col*k + k_off + e]).
// All 128 threads cooperate; F32_AELEMS = 2048 = 16 per thread.
for (uint i = lid; i < F32_AELEMS; i += F32_THREADS) {
const uint a_row = i / F32_TK; // 0..F32_TM-1 -> tile-local M row
const uint a_kk = i % F32_TK; // 0..F32_TK-1
float v = 0.0f;
if (a_row < valid_cols && a_kk < k_span) {
const uint a_col = col_base + a_row;
v = inputs[(ulong)a_col * (ulong)k + (ulong)k_off + (ulong)a_kk];
}
Ash[a_row * F32_TK + a_kk] = v;
}
threadgroup_barrier(mem_flags::mem_threadgroup);
// -- Matrix MACs: acc[mi][ni] += A_frag * D_frag over the F32_TK K-tile.
// A fragment : Ash[(sg_m0+mi*8)+ .. ][kf*8 + ..] (row-major, ld = F32_TK)
// D fragment : Dsh[kf*8 + ..][(sg_n0+ni*8)+ ..] (row-major, ld = F32_TN)
for (uint kf = 0u; kf < F32_KFRAGS; kf++) {
simdgroup_float8x8 afrag[F32_MFRAGS];
simdgroup_float8x8 dfrag[F32_NFRAGS];
for (uint mi = 0u; mi < F32_MFRAGS; mi++) {
const uint a_off = (sg_m0 + mi * F32_FRAG) * F32_TK + kf * F32_FRAG;
simdgroup_load(afrag[mi], Ash + a_off, F32_TK);
}
for (uint ni = 0u; ni < F32_NFRAGS; ni++) {
const uint d_off = (kf * F32_FRAG) * F32_TN + (sg_n0 + ni * F32_FRAG);
simdgroup_load(dfrag[ni], Dsh + d_off, F32_TN);
}
for (uint mi = 0u; mi < F32_MFRAGS; mi++) {
for (uint ni = 0u; ni < F32_NFRAGS; ni++) {
simdgroup_multiply_accumulate(acc[mi][ni], afrag[mi], dfrag[ni], acc[mi][ni]);
}
}
}
threadgroup_barrier(mem_flags::mem_threadgroup);
}
// -- Write back the 32x32 quadrant (column-major: outputs[col*n_rows + row]).
// Stage each 8x8 accumulator to threadgroup memory, then scatter with the
// boundary clamp (the matrix store wants a contiguous destination; the
// output is column-major, so a per-element scatter is required anyway).
threadgroup float Csh[F32_SG_M * F32_SG_N]; // 32 * 32 * 4 = 4 KiB (one simdgroup at a time)
for (uint sg = 0u; sg < F32_SIMDGROUPS; sg++) {
threadgroup_barrier(mem_flags::mem_threadgroup);
if (sg == sgid) {
for (uint mi = 0u; mi < F32_MFRAGS; mi++) {
for (uint ni = 0u; ni < F32_NFRAGS; ni++) {
const uint c_off = (mi * F32_FRAG) * F32_SG_N + ni * F32_FRAG;
simdgroup_store(acc[mi][ni], Csh + c_off, F32_SG_N);
}
}
}
threadgroup_barrier(mem_flags::mem_threadgroup);
if (sg == sgid) {
// Scatter Csh[mm][nn] -> outputs[col*n_rows + row] with clamps.
for (uint idx = lid % 32u; idx < F32_SG_M * F32_SG_N; idx += 32u) {
const uint mm = idx / F32_SG_N; // 0..31 local M within quadrant
const uint nn = idx % F32_SG_N; // 0..31 local N within quadrant
const uint m_local = sg_m0 + mm; // tile-local M
const uint n_local = sg_n0 + nn; // tile-local N
if (m_local < valid_cols && n_local < valid_rows) {
const uint col = col_base + m_local;
const uint row = row_base + n_local;
outputs[(ulong)col * (ulong)n_rows + (ulong)row] = Csh[mm * F32_SG_N + nn];
}
}
}
}
}
"#;