Skip to content

Commit 5f2277e

Browse files
committed
Arm backend: Add TOSA block-scaled cast
Add fake TOSA dialect support and serializer lowering for CAST_TO_BLOCK_SCALED. Co-authored-by: Sebastian Larsson <sebastian.larsson@arm.com> Signed-off-by: Martin Lindström <Martin.Lindstroem@arm.com> Change-Id: Ic7cdab5134f0fb9502f5985563f0662286ef5fb7
1 parent d98aa22 commit 5f2277e

9 files changed

Lines changed: 241 additions & 6 deletions

File tree

backends/arm/operator_support/tosa_supported_operators.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -295,9 +295,13 @@ def tosa_support_factory(
295295
disallowed_dtypes = [torch.float64]
296296
if not tosa_spec.support_extension("bf16"):
297297
disallowed_dtypes.append(torch.bfloat16)
298-
if not tosa_spec.support_extension("fp8e4m3"):
298+
if not (
299+
tosa_spec.support_extension("fp8e4m3") or tosa_spec.support_extension("mxfp")
300+
):
299301
disallowed_dtypes.append(torch.float8_e4m3fn)
300-
if not tosa_spec.support_extension("fp8e5m2"):
302+
if not (
303+
tosa_spec.support_extension("fp8e5m2") or tosa_spec.support_extension("mxfp")
304+
):
301305
disallowed_dtypes.append(torch.float8_e5m2)
302306
if tosa_spec.is_U55_subset:
303307
disallowed_dtypes.append(torch.bool)

backends/arm/operators/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
op_tanh,
4848
op_to_dim_order_copy,
4949
op_tosa_avg_pool2d,
50+
op_tosa_cast_to_block_scaled,
5051
op_tosa_conv2d,
5152
op_tosa_conv3d,
5253
op_tosa_custom,
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Copyright 2026 Arm Limited and/or its affiliates.
2+
#
3+
# This source code is licensed under the BSD-style license found in the
4+
# LICENSE file in the root directory of this source tree.
5+
"""Provide a visitor for lowering block-scaled casts to TOSA."""
6+
7+
import operator
8+
from typing import Any, cast, List
9+
10+
import torch
11+
import tosa_serializer as ts
12+
13+
from executorch.backends.arm.operators.node_visitor import (
14+
NodeVisitor,
15+
register_node_visitor,
16+
)
17+
from executorch.backends.arm.operators.operator_validation_utils import (
18+
validate_num_inputs,
19+
)
20+
from executorch.backends.arm.tosa.mapping import TosaArg
21+
from executorch.backends.arm.tosa.specification import TosaSpecification
22+
23+
24+
def _ordered_getitem_output_names(node: torch.fx.Node) -> list[str]:
25+
getitem_users = [
26+
user
27+
for user in node.users
28+
if user.op == "call_function" and user.target == operator.getitem
29+
]
30+
31+
ordered_users = sorted(getitem_users, key=lambda user: cast(int, user.args[1]))
32+
if len(ordered_users) != 2:
33+
raise ValueError(
34+
f"{CastToBlockScaledVisitor.target}: Expected exactly two getitem outputs, got {len(ordered_users)}"
35+
)
36+
37+
return [user.name for user in ordered_users]
38+
39+
40+
@register_node_visitor
41+
class CastToBlockScaledVisitor(NodeVisitor):
42+
"""Serialize TOSA ``CAST_TO_BLOCK_SCALED``."""
43+
44+
target = "tosa.CAST_TO_BLOCK_SCALED.default"
45+
tosa_specs = [TosaSpecification.create_from_string("TOSA-1.1+FP")]
46+
47+
def define_node(
48+
self,
49+
node: torch.fx.Node,
50+
tosa_graph: Any,
51+
inputs: List[TosaArg],
52+
output: TosaArg,
53+
) -> None:
54+
validate_num_inputs(self.target, inputs, 2)
55+
# The tosa_specs attribute cannot express extension requirements.
56+
# Therefore, check for the extension explicitly here.
57+
if not self.tosa_spec.support_extension("mxfp"):
58+
raise ValueError(f"{self.target} requires the TOSA mxfp extension")
59+
60+
input_tensor = inputs[0]
61+
block_size = inputs[1].number
62+
output_data_tensor, output_scale_tensor = node.meta["val"]
63+
64+
# TODO(MLETORCH-2018): This is a local workaround for multi-output TOSA ops.
65+
# Remove it once twe can handle multiple outputs generally.
66+
output_names = _ordered_getitem_output_names(node)
67+
68+
attr = ts.TosaSerializerAttribute()
69+
attr.CastToBlockScaledAttribute(block_size)
70+
71+
self._serialize_operator(
72+
node,
73+
tosa_graph,
74+
ts.Op.CAST_TO_BLOCK_SCALED,
75+
[input_tensor.name],
76+
output_names,
77+
attr,
78+
)

backends/arm/process_node.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,19 +30,24 @@
3030

3131
def _tensor_to_numpy(tensor: torch.Tensor) -> np.ndarray:
3232
tensor = tensor.detach().cpu().contiguous()
33-
if tensor.dtype in (torch.bfloat16, torch.float8_e4m3fn, torch.float8_e5m2):
33+
if tensor.dtype in (
34+
torch.bfloat16,
35+
torch.float8_e4m3fn,
36+
torch.float8_e5m2,
37+
torch.float8_e8m0fnu,
38+
):
3439
try:
3540
import ml_dtypes # type: ignore[import-not-found]
3641
except ImportError as e:
3742
raise RuntimeError(
3843
f"ml_dtypes is required to serialize {tensor.dtype} tensors for TOSA. "
3944
"Have you run setup.sh?"
4045
) from e
41-
4246
ml_dtype_map = {
4347
torch.bfloat16: (torch.uint16, ml_dtypes.bfloat16),
4448
torch.float8_e4m3fn: (torch.uint8, ml_dtypes.float8_e4m3fn),
4549
torch.float8_e5m2: (torch.uint8, ml_dtypes.float8_e5m2),
50+
torch.float8_e8m0fnu: (torch.uint8, ml_dtypes.float8_e8m0fnu),
4651
}
4752
storage_dtype, ml_dtype = ml_dtype_map[tensor.dtype]
4853
return tensor.view(storage_dtype).numpy().view(ml_dtype)
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Copyright 2026 Arm Limited and/or its affiliates.
2+
#
3+
# This source code is licensed under the BSD-style license found in the
4+
# LICENSE file in the root directory of this source tree.
5+
6+
import pytest
7+
import torch
8+
from executorch.backends.arm.tosa.dialect.lib import TosaValueError
9+
from executorch.backends.arm.tosa.dialect.ops import cast_to_block_scaled # noqa: F401
10+
from executorch.backends.arm.tosa.specification import (
11+
TosaLoweringContext,
12+
TosaSpecification,
13+
)
14+
from executorch.exir.dialects._ops import ops as exir_ops
15+
from torch._subclasses.fake_tensor import FakeTensorMode
16+
17+
18+
def test_cast_to_block_scaled_requires_mxfp_extension() -> None:
19+
tosa_spec = TosaSpecification.create_from_string("TOSA-1.1+FP")
20+
sample_input = torch.randn((2, 32), dtype=torch.float32)
21+
22+
with TosaLoweringContext(tosa_spec), FakeTensorMode() as mode:
23+
with pytest.raises(
24+
TosaValueError,
25+
match="doesn't support MXFP block-scaled casts",
26+
):
27+
exir_ops.backend.tosa.CAST_TO_BLOCK_SCALED.default(
28+
mode.from_tensor(sample_input),
29+
32,
30+
output_dtype=torch.float8_e4m3fn,
31+
)
32+
33+
34+
def test_cast_to_block_scaled_tosa_fp_mxfp() -> None:
35+
tosa_spec = TosaSpecification.create_from_string("TOSA-1.1+FP+mxfp")
36+
sample_input = torch.randn((2, 32), dtype=torch.float32)
37+
38+
with TosaLoweringContext(tosa_spec), FakeTensorMode() as mode:
39+
output_data, output_scale = exir_ops.backend.tosa.CAST_TO_BLOCK_SCALED.default(
40+
mode.from_tensor(sample_input),
41+
32,
42+
output_dtype=torch.float8_e4m3fn,
43+
)
44+
45+
assert output_data.dtype == torch.float8_e4m3fn
46+
assert tuple(output_data.shape) == (2, 32)
47+
assert output_scale.dtype == torch.float8_e8m0fnu
48+
assert tuple(output_scale.shape) == (2, 1)
49+
50+
51+
def test_cast_to_block_scaled_invalid_shape() -> None:
52+
tosa_spec = TosaSpecification.create_from_string("TOSA-1.1+FP+mxfp")
53+
54+
with TosaLoweringContext(tosa_spec), FakeTensorMode() as mode:
55+
with pytest.raises(
56+
TosaValueError,
57+
match="Last dim 30 must be divisible by block_size 32",
58+
):
59+
exir_ops.backend.tosa.CAST_TO_BLOCK_SCALED.default(
60+
mode.from_tensor(torch.randn((2, 30), dtype=torch.float32)),
61+
32,
62+
output_dtype=torch.float8_e4m3fn,
63+
)

backends/arm/test/targets.bzl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ def define_arm_tests():
5656
"misc/test_compile_spec.py",
5757
# "misc/test_evaluate_model.py",
5858
"misc/test_pass_pipeline_config.py",
59+
"misc/tosa_dialect/test_tosa_dialect_cast_to_block_scaled.py",
5960
"misc/tosa_dialect/test_tosa_resize.py",
6061
"misc/test_tosa_spec.py",
6162
"misc/test_bn_relu_folding_qat.py",

backends/arm/tosa/dialect/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from executorch.backends.arm.tosa.dialect.ops import ( # noqa F401
77
avg_pool2d,
88
avg_pool2d_adaptive,
9+
cast_to_block_scaled,
910
conv2d,
1011
conv3d,
1112
custom,
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Copyright 2026 Arm Limited and/or its affiliates.
2+
#
3+
# This source code is licensed under the BSD-style license found in the
4+
# LICENSE file in the root directory of this source tree.
5+
6+
from __future__ import annotations
7+
8+
import torch
9+
10+
from executorch.backends.arm.tosa.dialect.lib import TosaValueError
11+
from executorch.backends.arm.tosa.dialect.ops_registration import register_fake_tosa_op
12+
from executorch.backends.arm.tosa.specification import (
13+
get_context_spec,
14+
TosaSpecification,
15+
)
16+
17+
18+
@register_fake_tosa_op(
19+
"CAST_TO_BLOCK_SCALED(Tensor input, SymInt block_size, ScalarType output_dtype) -> (Tensor, Tensor)",
20+
[TosaSpecification.create_from_string("TOSA-1.1+FP")],
21+
)
22+
def CAST_TO_BLOCK_SCALED(
23+
input: torch.Tensor,
24+
block_size: int,
25+
output_dtype: torch.dtype,
26+
) -> tuple[torch.Tensor, torch.Tensor]:
27+
tosa_spec = get_context_spec()
28+
29+
if not tosa_spec.support_float() or not tosa_spec.support_extension("mxfp"):
30+
raise TosaValueError(
31+
f"TOSA spec {tosa_spec} doesn't support MXFP block-scaled casts",
32+
op="CAST_TO_BLOCK_SCALED",
33+
)
34+
35+
if input.dtype not in (torch.float32, torch.bfloat16):
36+
raise TosaValueError(
37+
f"Unsupported input dtype {input.dtype} for CAST_TO_BLOCK_SCALED",
38+
op="CAST_TO_BLOCK_SCALED",
39+
)
40+
if input.dtype == torch.bfloat16 and not (
41+
tosa_spec.support_extension("bf16") or tosa_spec.support_extension("mxfp")
42+
):
43+
raise TosaValueError(
44+
f"TOSA spec {tosa_spec} doesn't support bf16",
45+
op="CAST_TO_BLOCK_SCALED",
46+
)
47+
48+
if input.ndim < 1:
49+
raise TosaValueError(
50+
"CAST_TO_BLOCK_SCALED requires rank >= 1",
51+
op="CAST_TO_BLOCK_SCALED",
52+
)
53+
if block_size != 32:
54+
raise TosaValueError(
55+
f"Unsupported block_size {block_size} (must be 32)",
56+
op="CAST_TO_BLOCK_SCALED",
57+
)
58+
if input.shape[-1] % block_size != 0:
59+
raise TosaValueError(
60+
f"Last dim {input.shape[-1]} must be divisible by block_size {block_size}",
61+
op="CAST_TO_BLOCK_SCALED",
62+
)
63+
64+
scale_tensor_dtype = torch.float8_e8m0fnu
65+
if output_dtype not in (torch.float8_e4m3fn, torch.float8_e5m2):
66+
raise TosaValueError(
67+
f"Unsupported block-scaled output dtype {output_dtype}",
68+
op="CAST_TO_BLOCK_SCALED",
69+
)
70+
scale_shape = (*input.shape[:-1], input.shape[-1] // block_size)
71+
output_data = torch.empty_like(input, dtype=output_dtype)
72+
output_scale = input.new_empty(scale_shape, dtype=scale_tensor_dtype)
73+
return output_data, output_scale

backends/arm/tosa/mapping.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,9 @@ def map_dtype(data_type: torch.dtype) -> Any:
9999
torch.float16: ts.DType.FP16,
100100
torch.half: ts.DType.FP16,
101101
torch.bfloat16: ts.DType.BF16,
102+
torch.float8_e4m3fn: ts.DType.FP8E4M3,
103+
torch.float8_e5m2: ts.DType.FP8E5M2,
104+
torch.float8_e8m0fnu: ts.DType.FP8UE8M0,
102105
torch.int8: ts.DType.INT8,
103106
# TOSA uses signless int8; unsigned semantics are expressed via RESCALE.
104107
torch.uint8: ts.DType.INT8,
@@ -235,10 +238,16 @@ def __validate(self, tosa_spec: TosaSpecification) -> bool:
235238
if not tosa_spec.support_extension("bf16"):
236239
return False
237240
case ts.DType.FP8E4M3:
238-
if not tosa_spec.support_extension("fp8e4m3"):
241+
if not (
242+
tosa_spec.support_extension("fp8e4m3")
243+
or tosa_spec.support_extension("mxfp")
244+
):
239245
return False
240246
case ts.DType.FP8E5M2:
241-
if not tosa_spec.support_extension("fp8e5m2"):
247+
if not (
248+
tosa_spec.support_extension("fp8e5m2")
249+
or tosa_spec.support_extension("mxfp")
250+
):
242251
return False
243252

244253
return True

0 commit comments

Comments
 (0)