com.microsoft.GroupQueryAttention / build /webgpu /gqa-present.wgsl.jinja
Xenova's picture
Xenova HF Staff
sync 2e7068faf55e
f96162c verified
Raw
History Blame
19.3 kB
{% if source is defined and source.mode is defined %}
{% set mode = source.mode %}
{% elif mode is not defined %}
{% set mode = "transpose" %}
{% endif %}
{% set presentScalar = presentScalar | default(inputScalar | default("f32")) %}
{% set presentElement = presentElement | default(presentScalar) %}
{% set packed = packed | default(0) %}
{% set bits = bits | default(0) %}
{% set qmax = qmax | default(0) %}
{% set qmin = qmin | default(0) %}
{% if usesF16 is defined and usesF16 %}enable f16;
{% endif %}{{ env.wgsl.resourceDeclarations }}
{% if mode == "transpose" %}
const KV_HEADS: u32 = {{ kvNumHeads }}u;
const WG: u32 = {{ copyWorkgroupSize }}u;
{% if presentVec4 %}
// Transpose copy [batch, token, head*headDim] -> present [batch, head, token,
// headDim]. The headDim row is contiguous on both sides and divisible by four,
// so each invocation copies a vec4 along d with coalesced stores.
const HEAD_DIM_V4: u32 = {{ headDimV4 }}u;
const KV_HIDDEN_V4: u32 = {{ kvHiddenV4 }}u;
{% else %}
const HEAD_DIM: u32 = {{ headDim }}u;
const KV_HIDDEN: u32 = {{ kvHidden }}u;
{% endif %}
@compute @workgroup_size(WG, 1, 1)
fn main(
@builtin(global_invocation_id) gid: vec3<u32>,
@builtin(num_workgroups) nwg: vec3<u32>
) {
// The dispatch folds oversized one-dimensional grids into x/y. Rebuild the
// flat invocation index; this reduces to gid.x when no fold is needed.
let index = gid.x + gid.y * nwg.x * WG;
{% if presentVec4 %}
let total = params.batchSize * KV_HEADS * params.kvSeq * HEAD_DIM_V4;
if (index >= total) {
return;
}
let d4 = index % HEAD_DIM_V4;
let token = (index / HEAD_DIM_V4) % params.kvSeq;
let head = (index / (HEAD_DIM_V4 * params.kvSeq)) % KV_HEADS;
let batch = index / (HEAD_DIM_V4 * params.kvSeq * KV_HEADS);
let packed = (batch * params.kvSeq + token) * KV_HIDDEN_V4 + head * HEAD_DIM_V4 + d4;
present_key[index] = {{ presentElement }}(key[packed]);
present_value[index] = {{ presentElement }}(value[packed]);
{% else %}
let total = params.batchSize * KV_HEADS * params.kvSeq * HEAD_DIM;
if (index >= total) {
return;
}
let d = index % HEAD_DIM;
let token = (index / HEAD_DIM) % params.kvSeq;
let head = (index / (HEAD_DIM * params.kvSeq)) % KV_HEADS;
let batch = index / (HEAD_DIM * params.kvSeq * KV_HEADS);
let packed_index = (batch * params.kvSeq + token) * KV_HIDDEN + head * HEAD_DIM + d;
present_key[index] = {{ presentScalar }}(key[packed_index]);
present_value[index] = {{ presentScalar }}(value[packed_index]);
{% endif %}
}
{%- else %}
{% set shareAppend = mode == "merge_share" and shareRegion == "append" %}
{% set cooperativeCopy = device.adapterInfo.vendor != "apple" %}
{% set cooperativeMerge = cooperativeCopy or shareAppend %}
{% set cooperativeMode = cooperativeMerge and (mode == "copy" or mode == "merge" or mode == "merge_share") %}
{% set needsGid = not cooperativeMode or (shareAppend and not cooperativeCopy) %}
{% if mode == "copy" %}
// Copy the existing BNSH cache unchanged. Compression changes only the number
// of stored words per row.
{% elif mode == "merge" %}
// Copy the past BNSH cache, then append new BSH K/V tokens into the present cache.
{% elif mode == "merge_share" %}
// Buffer-sharing append: past and present share the full-capacity BNSH stride.
// Rows outside each batch's append window [seqlens_k[b]+1-keySeq, seqlens_k[b]+1)
// are an identity copy of the past cache; rows inside it come from the new BSH
// K/V (K optionally rotated at its absolute position).
// The append and retain passes own disjoint ranges, so their order is immaterial.
{% elif mode == "build" %}
// Build a BNSH cache from new BSH K/V, optionally applying K RMSNorm and rotary.
{% elif mode == "window_shift" %}
// Windowed cache: compact the surviving past rows down by the eviction count and
// append the new BSH K/V, keeping the most recent min(T, capacity) tokens
// contiguous at rows [0, L). Reads past, writes present (distinct buffers).
{% elif mode == "append_quant" %}
// Quantize new BSH K/V into an existing buffer-sharing INT8 BNSH cache. The
// active sequence end comes from seqlens_k; the physical cache stride remains
// the full present-cache capacity.
{% else %}
// Quantize new BSH K/V into an INT8 or packed INT4 BNSH cache.
{% endif %}
const HEAD_DIM: u32 = {{ headDim }}u;
{% if mode != "copy" %}const KV_HEADS: u32 = {{ kvHeads }}u;
{% endif %}
const WG: u32 = {{ tunables.COPY_WORKGROUP_SIZE }}u;
{% if mode != "copy" and (mode != "merge_share" or shareAppend) %}const KV_HIDDEN: u32 = {{ kvHeads }}u * {{ headDim }}u;
{% endif %}
{% if mode == "build_quant" or mode == "append_quant" %}const PACKED: u32 = {{ packed }}u;
{% endif %}
{% if hasRotary and mode != "copy" and (mode != "merge_share" or shareAppend) %}const HALF: u32 = {{ half }}u;
{% endif %}
{% if hasKNorm and mode != "copy" and (mode != "merge_share" or shareAppend) %}const QK_EPS: f32 = {{ qkEps }};
{% endif %}
{% if mode == "build_quant" or mode == "append_quant" %}
const QMAX: f32 = {{ qmax }};
const QMIN: f32 = {{ qmin }};
fn kscale(d: u32, hk: u32) -> f32 { return k_scale[select(0u, hk * HEAD_DIM + d, params.perChannel != 0u)]; }
fn vscale(d: u32, hk: u32) -> f32 { return v_scale[select(0u, hk * HEAD_DIM + d, params.perChannel != 0u)]; }
{% endif %}
@compute @workgroup_size(WG)
fn main({% if needsGid %}@builtin(global_invocation_id) gid: vec3<u32>,
{% endif %}{% if cooperativeMode %}@builtin(workgroup_id) wid: vec3<u32>,
@builtin(local_invocation_id) lid: vec3<u32>,
{% endif %}
@builtin(num_workgroups) nwg: vec3<u32>) {
{% macro append_cooperative_walk() %}
// Compact cooperative append walk: the thread range spans only the appended
// window (batch x kvHead x keySeq rows), not the full cache capacity; the
// destination row is the dynamic append offset plus the local row.
// The loop is bounded by the elements this workgroup owns rather than a fixed
// HEAD_DIM trip count.
let wgEnd = min(elemBase + WG * HEAD_DIM, totalElems);
for (var e = elemBase + lid.x; e < wgEnd; e = e + WG) {
{
let d = e % HEAD_DIM;
let et = e / HEAD_DIM;
let j = et % params.keySeq;
let etmp = et / params.keySeq;
let hk = etmp % KV_HEADS;
let b = etmp / KV_HEADS;
// Clamping below by keySeq maps a right-padded first prompt
// (seqlens_k[b]+1 < keySeq) to append offset 0.
let activeEnd = min(params.seq, max(params.keySeq, u32(seqlens_k[b]) + 1u));
let t = activeEnd - params.keySeq + j;
let dst = ((b * KV_HEADS + hk) * params.seq + t) * HEAD_DIM + d;
let nSrc = (b * params.keySeq + j) * KV_HIDDEN + hk * HEAD_DIM;
{% if hasRotary %}
// Element-wise NeoX rotation: the pair (dr, dr+HALF) shares one
// cos/sin entry, and row t is its own absolute position.
let dr = select(d - HALF, d, d < HALF);
let cs = cos_cache[t * HALF + dr];
let sn = sin_cache[t * HALF + dr];
let x0 = new_k[nSrc + dr];
let x1 = new_k[nSrc + dr + HALF];
present_key[dst] = select(x1 * cs + x0 * sn, x0 * cs - x1 * sn, d < HALF);
{% else %}
present_key[dst] = new_k[nSrc + d];
{% endif %}
present_value[dst] = new_v[nSrc + d];
}
}
{% endmacro %}
{% macro append_per_row_walk() %}
// Per-row append walk: one thread streams its row's contiguous bytes.
let i = gid.x + gid.y * nwg.x * WG;
if (i >= params.count) { return; }
let j = i % params.keySeq;
let tmp = i / params.keySeq;
let hk = tmp % KV_HEADS;
let b = tmp / KV_HEADS;
// Clamping below by keySeq maps a right-padded first prompt
// (seqlens_k[b]+1 < keySeq) to append offset 0.
let activeEnd = min(params.seq, max(params.keySeq, u32(seqlens_k[b]) + 1u));
let t = activeEnd - params.keySeq + j;
let dstBase = ((b * KV_HEADS + hk) * params.seq + t) * HEAD_DIM;
let nSrc = (b * params.keySeq + j) * KV_HIDDEN + hk * HEAD_DIM;
{% if hasRotary %}
var k: array<f32, HEAD_DIM>;
for (var d = 0u; d < HEAD_DIM; d = d + 1u) { k[d] = f32(new_k[nSrc + d]); }
// Row t is its own absolute position in the shared cache.
for (var d = 0u; d < HALF; d = d + 1u) {
let cs = cos_cache[t * HALF + d];
let sn = sin_cache[t * HALF + d];
let x0 = k[d];
let x1 = k[d + HALF];
k[d] = x0 * cs - x1 * sn;
k[d + HALF] = x1 * cs + x0 * sn;
}
for (var d = 0u; d < HEAD_DIM; d = d + 1u) {
present_key[dstBase + d] = k[d];
present_value[dstBase + d] = new_v[nSrc + d];
}
{% else %}
for (var d = 0u; d < HEAD_DIM; d = d + 1u) {
present_key[dstBase + d] = new_k[nSrc + d];
present_value[dstBase + d] = new_v[nSrc + d];
}
{% endif %}
{% endmacro %}
{% if cooperativeMerge and (mode == "copy" or mode == "merge" or mode == "merge_share") %}
// Each workgroup copies the contiguous element span of its WG rows
// cooperatively: at step s lane l touches element s*WG + l, so adjacent
// lanes hit adjacent addresses on both source and destination. Same
// dispatch geometry (ceil(rows/WG) workgroups, folded into x/y).
let wgFlat = wid.x + wid.y * nwg.x;
let elemBase = wgFlat * (WG * HEAD_DIM);
let totalElems = params.count * HEAD_DIM;
{% endif %}
{% if mode == "copy" and cooperativeCopy %}
// Copy mode is an identity copy over the whole BNSH range: dst index == src index.
for (var s = 0u; s < HEAD_DIM; s = s + 1u) {
let e = elemBase + s * WG + lid.x;
if (e < totalElems) {
present_key[e] = src_k[e];
present_value[e] = src_v[e];
}
}
{% elif mode == "merge" and cooperativeCopy %}
// present[0:pastSeq] = past (BNSH copy); present[pastSeq:totalSeq] = new K/V (BSH appended).
for (var s = 0u; s < HEAD_DIM; s = s + 1u) {
let e = elemBase + s * WG + lid.x;
if (e < totalElems) {
let d = e % HEAD_DIM;
let et = e / HEAD_DIM;
let t = et % params.seq;
let etmp = et / params.seq;
let hk = etmp % KV_HEADS;
let b = etmp / KV_HEADS;
if (t < params.pastSeq) {
let src = ((b * KV_HEADS + hk) * params.pastSeq + t) * HEAD_DIM + d;
present_key[e] = past_k[src];
present_value[e] = past_v[src];
} else {
let nSrc = (b * params.keySeq + (t - params.pastSeq)) * KV_HIDDEN + hk * HEAD_DIM + d;
present_key[e] = new_k[nSrc];
present_value[e] = new_v[nSrc];
}
}
}
{% elif mode == "merge_share" and cooperativeMerge %}
{% if shareAppend %}
{% if cooperativeCopy %}
{{ append_cooperative_walk() }}
{% else %}
if (params.count < WG) {
{{ append_cooperative_walk() }}
return;
}
{{ append_per_row_walk() }}
{% endif %}
{% else %}
// Cooperative element walk (see the copy branch); the same-stride layout
// makes the outside-window rows an identity copy (dst index == src index).
for (var s = 0u; s < HEAD_DIM; s = s + 1u) {
let e = elemBase + s * WG + lid.x;
if (e < totalElems) {
let et = e / HEAD_DIM;
let t = et % params.seq;
let b = et / (params.seq * KV_HEADS);
// Clamping below by keySeq maps a right-padded first prompt
// (seqlens_k[b]+1 < keySeq) to append offset 0.
let activeEnd = min(params.seq, max(params.keySeq, u32(seqlens_k[b]) + 1u));
let appendStart = activeEnd - params.keySeq;
// Everything outside the append window. The append pass owns the window itself,
// and the shared full-capacity stride makes this an identity copy (dst == src).
if (t < appendStart || t >= activeEnd) {
present_key[e] = past_k[e];
present_value[e] = past_v[e];
}
}
}
{% endif %}
{% elif mode == "copy" %}
// Thread i streams one contiguous (batch, kvHead, token) row.
let i = gid.x + gid.y * nwg.x * WG;
if (i >= params.count) { return; }
let base = i * HEAD_DIM;
for (var d = 0u; d < HEAD_DIM; d = d + 1u) {
present_key[base + d] = src_k[base + d];
present_value[base + d] = src_v[base + d];
}
{% elif mode == "merge" %}
// Per-row merge walk.
let i = gid.x + gid.y * nwg.x * WG;
if (i >= params.count) { return; }
let t = i % params.seq;
let tmp = i / params.seq;
let hk = tmp % KV_HEADS;
let b = tmp / KV_HEADS;
// present[0:pastSeq] = past (BNSH copy); present[pastSeq:totalSeq] = new K/V (BSH appended).
let dstBase = ((b * KV_HEADS + hk) * params.seq + t) * HEAD_DIM;
if (t < params.pastSeq) {
let srcBase = ((b * KV_HEADS + hk) * params.pastSeq + t) * HEAD_DIM;
for (var d = 0u; d < HEAD_DIM; d = d + 1u) {
present_key[dstBase + d] = past_k[srcBase + d];
present_value[dstBase + d] = past_v[srcBase + d];
}
} else {
let nSrc = (b * params.keySeq + (t - params.pastSeq)) * KV_HIDDEN + hk * HEAD_DIM;
for (var d = 0u; d < HEAD_DIM; d = d + 1u) {
present_key[dstBase + d] = new_k[nSrc + d];
present_value[dstBase + d] = new_v[nSrc + d];
}
}
{% elif mode == "merge_share" %}
// Past and present share the full-capacity stride, so outside-window rows
// copy at the same index.
let i = gid.x + gid.y * nwg.x * WG;
if (i >= params.count) { return; }
let t = i % params.seq;
let tmp = i / params.seq;
let hk = tmp % KV_HEADS;
let b = tmp / KV_HEADS;
// Clamping below by keySeq maps a right-padded first prompt
// (seqlens_k[b]+1 < keySeq) to append offset 0.
let activeEnd = min(params.seq, max(params.keySeq, u32(seqlens_k[b]) + 1u));
let appendStart = activeEnd - params.keySeq;
let dstBase = ((b * KV_HEADS + hk) * params.seq + t) * HEAD_DIM;
// Everything outside the append window. The append pass owns the window itself,
// and the shared full-capacity stride makes this an identity copy (dst == src).
if (t < appendStart || t >= activeEnd) {
for (var d = 0u; d < HEAD_DIM; d = d + 1u) {
present_key[dstBase + d] = past_k[dstBase + d];
present_value[dstBase + d] = past_v[dstBase + d];
}
}
{% elif mode == "window_shift" %}
// Windowed cache: `params.seq` is a fixed capacity C, and the invariant is that
// the L = min(T, C) most recent tokens live contiguously at rows [0, L).
//
// residentBefore = min(T - S, C) E = max(0, residentBefore + S - C)
// rows [0, appendStart) <- past rows shifted down by E
// rows [appendStart, append+S) <- the new tokens
// rows beyond that <- outside the window, cleared
//
// The shift reads past and writes present, which must be distinct buffers: an
// in-place compaction would have one invocation overwrite row t while another
// still needs it as the source for row t-E, with no ordering between them.
//
// Attention needs no change for this layout. It derives its key range from
// `min(capacity, seqlens_k[b] + 1)`, which is exactly L, and both the causal
// and local-window masks depend only on the query/key distance:
// q_abs - k_abs = (T - qSeq + s) - (origin + t) = (L - qSeq + s) - t
// so scoring a windowed cache as if it were a full L-length one is the same
// arithmetic. Only RoPE would need the true absolute position, which is why
// `windowShiftOk` refuses a rotary request outright rather than silently
// rotating at the cache row.
let i = gid.x + gid.y * nwg.x * WG;
if (i >= params.count) { return; }
let t = i % params.seq;
let tmp = i / params.seq;
let hk = tmp % KV_HEADS;
let b = tmp / KV_HEADS;
let absTotal = max(params.keySeq, u32(seqlens_k[b]) + 1u);
let residentBefore = min(absTotal - params.keySeq, params.seq);
// `max(0u, a + b - c)` does not clamp in u32: the subtraction wraps first, so a
// step that evicts nothing reads back ~2^32 instead of 0. Compare before
// subtracting.
let filled = residentBefore + params.keySeq;
let evicted = select(0u, filled - params.seq, filled > params.seq);
let appendStart = residentBefore - evicted;
let dstBase = ((b * KV_HEADS + hk) * params.seq + t) * HEAD_DIM;
if (t < appendStart) {
// Surviving past rows slide down by the eviction count.
let srcBase = ((b * KV_HEADS + hk) * params.seq + (t + evicted)) * HEAD_DIM;
for (var d = 0u; d < HEAD_DIM; d = d + 1u) {
present_key[dstBase + d] = past_k[srcBase + d];
present_value[dstBase + d] = past_v[srcBase + d];
}
} else if (t < appendStart + params.keySeq) {
let nSrc = (b * params.keySeq + (t - appendStart)) * KV_HIDDEN + hk * HEAD_DIM;
for (var d = 0u; d < HEAD_DIM; d = d + 1u) {
present_key[dstBase + d] = new_k[nSrc + d];
present_value[dstBase + d] = new_v[nSrc + d];
}
} else {
// Outside the resident window. Cleared rather than left stale so the present
// buffer does not expose stale contents from its distinct allocation.
for (var d = 0u; d < HEAD_DIM; d = d + 1u) {
present_key[dstBase + d] = {{ zeroScalar }}(0.0);
present_value[dstBase + d] = {{ zeroScalar }}(0.0);
}
}
{% else %}
// The dispatch folds oversized one-dimensional grids into x/y. Rebuild the
// flat invocation index; this reduces to gid.x when no fold is needed.
let i = gid.x + gid.y * nwg.x * WG;
if (i >= params.count) { return; }
let t = i % params.seq;
let tmp = i / params.seq;
let hk = tmp % KV_HEADS;
let b = tmp / KV_HEADS;
let srcBase = (b * params.seq + t) * KV_HIDDEN + hk * HEAD_DIM; // BSH new K/V
var k: array<f32, HEAD_DIM>;
for (var d = 0u; d < HEAD_DIM; d = d + 1u) { k[d] = src_k[srcBase + d]; }
{% if hasKNorm %}
var ms = 0.0;
for (var d = 0u; d < HEAD_DIM; d = d + 1u) { ms = ms + k[d] * k[d]; }
let invRms = inverseSqrt(ms / f32(HEAD_DIM) + QK_EPS);
for (var d = 0u; d < HEAD_DIM; d = d + 1u) { k[d] = k[d] * invRms * k_norm_weight[d]; }
{% endif %}
{% if hasRotary %}
let pos = params.pastSeq + t;
for (var d = 0u; d < HALF; d = d + 1u) {
let cs = cos_cache[pos * HALF + d];
let sn = sin_cache[pos * HALF + d];
let x0 = k[d];
let x1 = k[d + HALF];
k[d] = x0 * cs - x1 * sn;
k[d + HALF] = x1 * cs + x0 * sn;
}
{% endif %}
{% if mode == "build" %}
let dstBase = ((b * KV_HEADS + hk) * params.seq + t) * HEAD_DIM;
for (var d = 0u; d < HEAD_DIM; d = d + 1u) {
present_key[dstBase + d] = k[d];
present_value[dstBase + d] = src_v[srcBase + d];
}
{% else %}
{% if mode == "append_quant" %}
let activeEnd = u32(seqlens_k[b]) + 1u;
let appendStart = activeEnd - params.seq;
let dstBase = ((b * KV_HEADS + hk) * params.totalSeq + appendStart + t) * PACKED;
{% else %}
let dstBase = ((b * KV_HEADS + hk) * params.seq + t) * PACKED;
{% endif %}
{% if bits == 8 %}
for (var d = 0u; d < HEAD_DIM; d = d + 1u) {
present_key[dstBase + d] = i32(clamp(round(k[d] / kscale(d, hk)), QMIN, QMAX));
present_value[dstBase + d] = i32(clamp(round(src_v[srcBase + d] / vscale(d, hk)), QMIN, QMAX));
}
{% else %}
for (var dd = 0u; dd < PACKED; dd = dd + 1u) {
let d0 = dd * 2u;
let d1 = dd * 2u + 1u;
let qk0 = i32(clamp(round(k[d0] / kscale(d0, hk)), QMIN, QMAX));
let qk1 = i32(clamp(round(k[d1] / kscale(d1, hk)), QMIN, QMAX));
let qv0 = i32(clamp(round(src_v[srcBase + d0] / vscale(d0, hk)), QMIN, QMAX));
let qv1 = i32(clamp(round(src_v[srcBase + d1] / vscale(d1, hk)), QMIN, QMAX));
present_key[dstBase + dd] = (u32(qk0 + 8) & 0xFu) | ((u32(qk1 + 8) & 0xFu) << 4u);
present_value[dstBase + dd] = (u32(qv0 + 8) & 0xFu) | ((u32(qv1 + 8) & 0xFu) << 4u);
}
{% endif %}
{% endif %}
{% endif %}
}
{%- endif %}