Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
7c0c1b9
Initial commit
hariharans29 Apr 8, 2026
c42879d
Merge remote-tracking branch 'origin' into hari/webgpu_perf_1
hariharans29 Apr 8, 2026
a0550b6
More changes
hariharans29 Apr 9, 2026
c55adfe
Merge branch 'hari/webgpu_perf_1' of https://github.com/microsoft/onn…
hariharans29 Apr 9, 2026
ee09d8e
Stage
hariharans29 Apr 13, 2026
aa357ee
More changes
hariharans29 Apr 15, 2026
318b26b
Stage
hariharans29 Apr 20, 2026
ad53b3d
Worka nd good perf
hariharans29 Apr 22, 2026
b67ae81
Skip + MatmulNBitsSilu fusion - works and good perf
hariharans29 Apr 23, 2026
01671d9
Cleanup
hariharans29 Apr 30, 2026
30485dd
Move back to workgroup/tile_size default
hariharans29 Apr 30, 2026
27317b8
Merge main
hariharans29 Apr 30, 2026
a56fb56
Merge remote-tracking branch 'origin' into hari/webgpu_perf_1
hariharans29 May 1, 2026
13bf979
Copilot comments + Fix builds + Fix lint + Fusion diagrams
hariharans29 May 1, 2026
d1090c8
Fix test
hariharans29 May 1, 2026
ffacd4c
Fix builds
hariharans29 May 1, 2026
92874ce
Fixes
hariharans29 May 1, 2026
a7899c6
Slim PR: drop benchmark harness, lazy buffer-mgr fix, consteval fix, …
hariharans29 May 2, 2026
2039c7f
Remove unused dp4a_matmul_mlp.wgsl.template
hariharans29 May 2, 2026
a02cf12
Cleanup: drop unused empty namespace + env_var_utils include in graph…
hariharans29 May 2, 2026
beb1709
Merge remote-tracking branch 'origin' into hari/webgpu_perf_1
hariharans29 May 2, 2026
9065063
Copilot comments
hariharans29 May 2, 2026
4ac9c81
Fixes
hariharans29 May 2, 2026
306fba3
Fix
hariharans29 May 3, 2026
6c8c7a3
Use fresh WebGPU EP per session in fusion-vs-unfused tests
hariharans29 May 3, 2026
a90a049
Remove unused file
hariharans29 May 10, 2026
007a78e
[WebGPU] Extract shared LayerNorm/SkipLayerNorm program runners
hariharans29 May 11, 2026
37db5b8
[WebGPU] MatMulNBitsMlp: adopt shared norm helpers + activation enum
hariharans29 May 11, 2026
2c1a2a3
[WebGPU] MatMulNBitsMlpFusion: match fused-QuickGelu MLP shape
hariharans29 May 11, 2026
234bcf4
[WebGPU/JSEP] Enable QuickGeluFusion for WebGPU and JSEP EPs
hariharans29 May 11, 2026
eaa6635
Copilot comments
hariharans29 May 12, 2026
106c07e
Merge main and resolve conflicts
hariharans29 May 12, 2026
16f05f2
Address PR #28280 review comments
hariharans29 May 20, 2026
130111f
Merge remote-tracking branch 'origin/main' into hari/webgpu_perf_1
hariharans29 May 21, 2026
159894a
Merge remote-tracking branch 'origin/main' into HEAD
hariharans29 May 26, 2026
b5c2a4e
WebGPU MatMulNBits QKV/MLP fusion: address PR #28280 reviewer comments
hariharans29 May 27, 2026
8bfb7ee
Update onnxruntime/test/optimizer/matmul_nbits_qkv_fusion_test.cc
hariharans29 May 27, 2026
cb49505
Update onnxruntime/test/optimizer/matmul_nbits_qkv_fusion_test.cc
hariharans29 May 27, 2026
be8260a
Copilot comments + Build break
hariharans29 May 27, 2026
b8ee37e
Merge branch 'hari/webgpu_perf_1' of https://github.com/microsoft/onn…
hariharans29 May 27, 2026
7433696
Fix QKV asymmetry with respect to bias in schema
hariharans29 May 28, 2026
b3595d2
Address copilot comments
hariharans29 May 28, 2026
4f34f06
Docs + Merge main
hariharans29 May 28, 2026
128b887
Remove unnecessary docs
hariharans29 May 28, 2026
30054e5
Revert "Remove unnecessary docs"
hariharans29 May 28, 2026
6abd605
Remove unnecessary docs - 2
hariharans29 May 28, 2026
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
194 changes: 193 additions & 1 deletion docs/ContribOperators.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ Do not modify directly.*
* <a href="#com.microsoft.MatMulInteger16">com.microsoft.MatMulInteger16</a>
* <a href="#com.microsoft.MatMulIntegerToFloat">com.microsoft.MatMulIntegerToFloat</a>
* <a href="#com.microsoft.MatMulNBits">com.microsoft.MatMulNBits</a>
* <a href="#com.microsoft.MatMulNBitsMlp">com.microsoft.MatMulNBitsMlp</a>
* <a href="#com.microsoft.MatMulNBitsQkv">com.microsoft.MatMulNBitsQkv</a>
* <a href="#com.microsoft.MaxpoolWithMask">com.microsoft.MaxpoolWithMask</a>
* <a href="#com.microsoft.MoE">com.microsoft.MoE</a>
* <a href="#com.microsoft.MulInteger">com.microsoft.MulInteger</a>
Expand Down Expand Up @@ -3147,7 +3149,7 @@ This version of the operator has been available since version 1 of the 'com.micr
<dt><tt>accuracy_level</tt> : int</dt>
<dd>The minimum accuracy level of input A, can be: 0(unset), 1(fp32), 2(fp16), 3(bf16), or 4(int8) (default unset). It is used to control how input A is quantized or downcast internally while doing computation, for example: 0 means input A will not be quantized or downcast while doing computation. 4 means input A can be quantized with the same block_size to int8 internally from type T1.</dd>
<dt><tt>bits</tt> : int</dt>
<dd>Bit-width used to quantize the weights (valid range: 2~8)</dd>
<dd>Bit-width used to quantize the weights (supported values: 2, 4, 8)</dd>
<dt><tt>block_size</tt> : int (required)</dt>
<dd>Size of each quantization block along the K (input feature) dimension. Must be a power of two and ≥ 16 (e.g., 16, 32, 64, 128).</dd>
</dl>
Expand Down Expand Up @@ -3190,6 +3192,196 @@ This version of the operator has been available since version 1 of the 'com.micr
</dl>


### <a name="com.microsoft.MatMulNBitsMlp"></a><a name="com.microsoft.matmulnbitsmlp">**com.microsoft.MatMulNBitsMlp**</a>

MatMulNBitsMlp fuses two MatMulNBits projections that share the same input and computes

gate = MatMulNBits(A, gate_weight) + gate_bias
up = MatMulNBits(A, up_weight) + up_bias
Y = activation(gate) * up

It can also optionally fuse SimplifiedLayerNormalization or SkipSimplifiedLayerNormalization before the
two projections:

A_norm = SimplifiedLayerNormalization(A, norm_scale, epsilon)
gate = MatMulNBits(A_norm, gate_weight) + gate_bias
up = MatMulNBits(A_norm, up_weight) + up_bias
Y = activation(gate) * up

A_norm = SkipSimplifiedLayerNormalization(A, skip, norm_scale, epsilon)
gate = MatMulNBits(A_norm, gate_weight) + gate_bias
up = MatMulNBits(A_norm, up_weight) + up_bias
Y = activation(gate) * up

This operator is intended for decoder MLP patterns such as Qwen-style gate and up projections, but it remains
semantically valid for both prefill and decode because the output shape is the standard MatMul result shape
derived from the runtime shape of A and the shared attributes K and N.

The operator contract includes a string attribute describing the fused gate activation.

When fused from SkipSimplifiedLayerNormalization, the optional residual-sum output may also be materialized:

A_norm, input_skip_bias_sum = SkipSimplifiedLayerNormalization(A, skip, norm_scale, epsilon)
gate = MatMulNBits(A_norm, gate_weight) + gate_bias
up = MatMulNBits(A_norm, up_weight) + up_bias
Y = activation(gate) * up

#### Version

This version of the operator has been available since version 1 of the 'com.microsoft' operator set.

#### Attributes

<dl>
<dt><tt>K</tt> : int (required)</dt>
<dd>Input feature dimension shared by both quantized weight matrices.</dd>
<dt><tt>N</tt> : int (required)</dt>
<dd>Output feature dimension shared by both quantized weight matrices.</dd>
<dt><tt>accuracy_level</tt> : int</dt>
<dd>The minimum accuracy level of input A. It follows the same semantics as MatMulNBits.</dd>
<dt><tt>activation</tt> : string (required)</dt>
<dd>Activation applied to the gate projection.</dd>
<dt><tt>bits</tt> : int</dt>
<dd>Bit-width used to quantize both weight matrices. Currently only bits=4 is supported by the WebGPU kernel.</dd>
<dt><tt>block_size</tt> : int (required)</dt>
<dd>Size of each quantization block along the K dimension. Currently only block_size=32 is supported by the WebGPU kernel.</dd>
<dt><tt>epsilon</tt> : float</dt>
<dd>Epsilon used by the optional fused (Skip)SimplifiedLayerNormalization. Defaults to 1e-5.</dd>
</dl>

#### Inputs (8 - 9)

<dl>
<dt><tt>A</tt> : T1</dt>
<dd>The shared input tensor.</dd>
<dt><tt>skip</tt> (optional) : T1</dt>
<dd>Optional skip input used by SkipSimplifiedLayerNormalization.</dd>
<dt><tt>norm_scale</tt> (optional) : T1</dt>
<dd>Optional RMSNorm scale with shape [K] used by SimplifiedLayerNormalization or SkipSimplifiedLayerNormalization.</dd>
<dt><tt>gate_B</tt> : T2</dt>
<dd>Packed uint8 tensor for the gate projection weights.</dd>
<dt><tt>gate_scales</tt> : T1</dt>
<dd>Per-block scaling factors for the gate projection.</dd>
<dt><tt>gate_bias</tt> (optional) : T1</dt>
<dd>Optional bias for the gate projection with shape [N].</dd>
<dt><tt>up_B</tt> : T2</dt>
<dd>Packed uint8 tensor for the up projection weights.</dd>
<dt><tt>up_scales</tt> : T1</dt>
<dd>Per-block scaling factors for the up projection.</dd>
<dt><tt>up_bias</tt> (optional) : T1</dt>
<dd>Optional bias for the up projection with shape [N].</dd>
</dl>

#### Outputs (1 - 2)

<dl>
<dt><tt>Y</tt> : T1</dt>
<dd>The fused gated MLP output tensor.</dd>
<dt><tt>input_skip_bias_sum</tt> (optional) : T1</dt>
<dd>Optional residual-sum output for SkipSimplifiedLayerNormalization.</dd>
</dl>

#### Type Constraints

<dl>
<dt><tt>T1</tt> : tensor(float), tensor(float16), tensor(bfloat16)</dt>
<dd>Constrain input and output types to float tensors.</dd>
<dt><tt>T2</tt> : tensor(uint8)</dt>
<dd>Constrain quantized weight types to uint8.</dd>
</dl>


### <a name="com.microsoft.MatMulNBitsQkv"></a><a name="com.microsoft.matmulnbitsqkv">**com.microsoft.MatMulNBitsQkv**</a>

MatMulNBitsQkv fuses either SimplifiedLayerNormalization (RMSNorm)
or SkipSimplifiedLayerNormalization with three MatMulNBits projections that share the
same normalized activation.

A_norm = SimplifiedLayerNormalization(A, norm_scale, epsilon)
Q = MatMulNBits(A_norm, q_weight) + q_bias
K = MatMulNBits(A_norm, k_weight) + k_bias
V = MatMulNBits(A_norm, v_weight) + v_bias

If skip is provided, the operator computes the SkipSimplifiedLayerNormalization variant
and may also return the input+skip residual sum as output 3.

This operator is intended as a decode-oriented QKV fusion primitive.

#### Version

This version of the operator has been available since version 1 of the 'com.microsoft' operator set.

#### Attributes

<dl>
<dt><tt>K</tt> : int (required)</dt>
<dd>Input feature dimension shared by the normalized input and all projection weights.</dd>
<dt><tt>Nkv</tt> : int (required)</dt>
<dd>Output feature dimension shared by the K and V projections.</dd>
<dt><tt>Nq</tt> : int (required)</dt>
<dd>Output feature dimension of the Q projection.</dd>
<dt><tt>accuracy_level</tt> : int</dt>
<dd>The minimum accuracy level of input A. It follows the same semantics as MatMulNBits.</dd>
<dt><tt>bits</tt> : int</dt>
<dd>Bit-width used to quantize all weight matrices. Currently only bits=4 is supported by the WebGPU kernel.</dd>
<dt><tt>block_size</tt> : int (required)</dt>
<dd>Size of each quantization block along the K dimension. Currently only block_size=32 is supported by the WebGPU kernel.</dd>
<dt><tt>epsilon</tt> : float</dt>
<dd>Epsilon used by the simplified layer norm reduction.</dd>
</dl>

#### Inputs (11 - 12)

<dl>
<dt><tt>A</tt> : T1</dt>
<dd>The shared input tensor.</dd>
<dt><tt>skip</tt> (optional) : T1</dt>
<dd>Optional residual input for SkipSimplifiedLayerNormalization.</dd>
<dt><tt>norm_scale</tt> : T1</dt>
<dd>Scale input for the simplified layer norm with shape [K].</dd>
<dt><tt>q_B</tt> : T2</dt>
<dd>Packed uint8 tensor for the Q projection weights.</dd>
<dt><tt>q_scales</tt> : T1</dt>
<dd>Per-block scaling factors for the Q projection.</dd>
<dt><tt>q_bias</tt> (optional) : T1</dt>
<dd>Optional bias for the Q projection with shape [Nq].</dd>
<dt><tt>k_B</tt> : T2</dt>
<dd>Packed uint8 tensor for the K projection weights.</dd>
<dt><tt>k_scales</tt> : T1</dt>
<dd>Per-block scaling factors for the K projection.</dd>
<dt><tt>k_bias</tt> (optional) : T1</dt>
<dd>Optional bias for the K projection with shape [Nkv].</dd>
<dt><tt>v_B</tt> : T2</dt>
<dd>Packed uint8 tensor for the V projection weights.</dd>
<dt><tt>v_scales</tt> : T1</dt>
<dd>Per-block scaling factors for the V projection.</dd>
Comment thread
hariharans29 marked this conversation as resolved.
<dt><tt>v_bias</tt> (optional) : T1</dt>
<dd>Optional bias for the V projection with shape [Nkv].</dd>
</dl>

#### Outputs (3 - 4)

<dl>
<dt><tt>Q</tt> : T1</dt>
<dd>The Q projection output tensor.</dd>
<dt><tt>K</tt> : T1</dt>
<dd>The K projection output tensor.</dd>
<dt><tt>V</tt> : T1</dt>
<dd>The V projection output tensor.</dd>
<dt><tt>input_skip_bias_sum</tt> (optional) : T1</dt>
<dd>Optional residual-sum output for SkipSimplifiedLayerNormalization.</dd>
</dl>

#### Type Constraints

<dl>
<dt><tt>T1</tt> : tensor(float), tensor(float16), tensor(bfloat16)</dt>
<dd>Constrain input and output types to float tensors.</dd>
<dt><tt>T2</tt> : tensor(uint8)</dt>
<dd>Constrain quantized weight types to uint8.</dd>
</dl>


### <a name="com.microsoft.MaxpoolWithMask"></a><a name="com.microsoft.maxpoolwithmask">**com.microsoft.MaxpoolWithMask**</a>

For internal use.
Expand Down
27 changes: 20 additions & 7 deletions onnxruntime/contrib_ops/webgpu/bert/skip_layer_norm.cc
Original file line number Diff line number Diff line change
Expand Up @@ -154,8 +154,22 @@ Status SkipLayerNorm<simplified>::ComputeInternal(onnxruntime::webgpu::ComputeCo
auto* output = context.Output(0, x_shape);
auto* input_skip_bias_sum = context.Output(3, x_shape);

int64_t data_size = x_shape.Size();
if (data_size == 0) {
return RunSkipLayerNormProgram(context, x, skip, gamma, beta, bias, epsilon_, simplified,
output, input_skip_bias_sum);
}

Status RunSkipLayerNormProgram(ComputeContext& context,
const Tensor* x,
const Tensor* skip,
const Tensor* gamma,
const Tensor* beta,
const Tensor* bias,
float epsilon,
bool simplified,
Tensor* output,
Tensor* input_skip_bias_sum) {
const auto& x_shape = x->Shape();
if (x_shape.Size() == 0) {
return Status::OK();
}

Expand All @@ -165,26 +179,25 @@ Status SkipLayerNorm<simplified>::ComputeInternal(onnxruntime::webgpu::ComputeCo
const uint32_t norm_count = onnxruntime::narrow<uint32_t>(x_shape.SizeToDimension(x_shape.NumDimensions() - 1));
const bool split_hidden_dim = hidden_size % 512 == 0 && norm_count == 1;

const auto skip_shape = skip->Shape();
const uint32_t skip_size = onnxruntime::narrow<uint32_t>(skip_shape.Size());
const uint32_t skip_size = onnxruntime::narrow<uint32_t>(skip->Shape().Size());

SkipLayerNormProgram program{
beta != nullptr, bias != nullptr, epsilon_, hidden_size, has_input_skip_bias_sum, simplified, split_hidden_dim};
beta != nullptr, bias != nullptr, epsilon, hidden_size, has_input_skip_bias_sum, simplified, split_hidden_dim};
program
.CacheHint(simplified, beta != nullptr, bias != nullptr, has_input_skip_bias_sum, split_hidden_dim)
.AddInputs({{x, ProgramTensorMetadataDependency::Type, components}})
.AddInputs({{skip, ProgramTensorMetadataDependency::Type, components}})
.AddInputs({{gamma, ProgramTensorMetadataDependency::Type, components}})
.AddOutputs({{output, ProgramTensorMetadataDependency::None, components}})
.SetDispatchGroupSize(onnxruntime::narrow<uint32_t>(ceil(1.0 * data_size / hidden_size)))
.SetDispatchGroupSize(onnxruntime::narrow<uint32_t>(ceil(1.0 * x_shape.Size() / hidden_size)))
.AddUniformVariables({
{static_cast<uint32_t>(components)},
})
.AddUniformVariables({
{static_cast<uint32_t>(hidden_size)},
})
.AddUniformVariables({
{static_cast<float>(epsilon_)},
{static_cast<float>(epsilon)},
})
.AddUniformVariables({
{static_cast<uint32_t>(skip_size)},
Expand Down
15 changes: 15 additions & 0 deletions onnxruntime/contrib_ops/webgpu/bert/skip_layer_norm.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,21 @@ class SkipLayerNorm final : public WebGpuKernel {
float epsilon_;
};

// Configures and dispatches a SkipLayerNormProgram. Centralizes program-setup logic
// (uniform variables, components, split_hidden_dim heuristic, workgroup sizing) so callers
// other than the SkipLayerNorm kernel (e.g. fused MatMulNBits ops) do not need to duplicate it.
// `beta`, `bias` and `input_skip_bias_sum` may be nullptr.
Status RunSkipLayerNormProgram(ComputeContext& context,
const Tensor* x,
const Tensor* skip,
const Tensor* gamma,
const Tensor* beta,
const Tensor* bias,
float epsilon,
bool simplified,
Tensor* output,
Tensor* input_skip_bias_sum);

} // namespace webgpu
} // namespace contrib
} // namespace onnxruntime
21 changes: 17 additions & 4 deletions onnxruntime/contrib_ops/webgpu/quantization/dp4a_matmul_nbits.cc
Original file line number Diff line number Diff line change
Expand Up @@ -199,15 +199,28 @@ bool CanApplyDP4AMatrixMatMulNBits(onnxruntime::webgpu::ComputeContext& context,
uint32_t block_size,
uint32_t N,
uint32_t K,
uint32_t components_k) {
uint32_t components_k,
uint32_t M,
bool has_weight_idx_indirect,
const Tensor* y) {
// macOS - Avoid using dp4a on Metal, as it does not appear to have native dp4a support.
// https://github.com/gpuweb/gpuweb/issues/2677#issuecomment-1713292226
// Use 'vendor' to check for metal; 'backend' is always WEBGPU when running under wasm.
bool use_dp4a = context.HasFeature(wgpu::FeatureName::Subgroups) &&
context.AdapterInfo().vendor != std::string_view{"apple"};
return (accuracy_level == 4 && block_size % 32 == 0 &&
components_k == 4 && K % 128 == 0 && N % 16 == 0 &&
use_dp4a);
if (!(accuracy_level == 4 && block_size % 32 == 0 &&
components_k == 4 && K % 128 == 0 && N % 16 == 0 &&
use_dp4a)) {
return false;
}

// Dispatch precondition: DP4A is used either when M is large enough (and the
// weight is contiguous), or unconditionally on FP32-only GPUs and Qualcomm
// GPUs where integer math beats FP32.
const bool m_large_enough = (M >= kMinMForTileOptimization && !has_weight_idx_indirect);
const bool fp32_output = (y != nullptr && y->DataType() == DataTypeImpl::GetType<float>());
const bool qualcomm_vendor = context.AdapterInfo().vendor == std::string_view{"qualcomm"};
return m_large_enough || fp32_output || qualcomm_vendor;
}

} // namespace webgpu
Expand Down
12 changes: 11 additions & 1 deletion onnxruntime/contrib_ops/webgpu/quantization/dp4a_matmul_nbits.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

#pragma once

#include <limits>

#include "core/providers/webgpu/program.h"
#include "core/providers/webgpu/webgpu_kernel.h"

Expand Down Expand Up @@ -111,12 +113,20 @@ Status ApplyDP4AMatrixMatMulNBits(const Tensor* a, const Tensor* b, const Tensor
const uint32_t weight_index,
const Tensor* weight_index_indirect = nullptr);

// The optional M / has_weight_idx_indirect / y arguments fold the original
// dispatch-precondition (DP4A is preferred when M is large enough, or
// unconditionally on FP32 outputs and Qualcomm GPUs) into the feasibility check
// so callers don't need a separate wrapper. Defaults make the precondition
// trivially satisfied for callers that only want the feasibility check.
bool CanApplyDP4AMatrixMatMulNBits(onnxruntime::webgpu::ComputeContext& context,
uint64_t accuracy_level,
uint32_t block_size,
uint32_t N,
uint32_t K,
uint32_t components_k);
uint32_t components_k,
uint32_t M = std::numeric_limits<uint32_t>::max(),
bool has_weight_idx_indirect = false,
const Tensor* y = nullptr);

} // namespace webgpu
} // namespace contrib
Expand Down
Loading
Loading