Skip to content

Commit c9a06e2

Browse files
authored
Merge branch 'main' into add-q5k-mlx
2 parents 71361ac + ae64df0 commit c9a06e2

30 files changed

Lines changed: 686 additions & 246 deletions

backends/arm/runtime/VGFSetup.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ enum class FormatScalarKind {
6666
Uint,
6767
Sint,
6868
Float,
69+
BFloat,
6970
};
7071

7172
struct FormatInfo {
@@ -157,6 +158,7 @@ static uint32_t get_format_component_count(VkFormat format) {
157158
case VK_FORMAT_R16_UINT:
158159
case VK_FORMAT_R16_SINT:
159160
case VK_FORMAT_R16_SFLOAT:
161+
case VK_FORMAT_R16_SFLOAT_FPENCODING_BFLOAT16_ARM:
160162
case VK_FORMAT_R32_UINT:
161163
case VK_FORMAT_R32_SINT:
162164
case VK_FORMAT_R32_SFLOAT:
@@ -209,6 +211,9 @@ static bool get_format_info(VkFormat format, FormatInfo* info) {
209211
case VK_FORMAT_R16_SFLOAT:
210212
*info = FormatInfo{1, 2, FormatScalarKind::Float};
211213
return true;
214+
case VK_FORMAT_R16_SFLOAT_FPENCODING_BFLOAT16_ARM:
215+
*info = FormatInfo{1, 2, FormatScalarKind::BFloat};
216+
return true;
212217
case VK_FORMAT_R32_UINT:
213218
*info = FormatInfo{1, 4, FormatScalarKind::Uint};
214219
return true;
@@ -3696,6 +3701,7 @@ static uint32_t get_format_size(VkFormat format) {
36963701
case VK_FORMAT_R16_UINT:
36973702
case VK_FORMAT_R16_SINT:
36983703
case VK_FORMAT_R16_SFLOAT:
3704+
case VK_FORMAT_R16_SFLOAT_FPENCODING_BFLOAT16_ARM:
36993705
case VK_FORMAT_R8G8_UINT:
37003706
case VK_FORMAT_R8G8_SINT:
37013707
return 2;

backends/arm/scripts/vulkan_utils.sh

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,21 +26,21 @@ vulkan_sdk_arch="${ARCH}"
2626
# macOS and Linux x86_64 use the official LunarG SDK tarballs. Linux ARM64
2727
# uses a separately repackaged mirror of the same SDK version.
2828
if [[ "${os_name}" == "Darwin" ]]; then
29-
vulkan_sdk_version="1.4.341.1"
29+
vulkan_sdk_version="1.4.350.0"
3030
vulkan_sdk_arch="macOS"
3131
vulkan_sdk_url="https://sdk.lunarg.com/sdk/download/${vulkan_sdk_version}/mac/vulkansdk-macos-${vulkan_sdk_version}.zip"
32-
vulkan_sdk_sha256="632cbe96c8ed6ed00c6ce25e3a7738c466134f76586e1c51f1419410d7f9042e"
32+
vulkan_sdk_sha256="7acc181b8fd9b4781bf51ed086222ec95d22004b85b3d0a6683a7e48ca5a1679"
3333
elif [[ "${os_name}" == "Linux" ]] && [[ "${ARCH}" == "x86_64" ]]; then
34-
vulkan_sdk_version="1.4.341.1"
34+
vulkan_sdk_version="1.4.350.0"
3535
vulkan_sdk_url="https://sdk.lunarg.com/sdk/download/${vulkan_sdk_version}/linux/vulkansdk-linux-x86_64-${vulkan_sdk_version}.tar.xz"
36-
vulkan_sdk_sha256="3bf0f762afb6c79bc6a9d9fb5998745ccff928800a29619b501ed9de7fd9789b"
36+
vulkan_sdk_sha256="b65f068ab36263559da49d7cacd7e7b9df23824ca8b68ccc522a2b06f5725df2"
3737
elif [[ "${os_name}" == "Linux" ]] && ([[ "${ARCH}" == "aarch64" ]] || [[ "${ARCH}" == "arm64" ]]); then
38-
vulkan_sdk_version="1.4.341.1"
38+
vulkan_sdk_version="1.4.350.0"
3939
if [[ "${vulkan_sdk_arch}" == "arm64" ]]; then
4040
vulkan_sdk_arch="aarch64"
4141
fi
4242
vulkan_sdk_url="https://github.com/jakoch/vulkan-sdk-arm/releases/download/${vulkan_sdk_version}/vulkansdk-ubuntu-22.04-arm-${vulkan_sdk_version}.tar.xz"
43-
vulkan_sdk_sha256="345312aee2c835e128b30653278593f899a659a7ba287c571cafb22acb708b8f"
43+
vulkan_sdk_sha256="9e403d444219bb7c17e9231b580d704453e2afa30a1c2fdd568d1776dc68790b"
4444
else
4545
log_step "vulkan" "Error: only macOS and Linux are supported (detected ${os_name}); architecture must be x86-64 or aarch64/arm64"
4646
exit 1

backends/arm/test/ops/test_matmul.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -455,7 +455,7 @@ def test_matmul_u85_INT(test_case: test_case_t):
455455
pipeline.run()
456456

457457

458-
@common.parametrize("test_case", test_suite | test_suite_fp16)
458+
@common.parametrize("test_case", test_suite | test_suite_fp16 | test_suite_bf16)
459459
@common.SkipIfNoModelConverter
460460
def test_matmul_vgf_no_quant(test_case: test_case_t):
461461
test_data = test_case()

backends/arm/test/runner_utils.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -243,8 +243,7 @@ def is_concrete_shape(shape_like) -> bool:
243243
return all(isinstance(dim, numbers.Integral) for dim in shape_like)
244244

245245
def to_torch_tensor() -> torch.Tensor:
246-
if array.dtype.type is np.void:
247-
# If dtype is void, "cheat" and use the output_tensor dtype.
246+
if output_tensor.dtype == torch.bfloat16 or array.dtype.type is np.void:
248247
return torch.frombuffer(array, dtype=output_tensor.dtype)
249248
return torch.from_numpy(array)
250249

backends/arm/test/tester/test_pipeline.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1258,13 +1258,15 @@ def __init__(
12581258
):
12591259
if tosa_spec is None:
12601260
if tosa_version is None:
1261-
tosa_spec = VgfCompileSpec().tosa_spec
1262-
else:
1263-
if tosa_extensions is None:
1261+
tosa_version = str(VgfCompileSpec().tosa_spec)
1262+
if tosa_extensions is None:
1263+
if "FP" in tosa_version:
1264+
tosa_extensions = ["bf16"]
1265+
else:
12641266
tosa_extensions = []
1265-
tosa_spec = TosaSpecification.create_from_string(
1266-
tosa_version + "".join([f"+{ext}" for ext in tosa_extensions])
1267-
)
1267+
tosa_spec = TosaSpecification.create_from_string(
1268+
tosa_version + "".join([f"+{ext}" for ext in tosa_extensions])
1269+
)
12681270
elif isinstance(tosa_spec, str):
12691271
tosa_spec = TosaSpecification.create_from_string(tosa_spec)
12701272
compile_spec = common.get_vgf_compile_spec(

backends/nxp/backend/edge_helper.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,43 @@ def node_is_effectively_static_tensor(
109109
)
110110

111111

112+
def weights_are_effectively_static(
113+
node: Node, parameters_mapping: dict[str, Parameter], weight_index: int = 1
114+
) -> bool:
115+
"""Neutron IR sometimes requires some weights to be static. This method checks if this is the case for the
116+
provided `node`.
117+
118+
Sometimes a `permute_copy` is inserted to transpose the weights during edge lowering. The `permute_copy` is
119+
then removed during conversion to Neutron IR if it transposes static data. In those cases, the weights will be
120+
static. Therefore, it is ok if the weights are produced by a `permute_copy` with a static input.
121+
122+
:param node: Tensor node to check for data.
123+
:param parameters_mapping: Dict mapping tensor names to their static data. Should be inferred from the
124+
`state_dict` attribute of an edge program.
125+
:param weight_index: Index to the `node.args` where the weight is located. Defaults to 1.
126+
:return: True if the weight at the given index is effectively static.
127+
"""
128+
129+
def _is_permute_copy(node_: Node) -> bool:
130+
return hasattr(node_, "target") and node_.target == PermuteCopy
131+
132+
if (
133+
_is_dequantize(dq_node := node.args[weight_index])
134+
and _is_quantize(q_node := dq_node.args[0])
135+
and _is_permute_copy(permute_copy_node := q_node.args[0])
136+
):
137+
# The weights are produced by a `permute_copy`. Its input (the weights) must be static.
138+
return node_is_effectively_static_tensor(
139+
permute_copy_node.args[0], parameters_mapping
140+
)
141+
142+
else:
143+
# There is no `permute_copy`. The weights must be static directly.
144+
return node_is_effectively_static_tensor(
145+
node.args[weight_index], parameters_mapping
146+
)
147+
148+
112149
def try_get_tensor_constant_from_node(
113150
graph_module: GraphModule, node: Node
114151
) -> Parameter | None:

backends/nxp/backend/edge_program_converter.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,9 @@ def convert_program(
113113
self._convert_qdq_cluster_q_dq_nodes(edge_program.graph.nodes, cc)
114114
self._process_nodes(edge_program.graph.nodes, cc)
115115

116+
# Make sure the operators are correctly ordered.
117+
cc.tflite_builder.sort_operators_topologically()
118+
116119
# Assign the model its inputs and outputs.
117120
cc.tflite_builder.assign_model_io_to_subgraph(edge_program.graph_signature)
118121

backends/nxp/backend/ir/converter/builder/model_builder.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
# See the LICENSE_MIT for more details.
77
#
88

9+
import logging
10+
from collections import defaultdict, deque
911
from copy import deepcopy
1012
from typing import List, Optional, Union
1113

@@ -111,6 +113,67 @@ def __init__(
111113
self._zeros_tensor_map = {}
112114
self.edge_to_tflite_map = {}
113115

116+
def sort_operators_topologically(self):
117+
"""Sort the operators in the model graph in topological order using Kahn's algorithm.
118+
119+
Ensures that each operator appears after all operators that produce its input tensors.
120+
This is required for correct behavior of optimization passes and by the Neutron IR format.
121+
122+
The algorithm works in three steps:
123+
1. Build a mapping from each tensor to the operator that produces it.
124+
2. Compute the number of unresolved input dependencies for each operator.
125+
3. Iteratively add operators with no remaining dependencies to the sorted list,
126+
decrementing the number of dependencies of their consumers until all operators are processed.
127+
128+
If the sort succeeds, the operator list in the model's subgraph is replaced with the topologically sorted list.
129+
If it fails (e.g. due to an invalid graph), a warning is logged and the original operator order is preserved.
130+
"""
131+
132+
operators = self.get_operators()
133+
134+
# Map tensors to operators that produce them.
135+
tensor_to_producer_op = {}
136+
for producer_op in operators:
137+
for output in producer_op.tmp_outputs:
138+
tensor_to_producer_op[output] = producer_op
139+
140+
# Map operators to operators that consume their outputs.
141+
op_to_child_op = defaultdict(list)
142+
num_unresolved_input_dependencies = {op: 0 for op in operators}
143+
for child_op in operators:
144+
for input_ in child_op.tmp_inputs:
145+
parent_op = tensor_to_producer_op.get(input_)
146+
if parent_op is not None:
147+
op_to_child_op[parent_op].append(child_op)
148+
num_unresolved_input_dependencies[child_op] += 1
149+
150+
# Gradually build the graph.
151+
# Use `deque` for fast appends and pops.
152+
queue = deque(
153+
op
154+
for op, degree in num_unresolved_input_dependencies.items()
155+
if degree == 0
156+
)
157+
sorted_ops = []
158+
while len(queue) != 0:
159+
op = queue.popleft()
160+
sorted_ops.append(op)
161+
for child_op in op_to_child_op[op]:
162+
num_unresolved_input_dependencies[child_op] -= 1
163+
if num_unresolved_input_dependencies[child_op] == 0:
164+
# All input of the `child_op` are produced by operators that are already in `sorted_ops`. So we can
165+
# insert it into the graph.
166+
queue.append(child_op)
167+
168+
if len(sorted_ops) != operators.len():
169+
logging.warning(
170+
"NXP backend: ModelBuilder.sort_operators_topologically() failed. Please report this."
171+
)
172+
173+
else:
174+
# The topological sort was successful.
175+
operators.vector = sorted_ops
176+
114177
def create_zeros_tensor(
115178
self, dims: List[int], name: str, dtype: np.dtype, can_reuse: bool = False
116179
) -> tflite_model.Tensor:

backends/nxp/backend/ir/converter/node_converter.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,9 @@ def _has_shared_q_params_if_quantized(node: Node) -> bool:
253253

254254
@staticmethod
255255
def is_node_alone_in_partition(
256-
node: Node, partition_list: list[Partition], filter_fn: Callable[[Node], bool]
256+
node: Node,
257+
partition_list: list[Partition],
258+
filter_fn: Callable[[Node], bool] = is_not_qdq_node,
257259
) -> bool:
258260
"""Return True if `node` is the only node in its partition for which `filter_fn`
259261
returns True.
@@ -264,9 +266,8 @@ def is_node_alone_in_partition(
264266
265267
:param node: The torch.fx.Node to check.
266268
:param partition_list: List of proposed partitions.
267-
:param filter_fn: Predicate applied to nodes in the partition.
268-
`node` is considered alone if it is the only node
269-
for which this predicate returns True.
269+
:param filter_fn: Predicate applied to nodes in the partition. `node` is considered alone if it is the only node
270+
for which this predicate returns True. By default, Q/Dq nodes are ignored.
270271
"""
271272
partitions = [p for p in partition_list if node in p.nodes]
272273
if len(partitions) != 1:

backends/nxp/backend/ir/converter/node_converters/ops_converters/addmm_converter.py

Lines changed: 83 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
1-
# Copyright 2024-2025 NXP
1+
# Copyright 2024-2026 NXP
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.
55

6-
from executorch.backends.nxp.backend.edge_helper import input_rank
6+
import torch
7+
8+
from executorch.backends.nxp.backend.edge_helper import (
9+
input_rank,
10+
node_is_effectively_static_tensor,
11+
weights_are_effectively_static,
12+
)
713
from executorch.backends.nxp.backend.ir.converter.conversion.common import OpsList
814
from executorch.backends.nxp.backend.ir.converter.node_converter import (
915
CustomDelegationOptions,
@@ -12,10 +18,18 @@
1218
from executorch.backends.nxp.backend.ir.tflite_generator.builtin_options import (
1319
fully_connected_options,
1420
)
21+
22+
from executorch.backends.nxp.backend.neutron_target_spec import NeutronTargetSpec
1523
from torch.fx import Node
1624
from torch.nn import Parameter
1725

1826

27+
# The edge operator signature is: aten.addmm(bias, input, weight, *, beta=1, alpha=1)
28+
MAIN_INPUT_IDX = 1
29+
WEIGHT_IDX = 2
30+
BIAS_IDX = 0
31+
32+
1933
class AddMMConverter(NodeConverter):
2034
"""Convert the `aten.addmm` operator to TFLite `FullyConnected` with a bias input."""
2135

@@ -29,12 +43,76 @@ def _is_supported_in_IR(
2943
return False
3044

3145
# The weights must be 2D.
32-
if input_rank(node, 2) != 2:
46+
if input_rank(node, WEIGHT_IDX) != 2:
47+
return False
48+
49+
alpha, beta = node.kwargs.get("alpha", 1), node.kwargs.get("beta", 1)
50+
if alpha != 1 or beta != 1:
51+
# As these cases seem rare, conversion is not implemented for the time being.
52+
return False
53+
54+
# The `aten.addmm` operator allows any bias shape, as long as it is broadcastable with the result of the matrix
55+
# multiplication. That means it supports 4 different shapes: [N, P], [1, P], [P], [1] (provided the MM result
56+
# has shape [N, P]). Out of these 4, Neutron IR allows only [1, P] and [P], both of which are supported on
57+
# Neutron.
58+
bias_shape = list(node.args[BIAS_IDX].meta["val"].shape)
59+
_, p = node.meta["val"].shape
60+
if bias_shape not in [[1, p], [p]]:
61+
return False
62+
63+
return True
64+
65+
@staticmethod
66+
def _is_supported_on_target(
67+
node: Node,
68+
neutron_target_spec: NeutronTargetSpec,
69+
parameters_mapping: dict[str, Parameter],
70+
custom_delegation_options: CustomDelegationOptions,
71+
) -> bool:
72+
# Main input and output must be `int8` or `uint8`.
73+
if not NodeConverter.uses_quantization_type_for_io(
74+
node, [torch.int8, torch.uint8], [MAIN_INPUT_IDX], [0]
75+
):
76+
return False
77+
78+
# Weights must be `int8`.
79+
if not NodeConverter.uses_quantization_type_for_io(
80+
node, [torch.int8], [WEIGHT_IDX], []
81+
):
82+
return False
83+
84+
# Bias must be `int32`.
85+
if not NodeConverter.uses_quantization_type_for_io(
86+
node, [torch.int32], [BIAS_IDX], []
87+
):
88+
return False
89+
90+
# Weights must be constant.
91+
if not weights_are_effectively_static(
92+
node, parameters_mapping, weight_index=WEIGHT_IDX
93+
):
94+
return False
95+
96+
# The bias must be constant.
97+
if not node_is_effectively_static_tensor(
98+
node.args[BIAS_IDX], parameters_mapping
99+
):
33100
return False
34101

35102
return True
36103

37104
def convert(self, node: Node):
105+
"""Convert the `aten.addmm` operator to NeutronIR `FullyConnected`.
106+
The schema is:
107+
addmm(
108+
Tensor self,
109+
Tensor mat1,
110+
Tensor mat2,
111+
*,
112+
Scalar beta=1,
113+
Scalar alpha=1
114+
) -> Tensor
115+
"""
38116
self.assert_convertible(node)
39117

40118
t_op = self._create_tflite_op_with_io_tensors(node)
@@ -47,14 +125,14 @@ def convert(self, node: Node):
47125
w = t_op.tmp_inputs[2]
48126
y = t_op.tmp_outputs[0]
49127

50-
# Assign the operator its TFLite inputs and outputs
128+
# Assign the operator its Neutron IR inputs and outputs
51129
t_op.tmp_inputs = [x, w, bias]
52130
t_op.tmp_outputs = [y]
53131

54132
ops = OpsList(middle_op=t_op)
55133

56134
# The `aten.addmm` uses main input with shape [M, N] and the weights have the shape [N, O].
57-
# TFLite `FullyConnected` requires the weights to have shape [O, N] (if the main input has shape [M, N]).
135+
# Neutron IR `FullyConnected` requires the weights to have shape [O, N] (if the main input has shape [M, N]).
58136
# Insert a `Transpose` operator to permute the weights to achieve correct conversion. (The `Transpose` will not
59137
# be present in the output model if the weights are static.)
60138
ops.add_pre(self.builder.create_transpose_operator_before(t_op, 1, [1, 0]))

0 commit comments

Comments
 (0)