cera 0.2.5

Rust-native LLM inference engine
Documentation
#include <metal_stdlib>
using namespace metal;

// Bidirectional multi-head self-attention (MSL mirror of vit_attention.wgsl).
// One threadgroup of 256 threads per (query token, head):
//   scores[key] = dot(Q[q,h], K[key,h]) * scale
//   p          = softmax(scores)
//   out[q,h,d] = sum_key p[key] * V[key,h,d]
// No causal mask, no RoPE. Q/K/V/out are [tokens, n_head*head_dim] row-major.
// `scores` is threadgroup-resident, sized MAX_TOKENS=1024 (caller guards).
//
// Dispatch: threadgroups (tokens, n_head, 1), threads (256, 1, 1).

constant uint MAX_TOKENS = 1024u;

struct Params { uint tokens; uint n_head; uint head_dim; uint scale_bits; };

kernel void vit_attention(
    const device float* q [[buffer(0)]],
    const device float* k [[buffer(1)]],
    const device float* v [[buffer(2)]],
    device float* out [[buffer(3)]],
    constant Params& p [[buffer(4)]],
    uint3 tid_v [[thread_position_in_threadgroup]],
    uint3 tg [[threadgroup_position_in_grid]]
) {
    uint tid = tid_v.x;
    uint tokens = p.tokens;
    uint n_head = p.n_head;
    uint head_dim = p.head_dim;
    float scale = as_type<float>(p.scale_bits);
    uint q_idx = tg.x;
    uint h = tg.y;
    uint dim = n_head * head_dim;
    uint q_off = q_idx * dim + h * head_dim;

    threadgroup float scores[MAX_TOKENS];
    threadgroup float sg[8];
    uint lane = tid & 31u;
    uint sid = tid >> 5u;

    // Phase A: scores.
    for (uint key = tid; key < tokens; key += 256u) {
        uint k_off = key * dim + h * head_dim;
        float s = 0.0f;
        for (uint d = 0; d < head_dim; d++) {
            s += q[q_off + d] * k[k_off + d];
        }
        scores[key] = s * scale;
    }
    threadgroup_barrier(mem_flags::mem_threadgroup);

    // Phase B: max (two-stage simd reduction).
    float lmax = -3.402823e+38f;
    for (uint key = tid; key < tokens; key += 256u) {
        lmax = max(lmax, scores[key]);
    }
    float mm = simd_max(lmax);
    if (lane == 0u) sg[sid] = mm;
    threadgroup_barrier(mem_flags::mem_threadgroup);
    if (sid == 0u) {
        float v0 = lane < 8u ? sg[lane] : -3.402823e+38f;
        float t = simd_max(v0);
        if (lane == 0u) sg[0] = t;
    }
    threadgroup_barrier(mem_flags::mem_threadgroup);
    float mx = sg[0];
    threadgroup_barrier(mem_flags::mem_threadgroup);

    // Phase C: exp + sum.
    float lsum = 0.0f;
    for (uint key = tid; key < tokens; key += 256u) {
        float e = exp(scores[key] - mx);
        scores[key] = e;
        lsum += e;
    }
    threadgroup_barrier(mem_flags::mem_threadgroup);
    float sm = simd_sum(lsum);
    if (lane == 0u) sg[sid] = sm;
    threadgroup_barrier(mem_flags::mem_threadgroup);
    if (sid == 0u) {
        float v0 = lane < 8u ? sg[lane] : 0.0f;
        float t = simd_sum(v0);
        if (lane == 0u) sg[0] = t;
    }
    threadgroup_barrier(mem_flags::mem_threadgroup);
    float inv_sum = 1.0f / sg[0];

    // Phase D: weighted sum of V.
    for (uint d = tid; d < head_dim; d += 256u) {
        float acc = 0.0f;
        for (uint key = 0; key < tokens; key++) {
            acc += scores[key] * v[key * dim + h * head_dim + d];
        }
        out[q_off + d] = acc * inv_sum;
    }
}