Skip to content

Commit 7015299

Browse files
authored
Merge branch 'main' into exir-flatbuffer-serialize-fastpath_v2
2 parents f66e280 + f08db65 commit 7015299

15 files changed

Lines changed: 434 additions & 6 deletions

File tree

backends/arm/test/ops/test_amax.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ def test_amax_u55_INT(test_data: Amax.input_t):
143143
pipeline = EthosU55PipelineINT[Amax.input_t](
144144
Amax(dim, keep_dims),
145145
data,
146-
Amax.aten_op,
146+
amax_aten_op,
147147
)
148148
pipeline.run()
149149

backends/nxp/backend/edge_program_converter.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
exir_ops.edge.aten.mean.dim: MeanDimConverter, # noqa F405
4141
exir_ops.edge.aten.mm.default: MMConverter, # noqa F405
4242
exir_ops.edge.aten.mul.Tensor: MulTensorConverter, # noqa F405
43+
exir_ops.edge.aten.neg.default: NegConverter, # noqa F405
4344
exir_ops.edge.aten.permute_copy.default: PermuteCopyConverter, # noqa F405
4445
exir_ops.edge.aten.relu.default: ReLUConverter, # noqa F405
4546
exir_ops.edge.aten.sigmoid.default: SigmoidConverter, # noqa F405

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@
4040
from executorch.backends.nxp.backend.ir.converter.node_converters.ops_converters.mul_tensor_converter import (
4141
MulTensorConverter,
4242
)
43+
from executorch.backends.nxp.backend.ir.converter.node_converters.ops_converters.neg_converter import (
44+
NegConverter,
45+
)
4346
from executorch.backends.nxp.backend.ir.converter.node_converters.ops_converters.permute_copy_converter import (
4447
PermuteCopyConverter,
4548
)
@@ -93,6 +96,7 @@
9396
"MeanDimConverter",
9497
"MMConverter",
9598
"MulTensorConverter",
99+
"NegConverter",
96100
"PermuteCopyConverter",
97101
"QDQPerChannelDequantizeConverter",
98102
"QDQPerTensorDequantizeConverter",
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Copyright 2026 NXP
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 numpy as np
7+
8+
from executorch.backends.nxp.backend import edge_helper
9+
from executorch.backends.nxp.backend.ir.converter.node_converter import (
10+
CustomDelegationOptions,
11+
NodeConverter,
12+
)
13+
from executorch.backends.nxp.backend.ir.tflite_generator.builtin_options import (
14+
sub_options,
15+
)
16+
from executorch.backends.nxp.backend.ir.tflite_generator.tflite_model import (
17+
Quantization,
18+
Scale,
19+
ZeroPoint,
20+
)
21+
from torch.fx import Node
22+
from torch.nn import Parameter
23+
24+
25+
class NegConverter(NodeConverter):
26+
27+
@staticmethod
28+
def _is_supported_in_IR(
29+
node: Node,
30+
parameters_mapping: dict[str, Parameter],
31+
custom_delegation_options: CustomDelegationOptions,
32+
) -> bool:
33+
if len(node.args) != 1:
34+
# Should never happen
35+
return False
36+
37+
# The conversion code below expects a per tensor quantized operator.
38+
scale, zp = edge_helper.get_quantization_parameters_for(node.args[0])
39+
match scale, zp:
40+
case [float(), int()]:
41+
pass # Atomic quantization parameters -> per tensor quantization.
42+
case _:
43+
return False # Everything else is unexpected.
44+
45+
return True
46+
47+
def convert(self, node: Node):
48+
"""Convert 'aten.neg.default' operator to NeutronIR 0 - 'Sub'.
49+
50+
The ExecuTorch schema is 'aten::neg(Tensor self) -> Tensor'
51+
"""
52+
self.assert_convertible(node)
53+
54+
t_op = self._create_tflite_op_with_io_tensors(node)
55+
56+
x = t_op.tmp_inputs[0]
57+
58+
# Extract the zero_point, to use as the first input of the `Sub`.
59+
scale = x.quantization.scale.vector
60+
zp = x.quantization.zero_point.vector
61+
zero_tensor = self.builder.create_tensor_for_data(np.array(zp, "int8"), "zero")
62+
zero_tensor.quantization = Quantization(
63+
scale=Scale(list(scale)), zero_point=ZeroPoint(list(zp))
64+
)
65+
66+
# Assign the NeutronIR operator its builtin options and inputs.
67+
t_op.builtin_options = sub_options.Sub()
68+
t_op.tmp_inputs = [zero_tensor, x]
69+
70+
self.builder.append_operators([t_op])

backends/nxp/neutron_partitioner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ def tag_qdq_clusters(self, nodes: list[torch.fx.Node]):
213213
exir_ops.edge.aten.mean.dim: MeanDimConverter, # noqa F405
214214
exir_ops.edge.aten.mm.default: MMConverter, # noqa F405
215215
exir_ops.edge.aten.mul.Tensor: MulTensorConverter, # noqa F405
216+
exir_ops.edge.aten.neg.default: NegConverter, # noqa F405
216217
exir_ops.edge.aten.permute_copy.default: PermuteCopyConverter, # noqa F405
217218
exir_ops.edge.aten.relu.default: ReLUConverter, # noqa F405
218219
exir_ops.edge.aten.sigmoid.default: SigmoidConverter, # noqa F405

backends/nxp/quantizer/neutron_quantizer.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
MeanDimPattern,
3333
MmPattern,
3434
MulTensorPattern,
35+
NegPattern,
3536
NodeArgsIdx,
3637
PadPattern,
3738
PermutePattern,
@@ -263,6 +264,7 @@ def __init__(self, neutron_target_spec: NeutronTargetSpec, is_qat: bool = False)
263264
OpQuantizer(MeanDimPattern(is_qat=is_qat), static_qconfig),
264265
OpQuantizer(MmPattern(self, is_qat=is_qat), static_qconfig),
265266
OpQuantizer(MulTensorPattern(is_qat=is_qat), static_qconfig),
267+
OpQuantizer(NegPattern(is_qat=is_qat), static_qconfig),
266268
OpQuantizer(PadPattern(is_qat=is_qat), static_qconfig),
267269
OpQuantizer(PermutePattern(is_qat=is_qat), static_qconfig),
268270
OpQuantizer(ReluPattern(is_qat=is_qat), static_qconfig),

backends/nxp/quantizer/patterns.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -800,6 +800,15 @@ def get_anchors(
800800
)
801801

802802

803+
class NegPattern(SharedSpecPattern):
804+
"""
805+
Quantizer for the `aten.neg.default` operator.
806+
"""
807+
808+
def partition_types(self):
809+
return [torch.ops.aten.neg.default]
810+
811+
803812
class PadPattern(SharedSpecPattern):
804813
"""
805814
Quantizer for Pad operator.
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# Copyright 2026 NXP
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 numpy as np
7+
import pytest
8+
import torch
9+
10+
from executorch.backends.nxp.backend.edge_program_converter import (
11+
EdgeProgramToIRConverter,
12+
)
13+
from executorch.backends.nxp.tests.executorch_pipeline import to_quantized_edge_program
14+
from executorch.backends.nxp.tests.executors import (
15+
convert_run_compare,
16+
graph_contains_any_of_ops,
17+
ToChannelFirstPreprocess,
18+
ToChannelLastPreprocess,
19+
)
20+
from executorch.exir.dialects._ops import ops as exir_ops
21+
22+
23+
@pytest.fixture(autouse=True)
24+
def reseed_model_per_test_run():
25+
torch.manual_seed(42)
26+
np.random.seed(23)
27+
28+
29+
# noinspection PyProtectedMember
30+
ExecutorchDelegateCall = torch._higher_order_ops.executorch_call_delegate
31+
Neg = exir_ops.edge.aten.neg.default
32+
33+
34+
class NegModule(torch.nn.Module):
35+
36+
def __init__(self):
37+
super().__init__()
38+
39+
# noinspection PyMethodMayBeStatic
40+
def forward(self, x):
41+
return -x
42+
43+
44+
class ConvNegModule(torch.nn.Module):
45+
46+
def __init__(self):
47+
super().__init__()
48+
self.conv = torch.nn.Conv2d(3, 3, 1)
49+
50+
# noinspection PyMethodMayBeStatic
51+
def forward(self, x):
52+
x = self.conv(x)
53+
return -x
54+
55+
56+
@pytest.mark.parametrize(
57+
"input_shape",
58+
[
59+
pytest.param((8,), id="1D"),
60+
pytest.param((4, 2), id="2D"),
61+
pytest.param((1, 2, 3), id="3D"),
62+
pytest.param((1, 2, 3, 4), id="4D"),
63+
],
64+
)
65+
def test_convert_neg(mocker, input_shape):
66+
model = NegModule()
67+
68+
converter_spy = mocker.spy(EdgeProgramToIRConverter, "convert_program")
69+
delegated_ep = to_quantized_edge_program(model, input_shape).exported_program()
70+
71+
# Make sure the `neg` was delegated.
72+
assert graph_contains_any_of_ops(delegated_ep.graph, [ExecutorchDelegateCall])
73+
assert not graph_contains_any_of_ops(delegated_ep.graph, [Neg])
74+
75+
# Verify correct behavior of the converted NeutronIR model.
76+
intermediate_ep = converter_spy.call_args.args[1]
77+
neutron_ir_model, _ = converter_spy.spy_return
78+
79+
input_data = (
80+
np.random.random(input_shape).astype(np.float32) * 256.0 - 128.0
81+
).astype(np.int8)
82+
83+
# Make sure the tested program contains the `neg`.
84+
assert graph_contains_any_of_ops(intermediate_ep.graph, [Neg])
85+
86+
convert_run_compare(
87+
intermediate_ep,
88+
tfl_model=neutron_ir_model,
89+
input_data=input_data,
90+
)
91+
92+
93+
def test_convert_neg__channels_last(mocker):
94+
model = ConvNegModule()
95+
input_shape = (1, 3, 4, 5)
96+
97+
converter_spy = mocker.spy(EdgeProgramToIRConverter, "convert_program")
98+
delegated_ep = to_quantized_edge_program(
99+
model, input_shape, use_neutron_for_format_conversion=False
100+
).exported_program()
101+
102+
# Make sure the `neg` was delegated.
103+
assert graph_contains_any_of_ops(delegated_ep.graph, [ExecutorchDelegateCall])
104+
assert not graph_contains_any_of_ops(delegated_ep.graph, [Neg])
105+
106+
# Verify correct behavior of the converted NeutronIR model.
107+
intermediate_ep = converter_spy.call_args.args[1]
108+
neutron_ir_model, _ = converter_spy.spy_return
109+
110+
input_data = (
111+
np.random.random(input_shape).astype(np.float32) * 256.0 - 128.0
112+
).astype(np.int8)
113+
114+
# Make sure the tested program contains the `neg`.
115+
assert graph_contains_any_of_ops(intermediate_ep.graph, [Neg])
116+
117+
convert_run_compare(
118+
intermediate_ep,
119+
tfl_model=neutron_ir_model,
120+
input_data=input_data,
121+
tflite_input_preprocess=ToChannelLastPreprocess(),
122+
tflite_output_preprocess=ToChannelFirstPreprocess(),
123+
)

docs/source/backends/nxp/op-support.csv

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ aten.max_pool2d_with_indices.default,int8,static int8,"dilation=1, ceil_mode=Fal
1414
aten.mean.dim,int8,static int8,"4D tensor only, dims = [-1,-2] or [-2,-1]"
1515
aten.mul.Tensor, int8, static int8, "tensor-size % 8 = 0"
1616
aten.mm.default,int8,static int8,"2D tensor only"
17+
aten.neg.default,int8,static int8,
1718
aten.relu.default,int8,static int8,
1819
aten.sigmoid.default,int8,static int8,
1920
aten.slice_copy.Tensor, int8, static int8

kernels/aten/functions.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
- op: _cdist_forward.out
66

7+
- op: _conj_physical.out
8+
79
- op: _fake_quantize_per_tensor_affine_cachemask_tensor_qparams.out
810

911
- op: _fft_c2r.out

0 commit comments

Comments
 (0)