com.microsoft.FusedConv / build /webgpu /conv-splitk-reduce.wgsl.jinja
Xenova's picture
Xenova HF Staff
sync 91d990483a17
9008499 verified
Raw
History Blame
3.24 kB
// Pass 2 of the split-K Conv GEMM: sum the SPLIT_K raw partial slices that
// a Conv GEMM partial pass wrote, then apply the bias and epilogue
// that pass deliberately skipped, and publish the logical [batch, M, N] output.
//
// The partial scratch is [SPLIT_K, batch, PARTIAL_ROWS, PARTIAL_COLS] with both
// trailing axes padded to whole GEMM tiles; this pass indexes only the logical
// (batch, m, n), so the padded rows and columns are read by nobody and their
// contents never matter. One output element per thread, walking n fastest so
// consecutive threads touch consecutive addresses in every slice.
//
// Summing slices in index order changes the f32 association relative to one
// uninterrupted K loop, so the two orders need not be bit-identical.
{{ env.wgsl.resourceDeclarations }}
{% set applyActivation = hasActivation is defined and hasActivation %}
{% if applyActivation %}
{% set hasActivation = hasActivation is defined and hasActivation %}
{% set activation = activation | default("") %}
{% set actAlpha = actAlpha | default(0.0) %}
{% set actBeta = actBeta | default(0.0) %}
{% if hasActivation %}
// Apply the fused activation in the f32 accumulator before the single output
// cast, avoiding an intermediate convolution tensor.
{% macro fused_act_return(mode, alpha, beta) -%}
{% if mode == "Relu" %}
return max(v, 0.0);
{% elif mode == "Clip" %}
return clamp(v, f32({{ alpha }}), f32({{ beta }}));
{% elif mode == "LeakyRelu" %}
return select(v * f32({{ alpha }}), v, v >= 0.0);
{% elif mode == "Sigmoid" %}
return 1.0 / (1.0 + exp(-v));
{% elif mode == "Tanh" %}
// tanh is already +/-1 to f32 precision at the clamp bounds. Clamping before
// the builtin preserves that saturated result for larger accumulators.
return tanh(clamp(v, -10.0, 10.0));
{% elif mode == "HardSigmoid" %}
return clamp(f32({{ alpha }}) * v + f32({{ beta }}), 0.0, 1.0);
{% else %}
return v * clamp(v * 0.16666666666666666 + 0.5, 0.0, 1.0);
{% endif %}
{%- endmacro -%}
fn fused_act(v: f32) -> f32 {
{{ fused_act_return(activation, actAlpha, actBeta) -}}
}
{% endif %}
{% endif %}
const M: u32 = {{ M }}u;
const N: u32 = {{ N }}u;
const SPLIT_K: u32 = {{ splitK }}u;
const PARTIAL_ROWS: u32 = {{ mPadded }}u;
const PARTIAL_COLS: u32 = {{ nPadded }}u;
const PARTIAL_SLICE_STRIDE: u32 = {{ batchCount }}u * PARTIAL_ROWS * PARTIAL_COLS;
const COUNT: u32 = {{ batchCount }}u * M * N;
const WORKGROUP_SIZE: u32 = {{ reduceWorkgroupSize }}u;
@compute @workgroup_size({{ reduceWorkgroupSize }}, 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 and reduces to the 1D form at y=0.
let idx = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * WORKGROUP_SIZE;
if (idx >= COUNT) {
return;
}
let col = idx % N;
let row = (idx / N) % M;
let batch = idx / (M * N);
var src = (batch * PARTIAL_ROWS + row) * PARTIAL_COLS + col;
var acc = 0.0;
for (var z = 0u; z < SPLIT_K; z = z + 1u) {
acc = acc + partial[src];
src = src + PARTIAL_SLICE_STRIDE;
}
{% if hasBias %}
acc = acc + f32(bias[row]);
{% endif %}
{% if applyActivation %}
acc = fused_act(acc);
{% endif %}
y[idx] = {{ T }}(acc);
}