Skip to content

webgpu: using subgroup matrix to optimize Conv operator - #32252

Open
xhcao wants to merge 17 commits into
microsoft:mainfrom
xhcao:subgroup_matrix_conv_2
Open

xhcao wants to merge 17 commits into
microsoft:mainfrom
xhcao:subgroup_matrix_conv_2

Conversation

@xhcao

@xhcao xhcao commented Aug 25, 2026 •

Copy link
Copy Markdown
Contributor

Description

Adds a subgroup-matrix Conv path for the WebGPU EP that computes the convolution as
an implicit GEMM. Covers F16, channels-last, group == 1 convs on devices reporting
an 8x16x16 subgroup-matrix config, and falls back to the existing Conv paths
otherwise.

Motivation and Context

Conv dominates runtime in vision models, but the existing WebGPU Conv paths do not
use the subgroup-matrix units that the MatMul path already exploits on Intel
Xe2/Xe3. This brings the same acceleration to Conv.

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

@jchen10

Copy link
Copy Markdown
Contributor

/run bots

@qjia7

Copy link
Copy Markdown
Contributor

The /run bots request was received from Jie Chen (@jchen10), but no CI run was started. No pending action_required workflow runs matched the current PR head. This pull request currently has merge conflicts; please resolve them before requesting CI again.

@xhcao
xhcao force-pushed the subgroup_matrix_conv_2 branch from 5d0db8c to 3702f0f Compare September 20, 2026 04:53
@xhcao

xhcao commented Sep 21, 2026

Copy link
Copy Markdown
Contributor Author

Jie Chen (@jchen10) I had also fixed out-of-bound issue, you could start to review the PR, thanks.

@xhcao
xhcao marked this pull request as ready for review October 16, 2026 02:53
Copilot AI balanced review requested due to automatic review settings October 16, 2026 02:53
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

Copilot AI left a comment

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.

🟡 Changes recommended

Segmented-weight loads and FP16 partial-sum overflow can produce incorrect convolution results.

2 open findings
What changed in this PR

Adds subgroup-matrix acceleration for eligible FP16, channels-last convolutions in the WebGPU execution provider, retaining existing fallback paths.

Changes:

  • Adds an implicit-GEMM shader with bias and fused activations.
  • Integrates capability checks, weight caching, and Intel-specific tiling.
  • Adds convolution correctness tests across shapes and activations.
File Description
onnxruntime/​test/​providers/​webgpu/​subgroup_matrix_conv_test.cc Adds convolution correctness coverage.
onnxruntime/​core/​providers/​webgpu/​vendor/​intel/​math/​subgroup_matrix_tiling_selector.h Declares Conv selector and shared scratch budget.
onnxruntime/​core/​providers/​webgpu/​vendor/​intel/​math/​subgroup_matrix_tiling_selector.cc Adds Conv-specific tiling and safety clamps.
onnxruntime/​core/​providers/​webgpu/​nn/​subgroup_matrix_conv.h Declares optimized Conv interfaces.
onnxruntime/​core/​providers/​webgpu/​nn/​subgroup_matrix_conv.cc Implements eligibility, weight caching, and dispatch.
onnxruntime/​core/​providers/​webgpu/​nn/​subgroup_matrix_conv_8x16x16.wgsl.template Implements subgroup-matrix convolution.
onnxruntime/​core/​providers/​webgpu/​nn/​conv.h Adds optimized implementation state.
onnxruntime/​core/​providers/​webgpu/​nn/​conv.cc Integrates optimized dispatch and prepacking decisions.

🧠 Review effort: Balanced


💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

return false;
}

const std::optional<SubgroupMatrixConfig> config = GetSupportedConfig(context);
let a_scratch_base = sg_index * kTileM * kSgMatK; // this subgroup's A staging slot

// Accumulators: kSgMatCountM M blocks x kSgMatCountN N blocks (sg_mat_c<mi>_<ni>).
var sg_mat_c0_0: subgroup_matrix_result<f16, sg_mat_n, sg_mat_m>;
@xhcao

xhcao commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor Author

There were concerns regarding the Tuned Tree on the special devices, so I had removed the Conv Tuned Tree, now the PR only has Heuristic Tiling function. If there is a better way, we could add the Conv Tuned Tree in another PR.
Below table is performance comparation among Exact decision tree, Opitmized decision tree and Heuristic tiling + win + vulkan, Heuristic tiling + win + D3D.

<style> </style>
models Exact decision tree (%) Opitmized decision tree(%) Heuristic tiling(%)+ win + vulkan Heuristic tiling(%)+ win + D3D
depth-anything-base-fp16-pow-fp32 -6 -5 -2 -1.5
detr-resnet-50-fp16 -34 -28 -25 -18
modnet-fp16 -38 -40 -33 -28
resnet50-v1-f16-demo -50 -50 -51 -35
sd-turbo-unet-fp16-demo-layernorm -30 -11 -6 -12
sd-turbo-vae-decoder-fp16-demo -18 -10 -3 -4
sdunet-v1.5-demo-layernorm -19 -8 -24 -12
yolov8m-fp16 -41 -41 -29 -30
yolov8n-fp16 -39 -32 -28 -26
yolov8n-pose-fp16 -30 -28 -24 -32
yolov8x-pose-fp16 -30 -30 -20 -20

Comment on lines +99 to +102
let channel_i_vec = uniforms.channel_i / kVecSize;
let oh = m / uniforms.output_w;
let ow = m % uniforms.output_w;
let cin_vec = k_vec % channel_i_vec;

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.

Use cin or channel_i naming consistently.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Do you mean the same name as uniforms.channel_i? But channel_i_vec is related with kVecSize in line 99.

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.

How do you like this way:

    let cin_vec = uniforms.cin / kVecSize;
    let oh = m / uniforms.output_w;
    let ow = m % uniforms.output_w;
    let k_cin = k_vec % cin_vec;
    let k_hw = k_vec / cin_vec;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

Comment on lines +108 to +112
if (ih < 0 || ih >= i32(uniforms.src_h) || iw < 0 || iw >= i32(uniforms.src_w)) {
return input_src_value_t();
}
let src_vec_idx = src_vec_batch_offset +
(u32(ih) * uniforms.src_w + u32(iw)) * channel_i_vec + cin_vec;

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.

Similarly, use input_w/h and output_w/h, or src_w/h and dst_w/h. Don't mix them, like src_w, output_w.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

Comment on lines +278 to +279
let k_vec_base = kb * kSgMatK / kVecSize;
let k_vec_per_row = kSgMatK / kVecSize;

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.

let k_vec_per_sgmat_row = kSgMatK / kVecSize;
let k_vec_base = kb * k_vec_per_sgmat_row;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

return;
}
let slice_idx = workgroup_idx % tiles_per_slice; // tile index within the slice
let n_tile = slice_idx % uniforms.num_n_tile; // which kTileN-wide column tile

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.

tiles_per_slice is actually tiles_per_batch, right? So, slice_idx should be tile_idx. It seems there is no need to introduce a distracting concept of slice.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

Comment on lines +161 to +162
let src_vec_batch_offset = batch_id * uniforms.src_h * uniforms.src_w * (uniforms.channel_i / kVecSize);
let out_batch_offset = batch_id * uniforms.M * uniforms.N;

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.

base, start, and offset are used for naming a bit too casually. we'd better cleanup a little bit. Personally I am in favor in_batch_base_vec over src_vec_batch_offset, and out_batch_base over out_batch_offset.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

Comment on lines +127 to +129
if (parent.PrepackedKernel() != nullptr) {
return Status::OK();
}

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.

Use ORT_ENFORCE instead?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Cannot use ORT_ENFORCE here, I update the code comments

Comment on lines +282 to +293
// Safety net: the Conv selector already enforces the Conv scratch budget
// (output tiles + im2col A staging), so this clamp normally does nothing; it
// guards against a stray oversized split-K from the selector. Only split_k is
// clamped, not the tile area: ConvHeuristicTiling always picks the smallest
// tile_m (8), so the largest candidate tile needs 8 * (64 + 16) = 640 elements
// at split_k 1, or 8 * 8 * 80 = 5120 at the largest split_k (8), well inside
// the budget.
uint32_t split_k = tiling->split_k;
while (split_k > 1 &&
split_k * tile_m * (tile_n + config.K) > intel::kMaxScratchElems) {
split_k /= 2;
}

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.

As the comment says, this logic is unnecessary. Simply remove it or add an ORT_ENFORCE.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

Comment on lines +32 to +37
// Combined workgroup-memory (SLM) budget in f16 elements for the two scratch
// buffers the conv kernel uses: the split-K output tiles
// (split_k * tile_m * tile_n) plus the per-subgroup im2col A staging
// (split_k * tile_m * sg_mat_k). split_k is clamped to fit this. Shares
// intel::kMaxScratchElems rather than hardcoding a second copy of the same
// budget the selector already sizes tilings against.

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.

The comment seems irrelevant to the code lines nearby.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

Comment on lines +39 to +46
// Largest per-tile subgroup-matrix counts the WGSL template unrolls: it generates the
// accumulators sg_mat_c<mi>_<ni> for mi < 8 and ni < 4. A tiling above these would be
// accepted by the selector but silently mis-computed -- the kernel would accumulate
// only the blocks it generated while the write-out still covers the whole tile. The
// tile candidates the selector picks from stay inside this, so this is enforced as a
// safety net rather than something expected to fire.
constexpr uint32_t kMaxSgMatCountM = 8;
constexpr uint32_t kMaxSgMatCountN = 4;

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.

The selector should ensure this, and we don't need to bother to check it again here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

If a future change to the tiling selector ever violated this, the WGSL template would silently under-accumulate (it only unrolls ni < 4 accumulators) instead of crashing — a wrong answer with no visible error.

@jchen10 Jie Chen (jchen10) Oct 21, 2026 •

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.

The guard itself somewhat makes sense, but should we check it here? The values are specific to the 8x16x16 WGSL template. Perhaps we'd better put it somewhere template specific. Not mandatory in this PR. We can do it later.

@xhcao xhcao left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Jie, please take a look again, thanks

Comment on lines +127 to +129
if (parent.PrepackedKernel() != nullptr) {
return Status::OK();
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Cannot use ORT_ENFORCE here, I update the code comments

Comment on lines +39 to +46
// Largest per-tile subgroup-matrix counts the WGSL template unrolls: it generates the
// accumulators sg_mat_c<mi>_<ni> for mi < 8 and ni < 4. A tiling above these would be
// accepted by the selector but silently mis-computed -- the kernel would accumulate
// only the blocks it generated while the write-out still covers the whole tile. The
// tile candidates the selector picks from stay inside this, so this is enforced as a
// safety net rather than something expected to fire.
constexpr uint32_t kMaxSgMatCountM = 8;
constexpr uint32_t kMaxSgMatCountN = 4;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

If a future change to the tiling selector ever violated this, the WGSL template would silently under-accumulate (it only unrolls ni < 4 accumulators) instead of crashing — a wrong answer with no visible error.

Comment on lines +282 to +293
// Safety net: the Conv selector already enforces the Conv scratch budget
// (output tiles + im2col A staging), so this clamp normally does nothing; it
// guards against a stray oversized split-K from the selector. Only split_k is
// clamped, not the tile area: ConvHeuristicTiling always picks the smallest
// tile_m (8), so the largest candidate tile needs 8 * (64 + 16) = 640 elements
// at split_k 1, or 8 * 8 * 80 = 5120 at the largest split_k (8), well inside
// the budget.
uint32_t split_k = tiling->split_k;
while (split_k > 1 &&
split_k * tile_m * (tile_n + config.K) > intel::kMaxScratchElems) {
split_k /= 2;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

Comment on lines +32 to +37
// Combined workgroup-memory (SLM) budget in f16 elements for the two scratch
// buffers the conv kernel uses: the split-K output tiles
// (split_k * tile_m * tile_n) plus the per-subgroup im2col A staging
// (split_k * tile_m * sg_mat_k). split_k is clamped to fit this. Shares
// intel::kMaxScratchElems rather than hardcoding a second copy of the same
// budget the selector already sizes tilings against.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

Comment on lines +99 to +102
let channel_i_vec = uniforms.channel_i / kVecSize;
let oh = m / uniforms.output_w;
let ow = m % uniforms.output_w;
let cin_vec = k_vec % channel_i_vec;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Do you mean the same name as uniforms.channel_i? But channel_i_vec is related with kVecSize in line 99.

Comment on lines +108 to +112
if (ih < 0 || ih >= i32(uniforms.src_h) || iw < 0 || iw >= i32(uniforms.src_w)) {
return input_src_value_t();
}
let src_vec_idx = src_vec_batch_offset +
(u32(ih) * uniforms.src_w + u32(iw)) * channel_i_vec + cin_vec;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

return;
}
let slice_idx = workgroup_idx % tiles_per_slice; // tile index within the slice
let n_tile = slice_idx % uniforms.num_n_tile; // which kTileN-wide column tile

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

Comment on lines +161 to +162
let src_vec_batch_offset = batch_id * uniforms.src_h * uniforms.src_w * (uniforms.channel_i / kVecSize);
let out_batch_offset = batch_id * uniforms.M * uniforms.N;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

Comment on lines +278 to +279
let k_vec_base = kb * kSgMatK / kVecSize;
let k_vec_per_row = kSgMatK / kVecSize;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

Comment on lines +34 to +35
constexpr uint32_t kMatMulScratchNPad = 0;
constexpr uint32_t kConvScratchNPad = kSubgroupMatrixK;

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.

ScratchNPad doesn't look like a straightforward and informative name. May ask AI to suggest a better one.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

Comment on lines -94 to -105
// This tile fills the machine on its own. Among all such tiles, keep the
// largest-area one (best operand reuse); this also supersedes any
// under-filling pick made earlier.
if (!filled || area > best_area) {
filled = true;
best_area = area;
tile_m = tm;
tile_n = tn;
}
} else if (!filled && (tiles > best_tiles || (tiles == best_tiles && area > best_area))) {
// No tile has filled the machine yet: chase occupancy by maximizing the
// number of independent tiles, breaking ties toward larger area.

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.

Keep the original comments as the corresponding code logic is not easy to read.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

Comment on lines -114 to -115
// Add split-K only while the independent tiles under-fill the machine; cap the
// total at 2x hardware and keep enough K work per subgroup and scratch budget.

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.

Keep the comment as well

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

Comment on lines +144 to +151
// Halves split_k until it fits K's block count and the scratch budget. No-op today
// (ConvHeuristicTiling already guarantees both); a safety net for future callers.
void ClampSplitKToProblem(SubgroupMatrixTiling& t, uint32_t K, uint32_t scratch_n_pad) {
const uint32_t k_blocks = K / kSubgroupMatrixK;
while (t.split_k > 1 && (t.split_k > k_blocks || ScratchElems(t, scratch_n_pad) > kMaxScratchElems)) {
t.split_k /= 2;
}
}

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.

Remove the unused logic to be less distracting.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

Comment on lines +178 to +180
if (N < tiling->tile_n) {
return Status::OK();
}

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.

Use ORT_ENFORCE?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

No, decline means that subgroup matrix path cannot handle the current Conv node, let the others path to handle it. It does not need to throw an error here.

Comment on lines +39 to +46
// Largest per-tile subgroup-matrix counts the WGSL template unrolls: it generates the
// accumulators sg_mat_c<mi>_<ni> for mi < 8 and ni < 4. A tiling above these would be
// accepted by the selector but silently mis-computed -- the kernel would accumulate
// only the blocks it generated while the write-out still covers the whole tile. The
// tile candidates the selector picks from stay inside this, so this is enforced as a
// safety net rather than something expected to fire.
constexpr uint32_t kMaxSgMatCountM = 8;
constexpr uint32_t kMaxSgMatCountN = 4;

@jchen10 Jie Chen (jchen10) Oct 21, 2026 •

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.

The guard itself somewhat makes sense, but should we check it here? The values are specific to the 8x16x16 WGSL template. Perhaps we'd better put it somewhere template specific. Not mandatory in this PR. We can do it later.

Comment on lines +99 to +102
let channel_i_vec = uniforms.channel_i / kVecSize;
let oh = m / uniforms.output_w;
let ow = m % uniforms.output_w;
let cin_vec = k_vec % channel_i_vec;

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.

How do you like this way:

    let cin_vec = uniforms.cin / kVecSize;
    let oh = m / uniforms.output_w;
    let ow = m % uniforms.output_w;
    let k_cin = k_vec % cin_vec;
    let k_hw = k_vec / cin_vec;

@xhcao xhcao left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Jie, PTAL.

Comment on lines +178 to +180
if (N < tiling->tile_n) {
return Status::OK();
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

No, decline means that subgroup matrix path cannot handle the current Conv node, let the others path to handle it. It does not need to throw an error here.

Comment on lines +99 to +102
let channel_i_vec = uniforms.channel_i / kVecSize;
let oh = m / uniforms.output_w;
let ow = m % uniforms.output_w;
let cin_vec = k_vec % channel_i_vec;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

Comment on lines +34 to +35
constexpr uint32_t kMatMulScratchNPad = 0;
constexpr uint32_t kConvScratchNPad = kSubgroupMatrixK;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

Comment on lines -94 to -105
// This tile fills the machine on its own. Among all such tiles, keep the
// largest-area one (best operand reuse); this also supersedes any
// under-filling pick made earlier.
if (!filled || area > best_area) {
filled = true;
best_area = area;
tile_m = tm;
tile_n = tn;
}
} else if (!filled && (tiles > best_tiles || (tiles == best_tiles && area > best_area))) {
// No tile has filled the machine yet: chase occupancy by maximizing the
// number of independent tiles, breaking ties toward larger area.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

Comment on lines -114 to -115
// Add split-K only while the independent tiles under-fill the machine; cap the
// total at 2x hardware and keep enough K work per subgroup and scratch budget.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

Comment on lines +144 to +151
// Halves split_k until it fits K's block count and the scratch budget. No-op today
// (ConvHeuristicTiling already guarantees both); a safety net for future callers.
void ClampSplitKToProblem(SubgroupMatrixTiling& t, uint32_t K, uint32_t scratch_n_pad) {
const uint32_t k_blocks = K / kSubgroupMatrixK;
while (t.split_k > 1 && (t.split_k > k_blocks || ScratchElems(t, scratch_n_pad) > kMaxScratchElems)) {
t.split_k /= 2;
}
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

@jchen10

Copy link
Copy Markdown
Contributor

LGTM, thanks!

Jiajia Qin (@qjia7) PTAL

@jchen10

Copy link
Copy Markdown
Contributor

/run bots

@qjia7

Copy link
Copy Markdown
Contributor

The request was received. Approve workflow runs. Thanks.

Comment thread onnxruntime/core/providers/webgpu/nn/subgroup_matrix_conv.cc Fixed
@xhcao

xhcao commented Oct 26, 2026

Copy link
Copy Markdown
Contributor Author

/run bots

@qjia7

Copy link
Copy Markdown
Contributor

The request was received. Approve workflow runs. Thanks.

@xhcao

xhcao commented Oct 26, 2026

Copy link
Copy Markdown
Contributor Author

/run failed bots

@qjia7

Copy link
Copy Markdown
Contributor

The request was received. Failed workflow jobs were queued for rerun. Thanks.

// trailing partial N tile back to N - tile_n, which needs tile_n <= N, and N
// itself need not be a multiple of kSubgroupMatrixN. Callers must have already
// rejected N < kSubgroupMatrixN.
void ClampTileNToProblem(SubgroupMatrixTiling& t, uint32_t N) {

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.

How about renaming to CapTileNToAlignedN?

aligned_N = (N / subgroup_N) * subgroup_N;
tile_n = min(tile_n, aligned_N);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

auto transform_dim = [](int64_t dim) { return static_cast<int32_t>(dim); };
std::transform(local_pads.begin(), local_pads.end(), std::back_inserter(pads), transform_dim);
std::transform(local_strides.begin(), local_strides.end(), std::back_inserter(strides), transform_dim);
std::transform(local_dilations.begin(), local_dilations.end(), std::back_inserter(dilations), transform_dim);

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.

Could we extract lines 116–147 into a shared Conv geometry helper and reuse it in conv.cc? Both paths duplicate attribute defaulting and padding/output-shape inference. Keeping that logic shared would prevent differences in auto_pad handling. Please leave output allocation outside the helper so the optimized path can still decline safely.

Or Could we move the subgroup matrix Conv attempt after the common geometry calculation in conv.cc and pass in the resolved geometry? That would avoid duplicating shape/padding inference. We should keep the attempt before output allocation and path-specific transformations so fallback can reuse the same geometry safely.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done. The first option was adopted.

// this layout) -- blocked on PrePackInternal committing from the weight alone
// while Compute can still decline at runtime onto the im2col path.
Tensor run_local_ohwi_weight;
const Tensor* ohwi_weight = nullptr;

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.

Could we reuse the existing OHWI prepacking for constant weights instead of introducing a separate lazy transpose cache? Both paths consume the same layout. We would need to let subgroup Conv accept the packed weights and ensure runtime fallback remains valid after the original initializer is released. Non-constant weights would still need a per-run transpose.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done. Now SubgroupMatrixConv supported OHWI and HWIO layout, and removed the related code in original initializer.

return false;
}

LOGS_DEFAULT(VERBOSE) << "SubgroupMatrixConv ACCEPTED: kernel_shape[OIHW]=" << kernel_shape.ToString()

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.

Could we rename “ACCEPTED” to “initial eligibility checks passed”? Compute() can still decline afterward. Please put the actual dispatch message immediately before RunProgram() so it identifies the selected implementation reliably.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

@xhcao xhcao left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Jiajia Qin (@qjia7) Please take a look, again. Thanks.

// trailing partial N tile back to N - tile_n, which needs tile_n <= N, and N
// itself need not be a multiple of kSubgroupMatrixN. Callers must have already
// rejected N < kSubgroupMatrixN.
void ClampTileNToProblem(SubgroupMatrixTiling& t, uint32_t N) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

auto transform_dim = [](int64_t dim) { return static_cast<int32_t>(dim); };
std::transform(local_pads.begin(), local_pads.end(), std::back_inserter(pads), transform_dim);
std::transform(local_strides.begin(), local_strides.end(), std::back_inserter(strides), transform_dim);
std::transform(local_dilations.begin(), local_dilations.end(), std::back_inserter(dilations), transform_dim);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done. The first option was adopted.

// this layout) -- blocked on PrePackInternal committing from the weight alone
// while Compute can still decline at runtime onto the im2col path.
Tensor run_local_ohwi_weight;
const Tensor* ohwi_weight = nullptr;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done. Now SubgroupMatrixConv supported OHWI and HWIO layout, and removed the related code in original initializer.

return false;
}

LOGS_DEFAULT(VERBOSE) << "SubgroupMatrixConv ACCEPTED: kernel_shape[OIHW]=" << kernel_shape.ToString()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

@xhcao

xhcao commented Oct 27, 2026

Copy link
Copy Markdown
Contributor Author

/run bots

@qjia7

Copy link
Copy Markdown
Contributor

The request was received. Approve workflow runs. Thanks.

let b_base = kb * kSgMatK * uniforms.N + global_base_n;
var sg_mat_b0: subgroup_matrix_right<f16, sg_mat_n, sg_mat_k> =
subgroupMatrixLoad<subgroup_matrix_right<f16, sg_mat_n, sg_mat_k>, row_major>(
&input_b, b_base + 0 * kSgMatN, uniforms.N);

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.

C2 [P1, new]: Guard odd-N HWIO weights before row-major subgroup-matrix loads

Location: onnxruntime/core/providers/webgpu/nn/subgroup_matrix_conv_8x16x16.wgsl.template:233-238; layout selection at subgroup_matrix_conv.cc:164-173.

The new shared-prepack route accepts HWIO weights and loads them as a row-major FP16 matrix with stride uniforms.N. On a supported Intel D3D12 adapter, take a constant-weight FusedConv with HardSwish, auto_pad=NOTSET, X NCHW [1,16,10,10], and W OIHW [17,16,3,3]. HardSwish is excluded from Im2ColMatMul, so PrePackInternal packs HWIO, while subgroup Conv still accepts K=144 and N=17. The new branch passes a 34-byte row stride; its shifted second N tile also starts at byte offset 2.

The DirectX matrix-load contract requires Wave-scope device-memory load strides and starting addresses to be at least 4-byte aligned. This is not just an old MatMul comment: the pinned Dawn v20261013.193955 storage-access lowering converts the FP16 offset/stride to bytes without padding, and its printer uses MatrixScope::Wave. The sibling MatMul explicitly pads odd FP16 N for this requirement.

Consequently, a valid model now reaches an unsupported/undefined matrix load, so numerical correctness is not guaranteed. At the previous reviewed head the same eligible Conv skipped prepacking and transposed to OHWI, whose K=144 column stride and tile offsets are aligned. This regression is introduced by the new HWIO path, not by sharing OHWI itself.

Please decline odd-N HWIO weights before output allocation, preserving the existing HWIO fallback, or provide an aligned/staged weight representation and matching tile addressing. Keep odd-N OHWI support. Add a path-verified HardSwish/odd-Cout constant-weight regression; the new HWIO test uses only Cout=32. The alignment violation is established by code and contract, not a claimed GPU reproduction.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants