Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 27 additions & 15 deletions js/web/lib/wasm/jsep/webgpu/ops/3rd-party/matmul_packed_webgpu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,19 +67,23 @@ const calculateResultSnippet = (transposeA: boolean, innerElementSize: number) =
let ACached2 = mm_Asub[k * innerElementSize + 2][localRow];
${innerElementSize === 3 ? '' : 'let ACached3 = mm_Asub[k * innerElementSize + 3][localRow];'}
for (var i = 0; i < rowPerThread; i = i + 1) {
acc[i] = BCached0 * ACached0[i] + acc[i];
acc[i] = BCached1 * ACached1[i] + acc[i];
acc[i] = BCached2 * ACached2[i] + acc[i];
${innerElementSize === 3 ? '' : 'acc[i] = BCached3 * ACached3[i] + acc[i];'}
// Explicit f32 casts on each operand are required: Dawn/D3D12 re-demotes
// temporaries to f16 when 'enable f16;' is active (issue #26732).
acc[i] = vec4<f32>(BCached0) * f32(ACached0[i]) + acc[i];
acc[i] = vec4<f32>(BCached1) * f32(ACached1[i]) + acc[i];
acc[i] = vec4<f32>(BCached2) * f32(ACached2[i]) + acc[i];
${innerElementSize === 3 ? '' : 'acc[i] = vec4<f32>(BCached3) * f32(ACached3[i]) + acc[i];'}
}`;
} else {
return `
for (var i = 0; i < rowPerThread; i = i + 1) {
let ACached = mm_Asub[tileRow + i][k];
acc[i] = BCached0 * ACached.x + acc[i];
acc[i] = BCached1 * ACached.y + acc[i];
acc[i] = BCached2 * ACached.z + acc[i];
${innerElementSize === 3 ? '' : 'acc[i] = BCached3 * ACached.w + acc[i];'}
// Explicit f32 casts on each operand are required: Dawn/D3D12 re-demotes
// temporaries to f16 when 'enable f16;' is active (issue #26732).
acc[i] = vec4<f32>(BCached0) * f32(ACached.x) + acc[i];
acc[i] = vec4<f32>(BCached1) * f32(ACached.y) + acc[i];
acc[i] = vec4<f32>(BCached2) * f32(ACached.z) + acc[i];
${innerElementSize === 3 ? '' : 'acc[i] = vec4<f32>(BCached3) * f32(ACached.w) + acc[i];'}
}`;
}
};
Expand Down Expand Up @@ -140,7 +144,9 @@ fn main(@builtin(local_invocation_id) localId : vec3<u32>,
let num_tiles = ${splitK ? `${Math.ceil(splitedDimInner / tileInner)}` : '(uniforms.dim_inner - 1) / tileInner + 1'};
var kStart = ${splitK ? `i32(globalId.z) * ${splitedDimInner}` : '0'};
var acc: array<vec4<${type}>, rowPerThread>;
// Accumulate in f32 to prevent fp16 overflow in long dot products (issue #26732).
// Tiles (mm_Asub/mm_Bsub) stay in ${type}: no shared-memory or bandwidth regression.
var acc: array<vec4<f32>, rowPerThread>;
// Loop over shared dimension.
let tileRowB = localRow * ${rowPerThreadB};
Expand Down Expand Up @@ -177,7 +183,7 @@ fn main(@builtin(local_invocation_id) localId : vec3<u32>,
}
for (var innerRow = 0; innerRow < rowPerThread; innerRow = innerRow + 1) {
mm_write(batch, globalRow + innerRow, globalCol, acc[innerRow]);
mm_write(batch, globalRow + innerRow, globalCol, vec4<${type}>(acc[innerRow]));
}
}`;
};
Expand Down Expand Up @@ -268,8 +274,10 @@ export const makeMatMulPackedSource = (
: `mm_Asub[localRow + innerRow * ${workgroupSize[1]}][k];`
}
for (var innerCol = 0; innerCol < colPerThread; innerCol = innerCol + 1) {
// Explicit f32 casts required: Dawn/D3D12 re-demotes temporaries to f16
// when 'enable f16;' is active (issue #26732).
acc[innerRow][innerCol] = acc[innerRow][innerCol] +
ACached * BCached[innerCol];
f32(ACached) * f32(BCached[innerCol]);
}
}
}
Expand All @@ -279,7 +287,7 @@ export const makeMatMulPackedSource = (
let gRow = globalRowStart + localRow + innerRow * ${workgroupSize[1]};
for (var innerCol = 0; innerCol < colPerThread; innerCol = innerCol + 1) {
let gCol = globalColStart + localCol + innerCol * ${workgroupSize[0]};
mm_write(batch, gRow, gCol, acc[innerRow][innerCol]);
mm_write(batch, gRow, gCol, ${type}(acc[innerRow][innerCol]));
}
}
`
Expand Down Expand Up @@ -328,7 +336,9 @@ for (var t = 0; t < num_tiles; t = t + 1) {
for (var innerRow = 0; innerRow < rowPerThread; innerRow = innerRow + 1) {
${readDataFromSubASnippet(transposeA)}
for (var innerCol = 0; innerCol < colPerThread; innerCol = innerCol + 1) {
acc[innerRow][innerCol] = acc[innerRow][innerCol] + ACached * BCached[innerCol];
// Explicit f32 casts required: Dawn/D3D12 re-demotes temporaries to f16
// when 'enable f16;' is active (issue #26732).
acc[innerRow][innerCol] = acc[innerRow][innerCol] + f32(ACached) * f32(BCached[innerCol]);
}
}
}
Expand All @@ -339,7 +349,7 @@ for (var t = 0; t < num_tiles; t = t + 1) {
for (var innerRow = 0; innerRow < rowPerThread; innerRow = innerRow + 1) {
for (var innerCol = 0; innerCol < colPerThread; innerCol = innerCol + 1) {
mm_write(batch, globalRow + innerRow, globalCol + innerCol,
acc[innerRow][innerCol]);
${type}(acc[innerRow][innerCol]));
}
}
`;
Expand All @@ -362,7 +372,9 @@ fn main(@builtin(local_invocation_id) localId : vec3<u32>,
};
var kStart = ${splitK ? `i32(globalId.z) * ${splitedDimInner}` : '0'};
var acc : array<array<${type}, colPerThread>, rowPerThread>;
// Accumulate in f32 to prevent fp16 overflow in long dot products (issue #26732).
// Tiles (mm_Asub/mm_Bsub) stay in ${type}: no shared-memory or bandwidth regression.
var acc : array<array<f32, colPerThread>, rowPerThread>;
${matmulSnippet}
}
`;
Expand Down
11 changes: 8 additions & 3 deletions js/web/lib/wasm/jsep/webgpu/ops/matmul-shaders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@ export const createNaiveMatmulProgramInfo = (
];
appendActivationUniforms(activationAttributes, uniforms);

// Accumulate in f32 to prevent fp16 overflow in long dot products (issue #26732).
// Explicit f32 casts on each operand are required: Dawn/D3D12 re-demotes
// temporaries to f16 when 'enable f16;' is active.
const accType = components === 1 ? 'f32' : `vec${components}<f32>`;
const calcResult = (): string => {
let calcStr = `var a_data: ${a.type.value};`;
for (let i = 0; i < aComponents; i++) {
Expand All @@ -123,7 +127,7 @@ export const createNaiveMatmulProgramInfo = (

for (let j = 0; j < aComponents; j++) {
calcStr += `
values[${i}] = fma(${b.type.value}(a_data${aComponents === 1 ? '' : `[${j}]`}), b_data${j}, values[${i}]);\n`;
values[${i}] = fma(${accType}(a_data${aComponents === 1 ? '' : `[${j}]`}), ${accType}(b_data${j}), values[${i}]);\n`;
}
}
return calcStr;
Expand Down Expand Up @@ -155,12 +159,13 @@ export const createNaiveMatmulProgramInfo = (
${b.indicesSet('b_indices', b.rank - 2, 0)}
${b.indicesSet('b_indices', b.rank - 1, 0)}
let b_offset = ${b.indicesToOffset('b_indices')};
var values: array<${output.type.value}, ${outputNumber}>;
var values: array<${accType}, ${outputNumber}>;
for (var k: u32 = 0u; k < uniforms.K; k = k + ${aComponents}) {
${calcResult()}
}
for (var i = 0u; i < ${outputNumber}u; i++) {
var value = values[i];
// Downcast to the output type only at the final write.
var value = ${output.type.value}(values[i]);
${processBias}
${applyActivation}
let cur_indices = ${output.type.indices}(batch, row + i, col);
Expand Down
26 changes: 17 additions & 9 deletions js/web/lib/wasm/jsep/webgpu/ops/matmulnbits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,8 @@ export const createMatMulNBitsProgramInfo = (
(_, i) =>
`${
aComponents === 1
? `a_data${pass > 0 ? pass : ''}[${i}] * b_dequantized_values[${i}]`
: `dot(a_data${pass > 0 ? pass : ''}[${i}], b_dequantized_values[${i}])`
? `f32(a_data${pass > 0 ? pass : ''}[${i}]) * f32(b_dequantized_values[${i}])`
: `dot(vec${aComponents}<f32>(a_data${pass > 0 ? pass : ''}[${i}]), vec${aComponents}<f32>(b_dequantized_values[${i}]))`
}`,
).join(' + ')};
`;
Expand Down Expand Up @@ -242,8 +242,13 @@ export const createMatMulNBitsProgramInfo = (
var b_dequantized_values: ${qDqDataType};`;
return calcStr;
};
// Accumulate in f32 to prevent fp16 overflow in long dot products (issue #26732).
// Weights and activations stay in their original dtype; explicit f32 casts on each
// operand are required because Dawn/D3D12 re-demotes temporaries to f16 when
// 'enable f16;' is active. The result is downcast to the output type only at the write.
const accType = components === 1 ? 'f32' : `vec${components}<f32>`;
return `
var<workgroup> workgroup_shared: array<${output.type.value}, ${outputNumber * workgroupSize}>;
var<workgroup> workgroup_shared: array<${accType}, ${outputNumber * workgroupSize}>;
${shaderHelper.declareVariables(...inputVariables, output)}
${shaderHelper.mainStart([workgroupSize, 1, 1])}
let output_indices = ${output.offsetToIndices(`(global_idx / ${workgroupSize}) * ${outputNumber}`)};
Expand All @@ -267,13 +272,13 @@ export const createMatMulNBitsProgramInfo = (
workgroupBarrier();
if (local_id.x < ${outputNumber}) {
var output_value: ${output.type.value} = ${output.type.value}(0);
var output_value: ${accType} = ${accType}(0);
var workgroup_shared_offset: u32 = local_id.x;
for (var b: u32 = 0u; b < ${workgroupSize}u; b++) {
output_value += workgroup_shared[workgroup_shared_offset];
workgroup_shared_offset += ${outputNumber};
}
${output.setByIndices(`${output.type.indices}(batch, row, col + local_id.x)`, 'output_value')};
${output.setByIndices(`${output.type.indices}(batch, row, col + local_id.x)`, `${output.type.value}(output_value)`)};
}
}`;
};
Expand Down Expand Up @@ -368,7 +373,8 @@ export const createMatMulNBitsBlockSize32ProgramInfo = (

return `
var<workgroup> sub_a: array<${a.type.value}, ${aLengthPerTile}>;
var<workgroup> inter_results: array<array<${output.type.value}, ${workgroupX}>, ${workgroupY}>;
// Accumulate in f32 to prevent fp16 overflow in long dot products (issue #26732).
var<workgroup> inter_results: array<array<f32, ${workgroupX}>, ${workgroupY}>;
${shaderHelper.declareVariables(...inputVariables, output)}
${shaderHelper.mainStart([workgroupX, workgroupY, 1])}
let output_indices = ${output.offsetToIndices(`workgroup_index * ${workgroupY}`)};
Expand Down Expand Up @@ -444,9 +450,11 @@ export const createMatMulNBitsBlockSize32ProgramInfo = (
(_, i) => `${dataType}(b_value_lower[${i}]), ${dataType}(b_value_upper[${i}])`,
).join(', ')});
let b_dequantized_values = (b_quantized_values - mat2x4<${dataType}>(${Array(8).fill('zero_point').join(',')})) * scale;
// Explicit f32 casts on each operand are required: Dawn/D3D12 re-demotes
// temporaries to f16 when 'enable f16;' is active (issue #26732).
inter_results[local_id.y][local_id.x] += ${Array.from(
{ length: 2 },
(_, i) => `${`dot(a_data${i}, b_dequantized_values[${i}])`}`,
(_, i) => `${`dot(vec4<f32>(a_data${i}), vec4<f32>(b_dequantized_values[${i}]))`}`,
).join(' + ')};
}
word_offset += ${8 / aComponents};`;
Expand All @@ -458,13 +466,13 @@ export const createMatMulNBitsBlockSize32ProgramInfo = (
}
if (local_idx < ${workgroupY}) {
var output_value: ${output.type.value} = ${output.type.value}(0);
var output_value: f32 = f32(0);
for (var b = 0u; b < ${workgroupX}; b++) {
output_value += inter_results[local_idx][b];
}
if (col + local_idx < uniforms.output_shape[2])
{
${output.setByIndices(`${output.type.indices}(batch, row, col + local_idx)`, 'output_value')}
${output.setByIndices(`${output.type.indices}(batch, row, col + local_idx)`, `${output.type.value}(output_value)`)}
}
}
}`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@

// Shared memory
var<workgroup> tile_A : array<input_a_value_t, a_length_per_tile>;
var<workgroup> inter_results: array<array<output_element_t, tile_size_k_vec>, tile_size>;
// Accumulate partial sums along K in f32: with fp16 outputs, summing 2048+ f16
// products overflows the f16 max (65504) and poisons the output with +Inf/NaN
// (issue #26732). Only the block-local `sum` stays in output_element_t.
var<workgroup> inter_results: array<array<f32, tile_size_k_vec>, tile_size>;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This correctly fixes the generic kernel, but the same f16-accumulation pattern is left unfixed in the fused native kernels that get dispatched for the very models this PR targets:

  • matmul_nbits_mlp.wgsl.template: gate_sum/up_sum = output_element_t(0) and gate_inter_results/up_inter_results : array<array<output_element_t, ...>>
  • matmul_nbits_qkv.wgsl.template: sum = q_output_element_t(0) and q/k/v_inter_results
  • dp4a_matmul_small_m.wgsl.template: inter_results : array<array<output_element_t, ...>>

The MLP/QKV kernels are selected for fused MLP / QKV subgraphs (common in Gemma 3 / SmolLM), so those paths can still overflow. Please extend the f32 accumulation to them or note explicitly why they are excluded.


fn loadSHMA(batch: u32, a_global: u32, kidx: u32, col: u32)
{
Expand Down Expand Up @@ -222,7 +225,9 @@ $MAIN {
#endif
#endif

inter_results[local_row_offset + idy][idx] += sum;
// Explicit f32 cast: Dawn/D3D12 re-demotes temporaries to f16 when
// 'enable f16;' is active (issue #26732).
inter_results[local_row_offset + idy][idx] += f32(sum);
}
}
workgroupBarrier();
Expand All @@ -233,17 +238,18 @@ $MAIN {
}

if (local_idx < tile_size) {
var output_value = output_element_t(0);
var output_value = f32(0);
for (var b = 0u; b < tile_size_k_vec; b++) {
output_value += inter_results[local_idx][b];
}
let b_global = b_global_base + local_idx;
let output_idx = batch * uniforms.dispatch_M * uniforms.N + a_global * uniforms.N + b_global;
if (b_global < uniforms.N) {
#if has_bias
output_value += bias[b_global + b_bias_offset];
output_value += f32(bias[b_global + b_bias_offset]);
#endif
output.setByOffset(output_idx, output_value);
// Downcast to the output type only at the final write.
output.setByOffset(output_idx, output_element_t(output_value));
}
}
} // MAIN
116 changes: 116 additions & 0 deletions scripts/verify_f16_fix.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
#!/usr/bin/env python3

Check warning

Code scanning / lintrunner

RUFF-FORMAT/format Warning

Run lintrunner -a to apply this patch.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please drop this file from the PR. It introduces a new top-level scripts/ directory for a static regex check over shader source (not a runtime test), it already fails lintrunner (RUFF-FORMAT), and it will silently bit-rot the moment the shaders are refactored because it asserts on exact source substrings. If you want regression coverage, prefer a numerical unit test (large-K fp16 MatMul/MatMulNBits vs. an f32 reference) in the existing WebGPU op test suite, which exercises real runtime behavior.

"""
verify_f16_fix.py - Static verification for the fp16 overflow fix (issue #26732).
Checks that WebGPU matmul shader generators/templates accumulate in f32 instead
of the (overflow-prone) f16 output type. No build or GPU required.
Usage: python3 scripts/verify_f16_fix.py
"""
import re
import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent
JSEP = ROOT / "js/web/lib/wasm/jsep/webgpu/ops"
NATIVE = ROOT / "onnxruntime/contrib_ops/webgpu/quantization"

checks = []


def check(name, path, bad_patterns, good_patterns, description):
if not path.exists():
checks.append((name, "SKIP", f"file not found: {path}"))
return
content = path.read_text(encoding="utf-8")
bad = [p for p in bad_patterns if re.search(p, content)]
good = [p for p in good_patterns if re.search(p, content)]
if bad:
checks.append((name, "FAIL", f"{description} - f16 accumulator pattern still present: {bad}"))
elif len(good) == len(good_patterns):
checks.append((name, "PASS", description))
else:
missing = [p for p in good_patterns if p not in good]
checks.append((name, "UNKNOWN", f"{description} - expected pattern(s) not found: {missing}"))


check(
"matmulnbits.ts (default kernel)",
JSEP / "matmulnbits.ts",
[r"var<workgroup> workgroup_shared: array<\$\{output\.type\.value\}"],
[
r"var<workgroup> workgroup_shared: array<\$\{accType\}",
r"vec\$\{aComponents\}<f32>\(b_dequantized_values",
],
"MatMulNBits default kernel must accumulate in f32",
)

check(
"matmulnbits.ts (blockSize32 kernel)",
JSEP / "matmulnbits.ts",
[r"inter_results: array<array<\$\{output\.type\.value\}"],
[
r"inter_results: array<array<f32",
r"dot\(vec4<f32>\(a_data\$\{i\}\), vec4<f32>\(b_dequantized_values\[\$\{i\}\]\)\)",
],
"BlockwiseMatMulNBits32 must accumulate in f32",
)

check(
"matmul-shaders.ts (naive matmul)",
JSEP / "matmul-shaders.ts",
[r"var values: array<\$\{output\.type\.value\}"],
[r"var values: array<\$\{accType\}", r"fma\(\$\{accType\}\("],
"Naive MatMul must accumulate in f32",
)

check(
"3rd-party/matmul_packed_webgpu.ts (tiled matmul)",
JSEP / "3rd-party/matmul_packed_webgpu.ts",
[
r"var acc: array<vec4<\$\{type\}>, rowPerThread>",
r"var acc : array<array<\$\{type\}, colPerThread>",
],
[
r"var acc: array<vec4<f32>, rowPerThread>",
r"var acc : array<array<f32, colPerThread>",
r"vec4<f32>\(BCached0\)",
r"f32\(ACached\)",
],
"Packed MatMul (vec4 + scalar variants) must accumulate in f32",
)

check(
"native matmul_nbits.wgsl.template",
NATIVE / "matmul_nbits.wgsl.template",
[r"inter_results: array<array<output_element_t"],
[
r"inter_results: array<array<f32",
r"inter_results\[local_row_offset \+ idy\]\[idx\] \+= f32\(sum\)",
r"output\.setByOffset\(output_idx, output_element_t\(output_value\)\)",
],
"Native MatMulNBits template must accumulate in f32 along K",
)

check(
"native matmul_nbits_wide_tile.wgsl.template (pre-existing f32)",
NATIVE / "matmul_nbits_wide_tile.wgsl.template",
[],
[r"var results : array<f32, kTileM>"],
"Wide-tile kernel already accumulates in f32 (reference pattern)",
)

print("\n=== Static verification: fix for issue #26732 ===\n")
all_pass = True
for name, status, desc in checks:
icon = {"PASS": "[PASS]", "SKIP": "[SKIP]", "FAIL": "[FAIL]", "UNKNOWN": "[????]"}[status]
print(f"{icon} {name}: {desc}")
if status in ("FAIL", "UNKNOWN"):
all_pass = False

print()
if all_pass:
print("All checks passed - the fix appears to be applied correctly.")
sys.exit(0)
print("Some checks failed - review the patches.")
sys.exit(1)
Loading