{% 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, @builtin(num_workgroups) nwg: vec3 ) { // 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, {% endif %}{% if cooperativeMode %}@builtin(workgroup_id) wid: vec3, @builtin(local_invocation_id) lid: vec3, {% endif %} @builtin(num_workgroups) nwg: vec3) { {% 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; 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; 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 %}