cera 0.5.5

Rust-native LLM inference engine
Documentation
// RoPE (rotary position embedding), applied in-place to Q and K. One Slang
// source, but unlike gelu/bias_add/elementwise the two backends do NOT share a
// kernel here: they factor RoPE differently, so this is a `__target_switch` port
// like softmax and gemm_q8_0, each branch mirroring its handwritten twin and
// emitting only its own bindings.
//
// - metal branch mirrors `shaders/rope.metal`: NEOX (split-halves) only, 3
//   bindings (q, k, params), 5 params. On Metal the general RoPE case (NEOX +
//   interleaved + Llama-3 freq_factors) lives in the fused `qk_norm_rope.metal`,
//   so plain rope is deliberately the minimal NEOX kernel the LFM2 rope-only
//   path uses.
// - default (wgsl) branch mirrors `shaders/rope.wgsl`: NEOX *and* interleaved
//   (NORM) layouts plus optional `freq_factors`, 4 bindings (q, k, params,
//   freq_factors), 7 params. On wgpu the plain rope kernel carries this
//   generality itself.
//
// `freq_factors` (binding 3) is referenced only in the default branch, so the
// metal branch, being the surviving one on Metal, never uses it and Slang omits
// binding 3 from the emitted MSL. Same mechanism gemm_q8_0 uses to keep its
// `half` staging out of the WGSL emission.
//
// Dispatch: (ceil(max(n_heads, n_kv_heads) * head_dim/2 / 256), 1, 1) x 256.

[[vk::binding(0)]] RWStructuredBuffer<float> q            : register(u0);
[[vk::binding(1)]] RWStructuredBuffer<float> k            : register(u1);
[[vk::binding(2)]] StructuredBuffer<uint>    params       : register(t2);
[[vk::binding(3)]] StructuredBuffer<float>   freq_factors : register(t3);

/// Metal-only `powr`: the handwritten rope.metal computes `freq` with `powr`
/// (the positive-base variant), and Slang's portable `pow` differs by ~1 ULP.
/// That ULP is scaled by `pos` in `angle = pos * freq`, so at realistic decode
/// positions (pos in the thousands) the two diverge by ~1e-4 in cos/sin. Using
/// `powr` here keeps the generated MSL bit-identical to the handwritten kernel.
/// Referenced only in the metal branch, so Slang omits it from the WGSL (which
/// mirrors rope.wgsl's `pow` and needs no `powr`).
__target_intrinsic(metal, "powr($0, $1)")
float metal_powr(float base, float exponent);

/// Rotate one (x0, x1) pair by `angle`. Portable, used by both branches.
float2 rotate_pair(float x0, float x1, float angle) {
    float cos_a = cos(angle);
    float sin_a = sin(angle);
    return float2(x0 * cos_a - x1 * sin_a, x0 * sin_a + x1 * cos_a);
}

[shader("compute")]
[numthreads(256, 1, 1)]
void rope(uint3 gid : SV_DispatchThreadID) {
    uint idx = gid.x;

    __target_switch {
    case metal:
    {
        // Mirrors rope.metal: NEOX-only, params[0..4], no freq_factors.
        uint pos        = params[0];
        uint n_heads    = params[1];
        uint n_kv_heads = params[2];
        uint head_dim   = params[3];
        float freq_base = asfloat(params[4]);
        uint half_dim   = head_dim / 2u;

        // `1.0 / metal_powr(...)` matches rope.metal's `1.0f / powr(...)` exactly
        // (see metal_powr above for why pow would not).
        uint q_total = n_heads * half_dim;
        if (idx < q_total) {
            uint head = idx / half_dim;
            uint d    = idx % half_dim;
            float freq  = 1.0f / metal_powr(freq_base, float(2u * d) / float(head_dim));
            float angle = float(pos) * freq;
            uint i0 = head * head_dim + d;
            uint i1 = i0 + half_dim;
            float2 r = rotate_pair(q[i0], q[i1], angle);
            q[i0] = r.x;
            q[i1] = r.y;
        }

        uint k_total = n_kv_heads * half_dim;
        if (idx < k_total) {
            uint head = idx / half_dim;
            uint d    = idx % half_dim;
            float freq  = 1.0f / metal_powr(freq_base, float(2u * d) / float(head_dim));
            float angle = float(pos) * freq;
            uint i0 = head * head_dim + d;
            uint i1 = i0 + half_dim;
            float2 r = rotate_pair(k[i0], k[i1], angle);
            k[i0] = r.x;
            k[i1] = r.y;
        }
        break;
    }
    default:
    {
        // Mirrors rope.wgsl: NEOX + interleaved + optional freq_factors,
        // params[0..6] plus the freq_factors buffer.
        uint pos              = params[0];
        uint n_heads          = params[1];
        uint n_kv_heads       = params[2];
        uint head_dim         = params[3];
        float freq_base       = asfloat(params[4]);
        uint rope_type        = params[5];
        uint has_freq_factors = params[6];
        uint half_dim         = head_dim / 2u;

        uint q_total = n_heads * half_dim;
        if (idx < q_total) {
            uint head = idx / half_dim;
            uint d    = idx % half_dim;
            float angle = float(pos) * pow(freq_base, -2.0f * float(d) / float(head_dim));
            if (has_freq_factors == 1u) {
                angle = angle / freq_factors[d];
            }
            uint i0;
            uint i1;
            if (rope_type == 0u) {
                i0 = head * head_dim + d;
                i1 = head * head_dim + d + half_dim;
            } else {
                i0 = head * head_dim + 2u * d;
                i1 = head * head_dim + 2u * d + 1u;
            }
            float2 r = rotate_pair(q[i0], q[i1], angle);
            q[i0] = r.x;
            q[i1] = r.y;
        }

        uint k_total = n_kv_heads * half_dim;
        if (idx < k_total) {
            uint head = idx / half_dim;
            uint d    = idx % half_dim;
            float angle = float(pos) * pow(freq_base, -2.0f * float(d) / float(head_dim));
            if (has_freq_factors == 1u) {
                angle = angle / freq_factors[d];
            }
            uint i0;
            uint i1;
            if (rope_type == 0u) {
                i0 = head * head_dim + d;
                i1 = head * head_dim + d + half_dim;
            } else {
                i0 = head * head_dim + 2u * d;
                i1 = head * head_dim + 2u * d + 1u;
            }
            float2 r = rotate_pair(k[i0], k[i1], angle);
            k[i0] = r.x;
            k[i1] = r.y;
        }
        break;
    }
    }
}