Skip to content

Commit ad40fa9

Browse files
authored
[Vulkan] Delegate aten.scalar_tensor with exact integer values (pytorch#23249)
EXIR leaves scalar_tensor in the ATen dialect, so the partitioner never matched the edge-dialect registration, and the graph builder emitted a name the runtime does not recognize. The op is now registered for both targets, serialized as aten.scalar_tensor.default, and the scalar is uploaded in the output's type so integer values are exact. Scalars that cannot be represented stay on CPU. Fixes pytorch#23158 Part 10/15 of the Vulkan transformer and operator-conformance stack. Depends on pytorch#23248; review against the selected base branch. Integration PR: pytorch#23254. Validation: 8 passed, 2 warnings in 45.35s. Lintrunner and git diff --check pass. Native tests use MoltenVK with portable CPU kernels; hardware and SwiftShader CI are pending. Recreates pytorch#23210 through ghstack. Prior review discussion remains on that PR. Authored with OpenAI Codex; split planned with Claude Code. cc @SS-JIA @manuelcandales @digantdesai @cbilgin
1 parent d52d9f3 commit ad40fa9

8 files changed

Lines changed: 159 additions & 16 deletions

File tree

‎backends/vulkan/op_registry.py‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1620,12 +1620,21 @@ def register_full_cpp_ops():
16201620
# =============================================================================
16211621

16221622

1623-
@update_features(exir_ops.edge.aten.scalar_tensor.default)
1623+
@update_features(
1624+
[
1625+
exir_ops.edge.aten.scalar_tensor.default,
1626+
# EXIR deliberately keeps scalar_tensor in the ATen dialect.
1627+
torch.ops.aten.scalar_tensor.default,
1628+
]
1629+
)
16241630
def register_scalar_tensor():
16251631
return OpFeatures(
16261632
inputs_storage=utils.CHANNELS_PACKED_TEXTURE,
16271633
inputs_dtypes=utils.FP_INT_T,
16281634
supports_resize=True,
1635+
are_node_inputs_supported_fn=lambda node: is_scalar_value_supported(
1636+
node.args[0], node.meta["val"].dtype
1637+
),
16291638
)
16301639

16311640

‎backends/vulkan/runtime/graph/ops/glsl/scalar_tensor.glsl‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ ${define_explicit_type_extensions(SCALAR_VALUE_TYPE)}
1919
${define_active_storage_type(STORAGE)}
2020

2121
#include "indexing_utils.h"
22+
#include "convert.glslh"
2223

2324
layout(std430) buffer;
2425

@@ -52,6 +53,8 @@ void main() {
5253
}
5354

5455
VEC4_T outtex = VEC4_T(scalar_value);
56+
$if DTYPE == "half":
57+
outtex = round_to_half_rte(outtex);
5558
write_texel(t_out, pos, outtex);
5659
}
5760

‎backends/vulkan/runtime/graph/ops/glsl/scalar_tensor.yaml‎

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,14 @@ scalar_tensor:
1212
PACKING: C_packed
1313
STORAGE: texture3d
1414
generate_variant_forall:
15-
DTYPE:
16-
- VALUE: half
17-
- VALUE: float
18-
- VALUE: int32
19-
STORAGE:
20-
- VALUE: texture3d
21-
- VALUE: buffer
22-
SCALAR_VALUE_TYPE:
23-
- VALUE: float
24-
- VALUE: int32
25-
- VALUE: bool
15+
combination:
16+
parameter_names: [DTYPE, STORAGE, SCALAR_VALUE_TYPE]
17+
combos:
18+
- parameter_values: [half, texture3d, float]
19+
- parameter_values: [half, buffer, float]
20+
- parameter_values: [float, texture3d, float]
21+
- parameter_values: [float, buffer, float]
22+
- parameter_values: [int32, texture3d, int32]
23+
- parameter_values: [int32, buffer, int32]
2624
shader_variants:
2725
- NAME: scalar_tensor

‎backends/vulkan/runtime/graph/ops/impl/ScalarTensor.cpp‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,21 @@ namespace vkcompute {
1616
void scalar_tensor(ComputeGraph& graph, const std::vector<ValueRef>& args) {
1717
// Extract the scalar value from the first argument
1818
ValueRef scalar_in = args[0];
19-
float scalar_value = graph.extract_scalar<float>(scalar_in);
2019

2120
// Get the output tensor reference
2221
ValueRef out = args[args.size() - 1];
22+
const vkapi::ScalarType scalar_dtype =
23+
graph.dtype_of(out) == vkapi::kInt ? vkapi::kInt : vkapi::kFloat;
24+
const vkapi::BufferBindInfo scalar_buffer = scalar_dtype == vkapi::kInt
25+
? graph.create_params_buffer(graph.extract_scalar<int32_t>(scalar_in))
26+
: graph.create_params_buffer(graph.extract_scalar<float>(scalar_in));
2327

2428
std::string kernel_name("scalar_tensor");
2529
kernel_name.reserve(kShaderNameReserve);
2630

2731
add_dtype_suffix(kernel_name, graph.dtype_of(out));
2832
add_storage_type_suffix(kernel_name, graph.storage_type_of(out));
29-
add_dtype_suffix(kernel_name, graph.dtype_of(scalar_in));
33+
add_dtype_suffix(kernel_name, scalar_dtype);
3034

3135
graph.execute_nodes().emplace_back(new DispatchNode(
3236
graph,
@@ -36,7 +40,7 @@ void scalar_tensor(ComputeGraph& graph, const std::vector<ValueRef>& args) {
3640
// Inputs and Outputs
3741
{{out, vkapi::kWrite}},
3842
// Shader params buffers
39-
{graph.create_params_buffer(scalar_value)},
43+
{scalar_buffer},
4044
// Push Constants
4145
{},
4246
// Specialization Constants

‎backends/vulkan/serialization/vulkan_graph_builder.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -474,10 +474,13 @@ def process_call_function_node(self, node) -> None:
474474
if not self.delegate_mapping_builder
475475
else self.delegate_mapping_builder.insert_delegate_mapping_entry(node)
476476
)
477+
operator_name = node.target.__name__
478+
if node.target == torch.ops.aten.scalar_tensor.default:
479+
operator_name = "aten.scalar_tensor.default"
477480
self.chain.append(
478481
vk_graph_schema.OperatorCall(
479482
node_id=operator_node_id, # pyre-ignore[6]: this is going to be an int
480-
name=node.target.__name__,
483+
name=operator_name,
481484
args=operator_call_args,
482485
),
483486
)

‎backends/vulkan/test/op_tests/cases.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -892,10 +892,12 @@ def get_scalar_tensor_inputs():
892892
test_suite = VkTestSuite(
893893
[
894894
(42.0,),
895+
(42,),
895896
(3.14,),
896897
(2.72,),
897898
(0.0,),
898899
(-1.0,),
900+
(-7,),
899901
(100.0,),
900902
]
901903
)

‎backends/vulkan/test/test_vulkan_dynamic.py‎

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737

3838
from executorch.exir.backend.backend_api import to_backend
3939

40+
from executorch.exir.dialects._ops import ops as exir_ops
41+
4042
from executorch.exir.lowered_backend_module import LoweredBackendModule
4143

4244
from torch.export import Dim, export
@@ -165,6 +167,44 @@ def test_dynamic_gelu(self):
165167
tolerance = 5e-6 if dtype == torch.float32 else 1e-3
166168
self._run(edge, model, inputs, atol=tolerance, rtol=tolerance)
167169

170+
def test_scalar_tensor_values(self):
171+
class WhereScalars(torch.nn.Module):
172+
def __init__(self, positive, negative):
173+
super().__init__()
174+
self.positive = positive
175+
self.negative = negative
176+
177+
def forward(self, x):
178+
return torch.where(x, self.positive, self.negative)
179+
180+
inputs = [(torch.tensor([True, False, True, False]),)]
181+
for positive, negative in (
182+
(3, -7.0),
183+
(3.0, -7),
184+
(16777217, -7),
185+
(2**31 - 1, -(2**31)),
186+
):
187+
with self.subTest(positive=positive, negative=negative):
188+
model = WhereScalars(positive, negative)
189+
fully_delegated = isinstance(positive, float) or isinstance(
190+
negative, float
191+
)
192+
edge = self._lower(model, inputs[0], fully_delegated=fully_delegated)
193+
if not fully_delegated:
194+
self.assertEqual(
195+
[
196+
node.target
197+
for node in edge.exported_program().graph.nodes
198+
if node.op == "call_function"
199+
and node.target != operator.getitem
200+
],
201+
[
202+
torch.ops.higher_order.executorch_call_delegate,
203+
exir_ops.edge.aten.where.self,
204+
],
205+
)
206+
self._run(edge, model, inputs, atol=0, rtol=0)
207+
168208
def test_gelu_with_singleton_dimensions(self):
169209
for approximate in ("none", "tanh"):
170210
for shape in ((6, 1, 3), (2, 1, 3, 5)):
@@ -177,6 +217,22 @@ def test_gelu_with_singleton_dimensions(self):
177217
edge = self._lower(model, (x,), storage=storage)
178218
self._run(edge, model, [(x,)], atol=5e-6, rtol=5e-6)
179219

220+
def test_scalar_tensor_dtypes(self):
221+
class ScalarTensor(torch.nn.Module):
222+
def __init__(self, dtype):
223+
super().__init__()
224+
self.dtype = dtype
225+
226+
def forward(self, x):
227+
return torch.scalar_tensor(2.5, dtype=self.dtype)
228+
229+
inputs = [(torch.ones(1),)]
230+
for dtype in (torch.float16, torch.float32, torch.int32):
231+
with self.subTest(dtype=dtype):
232+
model = ScalarTensor(dtype)
233+
edge = self._lower(model, inputs[0])
234+
self._run(edge, model, inputs, atol=0, rtol=0)
235+
180236
def test_dynamic_logical_not(self):
181237
class LogicalNot(torch.nn.Module):
182238
def forward(self, x):
@@ -210,6 +266,22 @@ def test_constant_bool_mask(self):
210266
)
211267
self._run(edge, model, inputs, atol=0, rtol=0)
212268

269+
def test_scalar_types_before_conv_and_view(self):
270+
class ScalarTypes(torch.nn.Module):
271+
def __init__(self):
272+
super().__init__()
273+
self.conv = torch.nn.Conv2d(1, 2, 3, padding=1)
274+
275+
def forward(self, x):
276+
y = self.conv(torch.where(x > 0, 1.0, 0.5))
277+
return y.view(1, 2, x.shape[2], -1)
278+
279+
torch.manual_seed(0)
280+
model = ScalarTypes().eval()
281+
inputs = [(torch.randn(1, 1, s, 5),) for s in (7, 2, 15, 7)]
282+
edge = self._lower(model, inputs[0], ({2: Dim("s", min=2, max=16)},))
283+
self._run(edge, model, inputs)
284+
213285
def test_nan_scalars_fall_back(self):
214286
class NanScalar(torch.nn.Module):
215287
def __init__(self, kind):
@@ -236,6 +308,36 @@ def forward(self, x):
236308
edge = self._lower(model, inputs[0], fully_delegated=False)
237309
self._run(edge, model, inputs, atol=0, rtol=0, equal_nan=True)
238310

311+
def test_integer_scalar_range_fallback(self):
312+
class LargeScalar(torch.nn.Module):
313+
def __init__(self, kind, value):
314+
super().__init__()
315+
self.kind = kind
316+
self.value = value
317+
318+
def forward(self, x):
319+
if self.kind == "scalar_tensor":
320+
return torch.scalar_tensor(self.value, dtype=torch.int64)
321+
if self.kind == "full":
322+
return torch.full(x.shape, self.value, dtype=torch.int64)
323+
return torch.full_like(x, self.value, dtype=torch.int64)
324+
325+
inputs = [(torch.zeros(2, 3),)]
326+
for kind in ("scalar_tensor", "full", "full_like"):
327+
for value in (
328+
2**31 - 0.5,
329+
-(2**31) - 0.5,
330+
2**31,
331+
2**40,
332+
2**63 - 1,
333+
-(2**63),
334+
):
335+
with self.subTest(kind=kind, value=value):
336+
model = LargeScalar(kind, value)
337+
edge = self._lower(model, inputs[0], fully_delegated=False)
338+
self.assertEqual(_vulkan_graphs(edge), [])
339+
self._run(edge, model, inputs, atol=0, rtol=0)
340+
239341
def test_64_bit_arithmetic_without_downcasting(self):
240342
class Arithmetic(torch.nn.Module):
241343
def forward(self, x):

‎backends/vulkan/test/test_vulkan_graph_builder.py‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,28 @@ def test_scalar_cache_preserves_types_and_signed_zero(self):
4141
self.assertEqual(builder.get_or_create_scalar_value(value), value_id)
4242
self.assertEqual(repr(builder.values[value_id].value), repr(serialized))
4343

44+
def test_aten_scalar_tensor_keeps_namespace(self):
45+
class Mask(torch.nn.Module):
46+
def forward(self, x):
47+
return torch.where(x, 0.0, -torch.inf)
48+
49+
program = torch.export.export(Mask(), (torch.tensor([True, False]),))
50+
edge = to_edge(program)
51+
program = apply_passes(edge.exported_program(), [SpecPropPass()])
52+
self.assertEqual(
53+
sum(
54+
node.target == torch.ops.aten.scalar_tensor.default
55+
for node in program.graph.nodes
56+
),
57+
2,
58+
)
59+
graph = VkGraphBuilder(
60+
program, DelegateMappingBuilder(generated_identifiers=True)
61+
).build_graph()
62+
names = [op.name for op in graph.chain]
63+
self.assertEqual(names.count("aten.scalar_tensor.default"), 2)
64+
self.assertNotIn("scalar_tensor.default", names)
65+
4466

4567
class TestVkGraphBuilderInputIds(unittest.TestCase):
4668
"""The serialized input list has to match the delegate call's arguments.

0 commit comments

Comments
 (0)