Skip to content

Commit 484f72c

Browse files
authored
Arm backend: Add MXFP4/MXFP6 linear support (pytorch#20370)
Add support for running `torch.nn.Linear` modules in MXFP4E2M1, MXFP6E3M2 and MXFP6E2M3. Update the `MXFPOpConfig` to support the data types. Since `torch` lacks FP6 datatypes, the string-based definitions in `torchao` are used as a workaround. The custom TOSA op receives a new argument called `str weight_payload_dtype` which tells which dtype is used for the weights. The weight tensor itself does not contain this info since the FP4 and FP6 formats are storted into uint8 tensors. The CAST_TO_BLOCK_SCALED required to transform activations from/to MXFP is also updated to support the new datatype. Its custom TOSA op gets a `str output_dtype` similar to the `weight_payload_dtype` for the MXFP linear op. Signed-off-by: Martin Lindström <Martin.Lindstroem@arm.com>
1 parent 1a78804 commit 484f72c

16 files changed

Lines changed: 838 additions & 127 deletions

backends/arm/_passes/insert_rescales_pass.py

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
from executorch.backends.arm._passes.quant_args import QuantArgs
2020
from executorch.backends.arm.constants import DQ_OPS, Q_OPS
21+
from executorch.backends.arm.tosa.mapping import TosaSpecialDtype
2122
from executorch.exir.dialects._ops import ops as exir_ops
2223
from executorch.exir.pass_base import ExportPass, PassResult
2324
from torch.fx import GraphModule, Node
@@ -35,6 +36,12 @@ class InsertRescalePass(ArmPass):
3536

3637
_passes_required_after: Set[Type[ExportPass]] = set()
3738

39+
_mxfp_payload_dtypes = {
40+
TosaSpecialDtype.FP4E2M1,
41+
TosaSpecialDtype.FP6E2M3,
42+
TosaSpecialDtype.FP6E3M2,
43+
}
44+
3845
def _ensure_uint8_io_only(self, graph_module: GraphModule) -> None:
3946
"""Ensure uint8 tensors only appear at IO boundaries.
4047
@@ -51,21 +58,23 @@ def _ensure_uint8_io_only(self, graph_module: GraphModule) -> None:
5158
continue
5259
if node.op in ("placeholder", "output"):
5360
continue
54-
if node.op == "call_function" and node.target == operator.getitem:
55-
if all(user.op == "output" for user in node.users):
61+
if node.op == "call_function":
62+
if node.target == operator.getitem and all(
63+
user.op == "output" for user in node.users
64+
):
5665
continue
57-
if (
58-
node.op == "call_function"
59-
and node.target
60-
== exir_ops.edge.dim_order_ops._to_dim_order_copy.default
61-
):
62-
# dim_order is a view-like transform; allow it to preserve uint8 at IO.
63-
continue
64-
if (
65-
node.op == "call_function"
66-
and node.target == exir_ops.backend.tosa.RESCALE.default
67-
):
66+
if node.target == exir_ops.backend.tosa.RESCALE.default:
67+
continue
68+
if (
69+
node.target
70+
== exir_ops.edge.dim_order_ops._to_dim_order_copy.default
71+
):
72+
# dim_order is a view-like transform; allow it to preserve uint8 at IO.
73+
continue
74+
if node.meta.get(TosaSpecialDtype.meta_key()) in self._mxfp_payload_dtypes:
75+
# Sub-byte FP types are stored uint8 arrays, so we need an exception for those.
6876
continue
77+
6978
raise ValueError(
7079
f"Found internal uint8 tensor at node {node.name} "
7180
f"({node.target}). Uint8 is only allowed at IO boundaries."

backends/arm/_passes/rewrite_mxfp_linear.py

Lines changed: 85 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,53 @@
88
from typing import Any, cast, Sequence, Set, Type
99

1010
import torch
11-
from executorch.backends.arm._passes import ArmPass
11+
from executorch.backends.arm._passes import ArmOpTargetedPass
1212
from executorch.backends.arm._passes.arm_pass_utils import (
1313
create_node,
1414
get_first_fake_tensor,
1515
)
16+
from executorch.backends.arm.ao_ext.mxfp import (
17+
mxfp_dtype_to_str,
18+
mxfp_str_to_dtype,
19+
MXFPDType,
20+
)
21+
from executorch.backends.arm.tosa.mapping import TosaSpecialDtype
1622
from executorch.exir.dialects._ops import ops as exir_ops
1723
from executorch.exir.pass_base import ExportPass, PassResult
24+
from torchao.prototype.mx_formats.mx_tensor import DTYPE_FP6_E2M3, DTYPE_FP6_E3M2
25+
26+
27+
def _get_weights_payload_dtype(
28+
qdata_node: torch.fx.Node,
29+
dtype: str = "",
30+
) -> MXFPDType:
31+
if dtype:
32+
return mxfp_str_to_dtype(dtype)
33+
qdata = get_first_fake_tensor(qdata_node)
34+
if qdata.dtype == torch.uint8:
35+
return torch.float4_e2m1fn_x2
36+
return qdata.dtype
37+
38+
39+
def _mark_mxfp_payload(node: torch.fx.Node, payload_dtype: MXFPDType) -> None:
40+
"""Annotate uint8-backed MXFP payload nodes with their TOSA dtype.
1841
42+
PyTorch represents sub-byte MXFP payloads as ``torch.uint8`` tensors, so
43+
the tensor dtype alone cannot distinguish FP4E2M1, FP6E2M3, and FP6E3M2.
44+
Store the logical TOSA dtype in node metadata so later lowering and
45+
serialization treat the payload as MXFP data rather than ordinary uint8.
46+
FP8 payloads have native PyTorch dtypes and do not need this metadata.
1947
20-
class RewriteMXFPLinearPass(ArmPass):
48+
"""
49+
if payload_dtype == torch.float4_e2m1fn_x2:
50+
node.meta[TosaSpecialDtype.meta_key()] = TosaSpecialDtype.FP4E2M1
51+
elif payload_dtype == DTYPE_FP6_E2M3:
52+
node.meta[TosaSpecialDtype.meta_key()] = TosaSpecialDtype.FP6E2M3
53+
elif payload_dtype == DTYPE_FP6_E3M2:
54+
node.meta[TosaSpecialDtype.meta_key()] = TosaSpecialDtype.FP6E3M2
55+
56+
57+
class RewriteMXFPLinearPass(ArmOpTargetedPass):
2158
"""Rewrite ``tosa_mxfp.linear`` into explicit TOSA MXFP operators.
2259
2360
For each MXFP linear custom op, the pass:
@@ -32,15 +69,24 @@ class RewriteMXFPLinearPass(ArmPass):
3269
3370
"""
3471

72+
target_ops = {
73+
torch.ops.tosa_mxfp.linear.default,
74+
exir_ops.edge.tosa_mxfp.linear.default,
75+
}
3576
_passes_required_after: Set[Type[ExportPass]] = set()
3677

3778
def __init__(self, exported_program: torch.export.ExportedProgram, *args, **kwargs):
3879
super().__init__(*args, **kwargs)
3980
self.exported_program = exported_program
4081

41-
def _get_linear_args(
42-
self, node: torch.fx.Node
43-
) -> tuple[torch.fx.Node, torch.fx.Node, torch.fx.Node, torch.fx.Node | None, int]:
82+
def _get_linear_args(self, node: torch.fx.Node) -> tuple[
83+
torch.fx.Node,
84+
torch.fx.Node,
85+
torch.fx.Node,
86+
torch.fx.Node | None,
87+
int,
88+
MXFPDType,
89+
]:
4490
"""Extract the MXFP linear operands from a custom-op node."""
4591
input_node = cast(torch.fx.Node, node.args[0])
4692
weight_qdata_node = cast(torch.fx.Node, node.args[1])
@@ -53,7 +99,26 @@ def _get_linear_args(
5399
int,
54100
node.args[4] if len(node.args) > 4 else node.kwargs.get("block_size", 32),
55101
)
56-
return input_node, weight_qdata_node, weight_scale_node, bias_node, block_size
102+
payload_dtype_str = cast(
103+
str,
104+
(
105+
node.args[5]
106+
if len(node.args) > 5
107+
else node.kwargs.get(
108+
"weight_payload_dtype",
109+
node.kwargs.get("weight_dtype", ""),
110+
)
111+
),
112+
)
113+
payload_dtype = _get_weights_payload_dtype(weight_qdata_node, payload_dtype_str)
114+
return (
115+
input_node,
116+
weight_qdata_node,
117+
weight_scale_node,
118+
bias_node,
119+
block_size,
120+
payload_dtype,
121+
)
57122

58123
def _reshape_with_view(
59124
self,
@@ -84,12 +149,15 @@ def _create_block_scaled_inputs(
84149
weight_qdata_node: torch.fx.Node,
85150
weight_scale_node: torch.fx.Node,
86151
block_size: int,
152+
payload_dtype: MXFPDType,
87153
) -> tuple[torch.fx.Node, torch.fx.Node]:
88154
"""Create rank-3 inputs for the block-scaled cast and matmul ops."""
89155
graph = graph_module.graph
90156
input_fake = get_first_fake_tensor(input_node)
91157
weight_qdata_fake = get_first_fake_tensor(weight_qdata_node)
92158
weight_scale_fake = get_first_fake_tensor(weight_scale_node)
159+
payload_dtype_str = mxfp_dtype_to_str(payload_dtype)
160+
_mark_mxfp_payload(weight_qdata_node, payload_dtype)
93161

94162
batches = reduce(operator.mul, input_fake.shape[:-1], 1)
95163
input_reshape_shape = [1, batches, input_fake.shape[-1]]
@@ -109,13 +177,13 @@ def _create_block_scaled_inputs(
109177
graph=graph,
110178
op_target=exir_ops.backend.tosa.CAST_TO_BLOCK_SCALED.default,
111179
args=(input_reshaped, block_size),
112-
kwargs={"output_dtype": weight_qdata_fake.dtype},
180+
kwargs={"output_dtype": payload_dtype_str},
113181
from_node=mxfp_linear_node,
114182
)
115183
cast_node.meta["val"] = exir_ops.backend.tosa.CAST_TO_BLOCK_SCALED.default(
116184
get_first_fake_tensor(input_reshaped),
117185
block_size,
118-
output_dtype=weight_qdata_fake.dtype,
186+
output_dtype=payload_dtype_str,
119187
)
120188

121189
input_qdata_node = create_node(
@@ -126,6 +194,7 @@ def _create_block_scaled_inputs(
126194
from_node=mxfp_linear_node,
127195
)
128196
input_qdata_node.meta["val"] = cast_node.meta["val"][0]
197+
_mark_mxfp_payload(input_qdata_node, payload_dtype)
129198

130199
input_scale_node = create_node(
131200
graph=graph,
@@ -150,8 +219,10 @@ def _create_matmul_node(
150219
weight_qdata_node: torch.fx.Node,
151220
weight_scale_node: torch.fx.Node,
152221
block_size: int,
222+
payload_dtype: MXFPDType,
153223
) -> torch.fx.Node:
154224
"""Insert ``MATMUL_T_BLOCK_SCALED`` with updated fake metadata."""
225+
payload_dtype_str = mxfp_dtype_to_str(payload_dtype)
155226
matmul_node = create_node(
156227
graph=graph_module.graph,
157228
op_target=exir_ops.backend.tosa.MATMUL_T_BLOCK_SCALED.default,
@@ -162,7 +233,7 @@ def _create_matmul_node(
162233
weight_scale_node,
163234
block_size,
164235
),
165-
kwargs={},
236+
kwargs={"payload_dtype": payload_dtype_str},
166237
from_node=mxfp_linear_node,
167238
)
168239
matmul_node.meta["val"] = exir_ops.backend.tosa.MATMUL_T_BLOCK_SCALED.default(
@@ -171,6 +242,7 @@ def _create_matmul_node(
171242
get_first_fake_tensor(weight_qdata_node),
172243
get_first_fake_tensor(weight_scale_node),
173244
block_size,
245+
payload_dtype=payload_dtype_str,
174246
)
175247
return matmul_node
176248

@@ -255,6 +327,7 @@ def _rewrite_mxfp_linear_node(
255327
weight_scale_node,
256328
bias_node,
257329
block_size,
330+
payload_dtype,
258331
) = self._get_linear_args(mxfp_linear_node)
259332

260333
with graph.inserting_before(mxfp_linear_node):
@@ -268,6 +341,7 @@ def _rewrite_mxfp_linear_node(
268341
weight_qdata_node,
269342
weight_scale_node,
270343
block_size,
344+
payload_dtype,
271345
)
272346
matmul_node = self._create_matmul_node(
273347
graph_module,
@@ -277,6 +351,7 @@ def _rewrite_mxfp_linear_node(
277351
weight_qdata_node,
278352
weight_scale_node,
279353
block_size,
354+
payload_dtype,
280355
)
281356

282357
with graph.inserting_after(matmul_node):
@@ -299,10 +374,7 @@ def call(self, graph_module: torch.fx.GraphModule):
299374
graph = graph_module.graph
300375

301376
for node in list(graph.nodes):
302-
if node.op != "call_function" or node.target not in (
303-
torch.ops.tosa_mxfp.linear.default,
304-
exir_ops.edge.tosa_mxfp.linear.default,
305-
):
377+
if node.op != "call_function" or node.target not in self.target_ops:
306378
continue
307379

308380
modified = True

backends/arm/ao_ext/mxfp.py

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,56 @@
1010
from executorch.exir._warnings import experimental
1111
from torchao.core.config import AOBaseConfig
1212
from torchao.prototype.mx_formats.config import ScaleCalculationMode
13+
from torchao.prototype.mx_formats.mx_tensor import DTYPE_FP6_E2M3, DTYPE_FP6_E3M2
1314
from torchao.quantization import quantize_
1415

1516

17+
# Pytorch lacks dtypes for the FP6 types, so we use ao's string representations for those.
18+
MXFPDType = torch.dtype | str
19+
20+
21+
SUPPORTED_MXFP_DTYPES: set[MXFPDType] = {
22+
torch.float4_e2m1fn_x2,
23+
torch.float8_e4m3fn,
24+
torch.float8_e5m2,
25+
# Use ao's string representations.
26+
DTYPE_FP6_E2M3,
27+
DTYPE_FP6_E3M2,
28+
}
29+
30+
31+
_DTYPE_TO_STR: dict[MXFPDType, str] = {
32+
DTYPE_FP6_E2M3: "fp6e2m3",
33+
DTYPE_FP6_E3M2: "fp6e3m2",
34+
torch.float4_e2m1fn_x2: "f4e2m1",
35+
torch.float8_e4m3fn: "f8e4m3",
36+
torch.float8_e5m2: "f8e5m2",
37+
}
38+
39+
40+
_STR_TO_DTYPE = {value: key for (key, value) in _DTYPE_TO_STR.items()}
41+
42+
43+
def mxfp_dtype_to_str(dtype: MXFPDType) -> str:
44+
try:
45+
return _DTYPE_TO_STR[dtype]
46+
except KeyError as e:
47+
supported = ", ".join(str(dtype) for dtype in _DTYPE_TO_STR)
48+
raise ValueError(
49+
f"Unsupported MXFP dtype {dtype}. Supported dtypes: {supported}"
50+
) from e
51+
52+
53+
def mxfp_str_to_dtype(dtype: str) -> MXFPDType:
54+
try:
55+
return _STR_TO_DTYPE[dtype]
56+
except KeyError as e:
57+
supported = ", ".join(sorted(_STR_TO_DTYPE))
58+
raise ValueError(
59+
f"Unsupported MXFP dtype string {dtype!r}. Supported strings: {supported}"
60+
) from e
61+
62+
1663
def _match_supported_modules(module: torch.nn.Module, _name: str) -> bool:
1764
"""Default filter function that matches supported modules."""
1865
return isinstance(module, torch.nn.Linear)
@@ -23,7 +70,7 @@ def _match_supported_modules(module: torch.nn.Module, _name: str) -> bool:
2370
class MXFPOpConfig(AOBaseConfig):
2471
"""Configuration for Arm MXFP source transforms."""
2572

26-
weight_dtype: torch.dtype = torch.float8_e4m3fn
73+
weight_dtype: MXFPDType = torch.float8_e4m3fn
2774
weight_scaling_mode: ScaleCalculationMode = ScaleCalculationMode.RCEIL
2875

2976
# Only block size of 32 is currently supported for now, so we hardcode it here.
@@ -32,7 +79,7 @@ def block_size(self) -> int:
3279
return 32
3380

3481
def __post_init__(self) -> None:
35-
if self.weight_dtype not in (torch.float8_e4m3fn, torch.float8_e5m2):
82+
if self.weight_dtype not in SUPPORTED_MXFP_DTYPES:
3683
raise ValueError(f"Unsupported weight_dtype: {self.weight_dtype}")
3784
if not isinstance(self.weight_scaling_mode, ScaleCalculationMode):
3885
raise ValueError(

0 commit comments

Comments
 (0)