Skip to content

Commit 2bf7c3d

Browse files
author
ssjia
committed
Update
[ghstack-poisoned]
1 parent 45581d5 commit 2bf7c3d

4 files changed

Lines changed: 174 additions & 13 deletions

File tree

backends/vulkan/runtime/graph/ops/impl/Convolution.cpp

Lines changed: 70 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#include <executorch/backends/vulkan/runtime/graph/ops/OperatorRegistry.h>
1212

1313
#include <executorch/backends/vulkan/runtime/graph/ops/impl/Common.h>
14+
#include <executorch/backends/vulkan/runtime/graph/ops/impl/Conv2dGemm.h>
1415
#include <executorch/backends/vulkan/runtime/graph/ops/impl/Staging.h>
1516

1617
#include <executorch/backends/vulkan/runtime/graph/ops/utils/StagingUtils.h>
@@ -296,6 +297,42 @@ Conv2dMethod get_conv2d_method(
296297
return Conv2dMethod::SlidingWindow;
297298
}
298299

300+
// Decide whether a SlidingWindow conv2d should be computed via the
301+
// im2col + GEMM path (conv2d_gemm_impl) instead of the direct convolution
302+
// shader. Across 26 configs on Mali-G715 (buffer path) and Adreno SM8650
303+
// (texture path): FP32 cases were numerically verified against the reference;
304+
// FP16 cases were routing/dispatch-validated only (the reference is float-only
305+
// for the large shapes, so FP16 outputs were not numerically checked).
306+
//
307+
// Preconditions (fall back to direct conv if any fail — the im2col path is
308+
// either not applicable or not beneficial):
309+
// - groups == 1
310+
// - dilation == 1 (all dims)
311+
// - Kh * Kw > 1 (1x1 already routed to the optimized conv2d_pw path)
312+
//
313+
// Selection rule: use im2col on Mali universally, or once the output channel
314+
// count is large enough to amortize the fixed ~N*K_total im2col gather cost.
315+
bool should_use_conv2d_im2col(
316+
ComputeGraph& graph,
317+
const ValueRef weight_data,
318+
const int64_t groups_val,
319+
const Kernel2dParams& kernel_params) {
320+
if (groups_val != 1) {
321+
return false;
322+
}
323+
if (kernel_params.dilation[0] != 1 || kernel_params.dilation[1] != 1) {
324+
return false;
325+
}
326+
const auto weight_sizes = graph.sizes_of(weight_data);
327+
const int64_t kernel_h = weight_sizes.at(2);
328+
const int64_t kernel_w = weight_sizes.at(3);
329+
if (kernel_h * kernel_w <= 1) {
330+
return false;
331+
}
332+
const int64_t c_out = weight_sizes.at(0);
333+
return graph.device_is_mali() || c_out >= 128;
334+
}
335+
299336
utils::uvec3 create_conv2d_global_wg_size(
300337
ComputeGraph& graph,
301338
const Conv2dMethod method,
@@ -425,7 +462,8 @@ void add_conv2d_node(
425462
const ValueRef out_min,
426463
const ValueRef out_max,
427464
const ValueRef out,
428-
const bool clamp_out) {
465+
const bool clamp_out,
466+
const bool force_direct) {
429467
const bool transposed_val = graph.get_bool(transposed);
430468

431469
float out_min_val = 0.0f;
@@ -473,6 +511,37 @@ void add_conv2d_node(
473511
out_max_val);
474512
}
475513

514+
const Kernel2dParams kernel_params = create_kernel2d_params(
515+
graph,
516+
weight_data,
517+
/*kernel_size_only = */ false,
518+
stride,
519+
padding,
520+
dilation);
521+
522+
// SlidingWindow conv2d: route to the im2col + GEMM path when the heuristic
523+
// indicates it is beneficial, falling back to the direct convolution shader
524+
// otherwise. `force_direct` bypasses the heuristic entirely and forces the
525+
// direct path (used by tests to exercise the direct shader regardless of
526+
// device); the default (false) reproduces the production routing exactly.
527+
const bool use_im2col = force_direct
528+
? false
529+
: should_use_conv2d_im2col(graph, weight_data, groups_val, kernel_params);
530+
if (use_im2col) {
531+
return conv2d_gemm_impl(
532+
graph,
533+
in,
534+
weight_data,
535+
bias,
536+
stride,
537+
padding,
538+
dilation,
539+
out,
540+
clamp_out,
541+
out_min_val,
542+
out_max_val);
543+
}
544+
476545
ValueRef arg_weight = prepack_weights(graph, weight_data, method);
477546
ValueRef arg_bias = prepack_biases(
478547
graph,
@@ -489,13 +558,6 @@ void add_conv2d_node(
489558

490559
check_conv_args(graph, in, out);
491560

492-
Kernel2dParams kernel_params = create_kernel2d_params(
493-
graph,
494-
weight_data,
495-
/*kernel_size_only = */ false,
496-
stride,
497-
padding,
498-
dilation);
499561
Conv2dParams extra_params =
500562
create_conv2d_params(graph, weight_data, kernel_params, transposed_val);
501563

backends/vulkan/runtime/graph/ops/impl/Convolution.h

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,26 @@ void resize_conv2d_node(
5656
const std::vector<ArgGroup>& args,
5757
const std::vector<ValueRef>& extra_args);
5858

59+
// `force_direct` overrides the im2col-vs-direct routing heuristic: when true,
60+
// a SlidingWindow conv2d always takes the direct sliding-window path,
61+
// bypassing should_use_conv2d_im2col(). The default (false) preserves the
62+
// production routing exactly. Pointwise / Depthwise / Transposed methods are
63+
// unaffected by this flag.
64+
void add_conv2d_node(
65+
ComputeGraph& graph,
66+
const ValueRef in,
67+
const ValueRef weight_data,
68+
const ValueRef bias,
69+
const ValueRef stride,
70+
const ValueRef padding,
71+
const ValueRef dilation,
72+
const ValueRef transposed,
73+
const ValueRef output_padding,
74+
const ValueRef groups,
75+
const ValueRef out_min,
76+
const ValueRef out_max,
77+
const ValueRef out,
78+
const bool clamp_out,
79+
const bool force_direct = false);
80+
5981
} // namespace vkcompute

backends/vulkan/test/custom_ops/impl/TestConv2d.cpp

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
#include <executorch/backends/vulkan/runtime/graph/ops/impl/Common.h>
1212
#include <executorch/backends/vulkan/runtime/graph/ops/impl/Conv2dGemm.h>
13+
#include <executorch/backends/vulkan/runtime/graph/ops/impl/Convolution.h>
1314

1415
#include <optional>
1516

@@ -29,7 +30,10 @@ void test_conv2d(ComputeGraph& graph, const std::vector<ValueRef>& args) {
2930
// args[10] = output [N, C_out, H_out, W_out]
3031
//
3132
// impl_selector grammar:
32-
// "" -> aten.convolution.default (direct sliding-window)
33+
// "" -> aten.convolution.default (heuristic-routed:
34+
// should_use_conv2d_im2col() picks direct vs im2col)
35+
// "direct" -> add_conv2d_node(force_direct=true): forces the direct
36+
// sliding-window path, bypassing the routing heuristic
3337
// "im2col" -> et_vk.conv2d_gemm.default, auto im2col storage
3438
// "im2col_buffer"-> im2col/GEMM, force buffer im2col intermediate
3539
// "im2col_tex2d" -> im2col/GEMM, force texture2d im2col intermediate
@@ -88,6 +92,31 @@ void test_conv2d(ComputeGraph& graph, const std::vector<ValueRef>& args) {
8892
graph.add_scalar_list<int64_t>(std::vector<int64_t>{0, 0});
8993
ValueRef groups = graph.add_scalar<int64_t>(1);
9094

95+
// The "direct" selector must reach the exact direct sliding-window dispatch
96+
// the heuristic would otherwise pick. The registered op can only route via
97+
// the heuristic, so call add_conv2d_node directly with force_direct=true to
98+
// bypass it (mirroring how the forced-storage variants call
99+
// conv2d_gemm_impl).
100+
if (impl_selector == "direct") {
101+
add_conv2d_node(
102+
graph,
103+
input,
104+
weight,
105+
bias,
106+
stride,
107+
padding,
108+
dilation,
109+
transposed,
110+
output_padding,
111+
groups,
112+
/*out_min=*/kDummyValueRef,
113+
/*out_max=*/kDummyValueRef,
114+
out,
115+
/*clamp_out=*/false,
116+
/*force_direct=*/true);
117+
return;
118+
}
119+
91120
const std::string target_op = (impl_selector == "im2col")
92121
? "et_vk.conv2d_gemm.default"
93122
: "aten.convolution.default";

backends/vulkan/test/custom_ops/test_conv2d.cpp

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -478,13 +478,49 @@ static std::vector<TestCase> generate_conv2d_test_cases() {
478478
false},
479479
};
480480

481-
// Two implementation variants: direct sliding-window (default) and im2col.
482-
const std::vector<std::string> impls = {"", "im2col"};
481+
// Boundary pair straddling the should_use_conv2d_im2col() c_out >= 128
482+
// routing threshold. Spatial dims are tiny (8x8) so the FP32 float reference
483+
// stays cheap, but c_out = 64 / 128 are both >= kRefDimSizeLimit, so these
484+
// get the PERF label. FP32 PERF cases are still numerically VERIFIED (the
485+
// reference's invalid_argument throw that skips the check only fires for
486+
// half), so both implementations are cross-checked against the float
487+
// reference at the boundary. Run all three impls: at c_out = 64 the heuristic
488+
// ("") picks direct on Adreno / im2col on Mali; at c_out = 128 it picks
489+
// im2col on both — and "direct"/"im2col" force each path regardless, proving
490+
// the two implementations agree at the boundary on either device.
491+
std::vector<Conv2dTestConfig> boundary_configs = {
492+
// c_out = 64 (< 128): below the threshold
493+
{InputDims(1, 16, 8, 8),
494+
64,
495+
KernelSize(3, 3),
496+
Stride(1, 1),
497+
Padding(1, 1),
498+
Dilation(1, 1),
499+
false},
500+
// c_out = 128 (== 128): at/above the threshold
501+
{InputDims(1, 16, 8, 8),
502+
128,
503+
KernelSize(3, 3),
504+
Stride(1, 1),
505+
Padding(1, 1),
506+
Dilation(1, 1),
507+
false},
508+
};
509+
510+
// Implementation variants exercised for every small ACCU shape:
511+
// "" -> heuristic-routed (should_use_conv2d_im2col picks direct on
512+
// Adreno for small c_out, im2col on Mali)
513+
// "im2col" -> forced im2col/GEMM path
514+
// "direct" -> forced direct sliding-window path (force_direct=true)
515+
// Including "direct" guarantees the direct shader gets reference-checked on
516+
// BOTH devices — without it, Mali would always route "" to im2col and never
517+
// exercise the direct path.
518+
const std::vector<std::string> impls = {"", "im2col", "direct"};
483519
// Forced-storage im2col variants for the per-variant ACCU coverage.
484520
const std::vector<std::string> forced_storage_impls = {
485521
"im2col_buffer", "im2col_tex2d", "im2col_tex3d"};
486522

487-
// Generate accuracy test cases for both impls and both dtypes. FP16 small
523+
// Generate accuracy test cases for all impls and both dtypes. FP16 small
488524
// shapes get a real reference check (gated in conv2d_reference_impl); we run
489525
// both dtypes so we catch correctness regressions in either path. Large-K
490526
// half stays timing-only via the reference's PERF-shape throw.
@@ -515,7 +551,19 @@ static std::vector<TestCase> generate_conv2d_test_cases() {
515551
}
516552
}
517553

518-
// Generate performance test cases (float and half) for both impls.
554+
// Generate the c_out boundary pair (FP32 only) through all three impls.
555+
// FP32 PERF cases are reference-VERIFIED, so the direct and im2col paths are
556+
// both cross-checked against the float reference at the routing threshold.
557+
for (const auto& config : boundary_configs) {
558+
for (auto st : storage_types) {
559+
for (const auto& impl : impls) {
560+
test_cases.push_back(
561+
create_conv2d_test_case(config, vkapi::kFloat, st, layout, impl));
562+
}
563+
}
564+
}
565+
566+
// Generate performance test cases (float and half) for all impls.
519567
for (const auto& config : perf_configs) {
520568
std::vector<vkapi::ScalarType> dtypes = {vkapi::kFloat, vkapi::kHalf};
521569
for (auto dtype : dtypes) {

0 commit comments

Comments
 (0)