Foundry/metal/kernels/fm_kernel_attn.h

319 lines
14 KiB
C

/* SPDX-License-Identifier: AGPL-3.0-or-later */
/* Copyright (C) 2026 David (Corey) Herrera */
/**
* @file fm_kernel_attn.h
* @brief Attention family: fp32 KV cache append, flash decode (the batch-1
* vector path, split-K across workgroups plus a combine kernel), and flash
* prefill (tiled causal GQA with online softmax).
*
* All three kernels compute exactly one thing — softmax(scale * qK^T) V over
* a causal key range — and differ only in how that range is carved up:
*
* prefill one workgroup per (query row, kv head); keys [0, pos0+t]
* decode one workgroup per (split, kv head); keys [split_lo, split_hi)
* of the single query row, partial (m, l, acc) per split
* reduce one workgroup per query head; folds the splits back together
*
* The score matrix is never materialized: each workgroup carries a running
* max `m` and running sum `l` and rescales its output accumulator whenever
* the max moves (online softmax). Tile order therefore differs from a
* single-pass CPU softmax and the results are NOT bit-exact — the bar is
* ~1e-3 with a stable argmax (spec §11).
*
* @status kv_append, flash_decode + flash_decode_reduce and flash_prefill are
* implemented and tested (metal/tests/test_metal_attn.c, 14 cases) against
* an independent double-precision CPU reference at context depths 1, 2, 16,
* 17, 513 and 4097, including forced multi-split decode, empty splits, both
* causality probes and a constant-V normalization check. Worst drift
* 1.19e-07 absolute, 8.9e-06 of the output RMS.
* @issues None
* @todo
* - [ ] fm_attn_blk — per-KV-block mask classification so fully-masked
* blocks are skipped. Long-context lever, not needed for correctness.
*
* FUNCTION-CONSTANT INDICES OWNED BY THIS FAMILY: 260-279.
* Function constants share ONE index space across the whole assembled
* shader library, so each family reserves a block; two families colliding
* on an index fails the compile of every kernel in the backend, not just
* theirs. Allocated so far: 0-2 probe / 30 Q3_K / 50 Q5_K / 60 Q6_K / 70 Q8_0 / 80 Q4_K /
* 100-109 rope / 110-119 norm / 120-129 elementwise / 200-219 ssm /
* 220-239 ssm_chunked / 240-259 moe / 260-279 attn.
* In use here: 260 kv_groups, 261 head_dim. 262-279 free.
*/
#ifndef FOUNDRY_METAL_KERNEL_ATTN_H
#define FOUNDRY_METAL_KERNEL_ATTN_H
#include "metal/fm_metal.h"
#ifdef __cplusplus
extern "C" {
#endif
/* ================================================================
* Shape limits
*
* The threadgroup staging buffers are declared at these maxima because
* fm_metal's dispatch helpers never call setThreadgroupMemoryLength — a
* kernel's threadgroup arrays have to be statically sized. The staged K
* tile is FM_ATTN_TILE_KEYS * FM_ATTN_MAX_HEAD_DIM floats (16 KiB at the
* limits below), which is what caps them.
* ================================================================ */
/* Keys staged per tile. One tile is the unit the online softmax folds. */
#define FM_ATTN_TILE_KEYS 16
/* Largest head_dim any entry point accepts. Must be a multiple of 32
* (the SIMD width the QK dot is split across). */
#define FM_ATTN_MAX_HEAD_DIM 256
/* Largest GQA group (query heads per kv head) any entry point accepts.
* One SIMD group of the workgroup scores one query head, and a workgroup
* is 8 SIMD groups wide. */
#define FM_ATTN_MAX_GROUPS 8
/* Threads per workgroup for the flash kernels: 8 SIMD groups of 32. */
#define FM_ATTN_THREADS 256
/* Function-constant indices this family binds. Both flash kernels are
* specialized on them so the per-thread accumulator arrays have
* compile-time loop bounds and stay in registers. */
#define FM_ATTN_FC_KV_GROUPS 260
#define FM_ATTN_FC_HEAD_DIM 261
/* Workgroups the default decode plan aims for, in total across all kv heads.
*
* MEASURED by metal/tests/bench_attn_split.c on M4 / 10 GPU cores,
* n_head=16 n_head_kv=2 head_dim=256. Speedup over the unsplit kernel
* (2 workgroups, one per kv head):
*
* workgroups 2 4 8 16 32 64
* seq 128 1.00x 1.42x 1.66x --- tile rounding caps this length ---
* seq 512 1.00x 1.34x 1.73x 2.28x 2.19x 2.10x
* seq 4096 1.00x 2.57x 4.54x 6.68x 9.37x 8.69x
*
* Absolute milliseconds are not quoted because they moved 2x between runs on
* a shared machine; the ratios within a run were stable across five runs and
* are what the choice rests on. 32 is the smallest count at which the curve
* has flattened at every length tested, and 64 was never better than 32
* outside the noise. At long context the split is not a micro-optimization
* — it is most of the decode path's speed.
*
* Note the SHAPE of that table: the best split_len moves 16 -> 32 -> 256
* across the three lengths while the best workgroup count does not move at
* all. What the GPU wants held constant is the number of workgroups, which
* is why this plan is parameterized by workgroups rather than by the
* keys-per-split knob the CUDA decode path uses. */
#define FM_ATTN_DECODE_TARGET_WORKGROUPS 32
/* ================================================================
* Split-K planning
* ================================================================ */
/**
* Plans the decode split: how many workgroups share the key range and how
* many keys each one takes.
*
* `split_len` is rounded up to FM_ATTN_TILE_KEYS so every workgroup starts
* on a tile boundary, and the returned split count is recomputed from the
* rounded length — so no planned split is ever empty, and a short context
* naturally uses fewer workgroups than asked for rather than fanning out
* into slivers.
*
* @param seq_len Live keys in the cache (>= 1)
* @param n_head_kv Key/value heads; the dispatch runs
* n_head_kv * splits workgroups
* @param target_workgroups Total workgroups to aim for
* (0 selects FM_ATTN_DECODE_TARGET_WORKGROUPS)
* @param split_len_out Receives the keys per workgroup
* @return Number of splits, or 0 if the arguments are invalid
*
* Thread-safe: yes (pure function)
* Reentrant: yes
* Signal-safe: yes
*/
int fm_attn_decode_plan(int seq_len, int n_head_kv, int target_workgroups,
int *split_len_out);
/**
* Floats of scratch the split-K partials need, for each of the three
* buffers fm_attn_flash_decode_at writes.
*
* @param n_head Total query heads (n_head_kv * kv_groups)
* @param head_dim Per-head dimension
* @param n_splits Splits the decode will be dispatched with
* @param m_floats Receives the element count for the m and l buffers
* @param acc_floats Receives the element count for the acc buffer
*
* Thread-safe: yes (pure function)
* Reentrant: yes
* Signal-safe: yes
*/
void fm_attn_decode_scratch_floats(int n_head, int head_dim, int n_splits,
size_t *m_floats, size_t *acc_floats);
/* ================================================================
* KV cache append
* ================================================================ */
/**
* Appends `n` rows of post-RoPE K and V into the ring KV cache.
*
* Row t of the batch lands at cache position (pos0 + t) % max_seq, which is
* the same ring rule the host cache uses, so a host<->device sync stays a
* contiguous per-head row range.
*
* @param k_new [n][n_head_kv][head_dim] fp32
* @param v_new [n][n_head_kv][head_dim] fp32
* @param k_cache [n_head_kv][max_seq][head_dim] fp32 (written)
* @param v_cache [n_head_kv][max_seq][head_dim] fp32 (written)
* @param n Rows to append (0 is a no-op, not an error)
* @param n_head_kv Key/value heads
* @param head_dim Per-head dimension
* @param max_seq Ring capacity in rows
* @param pos0 Absolute position of row 0
*
* Q4_0 KV quantization is explicitly out of scope (spec §12).
*
* Thread-safe: no (shares the global command buffer)
* Reentrant: no
* Signal-safe: no
*/
void fm_attn_kv_append_at(fm_bufref k_new, fm_bufref v_new,
fm_bufref k_cache, fm_bufref v_cache,
int n, int n_head_kv, int head_dim, int max_seq,
int pos0);
/* fm_attn_kv_append_at with every operand bound at offset 0. */
void fm_attn_kv_append(fm_buffer *k_new, fm_buffer *v_new,
fm_buffer *k_cache, fm_buffer *v_cache,
int n, int n_head_kv, int head_dim, int max_seq,
int pos0);
/* ================================================================
* Flash decode (split-K vector path)
* ================================================================ */
/**
* Scores one query row against key slice [split*split_len, ...) of the
* cache and writes that slice's partial online-softmax state.
*
* This is half of the decode path: the partials it writes are meaningless
* until fm_attn_flash_decode_reduce_at folds them. Both take `n_splits`, and
* it must be the same value in both calls — it is the stride of the partial
* arrays, not just a workgroup count.
*
* The caller owns the split plan (see fm_attn_decode_plan). Passing a plan
* whose splits run past seq_len is safe: an empty split writes m = -inf,
* l = 0 and a zero accumulator, which the reduce weights to zero.
*
* @param q [n_head][head_dim] fp32, the single query row
* @param k_cache [n_head_kv][max_seq][head_dim] fp32
* @param v_cache [n_head_kv][max_seq][head_dim] fp32
* @param m_partial [n_head][n_splits] fp32 (written) running max
* @param l_partial [n_head][n_splits] fp32 (written) running sum
* @param acc_partial [n_head][n_splits][head_dim] fp32 (written) UNnormalized
* @param kv_groups Query heads per kv head, 1..FM_ATTN_MAX_GROUPS
* @param n_head_kv Key/value heads
* @param head_dim Per-head dimension, multiple of 32, <= FM_ATTN_MAX_HEAD_DIM
* @param max_seq Ring capacity in rows
* @param seq_len Live keys, >= 1; the query attends [0, seq_len)
* @param n_splits Workgroups per kv head, >= 1
* @param split_len Keys per split; the caller's plan, not derived here
* @param scale 1/sqrt(head_dim)
*
* Thread-safe: no (shares the global command buffer)
* Reentrant: no
* Signal-safe: no
*/
void fm_attn_flash_decode_at(fm_bufref q, fm_bufref k_cache, fm_bufref v_cache,
fm_bufref m_partial, fm_bufref l_partial,
fm_bufref acc_partial,
int kv_groups, int n_head_kv, int head_dim,
int max_seq, int seq_len, int n_splits,
int split_len, float scale);
/* fm_attn_flash_decode_at with every operand bound at offset 0. */
void fm_attn_flash_decode(fm_buffer *q, fm_buffer *k_cache, fm_buffer *v_cache,
fm_buffer *m_partial, fm_buffer *l_partial,
fm_buffer *acc_partial,
int kv_groups, int n_head_kv, int head_dim,
int max_seq, int seq_len, int n_splits,
int split_len, float scale);
/**
* Folds the per-split partials into the final attention output.
*
* Takes the max of the per-split maxima, reweights each split's sum and
* accumulator by exp(m_split - M), and divides. Algebraically identical to
* a single-pass softmax over the whole key range; a split that saw no keys
* carries m = -inf and drops out with weight zero.
*
* @param m_partial [n_head][n_splits] fp32 from fm_attn_flash_decode_at
* @param l_partial [n_head][n_splits] fp32
* @param acc_partial [n_head][n_splits][head_dim] fp32
* @param out [n_head][head_dim] fp32 (written)
* @param n_head Total query heads
* @param head_dim Per-head dimension
* @param n_splits Same value passed to fm_attn_flash_decode_at
*
* Thread-safe: no (shares the global command buffer)
* Reentrant: no
* Signal-safe: no
*/
void fm_attn_flash_decode_reduce_at(fm_bufref m_partial, fm_bufref l_partial,
fm_bufref acc_partial, fm_bufref out,
int n_head, int head_dim, int n_splits);
/* fm_attn_flash_decode_reduce_at with every operand bound at offset 0. */
void fm_attn_flash_decode_reduce(fm_buffer *m_partial, fm_buffer *l_partial,
fm_buffer *acc_partial, fm_buffer *out,
int n_head, int head_dim, int n_splits);
/* ================================================================
* Flash prefill (tiled causal GQA)
* ================================================================ */
/**
* Causal GQA attention for a batch of query rows against the cache.
*
* Query row t sits at absolute position pos0 + t and attends cache rows
* [0, pos0 + t] — causality is enforced by the key loop bound and by
* clamping the final tile to the live key count, so no future key is ever
* read, scored or weighted. The caller must have appended rows [0, n) to
* the cache first, and must guarantee pos0 + n <= max_seq (no ring wrap
* inside the attended window).
*
* @param q [n][n_head][head_dim] fp32, post QK-norm and RoPE
* @param k_cache [n_head_kv][max_seq][head_dim] fp32
* @param v_cache [n_head_kv][max_seq][head_dim] fp32
* @param out [n][n_head][head_dim] fp32 (written)
* @param n Query rows (0 is a no-op, not an error)
* @param kv_groups Query heads per kv head, 1..FM_ATTN_MAX_GROUPS
* @param n_head_kv Key/value heads
* @param head_dim Per-head dimension, multiple of 32, <= FM_ATTN_MAX_HEAD_DIM
* @param max_seq Ring capacity in rows
* @param pos0 Absolute position of query row 0
* @param scale 1/sqrt(head_dim)
*
* Thread-safe: no (shares the global command buffer)
* Reentrant: no
* Signal-safe: no
*/
void fm_attn_flash_prefill_at(fm_bufref q, fm_bufref k_cache, fm_bufref v_cache,
fm_bufref out, int n, int kv_groups,
int n_head_kv, int head_dim, int max_seq,
int pos0, float scale);
/* fm_attn_flash_prefill_at with every operand bound at offset 0. */
void fm_attn_flash_prefill(fm_buffer *q, fm_buffer *k_cache, fm_buffer *v_cache,
fm_buffer *out, int n, int kv_groups,
int n_head_kv, int head_dim, int max_seq,
int pos0, float scale);
#ifdef __cplusplus
}
#endif
#endif /* FOUNDRY_METAL_KERNEL_ATTN_H */