Skip to content

Commit 9cced90

Browse files
authored
[Vulkan] Honor downcast_64_bit=False in the partitioner (pytorch#23248)
With downcast_64_bit disabled, the graph builder refuses int64 and float64 tensors, but the partitioner still claimed those nodes, so lowering failed late or the delegate aborted at load. The partitioner now rejects nodes that need 64-bit dtypes in that mode, and keeps a fusable pattern entirely on CPU when it touches a 64-bit non-constant tensor rather than splitting it. Constant parameters are exempt because quantization parameters are folded away during fusion. Part 9/15 of the Vulkan transformer and operator-conformance stack. Depends on pytorch#23247; review against the selected base branch. Integration PR: pytorch#23254. Validation: 4 passed, 2 warnings in 32.37s. Lintrunner and git diff --check pass. Native tests use MoltenVK with portable CPU kernels; hardware and SwiftShader CI are pending. Recreates pytorch#23209 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 463c5c9 commit 9cced90

4 files changed

Lines changed: 176 additions & 1 deletion

File tree

‎backends/vulkan/partitioner/vulkan_partitioner.py‎

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343

4444
from torch.fx.passes.infra.partitioner import CapabilityBasedPartitioner
4545
from torch.fx.passes.operator_support import OperatorSupportBase
46+
from torch.utils._pytree import tree_leaves
4647

4748
# pyre-ignore
4849
ops_not_to_decompose = [
@@ -67,12 +68,15 @@ def __init__(
6768
fusable_subgraphs: Optional[List[PatternMatch]] = None,
6869
nn_module_blocklist: Optional[Set[str]] = None,
6970
nn_module_allowlist: Optional[Set[str]] = None,
71+
downcast_64_bit: bool = True,
72+
constant_nodes: Optional[Set[torch.fx.Node]] = None,
7073
) -> None:
7174
super().__init__()
7275
self.texture_limits: utils.ImageExtents = texture_limits
7376
self.buffer_limit = buffer_limit
7477
self.require_dynamic_shapes = require_dynamic_shape
7578
self.skip_bool_tensors = skip_bool_tensors
79+
self.downcast_64_bit = downcast_64_bit
7680
self.operator_blocklist: Set[OpKey] = (
7781
operator_blocklist if operator_blocklist is not None else set()
7882
)
@@ -82,8 +86,22 @@ def __init__(
8286
)
8387
# Create a set of all nodes that are part of fusable subgraphs for quick lookup
8488
self.fusable_nodes: Set[torch.fx.Node] = set()
89+
self.unsupported_fusable_nodes: Set[torch.fx.Node] = set()
8590
for match in self.fusable_subgraphs:
86-
self.fusable_nodes.update(match.all_nodes)
91+
nodes = {
92+
node for node in match.all_nodes if isinstance(node, torch.fx.Node)
93+
}
94+
self.fusable_nodes.update(nodes)
95+
inputs = {arg for node in nodes for arg in node.all_input_nodes}
96+
if not downcast_64_bit and any(
97+
isinstance(value, torch.Tensor)
98+
and value.dtype in (torch.int64, torch.float64)
99+
for node in (nodes | inputs) - (constant_nodes or set())
100+
for value in tree_leaves(node.meta.get("val"))
101+
):
102+
# Keep the whole pattern outside Vulkan instead of splitting a fusion.
103+
# Constant quantization parameters may disappear during fusion.
104+
self.unsupported_fusable_nodes.update(nodes)
87105

88106
self.nn_module_blocklist = nn_module_blocklist
89107
self.nn_module_allowlist = nn_module_allowlist
@@ -207,6 +225,10 @@ def _is_node_supported(self, node: torch.fx.Node) -> bool:
207225
return self._node_support[node]
208226

209227
def _check_node_support(self, node: torch.fx.Node) -> bool: # noqa: C901
228+
if node in self.unsupported_fusable_nodes:
229+
self.log_skip(node, "fusable pattern requires 64-bit tensor downcasting")
230+
return False
231+
210232
if any(
211233
isinstance(arg.meta.get("val"), (torch.SymFloat, torch.SymBool))
212234
for arg in [node, *node.all_input_nodes]
@@ -245,6 +267,17 @@ def _check_node_support(self, node: torch.fx.Node) -> bool: # noqa: C901
245267
if node in self.fusable_nodes:
246268
return True
247269

270+
if not self.downcast_64_bit:
271+
native_dtypes = utils.DtypeSetList(
272+
utils.ALL_T - {torch.int64, torch.float64}
273+
)
274+
dtype_valid, dtype_reason = utils.check_node_dtypes(
275+
node, native_dtypes, native_dtypes
276+
)
277+
if not dtype_valid:
278+
self.log_skip(node, f"{dtype_reason} with downcast_64_bit disabled")
279+
return False
280+
248281
target = node.target
249282
if (
250283
node.target == torch.ops.higher_order.auto_functionalized
@@ -433,6 +466,13 @@ def partition(self, exported_program: ExportedProgram) -> PartitionResult:
433466
fusable_subgraphs=fusable_subgraphs,
434467
nn_module_blocklist=self.nn_module_blocklist,
435468
nn_module_allowlist=self.nn_module_allowlist,
469+
downcast_64_bit=self.options.get("downcast_64_bit", True),
470+
constant_nodes={
471+
node
472+
for node in exported_program.graph.nodes
473+
if utils.is_param_node(exported_program, node)
474+
and not utils.is_mutable_buffer_node(node, exported_program)
475+
},
436476
),
437477
allows_single_node_partition=True,
438478
)

‎backends/vulkan/test/targets.bzl‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,12 @@ def define_common_targets(is_fbcode = False):
2222
"//executorch/backends/transforms:convert_dtype_pass",
2323
"//executorch/backends/vulkan:vulkan_preprocess",
2424
"//executorch/backends/vulkan/partitioner:vulkan_partitioner",
25+
"//executorch/backends/vulkan/quantizer:vulkan_quantizer",
2526
"//executorch/exir:lib",
2627
"//executorch/extension/pybindings:portable_lib", # @manual
2728
"//executorch/extension/pytree:pylib",
2829
"//executorch/kernels/portable:custom_ops_generated_lib",
30+
"//pytorch/ao:torchao", # @manual
2931
],
3032
)
3133

‎backends/vulkan/test/test_vulkan_delegate.py‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import ctypes
1010
import functools
11+
import operator
1112
import unittest
1213
from typing import Tuple
1314

@@ -16,6 +17,10 @@
1617
import torch.nn.functional as F
1718
from executorch.backends.transforms.convert_dtype_pass import I64toI32
1819
from executorch.backends.vulkan.partitioner.vulkan_partitioner import VulkanPartitioner
20+
from executorch.backends.vulkan.quantizer.vulkan_quantizer import (
21+
get_symmetric_quantization_config as get_vulkan_quantization_config,
22+
VulkanQuantizer,
23+
)
1924
from executorch.backends.vulkan.vulkan_preprocess import VulkanBackend
2025
from executorch.backends.xnnpack.quantizer.xnnpack_quantizer import (
2126
get_symmetric_quantization_config,
@@ -2754,6 +2759,43 @@ def apply_quantization(self):
27542759
quantized_linear_module_gemm, sample_inputs_gemm, atol=1e-2, rtol=1e-2
27552760
)
27562761

2762+
def test_vulkan_backend_pt2e_quantized_linear_without_downcasting(self):
2763+
torch.manual_seed(0)
2764+
sample_inputs = (torch.randn(4, 64),)
2765+
quantizer = VulkanQuantizer().set_global(get_vulkan_quantization_config())
2766+
model = prepare_pt2e(
2767+
export(torch.nn.Linear(64, 32).eval(), sample_inputs, strict=True).module(),
2768+
quantizer,
2769+
)
2770+
model(*sample_inputs)
2771+
model = convert_pt2e(model)
2772+
self.assertTrue(any(buffer.dtype == torch.int64 for buffer in model.buffers()))
2773+
2774+
for downcast in (False, True):
2775+
with self.subTest(downcast=downcast):
2776+
edge = lower_module(
2777+
model,
2778+
sample_inputs,
2779+
compile_options={"downcast_64_bit": downcast},
2780+
)
2781+
self.assertEqual(
2782+
[
2783+
node.target
2784+
for node in edge.exported_program().graph.nodes
2785+
if node.op == "call_function"
2786+
and node.target != operator.getitem
2787+
],
2788+
[torch.ops.higher_order.executorch_call_delegate],
2789+
)
2790+
program_buffer = edge.to_executorch().buffer
2791+
module = _load_for_executorch_from_buffer(program_buffer)
2792+
self.assert_outputs_equal(
2793+
module.run_method("forward", sample_inputs),
2794+
model(*sample_inputs),
2795+
atol=1e-5,
2796+
rtol=1e-5,
2797+
)
2798+
27572799
@disable_test("Cannot run on swiftshader due to no integer dot product support")
27582800
def test_vulkan_backend_xnnpack_pt2e_quantized_linear_sequence(self):
27592801
"""

‎backends/vulkan/test/test_vulkan_dynamic.py‎

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,97 @@ def forward(self, x):
236236
edge = self._lower(model, inputs[0], fully_delegated=False)
237237
self._run(edge, model, inputs, atol=0, rtol=0, equal_nan=True)
238238

239+
def test_64_bit_arithmetic_without_downcasting(self):
240+
class Arithmetic(torch.nn.Module):
241+
def forward(self, x):
242+
return x + x, x.to(torch.float32) + 1
243+
244+
for dtype in (torch.int64, torch.float64):
245+
with self.subTest(dtype=dtype):
246+
model = Arithmetic()
247+
inputs = [
248+
(torch.arange(3 * s, dtype=dtype).reshape(3, s),) for s in (7, 2)
249+
]
250+
edge = self._lower(
251+
model,
252+
inputs[0],
253+
({1: Dim("s", min=2, max=16)},),
254+
fully_delegated=False,
255+
downcast_64_bit=False,
256+
)
257+
graphs = _vulkan_graphs(edge)
258+
self.assertTrue(graphs)
259+
for graph in graphs:
260+
for value in graph.values:
261+
if isinstance(value.value, VkTensor):
262+
self.assertNotIn(
263+
value.value.datatype,
264+
(VkDataType.INT64, VkDataType.FLOAT64),
265+
)
266+
self._run(edge, model, inputs, atol=0, rtol=0)
267+
268+
def test_64_bit_fusion_inputs_without_downcasting(self):
269+
class SelectScalar(torch.nn.Module):
270+
def __init__(self, narrow):
271+
super().__init__()
272+
self.narrow = narrow
273+
274+
def forward(self, x, pos):
275+
value = pos[0].item()
276+
if self.narrow:
277+
torch._check(value >= 0)
278+
torch._check(value <= 6)
279+
return x.narrow(1, value, 2) + 1
280+
return x * value
281+
282+
inputs = [(torch.randn(2, 8), torch.tensor([pos])) for pos in (3, 5, 0)]
283+
for narrow in (False, True):
284+
for downcast in (False, True):
285+
with self.subTest(narrow=narrow, downcast=downcast):
286+
model = SelectScalar(narrow)
287+
edge = self._lower(
288+
model,
289+
inputs[0],
290+
fully_delegated=False,
291+
downcast_64_bit=downcast,
292+
)
293+
for graph in _vulkan_graphs(edge):
294+
for value in graph.values:
295+
if isinstance(value.value, VkTensor):
296+
self.assertNotIn(
297+
value.value.datatype,
298+
(VkDataType.INT64, VkDataType.FLOAT64),
299+
)
300+
self._run(edge, model, inputs, atol=0, rtol=0)
301+
302+
def test_quantized_embedding_without_downcasting(self):
303+
from torchao.quantization.granularity import PerGroup
304+
from torchao.quantization.quant_api import IntxWeightOnlyConfig, quantize_
305+
from torchao.utils import unwrap_tensor_subclass
306+
307+
torch.manual_seed(0)
308+
model = torch.nn.Sequential(torch.nn.Embedding(64, 128)).eval()
309+
quantize_(
310+
model,
311+
IntxWeightOnlyConfig(weight_dtype=torch.int4, granularity=PerGroup(32)),
312+
filter_fn=lambda module, fqn: isinstance(module, torch.nn.Embedding),
313+
)
314+
unwrap_tensor_subclass(model)
315+
inputs = [(torch.tensor(indices),) for indices in ([0, 5, 63, 7], [3, 3, 1, 0])]
316+
for downcast in (False, True):
317+
with self.subTest(downcast=downcast):
318+
if downcast and USING_SWIFTSHADER:
319+
self.skipTest("Quantized embedding requires 8-bit storage buffers")
320+
edge = self._lower(
321+
model,
322+
inputs[0],
323+
fully_delegated=downcast,
324+
downcast_64_bit=downcast,
325+
)
326+
self.assertEqual(bool(_vulkan_graphs(edge)), downcast)
327+
if downcast:
328+
self._run(edge, model, inputs)
329+
239330
def test_dynamic_scalar_values_fall_back(self):
240331
class DynamicScalars(torch.nn.Module):
241332
def forward(self, x):

0 commit comments

Comments
 (0)