Skip to content

Commit 73a1406

Browse files
authored
Add an aten.elu kernel and a single-kernel aten.silu (#24800)
Summary: ELU had no WebGPU kernel and SiLU ran as two kernels (sigmoid, then mul). This adds single-kernel elu and silu that handle NaN, infinities and exp overflow, kept from decomposition only where the partitioner will delegate them. This unblocks streaming audio decoders that use ELU and makes SiLU activations one dispatch. Reviewed By: SS-JIA Differential Revision: D127010335 Pull Request resolved: #24800
1 parent 47813d1 commit 73a1406

24 files changed

Lines changed: 658 additions & 45 deletions

‎backends/vulkan/op_registry.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2304,4 +2304,6 @@ def get_op_features(target: Any) -> OpFeatures:
23042304

23052305

23062306
def handles_own_prepacking(target: OpKey) -> bool:
2307-
return get_op_features(target).supports_prepacking
2307+
# Ops without a Vulkan registration (e.g. ones only a backend's
2308+
# op_features_overrides delegates) read constants through prepack nodes.
2309+
return has_impl(target) and get_op_features(target).supports_prepacking

‎backends/vulkan/partitioner/vulkan_partitioner.py‎

Lines changed: 43 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44
# This source code is licensed under the BSD-style license found in the
55
# LICENSE file in the root directory of this source tree.
66

7-
# pyre-strict
8-
97
import logging
108
from typing import (
119
Any,
@@ -521,17 +519,17 @@ def filter_fn(node: torch.fx.Node) -> bool:
521519

522520
return (ops_not_to_decompose, filter_fn)
523521

524-
def partition(self, exported_program: ExportedProgram) -> PartitionResult:
525-
# Run the CapabilityBasedPartitioner to return the largest possible
526-
# subgraphs containing the nodes with the tags
527-
partition_tags = {}
528-
529-
# Get all fusable subgraphs from fuse_patterns
530-
fusable_subgraphs = vk_patterns.get_all_fusable_subgraphs(
531-
exported_program.graph_module,
532-
self.options.get("skip_fusion_patterns", ()),
533-
)
534-
522+
def make_supported_operators(
523+
self,
524+
fusable_subgraphs: Optional[List[PatternMatch]] = None,
525+
constant_nodes: Optional[Set[torch.fx.Node]] = None,
526+
op_features_overrides: Optional[Dict[OpKey, OpFeatures]] = None,
527+
operator_allowlist: Optional[Set[OpKey]] = None,
528+
operator_blocklist: Optional[Set[OpKey]] = None,
529+
) -> VulkanSupportedOperators:
530+
"""The node support checker partition() uses, built from this
531+
partitioner's options. The optional arguments replace the
532+
corresponding partitioner settings."""
535533
# small_texture_limits opts into the conservative 3D texture limit that is
536534
# compatible with most desktop/laptop GPUs (the Vulkan spec only guarantees
537535
# 2048). An explicit texture_limits always takes precedence.
@@ -542,18 +540,46 @@ def partition(self, exported_program: ExportedProgram) -> PartitionResult:
542540
else:
543541
texture_limits = utils.DEFAULT_TEXTURE_LIMITS
544542
buffer_limit: int = self.options.get("buffer_limit", utils.DEFAULT_BUFFER_LIMIT)
545-
operator_support = VulkanSupportedOperators(
543+
return VulkanSupportedOperators(
546544
texture_limits,
547545
buffer_limit,
548546
require_dynamic_shape=self.options.get("require_dynamic_shapes", False),
549547
skip_bool_tensors=self.options.get("skip_bool_tensors", False),
550-
operator_blocklist=self.operator_blocklist,
551-
operator_allowlist=self.operator_allowlist,
548+
operator_blocklist=(
549+
self.operator_blocklist
550+
if operator_blocklist is None
551+
else operator_blocklist
552+
),
553+
operator_allowlist=(
554+
self.operator_allowlist
555+
if operator_allowlist is None
556+
else operator_allowlist
557+
),
552558
fusable_subgraphs=fusable_subgraphs,
553559
nn_module_blocklist=self.nn_module_blocklist,
554560
nn_module_allowlist=self.nn_module_allowlist,
555561
downcast_64_bit=self.options.get("downcast_64_bit", True),
556-
op_features_overrides=self.op_features_overrides,
562+
op_features_overrides=(
563+
self.op_features_overrides
564+
if op_features_overrides is None
565+
else op_features_overrides
566+
),
567+
constant_nodes=constant_nodes,
568+
)
569+
570+
def partition(self, exported_program: ExportedProgram) -> PartitionResult:
571+
# Run the CapabilityBasedPartitioner to return the largest possible
572+
# subgraphs containing the nodes with the tags
573+
partition_tags = {}
574+
575+
# Get all fusable subgraphs from fuse_patterns
576+
fusable_subgraphs = vk_patterns.get_all_fusable_subgraphs(
577+
exported_program.graph_module,
578+
self.options.get("skip_fusion_patterns", ()),
579+
)
580+
581+
operator_support = self.make_supported_operators(
582+
fusable_subgraphs=fusable_subgraphs,
557583
constant_nodes=utils.get_constant_nodes(exported_program),
558584
)
559585
capability_partitioner = CapabilityBasedPartitioner(

‎backends/webgpu/partitioner/partitioner.py‎

Lines changed: 72 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44
# This source code is licensed under the BSD-style license found in the
55
# LICENSE file in the root directory of this source tree.
66

7-
# pyre-strict
8-
97
import copy
108
from typing import Any, Callable, Dict, final, List, Optional, Tuple
119

@@ -18,7 +16,10 @@
1816
OpKey,
1917
vulkan_supported_ops,
2018
)
21-
from executorch.backends.vulkan.partitioner.vulkan_partitioner import VulkanPartitioner
19+
from executorch.backends.vulkan.partitioner.vulkan_partitioner import (
20+
VulkanPartitioner,
21+
VulkanSupportedOperators,
22+
)
2223
from executorch.exir.backend.partitioner import Partitioner, PartitionResult
2324
from executorch.exir.dialects._ops import ops as exir_ops
2425
from torch.export import ExportedProgram
@@ -54,8 +55,17 @@ def _layer_norm_check(
5455
return all(d == 1 for d in normalized_shape[:-1]) and vulkan_check(node)
5556

5657

57-
# Ops whose WebGPU kernels cover less than the Vulkan registry advertises, so the
58-
# unsupported forms stay on CPU instead of failing at delegate init.
58+
def _fp32_unary_features() -> OpFeatures:
59+
return OpFeatures(
60+
inputs_storage=vk_utils.ANY_STORAGE,
61+
inputs_dtypes={torch.float32},
62+
supports_resize=True,
63+
)
64+
65+
66+
# Ops whose WebGPU kernels cover less (or more) than the Vulkan registry
67+
# advertises, so unsupported forms stay on CPU instead of failing at delegate
68+
# init, and WebGPU-only kernels are delegated.
5969
_WEBGPU_OP_FEATURES: Dict[OpKey, OpFeatures] = {
6070
# fp32 and int32 kernels only; int64 is downcast to int32 on device.
6171
exir_ops.edge.aten.arange.start_step: OpFeatures(
@@ -66,12 +76,16 @@ def _layer_norm_check(
6676
exir_ops.edge.aten.native_layer_norm.default: _webgpu_features(
6777
exir_ops.edge.aten.native_layer_norm.default, _layer_norm_check
6878
),
79+
exir_ops.edge.aten.elu.default: _fp32_unary_features(),
80+
exir_ops.edge.aten.silu.default: _fp32_unary_features(),
6981
}
7082

7183

7284
# Vulkan fusion patterns whose fused ops the WebGPU runtime does not implement.
7385
_WEBGPU_SKIP_FUSION_PATTERNS = {"conv2d_with_epilogue"}
7486

87+
_MUL_TARGETS = (torch.ops.aten.mul.Tensor, exir_ops.edge.aten.mul.Tensor)
88+
7589

7690
@final
7791
class WebGPUPartitioner(Partitioner):
@@ -102,7 +116,59 @@ def __init__(
102116
def ops_to_not_decompose(
103117
self, ep: ExportedProgram
104118
) -> Tuple[List[torch._ops.OpOverload], Optional[Callable[[torch.fx.Node], bool]]]:
105-
return self._vulkan_partitioner.ops_to_not_decompose(ep)
119+
ops, vulkan_filter = self._vulkan_partitioner.ops_to_not_decompose(ep)
120+
silu_support = self._silu_support()
121+
122+
def filter_fn(node: torch.fx.Node) -> bool:
123+
if node.target in (
124+
torch.ops.aten.silu.default,
125+
exir_ops.edge.aten.silu.default,
126+
):
127+
return self._keeps_silu(node, ep, silu_support)
128+
return vulkan_filter is None or vulkan_filter(node)
129+
130+
return ops + [torch.ops.aten.silu.default], filter_fn
131+
132+
def _silu_support(self) -> VulkanSupportedOperators:
133+
"""The partitioner's own node support check, also accepting the aten
134+
silu that ops_to_not_decompose sees before the edge conversion."""
135+
vulkan = self._vulkan_partitioner
136+
aten_silu = torch.ops.aten.silu.default
137+
edge_silu = exir_ops.edge.aten.silu.default
138+
allowlist = vulkan.operator_allowlist
139+
if allowlist is not None and edge_silu in allowlist:
140+
allowlist = allowlist | {aten_silu}
141+
blocklist = vulkan.operator_blocklist
142+
if edge_silu in blocklist:
143+
blocklist = blocklist | {aten_silu}
144+
return vulkan.make_supported_operators(
145+
op_features_overrides={
146+
**_WEBGPU_OP_FEATURES,
147+
aten_silu: _WEBGPU_OP_FEATURES[edge_silu],
148+
},
149+
operator_allowlist=allowlist,
150+
operator_blocklist=blocklist,
151+
)
152+
153+
def _keeps_silu(
154+
self,
155+
node: torch.fx.Node,
156+
ep: ExportedProgram,
157+
silu_support: VulkanSupportedOperators,
158+
) -> bool:
159+
"""Whether `node` stays an aten.silu for the WebGPU silu kernel.
160+
161+
A kept op that is not delegated fails lowering, so keep it only where
162+
the partitioner will delegate it: in the top-level graph (control-flow
163+
submodules are not partitioned) and passing the partitioner's support
164+
check. Where it feeds a mul it decomposes to x * sigmoid(x), as before,
165+
so the SwiGLU fusion still matches.
166+
"""
167+
return (
168+
node.graph is ep.graph_module.graph
169+
and not any(user.target in _MUL_TARGETS for user in node.users)
170+
and silu_support.is_node_supported({}, node)
171+
)
106172

107173
def partition(self, exported_program: ExportedProgram) -> PartitionResult:
108174
return self._vulkan_partitioner.partition(exported_program)

‎backends/webgpu/runtime/WebGPUShaderRegistry.cpp‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,12 +135,14 @@
135135
#include <executorch/backends/webgpu/runtime/ops/unary/abs_wgsl.h>
136136
#include <executorch/backends/webgpu/runtime/ops/unary/clamp_wgsl.h>
137137
#include <executorch/backends/webgpu/runtime/ops/unary/cos_wgsl.h>
138+
#include <executorch/backends/webgpu/runtime/ops/unary/elu_wgsl.h>
138139
#include <executorch/backends/webgpu/runtime/ops/unary/exp_wgsl.h>
139140
#include <executorch/backends/webgpu/runtime/ops/unary/hardswish_wgsl.h>
140141
#include <executorch/backends/webgpu/runtime/ops/unary/neg_wgsl.h>
141142
#include <executorch/backends/webgpu/runtime/ops/unary/pow_scalar_wgsl.h>
142143
#include <executorch/backends/webgpu/runtime/ops/unary/round_wgsl.h>
143144
#include <executorch/backends/webgpu/runtime/ops/unary/rsqrt_wgsl.h>
145+
#include <executorch/backends/webgpu/runtime/ops/unary/silu_wgsl.h>
144146
#include <executorch/backends/webgpu/runtime/ops/unary/sin_wgsl.h>
145147
#include <executorch/backends/webgpu/runtime/ops/unary/sqrt_wgsl.h>
146148
#include <executorch/backends/webgpu/runtime/ops/unary/tanh_wgsl.h>
@@ -157,7 +159,7 @@
157159
namespace executorch::backends::webgpu {
158160
namespace {
159161

160-
constexpr std::array<WebGPUShaderInfo, 139> kShaderRegistry = {{
162+
constexpr std::array<WebGPUShaderInfo, 141> kShaderRegistry = {{
161163
{
162164
"abs",
163165
kAbsWGSL,
@@ -466,6 +468,13 @@ constexpr std::array<WebGPUShaderInfo, 139> kShaderRegistry = {{
466468
kDequantizePerTensorWorkgroupSizeY,
467469
kDequantizePerTensorWorkgroupSizeZ,
468470
},
471+
{
472+
"elu",
473+
kEluWGSL,
474+
kEluWorkgroupSizeX,
475+
kEluWorkgroupSizeY,
476+
kEluWorkgroupSizeZ,
477+
},
469478
{
470479
"embedding",
471480
kEmbeddingWGSL,
@@ -1012,6 +1021,13 @@ constexpr std::array<WebGPUShaderInfo, 139> kShaderRegistry = {{
10121021
kSigmoidWorkgroupSizeY,
10131022
kSigmoidWorkgroupSizeZ,
10141023
},
1024+
{
1025+
"silu",
1026+
kSiluWGSL,
1027+
kSiluWorkgroupSizeX,
1028+
kSiluWorkgroupSizeY,
1029+
kSiluWorkgroupSizeZ,
1030+
},
10151031
{
10161032
"silu_mul_fused",
10171033
kSiluMulFusedWGSL,

‎backends/webgpu/runtime/ops/unary/Activations.cpp‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,20 @@
66
* LICENSE file in the root directory of this source tree.
77
*/
88

9+
#include <executorch/backends/webgpu/runtime/WebGPUUtils.h>
910
#include <executorch/backends/webgpu/runtime/ops/OperatorRegistry.h>
1011
#include <executorch/backends/webgpu/runtime/ops/unary/UnaryOp.h>
1112
#include <executorch/backends/webgpu/runtime/ops/unary/abs_wgsl.h>
1213
#include <executorch/backends/webgpu/runtime/ops/unary/clamp_wgsl.h>
1314
#include <executorch/backends/webgpu/runtime/ops/unary/cos_wgsl.h>
15+
#include <executorch/backends/webgpu/runtime/ops/unary/elu_wgsl.h>
1416
#include <executorch/backends/webgpu/runtime/ops/unary/exp_wgsl.h>
1517
#include <executorch/backends/webgpu/runtime/ops/unary/hardswish_wgsl.h>
1618
#include <executorch/backends/webgpu/runtime/ops/unary/neg_wgsl.h>
1719
#include <executorch/backends/webgpu/runtime/ops/unary/pow_scalar_wgsl.h>
1820
#include <executorch/backends/webgpu/runtime/ops/unary/round_wgsl.h>
1921
#include <executorch/backends/webgpu/runtime/ops/unary/rsqrt_wgsl.h>
22+
#include <executorch/backends/webgpu/runtime/ops/unary/silu_wgsl.h>
2023
#include <executorch/backends/webgpu/runtime/ops/unary/sin_wgsl.h>
2124
#include <executorch/backends/webgpu/runtime/ops/unary/sqrt_wgsl.h>
2225
#include <executorch/backends/webgpu/runtime/ops/unary/tanh_wgsl.h>
@@ -100,6 +103,32 @@ void hardswish_impl(WebGPUGraph& graph, const std::vector<int>& args) {
100103
"hardswish");
101104
}
102105

106+
void silu_impl(WebGPUGraph& graph, const std::vector<int>& args) {
107+
add_unary_op(
108+
graph, args.at(0), args.at(1), kSiluWGSL, kSiluWorkgroupSizeX, "silu");
109+
}
110+
111+
void elu_impl(WebGPUGraph& graph, const std::vector<int>& args) {
112+
// aten.elu.default args: [in, alpha, scale, input_scale, out]; each scalar
113+
// defaults to 1.
114+
if (args.size() != 5) {
115+
throw std::runtime_error("WebGPU elu: expected 5 args");
116+
}
117+
const float alpha = utils::scalar_or(graph, args.at(1), 1.0f);
118+
const float scale = utils::scalar_or(graph, args.at(2), 1.0f);
119+
const float input_scale = utils::scalar_or(graph, args.at(3), 1.0f);
120+
add_unary_op(
121+
graph,
122+
args.at(0),
123+
args.at(4),
124+
kEluWGSL,
125+
kEluWorkgroupSizeX,
126+
"elu",
127+
alpha,
128+
scale,
129+
input_scale);
130+
}
131+
103132
void clamp_impl(WebGPUGraph& graph, const std::vector<int>& args) {
104133
// aten.clamp.default args: [in, min, max, out]; min/max None -> +/-inf.
105134
const float lo = get_val_or_inf(graph, args.at(1), /*is_max=*/false);
@@ -156,6 +185,8 @@ WEBGPU_REGISTER_OPERATORS {
156185
WEBGPU_REGISTER_OP(aten.round.default, round_impl);
157186
WEBGPU_REGISTER_OP(aten.neg.default, neg_impl);
158187
WEBGPU_REGISTER_OP(aten.hardswish.default, hardswish_impl);
188+
WEBGPU_REGISTER_OP(aten.silu.default, silu_impl);
189+
WEBGPU_REGISTER_OP(aten.elu.default, elu_impl);
159190
WEBGPU_REGISTER_OP(aten.clamp.default, clamp_impl);
160191
WEBGPU_REGISTER_OP(aten.hardtanh.default, hardtanh_impl);
161192
WEBGPU_REGISTER_OP(aten.pow.Tensor_Scalar, pow_scalar_impl);

‎backends/webgpu/runtime/ops/unary/UnaryOp.cpp‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,15 @@ namespace executorch::backends::webgpu {
2020
namespace {
2121

2222
// Canonical unary uniform layout; min/max mirror Vulkan min_max (used by the
23-
// clamp-family shaders; ignored by no-param activations).
23+
// clamp-family shaders; ignored by no-param activations). aux is a third
24+
// scalar (elu's input_scale).
2425
struct UnaryParams {
2526
uint32_t num_elements;
2627
float min;
2728
float max;
28-
uint32_t _pad;
29+
float aux;
2930
};
31+
static_assert(sizeof(UnaryParams) == 16, "UnaryParams must be 16 bytes");
3032

3133
} // namespace
3234

@@ -38,7 +40,8 @@ void add_unary_op(
3840
uint32_t wg_size_x,
3941
const char* op_name,
4042
float min,
41-
float max) {
43+
float max,
44+
float aux) {
4245
WGPUDevice device = graph.device();
4346

4447
const auto& in_tensor = graph.get_tensor(in_id);
@@ -66,6 +69,7 @@ void add_unary_op(
6669
params.num_elements = num_elements;
6770
params.min = min;
6871
params.max = max;
72+
params.aux = aux;
6973

7074
WGPUBuffer uniform_buffer =
7175
utils::make_uniform(device, &params, sizeof(UnaryParams));
@@ -99,7 +103,7 @@ void add_unary_op(
99103
WGPUBuffer params_buf = uniform_buffer;
100104
graph.add_tensor_resize_hook(
101105
in_id,
102-
[in_id, out_id, wg_size, dispatch_idx, params_buf, min, max](
106+
[in_id, out_id, wg_size, dispatch_idx, params_buf, min, max, aux](
103107
WebGPUGraph& g) {
104108
const auto& d = g.cur_dims(in_id);
105109
const uint64_t numel = utils::numel_of(d);
@@ -108,6 +112,7 @@ void add_unary_op(
108112
p.num_elements = static_cast<uint32_t>(numel);
109113
p.min = min;
110114
p.max = max;
115+
p.aux = aux;
111116
wgpuQueueWriteBuffer(g.queue(), params_buf, 0, &p, sizeof(p));
112117
const utils::WgCount wgc = utils::compute_2d_workgroup_count(
113118
g.device(), static_cast<uint32_t>(numel), wg_size, "unary(resize)");

0 commit comments

Comments
 (0)