Skip to content

Commit c82f1ab

Browse files
committed
exir: merge main into exir-flatbuffer-serialize-fastpath_v2
Change-Id: I23787d086aa9229f5d93d1e16c6f6297119e7f5a Signed-off-by: Chizkiyahu Raful <chizkiyahu.raful@arm.com>
2 parents bce3db3 + 0d010de commit c82f1ab

24 files changed

Lines changed: 394 additions & 55 deletions

File tree

.ci/scripts/unittest-buck2.sh

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,14 @@ done
4545

4646
# Build only without testing
4747
buck2 build //codegen/tools/... \
48+
//exir/_serialize/test/... \
49+
//exir/backend/test:test_delegate_map_builder \
50+
//exir/backend/test:test_graph_partition \
51+
//exir/backend/test:test_group_partitioner \
52+
//exir/backend/test:test_passes \
53+
//exir/dialects/backend/test/... \
54+
//exir/dialects/edge/op/... \
55+
//exir/operator/... \
4856
//extension/llm/runner/io_manager:io_manager \
4957
//extension/llm/modules/... \
5058
//extension/llm/runner:multimodal_runner_lib \

backends/arm/quantizer/arm_quantizer.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
ArmCompileSpec,
2626
) # isort: skip
2727
from executorch.backends.arm.vgf import VgfCompileSpec
28-
from executorch.exir.graph_module import get_cond_while_submodules
28+
from executorch.exir.graph_module import _get_control_flow_submodules
2929

3030
from torch.fx import GraphModule, Node
3131
from torchao.quantization.pt2e import (
@@ -639,6 +639,27 @@ def validate(self, model: GraphModule) -> None:
639639
f"Quantizer detected operator {node.name} with different device inputs: {devices}."
640640
)
641641

642+
@staticmethod
643+
def _get_submodules_not_handled_by_torchao(
644+
graph_module: GraphModule,
645+
):
646+
"""Returns control flow submodules that torchao's
647+
prepare_pt2e/convert_pt2e do not handle natively. torchao now
648+
recursively handles while_loop body_fn.
649+
650+
(arg 1), so we only need to manually handle:
651+
- cond true/false branches (args 1, 2)
652+
- while_loop cond_fn (arg 0)
653+
654+
"""
655+
return _get_control_flow_submodules(
656+
graph_module,
657+
{
658+
torch.ops.higher_order.cond: [1, 2],
659+
torch.ops.higher_order.while_loop: [0],
660+
},
661+
)
662+
642663
def quantize_with_submodules(
643664
self,
644665
model: GraphModule,
@@ -648,6 +669,10 @@ def quantize_with_submodules(
648669
"""Quantizes a GraphModule in a way such that conditional submodules are
649670
handled properly.
650671
672+
Note: torchao's prepare_pt2e and convert_pt2e natively handle
673+
while_loop body_fn submodules, so we only manually process cond
674+
branches and while_loop cond_fn here.
675+
651676
Args:
652677
model (GraphModule): The model to quantize.
653678
calibration_samples (list[tuple]): A list of inputs to used to
@@ -663,12 +688,12 @@ def quantize_with_submodules(
663688
prepare_fn = prepare_qat_pt2e if is_qat else prepare_pt2e
664689

665690
prepared = prepare_fn(model, self)
666-
for name, submodule, _ in get_cond_while_submodules(prepared):
691+
for name, submodule, _ in self._get_submodules_not_handled_by_torchao(prepared):
667692
prepared.set_submodule(name, prepare_fn(submodule, self), strict=True)
668693
for inp in calibration_samples:
669694
prepared(*inp)
670695

671-
for name, submodule, _ in get_cond_while_submodules(prepared):
696+
for name, submodule, _ in self._get_submodules_not_handled_by_torchao(prepared):
672697
prepared.set_submodule(name, convert_pt2e(submodule), strict=True)
673698
converted = convert_pt2e(prepared)
674699
return converted
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
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 pathlib import Path
7+
from typing import Any, cast
8+
9+
from executorch.backends.arm.test import runner_utils
10+
11+
12+
class _FakeExecutorchProgramManager:
13+
def __init__(self, buffer: bytes) -> None:
14+
self.buffer = buffer
15+
16+
def exported_program(self):
17+
return object()
18+
19+
20+
def test_run_corstone_no_target_uses_short_input_aliases_in_semihosting_cmd(
21+
monkeypatch, tmp_path: Path
22+
) -> None:
23+
long_input_paths = [
24+
str(tmp_path / ("very_long_input_name_" * 6 + "0.bin")),
25+
str(tmp_path / ("very_long_input_name_" * 6 + "1.bin")),
26+
]
27+
28+
monkeypatch.setattr(
29+
runner_utils,
30+
"save_inputs_to_file",
31+
lambda exported_program, inputs, intermediate_path: long_input_paths,
32+
)
33+
34+
copied_files: list[tuple[str, str]] = []
35+
36+
def _fake_copyfile(src: str, dst: str) -> None:
37+
copied_files.append((src, dst))
38+
39+
monkeypatch.setattr(runner_utils.shutil, "copyfile", _fake_copyfile)
40+
41+
captured: dict[str, list[str]] = {}
42+
43+
def _fake_run_cmd(cmd, check=True):
44+
captured["cmd"] = cmd
45+
return runner_utils.subprocess.CompletedProcess(
46+
cmd, 0, stdout=b"OK", stderr=b""
47+
)
48+
49+
monkeypatch.setattr(runner_utils, "_run_cmd", _fake_run_cmd)
50+
monkeypatch.setattr(
51+
runner_utils,
52+
"get_output_from_file",
53+
lambda exported_program, intermediate_path, output_base_name: ("sentinel",),
54+
)
55+
56+
elf_path = tmp_path / "arm_executor_runner"
57+
elf_path.write_bytes(b"")
58+
59+
output = runner_utils.run_corstone(
60+
executorch_program_manager=cast(
61+
Any, _FakeExecutorchProgramManager(buffer=b"pte")
62+
),
63+
inputs=cast(Any, ()),
64+
intermediate_path=tmp_path,
65+
target_board="corstone-320",
66+
elf_path=elf_path,
67+
timeout=1,
68+
)
69+
70+
assert output == ("sentinel",)
71+
assert [Path(dst).name for _, dst in copied_files] == ["i0.bin", "i1.bin"]
72+
73+
semihosting_cmd_arg = next(
74+
arg for arg in captured["cmd"] if "semihosting-cmd_line" in arg
75+
)
76+
assert "-i i0.bin" in semihosting_cmd_arg
77+
assert "-i i1.bin" in semihosting_cmd_arg
78+
assert long_input_paths[0] not in semihosting_cmd_arg
79+
assert long_input_paths[1] not in semihosting_cmd_arg

backends/arm/test/runner_utils.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -406,14 +406,24 @@ def run_corstone(
406406
f.write(executorch_program_manager.buffer)
407407

408408
input_paths = save_inputs_to_file(exported_program, inputs, intermediate_path)
409+
# Keep semihosting command line short: the FVP truncates long cmd strings.
410+
# Alias generated input files to compact names in the same directory.
411+
aliased_input_paths = []
412+
for idx, input_path in enumerate(input_paths):
413+
short_name = f"i{idx}.bin"
414+
short_path = os.path.join(intermediate_path, short_name)
415+
if os.path.abspath(input_path) != os.path.abspath(short_path):
416+
shutil.copyfile(input_path, short_path)
417+
aliased_input_paths.append(short_path)
409418

410419
output_base_name = "out"
411420

412421
cmd_line = "executor_runner -m program.pte -o out"
413-
for input_path in input_paths:
414-
relative_path = os.path.relpath(
415-
Path(input_path).resolve(), start=intermediate_path
416-
)
422+
for input_path in aliased_input_paths:
423+
# Use local basenames to avoid '/var' -> '/private/var' resolve expansion
424+
# on macOS, which can produce long '../../..' paths and exceed FVP's
425+
# semihosting cmd_line limit.
426+
relative_path = Path(input_path).name
417427
cmd_line += f" -i {relative_path}"
418428

419429
if len(cmd_line) > 256:

backends/cadence/aot/remove_ops.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -757,8 +757,8 @@ def targets(self) -> list[EdgeOpOverload]:
757757
def maybe_remove_or_replace(self, node: Node) -> bool:
758758
cat_node = get_arg(node, "input", Node)
759759
slice_dim = get_arg(node, "dim", int)
760-
start_idx = get_arg(node, "start", int)
761-
end_idx = get_arg(node, "end", int)
760+
start_idx = get_arg(node, "start", Optional[int])
761+
end_idx = get_arg(node, "end", Optional[int])
762762
step = get_arg(node, "step", int)
763763

764764
if cat_node.target != exir_ops.edge.aten.cat.default or step != 1:

backends/cortex_m/passes/convert_to_cortex_m_pass.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def _get_batch_size_from_conv(self, conv_node: torch.fx.Node):
7171

7272
def _get_linear_replacement(self, node):
7373
"""
74-
Let
74+
Let
7575
- yi be the output activations (y1, ... yn)
7676
- xj be the input activations (x1, ... xm)
7777
- wij be the weights (w11, ... wnm)

examples/arm/aot_arm_compiler.py

Lines changed: 91 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,12 @@
3636
from executorch.backends.arm.util._factory import create_partitioner, create_quantizer
3737

3838
from executorch.backends.arm.vgf import VgfCompileSpec
39+
from executorch.backends.cortex_m.passes.cortex_m_pass_manager import CortexMPassManager
3940

40-
# To use Cortex-M backend
4141
from executorch.backends.cortex_m.passes.replace_quant_nodes_pass import (
4242
ReplaceQuantNodesPass,
4343
)
44+
from executorch.backends.cortex_m.quantizer.quantizer import CortexMQuantizer
4445

4546
from executorch.devtools import generate_etrecord
4647
from executorch.devtools.backend_debug import get_delegation_info
@@ -396,6 +397,7 @@ def forward(self, x):
396397
"TOSA-1.0+INT",
397398
"TOSA-1.0+FP",
398399
"TOSA-1.0+INT+int16",
400+
"cortex-m55+int8",
399401
]
400402

401403

@@ -528,7 +530,7 @@ def get_args():
528530
required=False,
529531
default="ethos-u55-128",
530532
choices=TARGETS,
531-
help=f"For ArmBackend delegated models, pick the target, and therefore the instruction set generated. valid targets are {TARGETS}",
533+
help=f"Target backend. For delegated models: Ethos-U/VGF/TOSA variants. For non-delegated: cortex-m55+int8 (CMSIS-NN portable kernels). Valid targets: {TARGETS}",
532534
)
533535
parser.add_argument(
534536
"-e",
@@ -795,6 +797,75 @@ def to_edge_TOSA_delegate(
795797
return model_quant, edge
796798

797799

800+
def to_edge_cortex_m(
801+
exported_program: ExportedProgram,
802+
args,
803+
model: GraphModule,
804+
example_inputs: Tuple[torch.Tensor],
805+
):
806+
"""Cortex-M/CMSIS-NN compilation path with no delegation."""
807+
logging.info("Using Cortex-M/CMSIS-NN compilation path (no delegation)")
808+
809+
def _to_channels_last(x):
810+
if isinstance(x, torch.Tensor):
811+
if x.dim() == 4 and not x.is_contiguous(memory_format=torch.channels_last):
812+
logging.warning(
813+
"Converting input tensor with shape %s to channels_last",
814+
list(x.shape),
815+
)
816+
return x.to(memory_format=torch.channels_last)
817+
return x
818+
elif isinstance(x, tuple):
819+
return tuple(_to_channels_last(t) for t in x)
820+
return x
821+
822+
if not args.quantize:
823+
logging.warning(
824+
"Quantization is DISABLED. Cortex-M typically requires quantization."
825+
)
826+
else:
827+
model = model.to(memory_format=torch.channels_last)
828+
example_inputs = tuple(_to_channels_last(x) for x in example_inputs)
829+
830+
quantizer = CortexMQuantizer()
831+
prepared = prepare_pt2e(model, quantizer)
832+
833+
dataset = get_calibration_data(
834+
args.model_name, example_inputs, args.evaluate, args.evaluate_config
835+
)
836+
837+
if isinstance(dataset, DataLoader):
838+
for sample, _ in dataset:
839+
prepared(_to_channels_last(sample))
840+
else:
841+
prepared(*tuple(_to_channels_last(x) for x in dataset))
842+
843+
model_quant = convert_pt2e(prepared)
844+
845+
exported_program = torch.export.export(
846+
model_quant, example_inputs, strict=args.strict_export
847+
)
848+
849+
edge = to_edge_transform_and_lower(
850+
exported_program,
851+
compile_config=EdgeCompileConfig(
852+
preserve_ops=[
853+
torch.ops.aten.linear.default,
854+
torch.ops.aten.hardsigmoid.default,
855+
torch.ops.aten.hardsigmoid_.default,
856+
torch.ops.aten.hardswish.default,
857+
torch.ops.aten.hardswish_.default,
858+
],
859+
_check_ir_validity=False,
860+
),
861+
)
862+
863+
pass_manager = CortexMPassManager(edge.exported_program())
864+
edge._edge_programs["forward"] = pass_manager.transform()
865+
866+
return model_quant if args.quantize else None, edge
867+
868+
798869
def to_edge_no_delegate(
799870
exported_program: ExportedProgram,
800871
args,
@@ -873,7 +944,24 @@ def to_edge_no_delegate(
873944

874945
# Quantize if required
875946
model_quant = None
876-
if args.delegate:
947+
if args.target == "cortex-m55+int8":
948+
# Cortex-M path: CMSIS-NN portable kernels, no delegation
949+
if getattr(args, "evaluate", False):
950+
logging.error(
951+
"--evaluate is not supported for target 'cortex-m55+int8' "
952+
"because this path does not use a TOSA delegate."
953+
)
954+
sys.exit(1)
955+
if args.delegate:
956+
logging.warning(
957+
"--delegate is ignored for target 'cortex-m55+int8' "
958+
"(this target does not use delegated ops)."
959+
)
960+
args.delegate = False
961+
model_quant, edge = to_edge_cortex_m(
962+
exported_program, args, model, example_inputs
963+
)
964+
elif args.delegate:
877965
model_quant, edge = to_edge_TOSA_delegate(
878966
exported_program, args, model, example_inputs
879967
)

0 commit comments

Comments
 (0)