Xenova HF Staff commited on
Commit
36d2a82
·
verified ·
1 Parent(s): 2443b58

sync c928d21e6cc1

Browse files
build/webgpu/attn-materialized-sgmat-f32.wgsl.jinja CHANGED
@@ -1,4 +1,11 @@
 
 
 
 
1
  enable subgroups;
 
 
 
2
  enable chromium_experimental_subgroup_matrix;
3
  diagnostic(off, chromium.subgroup_matrix_uniformity);
4
 
@@ -6,10 +13,11 @@ diagnostic(off, chromium.subgroup_matrix_uniformity);
6
 
7
  {% set layout = source.layout | default("bsh") %}
8
  {% set headMajor = layout == "bhsd" %}
 
9
  {% macro q_index(row, d) %}(b * params.qSeq + {{ row }}) * HIDDEN + h * HEAD_DIM + {{ d }}{% endmacro %}
10
  {% macro kv_index(seq, d) %}(b * params.kvSeq + {{ seq }}) * KV_HIDDEN + h_kv * HEAD_DIM + {{ d }}{% endmacro %}
11
  {% set OUT_ROW_STRIDE = "HEAD_DIM" if headMajor else "HIDDEN" %}
12
- {% set KV_ROW_STRIDE = "HEAD_DIM" if headMajor else "KV_HIDDEN" %}
13
  {% set scorePhase = source.phase == "score" %}
14
  {% set FUSED_SOFTMAX = source.fusedSoftmax is defined and source.fusedSoftmax %}
15
  {% set PRIVATE_ROW_STATS = FUSED_SOFTMAX and (materializedSgmatPrivateRowStats is defined and materializedSgmatPrivateRowStats) %}
@@ -38,11 +46,11 @@ select(0.0, scores[{{ index }}], {{ guard[1] }})
38
  {% if hasBias is not defined %}{% set hasBias = false %}{% endif %}
39
  {% set EMIT_ROW_STATS = source.emitRowStats is defined and source.emitRowStats %}
40
  {% set DIRECT_SCORE_STORE = materializedSgmatDirectScoreStore and not EMIT_ROW_STATS %}
41
- {% set DIRECT_APPLY_STORE = materializedSgmatDirectApplyStore and not hasBias %}
42
  {% set DIRECT_OUTPUT_STORE = DIRECT_SCORE_STORE if scorePhase else DIRECT_APPLY_STORE %}
43
  {% set RUNTIME_DIRECT_STORE = (not DIRECT_OUTPUT_STORE)
44
  and materializedSgmatRuntimeDirectStore
45
- and (scorePhase or not hasBias)
46
  and not EMIT_ROW_STATS %}
47
  {% set ANY_DIRECT_STORE = DIRECT_OUTPUT_STORE or RUNTIME_DIRECT_STORE %}
48
  {% set SCALE_IN_Q = DIRECT_SCORE_STORE or (RUNTIME_DIRECT_STORE and scorePhase) %}
@@ -80,8 +88,14 @@ const SUBGROUP_ROWS: u32 = {{ SUBGROUP_ROWS }}u;
80
  const WORKGROUP_THREADS: u32 = {{ WORKGROUP_THREADS }}u;
81
  {% endif %}
82
 
83
- var<workgroup> tile_A: array<f32, {{ TILE_M_VALUE * TILE_K_VALUE }}>;
84
- var<workgroup> tile_B: array<f32, {{ TILE_N_VALUE * TILE_K_VALUE }}>;
 
 
 
 
 
 
85
  {% if FUSED_SOFTMAX and not PRIVATE_ROW_STATS %}
86
  // The tile's rows own their softmax constants for the whole k loop, so they are
87
  // read once per workgroup rather than once per staged element.
@@ -120,7 +134,7 @@ fn exp_shift(value: f32, maxValue: f32) -> f32 {
120
 
121
  {% endif %}
122
 
123
- @compute @workgroup_size({{ WORKGROUP_THREADS }}, 1, 1)
124
  fn main(
125
  @builtin(workgroup_id) wg: vec3<u32>,
126
  @builtin(local_invocation_index) li: u32{% if not DIRECT_OUTPUT_STORE %},
@@ -177,7 +191,7 @@ fn main(
177
  {% endif %}
178
  {% endif %}
179
  for (var k_base = 0u; k_base < inner; k_base += TILE_K) {
180
- {% if source.phase == "apply" and not FUSED_SOFTMAX %}
181
  // Full interior PV tiles can be loaded directly from storage. Query,
182
  // reduction, and output-dimension tails use the guarded shared path below.
183
  if (
@@ -190,16 +204,16 @@ fn main(
190
  let score_offset{{ row_block }} = (b * HEADS + h) * params.qSeq * params.kvSeq
191
  + (m_base + base_A + {{ row_block * 8 }}u) * params.kvSeq + k_base + step;
192
  var matA{{ row_block }}: subgroup_matrix_left<f32, 8, 8> =
193
- subgroupMatrixLoad<subgroup_matrix_left<f32, 8, 8>>(
194
- &scores, score_offset{{ row_block }}, false, params.kvSeq
195
  );
196
  {% endfor %}
197
  {% for col_block in range(COL_BLOCKS) %}
198
  let value_offset{{ col_block }} =
199
  {{ kv_index("k_base + step", "n_base + base_B + " ~ (col_block * 8) ~ "u") }};
200
  var matB{{ col_block }}: subgroup_matrix_right<f32, 8, 8> =
201
- subgroupMatrixLoad<subgroup_matrix_right<f32, 8, 8>>(
202
- &value, value_offset{{ col_block }}, false, {{ KV_ROW_STRIDE }}
203
  );
204
  {% endfor %}
205
  {% for row_block in range(ROW_BLOCKS) %}
@@ -227,12 +241,12 @@ fn main(
227
  {% if scorePhase %}
228
  {% if headDim % 32 == 0 %}
229
  tile_A[a_row * TILE_K + a_col + i] = select(
230
- 0.0,
231
  {{ q_tile_value(q_index("row", "k")) }},
232
  row < params.qSeq
233
  );
234
  {% else %}
235
- var loaded = 0.0;
236
  if (row < params.qSeq && k < HEAD_DIM) {
237
  loaded = {{ q_tile_value(q_index("row", "k")) }};
238
  }
@@ -241,7 +255,7 @@ fn main(
241
  {% else %}
242
  let score_base = (b * HEADS + h) * params.qSeq * params.kvSeq;
243
  tile_A[a_row * TILE_K + a_col + i] =
244
- {{ score_value("score_base + row * params.kvSeq + k", ["a_row", "row < params.qSeq && k < params.kvSeq"]) }};
245
  {% endif %}
246
  }
247
 
@@ -253,12 +267,12 @@ fn main(
253
  {% if scorePhase %}
254
  {% if headDim % 32 == 0 %}
255
  tile_B[b_row * TILE_K + b_col + i] = select(
256
- 0.0,
257
  key[{{ kv_index("col", "k") }}],
258
  col < params.kvSeq
259
  );
260
  {% else %}
261
- var loaded = 0.0;
262
  if (col < params.kvSeq && k < HEAD_DIM) {
263
  loaded = key[{{ kv_index("col", "k") }}];
264
  }
@@ -266,7 +280,7 @@ fn main(
266
  {% endif %}
267
  {% else %}
268
  tile_B[b_row * TILE_K + b_col + i] = select(
269
- 0.0,
270
  value[{{ kv_index("k", "col") }}],
271
  k < params.kvSeq && col < HEAD_DIM
272
  );
@@ -281,19 +295,19 @@ fn main(
281
  let row = m_base + tile_row;
282
  let k = k_base + tile_k;
283
  {% if scorePhase %}
284
- var loaded = 0.0;
285
  if (row < params.qSeq && k < HEAD_DIM) {
286
  loaded = {{ q_tile_value(q_index("row", "k")) }};
287
  }
288
  {% elif FUSED_SOFTMAX %}
289
  let score_base = (b * HEADS + h) * params.qSeq * params.kvSeq;
290
  let loaded =
291
- {{ score_value("score_base + row * params.kvSeq + k", ["tile_row", "row < params.qSeq && k < params.kvSeq"]) }};
292
  {% else %}
293
- var loaded = 0.0;
294
  if (row < params.qSeq && k < params.kvSeq) {
295
  let score_base = (b * HEADS + h) * params.qSeq * params.kvSeq;
296
- loaded = scores[score_base + row * params.kvSeq + k];
297
  }
298
  {% endif %}
299
  tile_A[idx] = loaded;
@@ -305,12 +319,12 @@ fn main(
305
  let col = n_base + tile_col;
306
  let k = k_base + tile_k;
307
  {% if scorePhase %}
308
- var loaded = 0.0;
309
  if (col < params.kvSeq && k < HEAD_DIM) {
310
  loaded = key[{{ kv_index("col", "k") }}];
311
  }
312
  {% else %}
313
- var loaded = 0.0;
314
  if (k < params.kvSeq && col < HEAD_DIM) {
315
  loaded = value[{{ kv_index("k", "col") }}];
316
  }
@@ -324,17 +338,17 @@ fn main(
324
  {% for row_block in range(ROW_BLOCKS) %}
325
  let matrix_a_offset{{ row_block }} =
326
  (subtile_idy * SUB_ROWS + {{ row_block * 8 }}u) * TILE_K + step;
327
- var matA{{ row_block }}: subgroup_matrix_left<f32, 8, 8> =
328
- subgroupMatrixLoad<subgroup_matrix_left<f32, 8, 8>>(
329
- &tile_A, matrix_a_offset{{ row_block }}, false, TILE_K
330
  );
331
  {% endfor %}
332
  {% for col_block in range(COL_BLOCKS) %}
333
  let matrix_b_offset{{ col_block }} =
334
  (subtile_idx * SUB_COLS + {{ col_block * 8 }}u) * TILE_K + step;
335
- var matB{{ col_block }}: subgroup_matrix_right<f32, 8, 8> =
336
- subgroupMatrixLoad<subgroup_matrix_right<f32, 8, 8>>(
337
- &tile_B, matrix_b_offset{{ col_block }}, true, TILE_K
338
  );
339
  {% endfor %}
340
  {% for row_block in range(ROW_BLOCKS) %}
@@ -368,16 +382,16 @@ fn main(
368
  (b * HEADS + h) * params.qSeq * params.kvSeq
369
  + (m_base + base_A + {{ row_block * 8 }}u) * params.kvSeq
370
  + n_base + base_B + {{ col_block * 8 }}u;
371
- subgroupMatrixStore(
372
  &scores, output_offset{{ row_block }}{{ col_block }},
373
- matC{{ row_block }}{{ col_block }}, false, params.kvSeq
374
  );
375
  {% else %}
376
  let output_offset{{ row_block }}{{ col_block }} =
377
  {{ q_index("m_base + base_A + " ~ (row_block * 8) ~ "u", "n_base + base_B + " ~ (col_block * 8) ~ "u") }};
378
- subgroupMatrixStore(
379
  &output, output_offset{{ row_block }}{{ col_block }},
380
- matC{{ row_block }}{{ col_block }}, false, {{ OUT_ROW_STRIDE }}
381
  );
382
  {% endif %}
383
  {% endfor %}
@@ -404,11 +418,10 @@ fn main(
404
  var stat_d{{ row_block }} = 0.0;
405
  {% endif %}
406
  {% for col_block in range(COL_BLOCKS) %}
407
- subgroupMatrixStore(
408
- &tile_A,
409
  (subgroup * {{ COL_BLOCKS }}u + {{ col_block }}u) * 64u,
410
  matC{{ row_block }}{{ col_block }},
411
- false,
412
  8u
413
  );
414
  {% endfor %}
@@ -422,7 +435,7 @@ fn main(
422
  row < params.qSeq &&
423
  col < {% if scorePhase %}params.kvSeq{% else %}HEAD_DIM{% endif %}
424
  ) {
425
- let result = tile_A[
426
  (subgroup * {{ COL_BLOCKS }}u + {{ col_block }}u) * 64u
427
  + row_in_block * 8u + col_in_block + pair
428
  ];
@@ -446,7 +459,7 @@ fn main(
446
  {% if hasBias %}
447
  // V bias row base: skip the packed Q and K blocks, then index this head.
448
  {% endif %}
449
- output[{{ q_index("row", "col") }}] = result{% if hasBias %} + bias[2u * HIDDEN + h * HEAD_DIM + col]{% endif %};
450
  {% endif %}
451
  }
452
  }
 
1
+ {% set MT = "f16" if (operandF16 is defined and operandF16) else "f32" %}
2
+ {% if MT == "f16" %}
3
+ enable f16;
4
+ {% endif %}
5
  enable subgroups;
6
+ {% if pinSubgroupSize32 %}
7
+ enable subgroup_size_control;
8
+ {% endif %}
9
  enable chromium_experimental_subgroup_matrix;
10
  diagnostic(off, chromium.subgroup_matrix_uniformity);
11
 
 
13
 
14
  {% set layout = source.layout | default("bsh") %}
15
  {% set headMajor = layout == "bhsd" %}
16
+ {% set kvHeadMajor = (source.kvLayout | default(layout)) == "bhsd" %}
17
  {% macro q_index(row, d) %}(b * params.qSeq + {{ row }}) * HIDDEN + h * HEAD_DIM + {{ d }}{% endmacro %}
18
  {% macro kv_index(seq, d) %}(b * params.kvSeq + {{ seq }}) * KV_HIDDEN + h_kv * HEAD_DIM + {{ d }}{% endmacro %}
19
  {% set OUT_ROW_STRIDE = "HEAD_DIM" if headMajor else "HIDDEN" %}
20
+ {% set KV_ROW_STRIDE = "HEAD_DIM" if kvHeadMajor else "KV_HIDDEN" %}
21
  {% set scorePhase = source.phase == "score" %}
22
  {% set FUSED_SOFTMAX = source.fusedSoftmax is defined and source.fusedSoftmax %}
23
  {% set PRIVATE_ROW_STATS = FUSED_SOFTMAX and (materializedSgmatPrivateRowStats is defined and materializedSgmatPrivateRowStats) %}
 
46
  {% if hasBias is not defined %}{% set hasBias = false %}{% endif %}
47
  {% set EMIT_ROW_STATS = source.emitRowStats is defined and source.emitRowStats %}
48
  {% set DIRECT_SCORE_STORE = materializedSgmatDirectScoreStore and not EMIT_ROW_STATS %}
49
+ {% set DIRECT_APPLY_STORE = materializedSgmatDirectApplyStore and not hasBias and MT == "f32" %}
50
  {% set DIRECT_OUTPUT_STORE = DIRECT_SCORE_STORE if scorePhase else DIRECT_APPLY_STORE %}
51
  {% set RUNTIME_DIRECT_STORE = (not DIRECT_OUTPUT_STORE)
52
  and materializedSgmatRuntimeDirectStore
53
+ and (scorePhase or (not hasBias and MT == "f32"))
54
  and not EMIT_ROW_STATS %}
55
  {% set ANY_DIRECT_STORE = DIRECT_OUTPUT_STORE or RUNTIME_DIRECT_STORE %}
56
  {% set SCALE_IN_Q = DIRECT_SCORE_STORE or (RUNTIME_DIRECT_STORE and scorePhase) %}
 
88
  const WORKGROUP_THREADS: u32 = {{ WORKGROUP_THREADS }}u;
89
  {% endif %}
90
 
91
+ var<workgroup> tile_A: array<{{ MT }}, {{ TILE_M_VALUE * TILE_K_VALUE }}>;
92
+ var<workgroup> tile_B: array<{{ MT }}, {{ TILE_N_VALUE * TILE_K_VALUE }}>;
93
+ {% set SCRATCH = "tile_A" if MT == "f32" else "store_scratch" %}
94
+ {% if MT == "f16" %}
95
+ // The compact epilogue banks f32 result fragments; an f16 operand tile cannot
96
+ // alias them, so the f16 build carries a dedicated store scratch.
97
+ var<workgroup> store_scratch: array<f32, {{ TILE_M_VALUE * TILE_K_VALUE }}>;
98
+ {% endif %}
99
  {% if FUSED_SOFTMAX and not PRIVATE_ROW_STATS %}
100
  // The tile's rows own their softmax constants for the whole k loop, so they are
101
  // read once per workgroup rather than once per staged element.
 
134
 
135
  {% endif %}
136
 
137
+ @compute @workgroup_size({{ WORKGROUP_THREADS }}, 1, 1){{ " @subgroup_size(32)" if pinSubgroupSize32 else "" }}
138
  fn main(
139
  @builtin(workgroup_id) wg: vec3<u32>,
140
  @builtin(local_invocation_index) li: u32{% if not DIRECT_OUTPUT_STORE %},
 
191
  {% endif %}
192
  {% endif %}
193
  for (var k_base = 0u; k_base < inner; k_base += TILE_K) {
194
+ {% if source.phase == "apply" and not FUSED_SOFTMAX and MT == "f32" %}
195
  // Full interior PV tiles can be loaded directly from storage. Query,
196
  // reduction, and output-dimension tails use the guarded shared path below.
197
  if (
 
204
  let score_offset{{ row_block }} = (b * HEADS + h) * params.qSeq * params.kvSeq
205
  + (m_base + base_A + {{ row_block * 8 }}u) * params.kvSeq + k_base + step;
206
  var matA{{ row_block }}: subgroup_matrix_left<f32, 8, 8> =
207
+ subgroupMatrixLoad<subgroup_matrix_left<f32, 8, 8>, row_major>(
208
+ &scores, score_offset{{ row_block }}, params.kvSeq
209
  );
210
  {% endfor %}
211
  {% for col_block in range(COL_BLOCKS) %}
212
  let value_offset{{ col_block }} =
213
  {{ kv_index("k_base + step", "n_base + base_B + " ~ (col_block * 8) ~ "u") }};
214
  var matB{{ col_block }}: subgroup_matrix_right<f32, 8, 8> =
215
+ subgroupMatrixLoad<subgroup_matrix_right<f32, 8, 8>, row_major>(
216
+ &value, value_offset{{ col_block }}, {{ KV_ROW_STRIDE }}
217
  );
218
  {% endfor %}
219
  {% for row_block in range(ROW_BLOCKS) %}
 
241
  {% if scorePhase %}
242
  {% if headDim % 32 == 0 %}
243
  tile_A[a_row * TILE_K + a_col + i] = select(
244
+ {{ "0.0h" if MT == "f16" else "0.0" }},
245
  {{ q_tile_value(q_index("row", "k")) }},
246
  row < params.qSeq
247
  );
248
  {% else %}
249
+ var loaded = {{ "0.0h" if MT == "f16" else "0.0" }};
250
  if (row < params.qSeq && k < HEAD_DIM) {
251
  loaded = {{ q_tile_value(q_index("row", "k")) }};
252
  }
 
255
  {% else %}
256
  let score_base = (b * HEADS + h) * params.qSeq * params.kvSeq;
257
  tile_A[a_row * TILE_K + a_col + i] =
258
+ {{ "f16(" if MT == "f16" else "" }}{{ score_value("score_base + row * params.kvSeq + k", ["a_row", "row < params.qSeq && k < params.kvSeq"]) }}{{ ")" if MT == "f16" else "" }};
259
  {% endif %}
260
  }
261
 
 
267
  {% if scorePhase %}
268
  {% if headDim % 32 == 0 %}
269
  tile_B[b_row * TILE_K + b_col + i] = select(
270
+ {{ "0.0h" if MT == "f16" else "0.0" }},
271
  key[{{ kv_index("col", "k") }}],
272
  col < params.kvSeq
273
  );
274
  {% else %}
275
+ var loaded = {{ "0.0h" if MT == "f16" else "0.0" }};
276
  if (col < params.kvSeq && k < HEAD_DIM) {
277
  loaded = key[{{ kv_index("col", "k") }}];
278
  }
 
280
  {% endif %}
281
  {% else %}
282
  tile_B[b_row * TILE_K + b_col + i] = select(
283
+ {{ "0.0h" if MT == "f16" else "0.0" }},
284
  value[{{ kv_index("k", "col") }}],
285
  k < params.kvSeq && col < HEAD_DIM
286
  );
 
295
  let row = m_base + tile_row;
296
  let k = k_base + tile_k;
297
  {% if scorePhase %}
298
+ var loaded = {{ "0.0h" if MT == "f16" else "0.0" }};
299
  if (row < params.qSeq && k < HEAD_DIM) {
300
  loaded = {{ q_tile_value(q_index("row", "k")) }};
301
  }
302
  {% elif FUSED_SOFTMAX %}
303
  let score_base = (b * HEADS + h) * params.qSeq * params.kvSeq;
304
  let loaded =
305
+ {{ "f16(" if MT == "f16" else "" }}{{ score_value("score_base + row * params.kvSeq + k", ["tile_row", "row < params.qSeq && k < params.kvSeq"]) }}{{ ")" if MT == "f16" else "" }};
306
  {% else %}
307
+ var loaded = {{ "0.0h" if MT == "f16" else "0.0" }};
308
  if (row < params.qSeq && k < params.kvSeq) {
309
  let score_base = (b * HEADS + h) * params.qSeq * params.kvSeq;
310
+ loaded = {{ "f16(" if MT == "f16" else "" }}scores[score_base + row * params.kvSeq + k]{{ ")" if MT == "f16" else "" }};
311
  }
312
  {% endif %}
313
  tile_A[idx] = loaded;
 
319
  let col = n_base + tile_col;
320
  let k = k_base + tile_k;
321
  {% if scorePhase %}
322
+ var loaded = {{ "0.0h" if MT == "f16" else "0.0" }};
323
  if (col < params.kvSeq && k < HEAD_DIM) {
324
  loaded = key[{{ kv_index("col", "k") }}];
325
  }
326
  {% else %}
327
+ var loaded = {{ "0.0h" if MT == "f16" else "0.0" }};
328
  if (k < params.kvSeq && col < HEAD_DIM) {
329
  loaded = value[{{ kv_index("k", "col") }}];
330
  }
 
338
  {% for row_block in range(ROW_BLOCKS) %}
339
  let matrix_a_offset{{ row_block }} =
340
  (subtile_idy * SUB_ROWS + {{ row_block * 8 }}u) * TILE_K + step;
341
+ var matA{{ row_block }}: subgroup_matrix_left<{{ MT }}, 8, 8> =
342
+ subgroupMatrixLoad<subgroup_matrix_left<{{ MT }}, 8, 8>, row_major>(
343
+ &tile_A, matrix_a_offset{{ row_block }}, TILE_K
344
  );
345
  {% endfor %}
346
  {% for col_block in range(COL_BLOCKS) %}
347
  let matrix_b_offset{{ col_block }} =
348
  (subtile_idx * SUB_COLS + {{ col_block * 8 }}u) * TILE_K + step;
349
+ var matB{{ col_block }}: subgroup_matrix_right<{{ MT }}, 8, 8> =
350
+ subgroupMatrixLoad<subgroup_matrix_right<{{ MT }}, 8, 8>, col_major>(
351
+ &tile_B, matrix_b_offset{{ col_block }}, TILE_K
352
  );
353
  {% endfor %}
354
  {% for row_block in range(ROW_BLOCKS) %}
 
382
  (b * HEADS + h) * params.qSeq * params.kvSeq
383
  + (m_base + base_A + {{ row_block * 8 }}u) * params.kvSeq
384
  + n_base + base_B + {{ col_block * 8 }}u;
385
+ subgroupMatrixStore<row_major>(
386
  &scores, output_offset{{ row_block }}{{ col_block }},
387
+ matC{{ row_block }}{{ col_block }}, params.kvSeq
388
  );
389
  {% else %}
390
  let output_offset{{ row_block }}{{ col_block }} =
391
  {{ q_index("m_base + base_A + " ~ (row_block * 8) ~ "u", "n_base + base_B + " ~ (col_block * 8) ~ "u") }};
392
+ subgroupMatrixStore<row_major>(
393
  &output, output_offset{{ row_block }}{{ col_block }},
394
+ matC{{ row_block }}{{ col_block }}, {{ OUT_ROW_STRIDE }}
395
  );
396
  {% endif %}
397
  {% endfor %}
 
418
  var stat_d{{ row_block }} = 0.0;
419
  {% endif %}
420
  {% for col_block in range(COL_BLOCKS) %}
421
+ subgroupMatrixStore<row_major>(
422
+ &{{ SCRATCH }},
423
  (subgroup * {{ COL_BLOCKS }}u + {{ col_block }}u) * 64u,
424
  matC{{ row_block }}{{ col_block }},
 
425
  8u
426
  );
427
  {% endfor %}
 
435
  row < params.qSeq &&
436
  col < {% if scorePhase %}params.kvSeq{% else %}HEAD_DIM{% endif %}
437
  ) {
438
+ let result = {{ SCRATCH }}[
439
  (subgroup * {{ COL_BLOCKS }}u + {{ col_block }}u) * 64u
440
  + row_in_block * 8u + col_in_block + pair
441
  ];
 
459
  {% if hasBias %}
460
  // V bias row base: skip the packed Q and K blocks, then index this head.
461
  {% endif %}
462
+ output[{{ q_index("row", "col") }}] = {{ "f16(" if MT == "f16" else "" }}result{{ ")" if MT == "f16" else "" }}{% if hasBias %} + bias[2u * HIDDEN + h * HEAD_DIM + col]{% endif %};
463
  {% endif %}
464
  }
465
  }
build/webgpu/manifest.json CHANGED
@@ -103,6 +103,9 @@
103
  "wave32Adapter": "has(device.adapterInfo, \"subgroupMinSize\") and has(device.adapterInfo, \"subgroupMaxSize\") and device.adapterInfo.subgroupMinSize == 32 and device.adapterInfo.subgroupMaxSize == 32",
104
  "subgroupsWave32": "device.features.has(\"subgroups\") and wave32Adapter",
105
  "narrowSubgroupRange": "has(device.adapterInfo, \"subgroupMinSize\") and has(device.adapterInfo, \"subgroupMaxSize\") and device.adapterInfo.subgroupMinSize < device.adapterInfo.subgroupMaxSize and device.adapterInfo.subgroupMaxSize <= 16",
 
 
 
106
  "headDim": "dim(shapes.query, 2) / attrs.num_heads if (ranks.query == 3 and attrs.num_heads > 0) else 0",
107
  "qkvDtypesOk": "tensorDtypes.key == tensorDtypes.query and tensorDtypes.value == tensorDtypes.query and tensorDtypes.output == tensorDtypes.query",
108
  "floatDtypeOk": "(tensorDtypes.query == \"float32\" or tensorDtypes.query == \"float16\") and f16Ok(tensorDtypes.query)",
@@ -171,7 +174,7 @@
171
  "materializedSgmatDirectScoreStore": "dim(shapes.query, 1) % materializedSgmatQueryTile == 0 and dim(shapes.key, 1) % materializedSgmatKeyTile == 0",
172
  "materializedSgmatDirectApplyStore": "dim(shapes.query, 1) % materializedSgmatQueryTile == 0 and headDim % materializedSgmatKeyTile == 0",
173
  "materializedSgmatRuntimeDirectStore": "dim(shapes.query, 1) >= 2 * materializedSgmatQueryTile and dim(shapes.key, 1) >= 2 * materializedSgmatKeyTile",
174
- "materializedSgmatCoreOk": "qkvContractOk and tensorDtypes.query == \"float32\" and attrs.unidirectional == 0 and headDim >= 32 and headDim <= 256 and dim(shapes.query, 1) >= 512 and dim(shapes.key, 1) >= 512 and subgroupsWave32 and device.features.has(\"chromium-experimental-subgroup-matrix\") and materializedSgmatBuffersFit and materializedSgmatResourcesFit and materializedSgmatDispatchFits",
175
  "materializedCachedSoftmaxWg": "min(tunables.MATERIALIZED_CACHED_SOFTMAX_WORKGROUP_SIZE, device.limits.maxComputeInvocationsPerWorkgroup, device.limits.maxComputeWorkgroupSizeX)",
176
  "materializedCachedSoftmaxVecsPerLane": "ceilDiv(ceilDiv(dim(shapes.key, 1), 4), max(1, materializedCachedSoftmaxWg))",
177
  "materializedCachedSoftmaxStorageBytes": "materializedCachedSoftmaxWg * 8 + 8",
@@ -196,7 +199,9 @@
196
  "smallSeqWorkgroupSize": "max(32, pow2ceil(dim(shapes.query, 1)))",
197
  "smallSeqSharedBytes": "dim(shapes.key, 1) * headDim * 8",
198
  "smallSeqResourcesFit": "smallSeqPrivateFloats <= tunables.SMALL_SEQ_MAX_PRIVATE_FLOATS and smallSeqWorkgroupSize <= device.limits.maxComputeInvocationsPerWorkgroup and smallSeqWorkgroupSize <= device.limits.maxComputeWorkgroupSizeX and smallSeqSharedBytes <= device.limits.maxComputeWorkgroupStorageSize",
199
- "smallSeqDispatchFits": "attrs.num_heads <= device.limits.maxComputeWorkgroupsPerDimension and dim(shapes.query, 0) <= device.limits.maxComputeWorkgroupsPerDimension"
 
 
200
  },
201
  "constants": {
202
  "attentionScaleExpression": "\"select(inverseSqrt(f32(HEAD_DIM)), params.scale, params.scale != 0.0)\""
@@ -1193,6 +1198,82 @@
1193
  ]
1194
  }
1195
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1196
  ]
1197
  },
1198
  "variants": [
@@ -1343,7 +1424,6 @@
1343
  "priority": 52,
1344
  "requires": {
1345
  "features": ["subgroups", "chromium-experimental-subgroup-matrix"],
1346
- "subgroupMinSize": 32,
1347
  "subgroupMatrixConfigs": [{ "componentType": "f32", "resultComponentType": "f32", "M": 8, "N": 8, "K": 8 }]
1348
  },
1349
  "when": ["materializedSgmatFusedOk", "not present.biasT", "materializedFusedSoftmaxWorthIt"],
@@ -1416,7 +1496,6 @@
1416
  "priority": 52,
1417
  "requires": {
1418
  "features": ["subgroups", "chromium-experimental-subgroup-matrix"],
1419
- "subgroupMinSize": 32,
1420
  "subgroupMatrixConfigs": [{ "componentType": "f32", "resultComponentType": "f32", "M": 8, "N": 8, "K": 8 }]
1421
  },
1422
  "when": ["materializedSgmatFusedOk", "biasOk", "materializedFusedSoftmaxWorthIt"],
@@ -1483,6 +1562,81 @@
1483
  }
1484
  ]
1485
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1486
  {
1487
  "id": "qkv_no_bias_flash_cluster_lpq4_nosg",
1488
  "description": "Keeps query/key tiles and online-softmax state inside one workgroup instead of materializing all scores. The family supplies subgroup and portable forms for compatible prefill shapes.",
@@ -1626,7 +1780,7 @@
1626
  "headDim": "dim(shapes.query, 2) / attrs.num_heads",
1627
  "headDimV4": "(dim(shapes.query, 2) / attrs.num_heads) / 4",
1628
  "TILE_Q": 16,
1629
- "TILE_K": "32 if (tensorDtypes.query != \"float16\" and (dim(shapes.query, 2) / attrs.num_heads) <= 128) else 8",
1630
  "LPQ": 4
1631
  },
1632
  "passes": [
@@ -1664,7 +1818,7 @@
1664
  "headDim": "dim(shapes.query, 2) / attrs.num_heads",
1665
  "headDimV4": "(dim(shapes.query, 2) / attrs.num_heads) / 4",
1666
  "TILE_Q": 16,
1667
- "TILE_K": "32 if (tensorDtypes.query != \"float16\" and (dim(shapes.query, 2) / attrs.num_heads) <= 128) else 8",
1668
  "LPQ": 8
1669
  },
1670
  "passes": [
@@ -1702,7 +1856,7 @@
1702
  "headDim": "dim(shapes.query, 2) / attrs.num_heads",
1703
  "headDimV4": "(dim(shapes.query, 2) / attrs.num_heads) / 4",
1704
  "TILE_Q": 16,
1705
- "TILE_K": "32 if (tensorDtypes.query != \"float16\" and (dim(shapes.query, 2) / attrs.num_heads) <= 128) else 8",
1706
  "qHidden": "dim(shapes.query, 2)",
1707
  "inputScalar": "\"f16\" if tensorDtypes.query == \"float16\" else \"f32\"",
1708
  "LPQ": 8
@@ -1745,7 +1899,7 @@
1745
  "headDim": "dim(shapes.query, 2) / attrs.num_heads",
1746
  "headDimV4": "(dim(shapes.query, 2) / attrs.num_heads) / 4",
1747
  "TILE_Q": 16,
1748
- "TILE_K": "32 if (tensorDtypes.query != \"float16\" and (dim(shapes.query, 2) / attrs.num_heads) <= 128) else 8",
1749
  "LPQ": 8
1750
  },
1751
  "passes": [
@@ -1786,7 +1940,7 @@
1786
  "headDim": "dim(shapes.query, 2) / attrs.num_heads",
1787
  "headDimV4": "(dim(shapes.query, 2) / attrs.num_heads) / 4",
1788
  "TILE_Q": 16,
1789
- "TILE_K": "32 if (tensorDtypes.query != \"float16\" and (dim(shapes.query, 2) / attrs.num_heads) <= 128) else 8",
1790
  "qHidden": "dim(shapes.query, 2)",
1791
  "inputScalar": "\"f16\" if tensorDtypes.query == \"float16\" else \"f32\"",
1792
  "LPQ": 8
@@ -2792,7 +2946,6 @@
2792
  "priority": 52,
2793
  "requires": {
2794
  "features": ["subgroups", "chromium-experimental-subgroup-matrix"],
2795
- "subgroupMinSize": 32,
2796
  "subgroupMatrixConfigs": [{ "componentType": "f32", "resultComponentType": "f32", "M": 8, "N": 8, "K": 8 }]
2797
  },
2798
  "when": ["materializedSgmatOk", "not present.biasT", "not materializedFusedSoftmaxWorthIt"],
@@ -2861,7 +3014,6 @@
2861
  "priority": 52,
2862
  "requires": {
2863
  "features": ["subgroups", "chromium-experimental-subgroup-matrix"],
2864
- "subgroupMinSize": 32,
2865
  "subgroupMatrixConfigs": [{ "componentType": "f32", "resultComponentType": "f32", "M": 8, "N": 8, "K": 8 }]
2866
  },
2867
  "when": ["materializedSgmatOk", "biasOk", "not materializedFusedSoftmaxWorthIt"],
 
103
  "wave32Adapter": "has(device.adapterInfo, \"subgroupMinSize\") and has(device.adapterInfo, \"subgroupMaxSize\") and device.adapterInfo.subgroupMinSize == 32 and device.adapterInfo.subgroupMaxSize == 32",
104
  "subgroupsWave32": "device.features.has(\"subgroups\") and wave32Adapter",
105
  "narrowSubgroupRange": "has(device.adapterInfo, \"subgroupMinSize\") and has(device.adapterInfo, \"subgroupMaxSize\") and device.adapterInfo.subgroupMinSize < device.adapterInfo.subgroupMaxSize and device.adapterInfo.subgroupMaxSize <= 16",
106
+ "canPinSubgroupSize32": "device.features.has(\"subgroups\") and device.features.has(\"subgroup-size-control\") and has(device.adapterInfo, \"subgroupMinSize\") and has(device.adapterInfo, \"subgroupMaxSize\") and device.adapterInfo.subgroupMinSize <= 32 and device.adapterInfo.subgroupMaxSize >= 32",
107
+ "pinSubgroupSize32": "canPinSubgroupSize32 and not wave32Adapter",
108
+ "wave32Effective": "wave32Adapter or pinSubgroupSize32",
109
  "headDim": "dim(shapes.query, 2) / attrs.num_heads if (ranks.query == 3 and attrs.num_heads > 0) else 0",
110
  "qkvDtypesOk": "tensorDtypes.key == tensorDtypes.query and tensorDtypes.value == tensorDtypes.query and tensorDtypes.output == tensorDtypes.query",
111
  "floatDtypeOk": "(tensorDtypes.query == \"float32\" or tensorDtypes.query == \"float16\") and f16Ok(tensorDtypes.query)",
 
174
  "materializedSgmatDirectScoreStore": "dim(shapes.query, 1) % materializedSgmatQueryTile == 0 and dim(shapes.key, 1) % materializedSgmatKeyTile == 0",
175
  "materializedSgmatDirectApplyStore": "dim(shapes.query, 1) % materializedSgmatQueryTile == 0 and headDim % materializedSgmatKeyTile == 0",
176
  "materializedSgmatRuntimeDirectStore": "dim(shapes.query, 1) >= 2 * materializedSgmatQueryTile and dim(shapes.key, 1) >= 2 * materializedSgmatKeyTile",
177
+ "materializedSgmatCoreOk": "qkvContractOk and tensorDtypes.query == \"float32\" and attrs.unidirectional == 0 and headDim >= 32 and headDim <= 256 and dim(shapes.query, 1) >= 512 and dim(shapes.key, 1) >= 512 and device.features.has(\"subgroups\") and wave32Effective and device.features.has(\"chromium-experimental-subgroup-matrix\") and materializedSgmatBuffersFit and materializedSgmatResourcesFit and materializedSgmatDispatchFits",
178
  "materializedCachedSoftmaxWg": "min(tunables.MATERIALIZED_CACHED_SOFTMAX_WORKGROUP_SIZE, device.limits.maxComputeInvocationsPerWorkgroup, device.limits.maxComputeWorkgroupSizeX)",
179
  "materializedCachedSoftmaxVecsPerLane": "ceilDiv(ceilDiv(dim(shapes.key, 1), 4), max(1, materializedCachedSoftmaxWg))",
180
  "materializedCachedSoftmaxStorageBytes": "materializedCachedSoftmaxWg * 8 + 8",
 
199
  "smallSeqWorkgroupSize": "max(32, pow2ceil(dim(shapes.query, 1)))",
200
  "smallSeqSharedBytes": "dim(shapes.key, 1) * headDim * 8",
201
  "smallSeqResourcesFit": "smallSeqPrivateFloats <= tunables.SMALL_SEQ_MAX_PRIVATE_FLOATS and smallSeqWorkgroupSize <= device.limits.maxComputeInvocationsPerWorkgroup and smallSeqWorkgroupSize <= device.limits.maxComputeWorkgroupSizeX and smallSeqSharedBytes <= device.limits.maxComputeWorkgroupStorageSize",
202
+ "smallSeqDispatchFits": "attrs.num_heads <= device.limits.maxComputeWorkgroupsPerDimension and dim(shapes.query, 0) <= device.limits.maxComputeWorkgroupsPerDimension",
203
+ "materializedSgmatCoreF16Ok": "qkvContractOk and tensorDtypes.query == \"float16\" and tensorDtypes.key == \"float16\" and tensorDtypes.value == \"float16\" and device.features.has(\"shader-f16\") and attrs.unidirectional == 0 and headDim >= 32 and headDim <= 256 and dim(shapes.query, 1) >= 512 and dim(shapes.key, 1) >= 512 and device.features.has(\"subgroups\") and wave32Effective and device.features.has(\"chromium-experimental-subgroup-matrix\") and materializedSgmatBuffersFit and materializedSgmatResourcesFit and materializedSgmatDispatchFits",
204
+ "materializedSgmatFusedF16Ok": "materializedSgmatCoreF16Ok"
205
  },
206
  "constants": {
207
  "attentionScaleExpression": "\"select(inverseSqrt(f32(HEAD_DIM)), params.scale, params.scale != 0.0)\""
 
1198
  ]
1199
  }
1200
  }
1201
+ ],
1202
+ "materializedScoreStatsDyn": [
1203
+ {
1204
+ "name": "query",
1205
+ "arg": "queryT",
1206
+ "semantic": "query",
1207
+ "buffer": { "type": "read-only-storage" },
1208
+ "elementType": "$inputElement"
1209
+ },
1210
+ {
1211
+ "name": "key",
1212
+ "arg": "keyT",
1213
+ "semantic": "key",
1214
+ "buffer": { "type": "read-only-storage" },
1215
+ "elementType": "$inputElement"
1216
+ },
1217
+ { "name": "scores", "semantic": "materializedScores", "buffer": { "type": "storage" }, "elementType": "f32" },
1218
+ {
1219
+ "name": "scorePartials",
1220
+ "semantic": "materializedScorePartials",
1221
+ "buffer": { "type": "storage" },
1222
+ "elementType": "f32"
1223
+ },
1224
+ {
1225
+ "name": "params",
1226
+ "semantic": "kernel.params",
1227
+ "buffer": { "type": "uniform" },
1228
+ "struct": {
1229
+ "name": "Params",
1230
+ "fields": [
1231
+ { "name": "qSeq", "type": "u32", "value": "dim(shapes.query, 1)" },
1232
+ { "name": "kvSeq", "type": "u32", "value": "dim(shapes.key, 1)" },
1233
+ { "name": "scale", "type": "f32", "value": "attrs.scale if has(attrs, \"scale\") else 0" }
1234
+ ]
1235
+ }
1236
+ }
1237
+ ],
1238
+ "materializedApplyFusedDyn": [
1239
+ {
1240
+ "name": "scores",
1241
+ "semantic": "materializedScores",
1242
+ "buffer": { "type": "read-only-storage" },
1243
+ "elementType": "f32"
1244
+ },
1245
+ {
1246
+ "name": "value",
1247
+ "arg": "valueT",
1248
+ "semantic": "value",
1249
+ "buffer": { "type": "read-only-storage" },
1250
+ "elementType": "$inputElement"
1251
+ },
1252
+ {
1253
+ "name": "output",
1254
+ "arg": "outputT",
1255
+ "semantic": "output",
1256
+ "buffer": { "type": "storage" },
1257
+ "elementType": "$outputElement"
1258
+ },
1259
+ {
1260
+ "name": "rowStats",
1261
+ "semantic": "materializedRowStats",
1262
+ "buffer": { "type": "read-only-storage" },
1263
+ "elementType": "f32"
1264
+ },
1265
+ {
1266
+ "name": "params",
1267
+ "semantic": "kernel.params",
1268
+ "buffer": { "type": "uniform" },
1269
+ "struct": {
1270
+ "name": "Params",
1271
+ "fields": [
1272
+ { "name": "qSeq", "type": "u32", "value": "dim(shapes.query, 1)" },
1273
+ { "name": "kvSeq", "type": "u32", "value": "dim(shapes.key, 1)" }
1274
+ ]
1275
+ }
1276
+ }
1277
  ]
1278
  },
1279
  "variants": [
 
1424
  "priority": 52,
1425
  "requires": {
1426
  "features": ["subgroups", "chromium-experimental-subgroup-matrix"],
 
1427
  "subgroupMatrixConfigs": [{ "componentType": "f32", "resultComponentType": "f32", "M": 8, "N": 8, "K": 8 }]
1428
  },
1429
  "when": ["materializedSgmatFusedOk", "not present.biasT", "materializedFusedSoftmaxWorthIt"],
 
1496
  "priority": 52,
1497
  "requires": {
1498
  "features": ["subgroups", "chromium-experimental-subgroup-matrix"],
 
1499
  "subgroupMatrixConfigs": [{ "componentType": "f32", "resultComponentType": "f32", "M": 8, "N": 8, "K": 8 }]
1500
  },
1501
  "when": ["materializedSgmatFusedOk", "biasOk", "materializedFusedSoftmaxWorthIt"],
 
1562
  }
1563
  ]
1564
  },
1565
+ {
1566
+ "id": "qkv_no_bias_materialized_sgmat_fused_f16",
1567
+ "description": "Materializes FP32 scores for subgroup-matrix score and value products, and folds the softmax into the apply pass using row statistics the score pass emits. Selected over the separate-softmax route when the score matrix is large enough that reading it back costs more than the apply-side staging fusing forces.",
1568
+ "priority": 52,
1569
+ "requires": {
1570
+ "features": ["subgroups", "chromium-experimental-subgroup-matrix"],
1571
+ "subgroupMatrixConfigs": [{ "componentType": "f16", "M": 8, "N": 8, "K": 8 }]
1572
+ },
1573
+ "when": ["materializedSgmatFusedF16Ok", "not present.biasT", "materializedFusedSoftmaxWorthIt"],
1574
+ "constants": {
1575
+ "qNumHeads": "attrs.num_heads",
1576
+ "headDim": "headDim",
1577
+ "qHidden": "dim(shapes.query, 2)",
1578
+ "materializedSgmatQueryTile": "materializedSgmatQueryTile",
1579
+ "materializedSgmatKeyTile": "materializedSgmatKeyTile",
1580
+ "materializedSgmatInnerTile": "materializedSgmatInnerTile",
1581
+ "hasBias": false,
1582
+ "useSubgroups": true,
1583
+ "materializedRowStatsWg": "materializedRowStatsWg",
1584
+ "statSlots": "materializedSgmatStatSlots",
1585
+ "statQuerySeq": "dim(shapes.query, 1)",
1586
+ "operandF16": true,
1587
+ "inputElement": "\"f16\"",
1588
+ "outputElement": "\"f16\""
1589
+ },
1590
+ "intermediates": [
1591
+ {
1592
+ "id": "materializedScores",
1593
+ "dtype": "float32",
1594
+ "shape": "[dim(shapes.query, 0) * attrs.num_heads * dim(shapes.query, 1) * dim(shapes.key, 1)]"
1595
+ },
1596
+ { "id": "materializedRowStats", "dtype": "float32", "shape": "[materializedRowStatsElements]" },
1597
+ { "id": "materializedScorePartials", "dtype": "float32", "shape": "[materializedScorePartialElements]" }
1598
+ ],
1599
+ "passes": [
1600
+ {
1601
+ "id": "scores",
1602
+ "name": "MultiHeadAttention.MaterializedScoresSgmatF16",
1603
+ "source": {
1604
+ "shader": "attn-materialized-sgmat-f32.wgsl.jinja",
1605
+ "inputs": { "phase": "\"score\"", "emitRowStats": true }
1606
+ },
1607
+ "bindings": "materializedScoreStatsDyn",
1608
+ "dispatch": {
1609
+ "x": "ceilDiv(dim(shapes.key, 1), materializedSgmatKeyTile)",
1610
+ "y": "ceilDiv(dim(shapes.query, 1), materializedSgmatQueryTile)",
1611
+ "z": "dim(shapes.query, 0) * attrs.num_heads"
1612
+ }
1613
+ },
1614
+ {
1615
+ "id": "rowstats",
1616
+ "name": "MultiHeadAttention.MaterializedRowStatsCombine",
1617
+ "source": { "shader": "attn-materialized-rowstats-combine-f32.wgsl.jinja" },
1618
+ "bindings": "materializedRowStatsCombine",
1619
+ "dispatch": {
1620
+ "gridStride": "dim(shapes.query, 0) * attrs.num_heads * dim(shapes.query, 1)",
1621
+ "workgroupSize": "materializedRowStatsWg"
1622
+ }
1623
+ },
1624
+ {
1625
+ "id": "apply",
1626
+ "name": "MultiHeadAttention.MaterializedApplySgmatF16",
1627
+ "source": {
1628
+ "shader": "attn-materialized-sgmat-f32.wgsl.jinja",
1629
+ "inputs": { "phase": "\"apply\"", "fusedSoftmax": true }
1630
+ },
1631
+ "bindings": "materializedApplyFusedDyn",
1632
+ "dispatch": {
1633
+ "x": "ceilDiv(headDim, materializedSgmatKeyTile)",
1634
+ "y": "ceilDiv(dim(shapes.query, 1), materializedSgmatQueryTile)",
1635
+ "z": "dim(shapes.query, 0) * attrs.num_heads"
1636
+ }
1637
+ }
1638
+ ]
1639
+ },
1640
  {
1641
  "id": "qkv_no_bias_flash_cluster_lpq4_nosg",
1642
  "description": "Keeps query/key tiles and online-softmax state inside one workgroup instead of materializing all scores. The family supplies subgroup and portable forms for compatible prefill shapes.",
 
1780
  "headDim": "dim(shapes.query, 2) / attrs.num_heads",
1781
  "headDimV4": "(dim(shapes.query, 2) / attrs.num_heads) / 4",
1782
  "TILE_Q": 16,
1783
+ "TILE_K": "32 if (tensorDtypes.query != \"float16\" and (dim(shapes.query, 2) / attrs.num_heads) <= 64) else 8",
1784
  "LPQ": 4
1785
  },
1786
  "passes": [
 
1818
  "headDim": "dim(shapes.query, 2) / attrs.num_heads",
1819
  "headDimV4": "(dim(shapes.query, 2) / attrs.num_heads) / 4",
1820
  "TILE_Q": 16,
1821
+ "TILE_K": "32 if (tensorDtypes.query != \"float16\" and (dim(shapes.query, 2) / attrs.num_heads) <= 64) else 8",
1822
  "LPQ": 8
1823
  },
1824
  "passes": [
 
1856
  "headDim": "dim(shapes.query, 2) / attrs.num_heads",
1857
  "headDimV4": "(dim(shapes.query, 2) / attrs.num_heads) / 4",
1858
  "TILE_Q": 16,
1859
+ "TILE_K": "32 if (tensorDtypes.query != \"float16\" and (dim(shapes.query, 2) / attrs.num_heads) <= 64) else 8",
1860
  "qHidden": "dim(shapes.query, 2)",
1861
  "inputScalar": "\"f16\" if tensorDtypes.query == \"float16\" else \"f32\"",
1862
  "LPQ": 8
 
1899
  "headDim": "dim(shapes.query, 2) / attrs.num_heads",
1900
  "headDimV4": "(dim(shapes.query, 2) / attrs.num_heads) / 4",
1901
  "TILE_Q": 16,
1902
+ "TILE_K": "32 if (tensorDtypes.query != \"float16\" and (dim(shapes.query, 2) / attrs.num_heads) <= 64) else 8",
1903
  "LPQ": 8
1904
  },
1905
  "passes": [
 
1940
  "headDim": "dim(shapes.query, 2) / attrs.num_heads",
1941
  "headDimV4": "(dim(shapes.query, 2) / attrs.num_heads) / 4",
1942
  "TILE_Q": 16,
1943
+ "TILE_K": "32 if (tensorDtypes.query != \"float16\" and (dim(shapes.query, 2) / attrs.num_heads) <= 64) else 8",
1944
  "qHidden": "dim(shapes.query, 2)",
1945
  "inputScalar": "\"f16\" if tensorDtypes.query == \"float16\" else \"f32\"",
1946
  "LPQ": 8
 
2946
  "priority": 52,
2947
  "requires": {
2948
  "features": ["subgroups", "chromium-experimental-subgroup-matrix"],
 
2949
  "subgroupMatrixConfigs": [{ "componentType": "f32", "resultComponentType": "f32", "M": 8, "N": 8, "K": 8 }]
2950
  },
2951
  "when": ["materializedSgmatOk", "not present.biasT", "not materializedFusedSoftmaxWorthIt"],
 
3014
  "priority": 52,
3015
  "requires": {
3016
  "features": ["subgroups", "chromium-experimental-subgroup-matrix"],
 
3017
  "subgroupMatrixConfigs": [{ "componentType": "f32", "resultComponentType": "f32", "M": 8, "N": 8, "K": 8 }]
3018
  },
3019
  "when": ["materializedSgmatOk", "biasOk", "not materializedFusedSoftmaxWorthIt"],
build/webgpu/metadata.json CHANGED
@@ -1,6 +1,6 @@
1
  {
2
  "name": "com.microsoft.MultiHeadAttention",
3
- "id": "_com_microsoft_multiheadattention_webgpu_9e2cafc",
4
  "version": 1,
5
  "license": "Apache-2.0",
6
  "backend": { "type": "webgpu" },
@@ -16,16 +16,16 @@
16
  "attn-materialized-apply-f32.wgsl.jinja": "qbrsh9S14QYuAM+xF49dgcooO6f19yNg9lC8eoBeWFQ=",
17
  "attn-materialized-rowstats-combine-f32.wgsl.jinja": "rdpdnh6y3m0tBwFMVCc903j2cvGsnGTcT8EAWRr5TRk=",
18
  "attn-materialized-score-f32.wgsl.jinja": "EAhU/wHSgmJZ/YHCnwG/takpgfMAtGqKXo3Cf8TfvxU=",
19
- "attn-materialized-sgmat-f32.wgsl.jinja": "EpEZp/6PjkWmTzB2njeciGho5JQbCMadXVxBhUqfVyE=",
20
  "attn-materialized-softmax-f32.wgsl.jinja": "7z288syeGhUUyBpaZzVVqdtFVMQALuN88sNfytvClWw=",
21
  "attn-online-scalar.wgsl.jinja": "CW0ru5yg8YFpIv31XnnD5IuKBXppsKc+2BU7u2uAQv4=",
22
  "attn-small-head-parallel.wgsl.jinja": "ejotx9NjdqElO3EIQWj8E3hJk2iW8Exq1vUjxK4FUk0=",
23
  "bench.json": "VV4kVBFJ112r3U2RGQlQd1Z5eAzgm+y30+oGG6TpK3g=",
24
- "manifest.json": "KlaFGLtELDJFyP/xCymid5FGW+u1XJRXukAwblz62b4=",
25
  "mha-small-seq.wgsl.jinja": "F825nrB6RLu55ziDnDI7BTJ3JSdT1GwsuMYmtrHQSic=",
26
- "test.json": "8uMdVLj7AWq1cMy7/Iyl35jJPDjE5z/VztKieOgg20U="
27
  }
28
  },
29
- "provenance": { "kernel": { "sha": "2e7068faf55e7f43df740015f6d1ee49391a41c5", "dirty": false } },
30
  "webgpu": { "manifestSpec": "1.0", "specialized": true, "opPath": "ops/com.microsoft.MultiHeadAttention" }
31
  }
 
1
  {
2
  "name": "com.microsoft.MultiHeadAttention",
3
+ "id": "_com_microsoft_multiheadattention_webgpu_03bfaa6",
4
  "version": 1,
5
  "license": "Apache-2.0",
6
  "backend": { "type": "webgpu" },
 
16
  "attn-materialized-apply-f32.wgsl.jinja": "qbrsh9S14QYuAM+xF49dgcooO6f19yNg9lC8eoBeWFQ=",
17
  "attn-materialized-rowstats-combine-f32.wgsl.jinja": "rdpdnh6y3m0tBwFMVCc903j2cvGsnGTcT8EAWRr5TRk=",
18
  "attn-materialized-score-f32.wgsl.jinja": "EAhU/wHSgmJZ/YHCnwG/takpgfMAtGqKXo3Cf8TfvxU=",
19
+ "attn-materialized-sgmat-f32.wgsl.jinja": "7fJFDK+kWEhN4rqFs3fE9uPkZdHh5C9xl7PXe9vZze4=",
20
  "attn-materialized-softmax-f32.wgsl.jinja": "7z288syeGhUUyBpaZzVVqdtFVMQALuN88sNfytvClWw=",
21
  "attn-online-scalar.wgsl.jinja": "CW0ru5yg8YFpIv31XnnD5IuKBXppsKc+2BU7u2uAQv4=",
22
  "attn-small-head-parallel.wgsl.jinja": "ejotx9NjdqElO3EIQWj8E3hJk2iW8Exq1vUjxK4FUk0=",
23
  "bench.json": "VV4kVBFJ112r3U2RGQlQd1Z5eAzgm+y30+oGG6TpK3g=",
24
+ "manifest.json": "kaCZGh3/PB8EmCgBc5QCc0aVGSrjf55xB6OM4s0yhgg=",
25
  "mha-small-seq.wgsl.jinja": "F825nrB6RLu55ziDnDI7BTJ3JSdT1GwsuMYmtrHQSic=",
26
+ "test.json": "6PCZYazz1hXA9+nbVJmd0/btWsIkkp29/xYqWZ4OSrs="
27
  }
28
  },
29
+ "provenance": { "kernel": { "sha": "c928d21e6cc1310861cba3bafb75f5f679ecf5f3", "dirty": false } },
30
  "webgpu": { "manifestSpec": "1.0", "specialized": true, "opPath": "ops/com.microsoft.MultiHeadAttention" }
31
  }
build/webgpu/test.json CHANGED
@@ -1051,6 +1051,56 @@
1051
  "outputT": { "dtype": "float32", "shape": [1, 512, 128], "tolerance": 0.0004, "relTolerance": 0.0004 }
1052
  }
1053
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1054
  {
1055
  "name": "materialized_sgmat_q512_kv512_h1_d128_bias",
1056
  "provenance": {
 
1051
  "outputT": { "dtype": "float32", "shape": [1, 512, 128], "tolerance": 0.0004, "relTolerance": 0.0004 }
1052
  }
1053
  },
1054
+ {
1055
+ "name": "materialized_sgmat_fused_f16_q1024_h4_d64_no_bias",
1056
+ "provenance": {
1057
+ "notes": "Pins the f16-operand fused materialized subgroup-matrix route just over its 16MB score-bytes admission floor (4 heads x 1024 x 1024 x 4B): f16 QK operands with f32 accumulation, raw f32 scores plus row stats, and the f16-probability PV apply with the dedicated f32 store scratch."
1058
+ },
1059
+ "attrs": { "num_heads": 4, "scale": 0.125 },
1060
+ "inputs": {
1061
+ "queryT": {
1062
+ "dtype": "float16",
1063
+ "shape": [1, 1024, 256],
1064
+ "data": { "kind": "fillFloat32", "scale": 0.08, "sinStep": 0.013, "cosStep": 0.029 }
1065
+ },
1066
+ "keyT": {
1067
+ "dtype": "float16",
1068
+ "shape": [1, 1024, 256],
1069
+ "data": { "kind": "fillFloat32", "scale": 0.08, "sinStep": 0.019, "cosStep": 0.037 }
1070
+ },
1071
+ "valueT": {
1072
+ "dtype": "float16",
1073
+ "shape": [1, 1024, 256],
1074
+ "data": { "kind": "fillFloat32", "scale": 0.08, "sinStep": 0.029, "cosStep": 0.017, "offset": 1.0 }
1075
+ }
1076
+ },
1077
+ "outputs": { "outputT": { "dtype": "float16", "shape": [1, 1024, 256], "tolerance": 0.02, "relTolerance": 0.02 } }
1078
+ },
1079
+ {
1080
+ "name": "materialized_sgmat_fused_f16_q1024_h4_d80_no_bias",
1081
+ "provenance": {
1082
+ "notes": "The d=80 twin of the f16 fused materialized fixture: a head dim that is not a multiple of 32 takes the guarded staging forms instead of the unguarded selects, on the same 16MB score-bytes admission floor."
1083
+ },
1084
+ "attrs": { "num_heads": 4, "scale": 0.125 },
1085
+ "inputs": {
1086
+ "queryT": {
1087
+ "dtype": "float16",
1088
+ "shape": [1, 1024, 320],
1089
+ "data": { "kind": "fillFloat32", "scale": 0.08, "sinStep": 0.013, "cosStep": 0.029 }
1090
+ },
1091
+ "keyT": {
1092
+ "dtype": "float16",
1093
+ "shape": [1, 1024, 320],
1094
+ "data": { "kind": "fillFloat32", "scale": 0.08, "sinStep": 0.019, "cosStep": 0.037 }
1095
+ },
1096
+ "valueT": {
1097
+ "dtype": "float16",
1098
+ "shape": [1, 1024, 320],
1099
+ "data": { "kind": "fillFloat32", "scale": 0.08, "sinStep": 0.029, "cosStep": 0.017, "offset": 1.0 }
1100
+ }
1101
+ },
1102
+ "outputs": { "outputT": { "dtype": "float16", "shape": [1, 1024, 320], "tolerance": 0.02, "relTolerance": 0.02 } }
1103
+ },
1104
  {
1105
  "name": "materialized_sgmat_q512_kv512_h1_d128_bias",
1106
  "provenance": {