com.microsoft.SparseAttention / build /webgpu /sparse-kv-append.wgsl.jinja
Xenova's picture
Xenova HF Staff
sync 91d990483a17
f03331e verified
Raw
History Blame
4.67 kB
{% macro sparse_schedule() %}
// How much history precedes this call's tokens. Prompt mode is determined from
// scalar total_sequence_length rather than per-batch values and forces the past
// length to zero; a padded prompt row must not be read as if it had history.
fn past_sequence_length(batch: u32) -> u32 {
if (u32(total_sequence_length[0]) == params.seqLen) {
return 0u;
}
let total = u32(key_total_sequence_lengths[batch]);
return select(0u, total - params.seqLen, total >= params.seqLen);
}
{%- endmacro %}
{% macro sparse_rotary(interleaved) %}
// Which cos/sin entry a component uses, and which member of its rotation pair it is.
// The two layouts differ only here: the NeoX split pairs d with d + ROTARY_HALF, and the
// interleaved layout pairs the even lane with the odd one beside it.
fn rotary_slot(d: u32) -> u32 {
{% if interleaved %}
return d / 2u;
{% else %}
return select(d - ROTARY_HALF, d, d < ROTARY_HALF);
{% endif %}
}
fn rotary_partner(d: u32) -> u32 {
{% if interleaved %}
return select(d - 1u, d + 1u, (d % 2u) == 0u);
{% else %}
return select(d - ROTARY_HALF, d + ROTARY_HALF, d < ROTARY_HALF);
{% endif %}
}
fn rotary_is_first(d: u32) -> bool {
{% if interleaved %}
return (d % 2u) == 0u;
{% else %}
return d < ROTARY_HALF;
{% endif %}
}
// One component of the rotation, written from the component's own point of view so a
// single expression covers both members of the pair: the leading one subtracts its
// partner's sine term, the trailing one adds it.
fn rotary_value(own: f32, partner: f32, cs: f32, sn: f32, first: bool) -> f32 {
return select(own * cs + partner * sn, own * cs - partner * sn, first);
}
{%- endmacro %}
{{ env.wgsl.resourceDeclarations }}
// com.microsoft.SparseAttention, KV append pass.
// past_key and present_key are the same allocation, so this writes only the new rows:
// one invocation per (batch, kv head, new token, component) of the BNSH cache, landing
// at absolute position past + t. Nothing rewrites the retained history.
const KV_HEADS: u32 = {{ kvNumHeads }}u;
const HEAD_DIM: u32 = {{ headSize }}u;
const MAX_CACHE_SEQ: u32 = {{ maxCacheSeq }}u;
{% if packedQkv %}
// Packed layout: query carries [Q | K | V] on one row, so K starts after the query
// heads and V after the key heads.
const Q_STRIDE: u32 = {{ packedStride }}u;
const PACKED_K_OFFSET: u32 = {{ numHeads * headSize }}u;
const PACKED_V_OFFSET: u32 = {{ (numHeads + kvNumHeads) * headSize }}u;
{% else %}
const KV_HIDDEN: u32 = {{ kvNumHeads * headSize }}u;
{% endif %}
const WG: u32 = {{ appendWorkgroupSize }}u;
{% if usesRotary %}
const ROTARY_HALF: u32 = {{ rotaryHalf }}u;
const ROTARY_DIM: u32 = {{ rotaryDim }}u;
{% endif %}
{{ sparse_schedule() }}
{% if usesRotary %}
{{ sparse_rotary(rotaryInterleaved) }}
{% endif %}
@compute @workgroup_size(WG, 1, 1)
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
// 2D-folded flat index: gid.y carries the high bits past the
// per-axis dispatch fold width. Reduces to gid.x when the dispatch does not fold.
let index = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * WG;
let count = params.batchSize * KV_HEADS * params.seqLen * HEAD_DIM;
if (index >= count) {
return;
}
let d = index % HEAD_DIM;
let t = (index / HEAD_DIM) % params.seqLen;
let kv_head = (index / (HEAD_DIM * params.seqLen)) % KV_HEADS;
let batch = index / (HEAD_DIM * params.seqLen * KV_HEADS);
let position = past_sequence_length(batch) + t;
let dst = ((batch * KV_HEADS + kv_head) * MAX_CACHE_SEQ + position) * HEAD_DIM + d;
{% if packedQkv %}
let row = (batch * params.seqLen + t) * Q_STRIDE;
let k_base = row + PACKED_K_OFFSET + kv_head * HEAD_DIM;
let v_base = row + PACKED_V_OFFSET + kv_head * HEAD_DIM;
{% else %}
let k_base = ((batch * params.seqLen + t) * KV_HIDDEN) + kv_head * HEAD_DIM;
let v_base = k_base;
{% endif %}
{% if usesRotary %}
// The key is rotated at its own absolute position before it enters the cache, so the
// cache holds post-rotary keys and the attention pass never rotates again.
if (d < ROTARY_DIM) {
let slot = rotary_slot(d);
let cs = f32(cos_cache[position * ROTARY_HALF + slot]);
let sn = f32(sin_cache[position * ROTARY_HALF + slot]);
let own = f32({{ kvSource }}[k_base + d]);
let partner = f32({{ kvSource }}[k_base + rotary_partner(d)]);
present_key[dst] = {{ scalar }}(rotary_value(own, partner, cs, sn, rotary_is_first(d)));
} else {
present_key[dst] = {{ scalar }}({{ kvSource }}[k_base + d]);
}
{% else %}
present_key[dst] = {{ scalar }}({{ kvSource }}[k_base + d]);
{% endif %}
present_value[dst] = {{ scalar }}({{ vSource }}[v_base + d]);
}