Skip to content

Commit a87df42

Browse files
authored
Arm backend: Fix int64 buffers before index gather (#20764)
Move CastInt64BuffersToInt32Pass before DecomposeIndexTensorToGatherPass. This converts int64 get_attr index buffers before index.Tensor is lowered to TOSA GATHER. This matches Swin's relative-position lookup pattern: table[index:int64 buffer] -> table[index:int32 buffer] -> TOSA GATHER Non-persistent buffers are also handled. They are present in inputs_to_buffers, but their tensor values live in ExportedProgram constants instead of the state_dict. This covers transformer buffers such as position_ids. Without this ordering, gather decomposition sees the original int64 buffer and fails with "index.Tensor requires index dtype must be int32". Without the constants handling, non-persistent int64 buffers fail with a KeyError while being cast. Fixes the flow test: test_swin_v2_t[arm_tosa_fp-static_shapes-float32] Signed-off-by: Zingo Andersen <Zingo.Andersen@arm.com>
1 parent 0e0db95 commit a87df42

4 files changed

Lines changed: 90 additions & 14 deletions

File tree

backends/arm/_passes/arm_pass_manager.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -547,6 +547,7 @@ def _tosa_pipeline(
547547
DecomposeUnfoldToGatherPass(),
548548
DecomposeEmbeddingPass(),
549549
DecomposeIndexSelectToGatherPass(),
550+
CastInt64BuffersToInt32Pass(exported_program),
550551
DecomposeStridedSliceCopyPass(),
551552
DecomposeSliceScatterPass(),
552553
AccumulateIndexPutPass(),

backends/arm/_passes/cast_int64_pass.py

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,34 @@ def _assert_within_int32(self, tensor: torch.Tensor, node: torch.fx.Node):
4848
f"Node {node.name} has value > {torch.iinfo(torch.int32).max}"
4949
)
5050

51+
def _cast_buffer_to_int32(self, node: torch.fx.Node) -> bool:
52+
buffer_name = self.exported_program.graph_signature.inputs_to_buffers[node.name]
53+
if buffer_name in self.exported_program.state_dict:
54+
store = self.exported_program.state_dict
55+
elif buffer_name in self.exported_program.constants:
56+
# Non-persistent buffers are tracked by inputs_to_buffers, but their
57+
# tensor values live in ExportedProgram.constants instead of the
58+
# state_dict. Examples include transformer position_ids buffers.
59+
store = self.exported_program.constants
60+
else:
61+
logger.warning(
62+
"Skipping int64 buffer %s (%s): value not found in state_dict "
63+
"or constants",
64+
node.name,
65+
buffer_name,
66+
)
67+
return False
68+
69+
buffer = store[buffer_name]
70+
self._assert_within_int32(buffer, node)
71+
logger.warning(
72+
f"Casting buffer {node.name} from torch.int64 to torch.int32"
73+
f" defined in {node.meta.get('stack_trace','[no stack trace found]')}"
74+
)
75+
store[buffer_name] = buffer.to(torch.int32)
76+
node.meta["val"] = node.meta["val"].to(torch.int32)
77+
return True
78+
5179
def _to_int32(self, graph_module: torch.fx.GraphModule) -> bool:
5280
modified = False
5381
for node in graph_module.graph.nodes:
@@ -61,19 +89,7 @@ def _to_int32(self, graph_module: torch.fx.GraphModule) -> bool:
6189
if fake_tensor.dtype != torch.int64:
6290
continue
6391
if is_buffer(self.exported_program, node):
64-
node.meta["val"] = fake_tensor.to(torch.int32)
65-
buffer_name = self.exported_program.graph_signature.inputs_to_buffers[
66-
node.name
67-
]
68-
buffer = self.exported_program.state_dict[buffer_name]
69-
self._assert_within_int32(buffer, node)
70-
logger.warning(
71-
f"Casting buffer {node.name} from torch.int64 to torch.int32"
72-
f" defined in {node.meta.get('stack_trace','[no stack trace found]')}"
73-
)
74-
buffer_int32 = buffer.to(torch.int32)
75-
self.exported_program.state_dict[buffer_name] = buffer_int32
76-
modified = True
92+
modified |= self._cast_buffer_to_int32(node)
7793
return modified
7894

7995
def call(self, graph_module: torch.fx.GraphModule):

backends/arm/test/ops/test_index_tensor.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,33 @@ class IndexTensorTestCommon:
2929
AFTER = "AFTER"
3030

3131

32+
class IndexTensorInt64Buffer(torch.nn.Module):
33+
"""Swin-style indexing with an int64 get_attr index buffer."""
34+
35+
def __init__(self):
36+
super().__init__()
37+
self.register_buffer("index", torch.tensor([0, 2], dtype=torch.int64))
38+
39+
def forward(self, x: torch.Tensor):
40+
return x[self.index]
41+
42+
43+
def test_index_tensor_tosa_FP_int64_buffer_index():
44+
# This mirrors torchvision Swin relative_position_bias_table[index]. The
45+
# int64 get_attr must be cast before index.Tensor is decomposed to GATHER:
46+
#
47+
# x[index:int64 buffer] -> x[index:int32 buffer] -> TOSA GATHER
48+
pipeline = TosaPipelineFP[Tuple[torch.Tensor]](
49+
IndexTensorInt64Buffer(),
50+
(torch.rand(4, 3),),
51+
IndexTensorTestCommon.aten_op,
52+
IndexTensorTestCommon.exir_op,
53+
atol=IndexTensorTestCommon.atol,
54+
rtol=IndexTensorTestCommon.rtol,
55+
)
56+
pipeline.run()
57+
58+
3259
input_params_slice = Tuple[torch.Tensor, int, int, str, Tuple[torch.Tensor]]
3360
input_params = Tuple[torch.Tensor, Tuple[torch.Tensor]]
3461

backends/arm/test/passes/test_cast_int64_pass.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2025 Arm Limited and/or its affiliates.
1+
# Copyright 2025-2026 Arm Limited and/or its affiliates.
22
#
33
# This source code is licensed under the BSD-style license found in the
44
# LICENSE file in the root directory of this source tree.
@@ -25,6 +25,21 @@ def forward(self, x: torch.Tensor) -> torch.Tensor:
2525
return x + 3
2626

2727

28+
class NonPersistentInt64BufferModel(torch.nn.Module):
29+
test_data = {
30+
"rand": (torch.rand(4),),
31+
}
32+
33+
def __init__(self):
34+
super().__init__()
35+
self.register_buffer(
36+
"position_ids", torch.arange(4, dtype=torch.int64), persistent=False
37+
)
38+
39+
def forward(self, x: torch.Tensor) -> torch.Tensor:
40+
return x + self.get_buffer("position_ids").to(torch.float32)
41+
42+
2843
@common.parametrize("test_data", Int64Model.test_data)
2944
def test_cast_int64_buffers_to_int32_tosa_FP(test_data: input_t):
3045
module = Int64Model()
@@ -47,3 +62,20 @@ def test_cast_int64_buffers_to_int32_tosa_FP(test_data: input_t):
4762
).exported_program()
4863
for state in exported_program.state_dict:
4964
assert exported_program.state_dict[state].dtype == torch.int32
65+
66+
67+
@common.parametrize("test_data", NonPersistentInt64BufferModel.test_data)
68+
def test_cast_non_persistent_int64_buffers_to_int32_tosa_FP(test_data: input_t):
69+
module = NonPersistentInt64BufferModel()
70+
pipeline = PassPipeline[input_t](
71+
module,
72+
test_data,
73+
quantize=False,
74+
passes_with_exported_program=[CastInt64BuffersToInt32Pass],
75+
)
76+
pipeline.run()
77+
78+
exported_program = pipeline.tester.get_artifact(
79+
StageType.RUN_PASSES
80+
).exported_program()
81+
assert exported_program.constants["position_ids"].dtype == torch.int32

0 commit comments

Comments
 (0)