File size: 18,490 Bytes
f18314a 6d43358 f18314a f03331e f18314a 6d43358 f18314a 35f8daf f03331e 35f8daf 6d43358 f18314a f03331e f18314a 6d43358 f18314a 6d43358 f18314a 6d43358 f18314a 6d43358 f18314a 6d43358 35f8daf f18314a 35f8daf f18314a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 | {% 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 %}
{{ env.wgsl.resourceDeclarations }}
// com.microsoft.SparseAttention, attention pass.
// One workgroup per (batch, query-token tile, query head). The workgroup sweeps only the key
// blocks this query's layout row selects, in tiles of WG keys with one key per thread,
// merging each tile into a running online softmax so the score row is never
// materialized. Repeated CSR columns are suppressed because the CSR data represents a
// boolean block mask.
//
// The block mask is CSR: row q_abs / SPARSE_BLOCK of layout (head % NUM_LAYOUT) spans
// block_col_indices[start .. end), and each of those columns names SPARSE_BLOCK
// contiguous keys. Sweeping (end - start) * SPARSE_BLOCK virtual slots and resolving
// each through the column list is what makes the kernel cost the SELECTED blocks rather
// than the whole history -- the reason the operator exists.
//
// Causality bounds every visited row at this query's absolute position. A layout
// with the triangular dense count and a sparse row with row + 1 entries take the
// dense shortcut without inspecting their column values.
const Q_HEADS: u32 = {{ numHeads }}u;
const KV_HEADS: u32 = {{ kvNumHeads }}u;
const HEAD_DIM: u32 = {{ headSize }}u;
// The cache is read four elements at a time. `head_size` is a multiple of eight,
// so every row divides into whole vec4 values and requires no scalar tail.
const HEAD_VEC: u32 = {{ headVec }}u;
const MAX_CACHE_SEQ: u32 = {{ maxCacheSeq }}u;
const SPARSE_BLOCK: u32 = {{ sparseBlockSize }}u;
const NUM_LAYOUT: u32 = {{ numLayout }}u;
const ROW_STRIDE: u32 = {{ maxBlocks + 1 }}u;
const COL_STRIDE: u32 = {{ maxNnz }}u;
const Q_HIDDEN: u32 = {{ numHeads * headSize }}u;
{% if not usesRotary %}
const Q_STRIDE: u32 = {{ packedStride if packedQkv else numHeads * headSize }}u;
{% endif %}
const WG: u32 = {{ attnWorkgroup }}u;
const Q_TILE: u32 = {{ qTile }}u;
// FLT_MAX, not -inf, as the online (m, d) accumulator init: merges must keep
// `m - m` finite so an empty lane / all--inf row contributes the exact
// accumulator identity (m, d) = (-FLT_MAX, 0). Operator epilogues interpret
// a zero final denominator according to their public semantics. Using -inf
// here changes +inf-row behavior.
const FLT_MAX: f32 = 3.4028234663852886e38;
fn is_finite_f32(value: f32) -> bool {
return select(false, value <= FLT_MAX, value >= -FLT_MAX);
}
// x - m that is exactly 0 when x equals a finite m, so exp(shifted) == 1
// exactly at the row max. `x - x` on an infinite max is a legal fast-math
// fold to 0, which would silently turn +inf rows finite — the explicit
// equality test keeps the NaN propagation of the serial kernels.
fn shifted_value(value: f32, maxValue: f32) -> f32 {
let equalFiniteMax = select(false, value == maxValue, is_finite_f32(maxValue));
return select(value - maxValue, 0.0, equalFiniteMax);
}
fn exp_shift(value: f32, maxValue: f32) -> f32 {
return exp(shifted_value(value, maxValue));
}
var<workgroup> q_shared: array<vec4<f32>, Q_TILE * HEAD_VEC>;
var<workgroup> running_out: array<vec4<f32>, Q_TILE * HEAD_VEC>;
var<workgroup> probs: array<f32, Q_TILE * WG>;
{% if vStageWorthIt %}
// The workgroup stages each chunk of value rows, then HEAD_VEC lanes consume
// those rows from shared memory during the running-output update.
const V_STAGE_KEYS: u32 = 16u;
var<workgroup> v_stage: array<vec4<f32>, V_STAGE_KEYS * HEAD_VEC>;
{% endif %}
{% if valueParts > 1 %}
// Each lane owns one (key partition, output vec4); query streams share V loads.
var<workgroup> value_partials: array<vec4<f32>, {{ valueParts * headVec * qTile | int }}>;
{% endif %}
// One resolved cache row base per key of the current tile, so the value accumulation
// re-reads a base instead of re-walking the column list per head dimension.
var<workgroup> key_rows: array<u32, WG>;
{% set mdStreams = qTile %}
// Workgroup-cooperative merge of per-thread online-softmax (m, d) partials:
// mNew = max(m1, m2)
// dNew = d1 * exp(m1 - mNew) + d2 * exp(m2 - mNew)
// Both the subgroup and portable barrier-tree engines return the same merged
// pair to every invocation. Repeated merges require a workgroup barrier between
// calls before their shared partial storage is reused.
{% set mdStreamed = mdStreams is defined %}
{% set mdStreams = mdStreams if mdStreams is defined else 1 %}
{% set mdExtent = "WG" if mdStreams == 1 else "WG * " ~ mdStreams ~ "u" %}
var<workgroup> partialM: array<f32, {{ mdExtent }}>;
var<workgroup> partialD: array<f32, {{ mdExtent }}>;
{% if mdStreamed %}
// In-place fold of {{ mdStreams }} streams. Input partials occupy
// partialM/partialD; stream s returns its merged pair in slot s * WG.
fn combine_partials_streams(lidx: u32) {
workgroupBarrier();
var stride = WG / 2u;
loop {
if (stride == 0u) {
break;
}
if (lidx < stride) {
{% for s in range(mdStreams) %}
{
let slot = {{ s }}u * WG + lidx;
let m1 = partialM[slot];
let d1 = partialD[slot];
let m2 = partialM[slot + stride];
let d2 = partialD[slot + stride];
let mNew = max(m1, m2);
partialD[slot] = d1 * exp_shift(m1, mNew) + d2 * exp_shift(m2, mNew);
partialM[slot] = mNew;
}
{% endfor %}
}
workgroupBarrier();
stride = stride / 2u;
}
}
{% else %}
fn combine_partials(m: f32, d: f32, lidx: u32) -> vec2<f32> {
partialM[lidx] = m;
partialD[lidx] = d;
workgroupBarrier();
var stride = WG / 2u;
loop {
if (stride == 0u) {
break;
}
if (lidx < stride) {
let m1 = partialM[lidx];
let d1 = partialD[lidx];
let m2 = partialM[lidx + stride];
let d2 = partialD[lidx + stride];
let mNew = max(m1, m2);
partialD[lidx] = d1 * exp_shift(m1, mNew) + d2 * exp_shift(m2, mNew);
partialM[lidx] = mNew;
}
workgroupBarrier();
stride = stride / 2u;
}
let merged = vec2<f32>(partialM[0], partialD[0]);
// Trailing barrier so back-to-back calls cannot race a next call's partial
// stores against this call's reads of slot 0.
workgroupBarrier();
return merged;
}
{% endif %}
{% set ATTN_SCALE_DIM = "HEAD_DIM" %}fn scale_value() -> f32 {
if (params.scale != 0.0) { return params.scale; }
return inverseSqrt(f32({{ ATTN_SCALE_DIM }}));
}
{{ sparse_schedule() }}
@compute @workgroup_size(WG, 1, 1)
fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
{% if promptTail %}
// The matrix pass retains every query when this call includes past history.
if (u32(total_sequence_length[0]) != params.seqLen) { return; }
{% endif %}
let tile0 = wg.x * Q_TILE{% if queryOffset > 0 %} + {{ queryOffset }}u{% endif %};
let head = wg.y % Q_HEADS;
let batch = wg.y / Q_HEADS;
let tid = lid.x;
let past = past_sequence_length(batch);
let kv_head = head / (Q_HEADS / KV_HEADS);
let scale = scale_value();
// Layouts cycle over the heads: four layouts and eight heads give S0 S1 S2 S3 S0 ...
let layout_id = head % NUM_LAYOUT;
let row_base = layout_id * ROW_STRIDE;
// A layout at or above the triangular dense-block count is treated as dense.
let dense_nnz = ROW_STRIDE * (ROW_STRIDE - 1u) / 2u;
let layout_is_dense = u32(block_row_indices[row_base + ROW_STRIDE - 1u]) >= dense_nnz;
{% for j in range(qTile) %}
// Keys 0 .. q_abs are visible; the +1 makes the bound exclusive. A tile that runs past
// the last token carries inactive queries; they take no mask row and store nothing.
let live_{{ j }} = tile0 + {{ j }}u < params.seqLen;
let q_abs_{{ j }} = past + tile0 + {{ j }}u;
let key_bound_{{ j }} = q_abs_{{ j }} + 1u;
let mask_row_{{ j }} = q_abs_{{ j }} / SPARSE_BLOCK;
{% endfor %}
{% for j in range(qTile) %}
{
{% if usesRotary %}
let q_base = ((batch * Q_HEADS + head) * params.seqLen + min(tile0 + {{ j }}u, params.seqLen - 1u)) * HEAD_DIM;
{% else %}
let q_base = (batch * params.seqLen + min(tile0 + {{ j }}u, params.seqLen - 1u)) * Q_STRIDE + head * HEAD_DIM;
{% endif %}
for (var dv: u32 = tid; dv < HEAD_VEC; dv = dv + WG) {
let qb = q_base + dv * 4u;
{% if usesRotary %}
q_shared[{{ j }}u * HEAD_VEC + dv] = vec4<f32>(q_rotary[qb], q_rotary[qb + 1u], q_rotary[qb + 2u], q_rotary[qb + 3u]);
{% else %}
q_shared[{{ j }}u * HEAD_VEC + dv] = vec4<f32>(f32(query[qb]), f32(query[qb + 1u]), f32(query[qb + 2u]), f32(query[qb + 3u]));
{% endif %}
running_out[{{ j }}u * HEAD_VEC + dv] = vec4<f32>(0.0);
}
}
{% endfor %}
workgroupBarrier();
{% for j in range(qTile) %}
var runningMax_{{ j }} = -FLT_MAX;
var runningDenom_{{ j }} = 0.0;
{% endfor %}
// Consecutive tokens span at most two mask rows, and every query of a row selects the
// same blocks, so one sweep per row covers the tile. A query contributes only to the
// sweep of its own row, which is why its online state is never merged across rows.
{% if qTile > 1 %}
let row_first = q_abs_0 / SPARSE_BLOCK;
{% if (seqLen - queryOffset) % qTile != 0 %}
// Inactive query lanes must not extend traversal past the final CSR row.
let row_last = (past + min(tile0 + Q_TILE, params.seqLen) - 1u) / SPARSE_BLOCK;
{% else %}
let row_last = mask_row_{{ qTile - 1 }};
{% endif %}
for (var mask_row = row_first; mask_row <= row_last; mask_row = mask_row + 1u) {
{% else %}
{
let mask_row = mask_row_0;
{% endif %}
let start = u32(block_row_indices[row_base + mask_row]);
let end = u32(block_row_indices[row_base + mask_row + 1u]);
// A row listing every causal block is also dense, so its column values are ignored.
let dense_row = layout_is_dense || (end - start == mask_row + 1u);
// The dense sweep runs to the furthest causal bound of the queries on this row; each
// query still drops the keys past its own.
var bound_max = 0u;
{% for j in range(qTile) %}
if (live_{{ j }} && mask_row_{{ j }} == mask_row) { bound_max = max(bound_max, key_bound_{{ j }}); }
{% endfor %}
let slot_count = select((end - start) * SPARSE_BLOCK, bound_max, dense_row);
var tileBase: u32 = 0u;
loop {
if (tileBase >= slot_count) {
break;
}
let slot = tileBase + tid;
// Resolving the slot is query-independent: the block list belongs to the row, and
// only the causal bound below differs between the queries sharing it.
var key = 0u;
var keyResolved = false;
if (slot < slot_count) {
if (!dense_row) {
// CSR is a boolean mask. Repeating a column therefore cannot repeat its
// probability mass, even though malformed-but-valid CSR may contain duplicates.
let entry = slot / SPARSE_BLOCK;
let block = u32(block_col_indices[layout_id * COL_STRIDE + start + entry]);
var duplicate = false;
for (var previous = 0u; previous < entry; previous = previous + 1u) {
duplicate = duplicate ||
u32(block_col_indices[layout_id * COL_STRIDE + start + previous]) == block;
}
key = block * SPARSE_BLOCK + slot % SPARSE_BLOCK;
keyResolved = !duplicate;
} else {
key = slot;
keyResolved = true;
}
}
// Always a row this cache holds, so an unresolved slot leaves a readable base rather
// than a stale or uninitialized one and the accumulation below needs no guard.
let base = ((batch * KV_HEADS + kv_head) * MAX_CACHE_SEQ + select(0u, key, keyResolved)) * HEAD_DIM;
key_rows[tid] = base;
{% for j in range(qTile) %}
let allowed_{{ j }} = keyResolved && live_{{ j }} && mask_row_{{ j }} == mask_row && key < key_bound_{{ j }};
var acc_{{ j }} = 0.0;
{% endfor %}
if (keyResolved) {
// One key vector, every query's score: this is the reuse the tile exists for.
let row_v = base / 4u;
for (var dv: u32 = 0u; dv < HEAD_VEC; dv = dv + 1u) {
let kv = vec4<f32>(present_key[row_v + dv]);
{% for j in range(qTile) %}
acc_{{ j }} = acc_{{ j }} + dot(q_shared[{{ j }}u * HEAD_VEC + dv], kv);
{% endfor %}
}
}
// One barrier tree folds all Q_TILE online-softmax partials.
{% for j in range(qTile) %}
let score_{{ j }} = acc_{{ j }} * scale;
partialM[{{ j }}u * WG + tid] = select(-FLT_MAX, score_{{ j }}, allowed_{{ j }});
partialD[{{ j }}u * WG + tid] = select(0.0, 1.0, allowed_{{ j }});
{% endfor %}
combine_partials_streams(tid);
{% for j in range(qTile) %}
let tile_{{ j }} = vec2<f32>(partialM[{{ j }}u * WG], partialD[{{ j }}u * WG]);
{% endfor %}
{% for j in range(qTile) %}
let newMax_{{ j }} = max(runningMax_{{ j }}, tile_{{ j }}.x);
let correction_{{ j }} = exp_shift(runningMax_{{ j }}, newMax_{{ j }});
runningDenom_{{ j }} = runningDenom_{{ j }} * correction_{{ j }}
+ tile_{{ j }}.y * exp_shift(tile_{{ j }}.x, newMax_{{ j }});
runningMax_{{ j }} = newMax_{{ j }};
probs[{{ j }}u * WG + tid] = select(0.0, exp_shift(score_{{ j }}, newMax_{{ j }}), allowed_{{ j }});
{% endfor %}
workgroupBarrier();
// One value vector serves every query. When head columns underfill the
// workgroup, spare lanes sum disjoint contiguous key ranges and publish
// partials; each output owner then folds them in key-range order. The
// manifest limits this storage and keeps a single-owner fallback.
{% if valueParts > 1 %}
let tileCount = min(WG, slot_count - tileBase);
let valueDim = tid % HEAD_VEC;
let valuePart = tid / HEAD_VEC;
if (valuePart < {{ valueParts }}u) {
let first = tileCount * valuePart / {{ valueParts }}u;
let last = tileCount * (valuePart + 1u) / {{ valueParts }}u;
{% for j in range(qTile) %}
var valueSum_{{ j }} = vec4<f32>(0.0);
{% endfor %}
for (var i = first; i < last; i++) {
let vv = vec4<f32>(present_value[key_rows[i] / 4u + valueDim]);
{% for j in range(qTile) %}
valueSum_{{ j }} += probs[{{ j }}u * WG + i] * vv;
{% endfor %}
}
{% for j in range(qTile) %}
value_partials[{{ j * valueParts * headVec | int }}u + tid] = valueSum_{{ j }};
{% endfor %}
}
workgroupBarrier();
if (tid < HEAD_VEC) {
{% for j in range(qTile) %}
var valueSum_{{ j }} = value_partials[{{ j * valueParts * headVec | int }}u + tid];
{% for p in range(1, valueParts) %}
valueSum_{{ j }} += value_partials[{{ (j * valueParts + p) * headVec | int }}u + tid];
{% endfor %}
running_out[{{ j }}u * HEAD_VEC + tid] = running_out[{{ j }}u * HEAD_VEC + tid] * correction_{{ j }} + valueSum_{{ j }};
{% endfor %}
}
workgroupBarrier();
{% elif vStageWorthIt %}
let tileCount = min(WG, slot_count - tileBase);
{% for j in range(qTile) %}
var vSum_{{ j }} = vec4<f32>(0.0);
{% endfor %}
for (var chunkBase: u32 = 0u; chunkBase < tileCount; chunkBase = chunkBase + V_STAGE_KEYS) {
let chunkCount = min(V_STAGE_KEYS, tileCount - chunkBase);
for (var slot = tid; slot < chunkCount * HEAD_VEC; slot = slot + WG) {
let ki = slot / HEAD_VEC;
let dvs = slot % HEAD_VEC;
v_stage[slot] = vec4<f32>(present_value[key_rows[chunkBase + ki] / 4u + dvs]);
}
workgroupBarrier();
for (var dv: u32 = tid; dv < HEAD_VEC; dv = dv + WG) {
for (var i: u32 = 0u; i < chunkCount; i = i + 1u) {
let vv = v_stage[i * HEAD_VEC + dv];
{% for j in range(qTile) %}
vSum_{{ j }} = vSum_{{ j }} + probs[{{ j }}u * WG + chunkBase + i] * vv;
{% endfor %}
}
}
// Orders this chunk's v_stage reads before the next chunk overwrites it.
workgroupBarrier();
}
for (var dv: u32 = tid; dv < HEAD_VEC; dv = dv + WG) {
{% for j in range(qTile) %}
running_out[{{ j }}u * HEAD_VEC + dv] = running_out[{{ j }}u * HEAD_VEC + dv] * correction_{{ j }} + vSum_{{ j }};
{% endfor %}
}
// Orders this tile's probs and key_rows reads before the next tile writes them.
workgroupBarrier();
{% else %}
let tileCount = min(WG, slot_count - tileBase);
for (var dv: u32 = tid; dv < HEAD_VEC; dv = dv + WG) {
{% for j in range(qTile) %}
var vSum_{{ j }} = vec4<f32>(0.0);
{% endfor %}
for (var i: u32 = 0u; i < tileCount; i = i + 1u) {
let vv = vec4<f32>(present_value[key_rows[i] / 4u + dv]);
{% for j in range(qTile) %}
vSum_{{ j }} = vSum_{{ j }} + probs[{{ j }}u * WG + i] * vv;
{% endfor %}
}
{% for j in range(qTile) %}
running_out[{{ j }}u * HEAD_VEC + dv] = running_out[{{ j }}u * HEAD_VEC + dv] * correction_{{ j }} + vSum_{{ j }};
{% endfor %}
}
// Orders this tile's probs and key_rows reads before the next tile writes them.
workgroupBarrier();
{% endif %}
tileBase = tileBase + WG;
}
}
// An all-masked row becomes a uniform average over causal history rather than zero.
{% for j in range(qTile) %}
if (live_{{ j }}) {
let hasMass_{{ j }} = runningDenom_{{ j }} > 0.0;
let invDenom_{{ j }} = select(0.0, 1.0 / runningDenom_{{ j }}, hasMass_{{ j }});
let out_base_{{ j }} = (batch * params.seqLen + tile0 + {{ j }}u) * Q_HIDDEN + head * HEAD_DIM;
for (var dv: u32 = tid; dv < HEAD_VEC; dv = dv + WG) {
var out_v: vec4<f32>;
if (hasMass_{{ j }}) {
out_v = running_out[{{ j }}u * HEAD_VEC + dv] * invDenom_{{ j }};
} else {
var total = vec4<f32>(0.0);
for (var key = 0u; key < key_bound_{{ j }}; key = key + 1u) {
let row_v = (((batch * KV_HEADS + kv_head) * MAX_CACHE_SEQ + key) * HEAD_DIM) / 4u;
total = total + vec4<f32>(present_value[row_v + dv]);
}
out_v = total / f32(key_bound_{{ j }});
}
let ob = out_base_{{ j }} + dv * 4u;
output[ob] = {{ scalar }}(out_v.x);
output[ob + 1u] = {{ scalar }}(out_v.y);
output[ob + 2u] = {{ scalar }}(out_v.z);
output[ob + 3u] = {{ scalar }}(out_v.w);
}
}
{% endfor %}
}
|