From 8a98594ae3baed74d59e5adafea756cac059ba88 Mon Sep 17 00:00:00 2001 From: pytorchbot Date: Fri, 11 Sep 2026 11:30:13 -0700 Subject: [PATCH 1/8] Arm backend: Add static public API manifest for 1.5 (#22735) cc @digantdesai @freddan80 @per @zingo @oscarandersson8218 @mansnils @Sebastian-Larsson @robell @rascani Signed-off-by: Sebastian Larsson Co-authored-by: Sebastian Larsson <38941629+Sebastian-Larsson@users.noreply.github.com> --- .../api_manifest_1_5.toml | 287 ++++++++++++++++++ 1 file changed, 287 insertions(+) create mode 100644 backends/arm/public_api_manifests/api_manifest_1_5.toml diff --git a/backends/arm/public_api_manifests/api_manifest_1_5.toml b/backends/arm/public_api_manifests/api_manifest_1_5.toml new file mode 100644 index 00000000000..dc87ac9ee96 --- /dev/null +++ b/backends/arm/public_api_manifests/api_manifest_1_5.toml @@ -0,0 +1,287 @@ +# Copyright 2026 Arm Limited and/or its affiliates. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. +# +# This file is generated by +# backends/arm/scripts/public_api_manifest/generate_public_api_manifest.py + +[python] + +[python.EthosUBackend] +kind = "class" +signature = "EthosUBackend()" + +[python.EthosUBackend.preprocess] +kind = "function" +signature = "EthosUBackend.preprocess(edge_program: torch.export.exported_program.ExportedProgram, compile_specs: List[executorch.exir.backend.compile_spec_schema.CompileSpec]) -> executorch.exir.backend.backend_details.PreprocessResult" + +[python.EthosUCompileSpec] +kind = "class" +signature = "EthosUCompileSpec(target: str, system_config: str | None = None, memory_mode: str | None = None, extra_flags: list[str] | None = None, config_ini: str | None = 'Arm/vela.ini', external_block_placements: executorch.backends.arm.ethosu.compile_spec.VelaExternalBlockPlacements | None = None)" + +[python.EthosUCompileSpec.DebugMode] +kind = "enum" +signature = "EthosUCompileSpec.DebugMode(*values)" + +[python.EthosUCompileSpec.__eq__] +kind = "function" +signature = "EthosUCompileSpec.__eq__(self, other)" + +[python.EthosUCompileSpec.__repr__] +kind = "function" +signature = "EthosUCompileSpec.__repr__(self)" + +[python.EthosUCompileSpec.dump_debug_info] +kind = "function" +signature = "EthosUCompileSpec.dump_debug_info(self, debug_mode: executorch.backends.arm.common.arm_compile_spec.ArmCompileSpec.DebugMode | None)" + +[python.EthosUCompileSpec.dump_intermediate_artifacts_to] +kind = "function" +signature = "EthosUCompileSpec.dump_intermediate_artifacts_to(self, output_path: str | None)" + +[python.EthosUCompileSpec.set_pass_pipeline_config] +kind = "function" +signature = "EthosUCompileSpec.set_pass_pipeline_config(self, config: executorch.backends.arm.common.pipeline_config.ArmPassPipelineConfig) -> None" + +[python.EthosUPartitioner] +kind = "class" +signature = "EthosUPartitioner(compile_spec: executorch.backends.arm.ethosu.compile_spec.EthosUCompileSpec, additional_checks: Optional[Sequence[torch.fx.passes.operator_support.OperatorSupportBase]] = None) -> None" + +[python.EthosUPartitioner.ops_to_not_decompose] +kind = "function" +signature = "EthosUPartitioner.ops_to_not_decompose(self, ep: torch.export.exported_program.ExportedProgram) -> Tuple[List[torch._ops.OpOverload], Optional[Callable[[torch.fx.node.Node], bool]]]" + +[python.EthosUPartitioner.partition] +kind = "function" +signature = "EthosUPartitioner.partition(self, exported_program: torch.export.exported_program.ExportedProgram) -> executorch.exir.backend.partitioner.PartitionResult" + +[python.EthosUPartitioner.register_custom_partition_op] +kind = "function" +signature = "EthosUPartitioner.register_custom_partition_op(self, op: torch._ops.OpOverload) -> None" + +[python.EthosUPartitioner.transform_for_pre_decomposition] +kind = "function" +signature = "EthosUPartitioner.transform_for_pre_decomposition(self, exported_program: torch.export.exported_program.ExportedProgram) -> torch.export.exported_program.ExportedProgram" + +[python.EthosUQuantizer] +kind = "class" +signature = "EthosUQuantizer(compile_spec: 'EthosUCompileSpec', use_composable_quantizer: 'bool' = True) -> 'None'" + +[python.EthosUQuantizer.annotate] +kind = "function" +signature = "EthosUQuantizer.annotate(self, model: 'GraphModule') -> 'GraphModule'" + +[python.EthosUQuantizer.set_global] +kind = "function" +signature = "EthosUQuantizer.set_global(self, quantization_config: 'Optional[QuantizationConfig]') -> 'TOSAQuantizer'" + +[python.EthosUQuantizer.set_io] +kind = "function" +signature = "EthosUQuantizer.set_io(self, quantization_config: 'Optional[QuantizationConfig]') -> 'TOSAQuantizer'" + +[python.EthosUQuantizer.set_module_name] +kind = "function" +signature = "EthosUQuantizer.set_module_name(self, module_name: 'str', quantization_config: 'Optional[QuantizationConfig]') -> 'TOSAQuantizer'" + +[python.EthosUQuantizer.set_module_type] +kind = "function" +signature = "EthosUQuantizer.set_module_type(self, module_type: 'Callable', quantization_config: 'Optional[QuantizationConfig]') -> 'TOSAQuantizer'" + +[python.EthosUQuantizer.transform_for_annotation] +kind = "function" +signature = "EthosUQuantizer.transform_for_annotation(self, model: 'GraphModule') -> 'GraphModule'" + +[python.EthosUQuantizer.validate] +kind = "function" +signature = "EthosUQuantizer.validate(self, model: 'GraphModule') -> 'None'" + +[python.VelaExternalBlockPlacements] +kind = "class" +signature = "VelaExternalBlockPlacements(cmd_data: str | None = None, weight_data: str | None = None) -> None" + +[python.VelaExternalBlockPlacements.__delattr__] +kind = "function" +signature = "VelaExternalBlockPlacements.__delattr__(self, name)" + +[python.VelaExternalBlockPlacements.__eq__] +kind = "function" +signature = "VelaExternalBlockPlacements.__eq__(self, other)" + +[python.VelaExternalBlockPlacements.__hash__] +kind = "function" +signature = "VelaExternalBlockPlacements.__hash__(self)" + +[python.VelaExternalBlockPlacements.__post_init__] +kind = "function" +signature = "VelaExternalBlockPlacements.__post_init__(self) -> None" + +[python.VelaExternalBlockPlacements.__repr__] +kind = "function" +signature = "VelaExternalBlockPlacements.__repr__(self)" + +[python.VelaExternalBlockPlacements.__setattr__] +kind = "function" +signature = "VelaExternalBlockPlacements.__setattr__(self, name, value)" + +[python.VelaExternalBlockPlacements.to_block_placements] +kind = "function" +signature = "VelaExternalBlockPlacements.to_block_placements(self) -> dict[str, str]" + +[python.VgfBackend] +kind = "class" +signature = "VgfBackend()" + +[python.VgfBackend.preprocess] +kind = "function" +signature = "VgfBackend.preprocess(edge_program: torch.export.exported_program.ExportedProgram, compile_specs: List[executorch.exir.backend.compile_spec_schema.CompileSpec]) -> executorch.exir.backend.backend_details.PreprocessResult" + +[python.VgfCompileSpec] +kind = "class" +signature = "VgfCompileSpec(tosa_spec: executorch.backends.arm.tosa.specification.TosaSpecification | str | None = None, compiler_flags: list[str] | None = None)" + +[python.VgfCompileSpec.DebugMode] +kind = "enum" +signature = "VgfCompileSpec.DebugMode(*values)" + +[python.VgfCompileSpec.__eq__] +kind = "function" +signature = "VgfCompileSpec.__eq__(self, other)" + +[python.VgfCompileSpec.__repr__] +kind = "function" +signature = "VgfCompileSpec.__repr__(self)" + +[python.VgfCompileSpec.dump_debug_info] +kind = "function" +signature = "VgfCompileSpec.dump_debug_info(self, debug_mode: executorch.backends.arm.common.arm_compile_spec.ArmCompileSpec.DebugMode | None)" + +[python.VgfCompileSpec.dump_intermediate_artifacts_to] +kind = "function" +signature = "VgfCompileSpec.dump_intermediate_artifacts_to(self, output_path: str | None)" + +[python.VgfCompileSpec.set_pass_pipeline_config] +kind = "function" +signature = "VgfCompileSpec.set_pass_pipeline_config(self, config: executorch.backends.arm.common.pipeline_config.ArmPassPipelineConfig) -> None" + +[python.VgfCompileSpec.validate_environment] +kind = "function" +signature = "VgfCompileSpec.validate_environment(self, build_dir: str | None = None, *, require_runtime_build: bool = False) -> 'VgfEnvironmentReport'" + +[python.VgfPartitioner] +kind = "class" +signature = "VgfPartitioner(compile_spec: executorch.backends.arm.vgf.compile_spec.VgfCompileSpec, additional_checks: Optional[Sequence[torch.fx.passes.operator_support.OperatorSupportBase]] = None) -> None" + +[python.VgfPartitioner.ops_to_not_decompose] +kind = "function" +signature = "VgfPartitioner.ops_to_not_decompose(self, ep: torch.export.exported_program.ExportedProgram) -> Tuple[List[torch._ops.OpOverload], Optional[Callable[[torch.fx.node.Node], bool]]]" + +[python.VgfPartitioner.partition] +kind = "function" +signature = "VgfPartitioner.partition(self, exported_program: torch.export.exported_program.ExportedProgram) -> executorch.exir.backend.partitioner.PartitionResult" + +[python.VgfPartitioner.register_custom_partition_op] +kind = "function" +signature = "VgfPartitioner.register_custom_partition_op(self, op: torch._ops.OpOverload) -> None" + +[python.VgfPartitioner.transform_for_pre_decomposition] +kind = "function" +signature = "VgfPartitioner.transform_for_pre_decomposition(self, exported_program: torch.export.exported_program.ExportedProgram) -> torch.export.exported_program.ExportedProgram" + +[python.VgfQuantizer] +kind = "class" +signature = "VgfQuantizer(compile_spec: 'VgfCompileSpec', use_composable_quantizer: 'bool' = True) -> 'None'" + +[python.VgfQuantizer.annotate] +kind = "function" +signature = "VgfQuantizer.annotate(self, model: 'GraphModule') -> 'GraphModule'" + +[python.VgfQuantizer.set_global] +kind = "function" +signature = "VgfQuantizer.set_global(self, quantization_config: 'Optional[QuantizationConfig]') -> 'TOSAQuantizer'" + +[python.VgfQuantizer.set_io] +kind = "function" +signature = "VgfQuantizer.set_io(self, quantization_config: 'Optional[QuantizationConfig]') -> 'TOSAQuantizer'" + +[python.VgfQuantizer.set_module_name] +kind = "function" +signature = "VgfQuantizer.set_module_name(self, module_name: 'str', quantization_config: 'Optional[QuantizationConfig]') -> 'TOSAQuantizer'" + +[python.VgfQuantizer.set_module_type] +kind = "function" +signature = "VgfQuantizer.set_module_type(self, module_type: 'Callable', quantization_config: 'Optional[QuantizationConfig]') -> 'TOSAQuantizer'" + +[python.VgfQuantizer.transform_for_annotation] +kind = "function" +signature = "VgfQuantizer.transform_for_annotation(self, model: 'GraphModule') -> 'GraphModule'" + +[python.VgfQuantizer.validate] +kind = "function" +signature = "VgfQuantizer.validate(self, model: 'GraphModule') -> 'None'" + +[python.get_symmetric_a16w8_quantization_config] +kind = "function" +signature = "get_symmetric_a16w8_quantization_config(is_per_channel: 'bool' = True, is_qat: 'bool' = False, is_dynamic: 'bool' = False, weight_qmin: 'int' = -127, weight_qmax: 'int' = 127, epsilon: 'float' = 0.000244140625) -> 'QuantizationConfig'" + +[python.get_symmetric_quantization_config] +kind = "function" +signature = "get_symmetric_quantization_config(is_per_channel: 'bool' = True, is_qat: 'bool' = False, is_dynamic: 'bool' = False, act_qmin: 'int' = -128, act_qmax: 'int' = 127, weight_qmin: 'int' = -127, weight_qmax: 'int' = 127, eps: 'float' = 1.52587890625e-05) -> 'QuantizationConfig'" + +[cmake] + +[cmake.arm_runner_add_minimal_executable] +kind = "function" +signature = "arm_runner_add_minimal_executable(*, TARGET, SOURCE, OPS_PREFIX, COMPILE_DEFINITIONS=())" + +[cmake.arm_runner_add_standalone_executorch] +kind = "macro" +signature = "arm_runner_add_standalone_executorch()" + +[cmake.arm_runner_configure_ethos_u_platform] +kind = "function" +signature = "arm_runner_configure_ethos_u_platform(*, SDK_PATH, SYSTEM_CONFIG, MEMORY_MODE)" + +[cmake.arm_runner_configure_linker_script] +kind = "function" +signature = "arm_runner_configure_linker_script(*, TARGET, SYSTEM_CONFIG, OUTPUT_NAME=None)" + +[cmake.arm_runner_configure_model] +kind = "function" +signature = "arm_runner_configure_model(*, TARGET, PTE_FILE=None, MODEL_PTE_ADDR=None, MODEL_PTE_SIZE=None, PUBLIC=False)" + +[cmake.arm_runner_configure_runtime_output] +kind = "function" +signature = "arm_runner_configure_runtime_output(TARGET_NAME, FALLBACK_DIR)" + +[cmake.arm_runner_create_default_selected_ops_libs] +kind = "function" +signature = "arm_runner_create_default_selected_ops_libs(*, PREFIX, SUFFIX=None, OP_LIST=None, OPS_FROM_MODEL=None, DTYPE_SELECTIVE_BUILD=None, OUT_LIBS=None, DEPS=())" + +[cmake.arm_runner_create_selected_ops_lib] +kind = "function" +signature = "arm_runner_create_selected_ops_lib(*, LIB_NAME, FUNCTIONS_YAML=None, CUSTOM_OPS_YAML=None, OP_LIST=None, OPS_FROM_MODEL=None, DTYPE_SELECTIVE_BUILD=None, KERNEL_LIBS=(), DEPS=(), INCLUDE_ALL_OPS=False, PRIM_OPS=False)" + +[cmake.arm_runner_define_cache_options] +kind = "function" +signature = "arm_runner_define_cache_options(*, METHOD_ALLOCATOR_SIZE=None)" + +[cmake.arm_runner_link_minimal_specs] +kind = "function" +signature = "arm_runner_link_minimal_specs(TARGET_NAME)" + +[cmake.arm_runner_link_registration_libraries] +kind = "function" +signature = "arm_runner_link_registration_libraries(*, TARGET, SCOPE=None, BASE_LIBS=(), REGISTRATION_LIBS=(), NORMAL_LIBS=(), SUPPRESS_LIBS=())" + +[cmake.arm_runner_require_baremetal_targets] +kind = "function" +signature = "arm_runner_require_baremetal_targets()" + +[cmake.arm_runner_require_python] +kind = "macro" +signature = "arm_runner_require_python()" + +[cmake.arm_runner_validate_model_source] +kind = "function" +signature = "arm_runner_validate_model_source(*, ALLOW_SEMIHOSTING=False)" From f7140a46ff38e919c557d45b102d3ff26097c8c9 Mon Sep 17 00:00:00 2001 From: Jacob Szwejbka Date: Mon, 14 Sep 2026 12:09:09 -0700 Subject: [PATCH 2/8] [RELEASE ONLY CHANGES] Finalize ExecuTorch 1.5 dependencies (#22724) ## Summary - pin the tokenizers submodule to v1.5.0-rc2 while keeping the wheel dependency at the latest published version, pytorch-tokenizers 1.4.1 - pin torchao to v0.18.0 and update the submodule to its release tag - move TorchCodec, QNN, and ROCm release installs to stable packages and production indexes - restore the Qwen CUDA export matmul mode that TorchAO 0.18 no longer sets globally - update source, SwiftPM, and Raspberry Pi documentation for release/1.5 - accommodate the current Transformers Qwen3-VL annotations in mypy ## Validation - python3 -m py_compile setup.py examples/models/qwen3_5_moe/export.py backends/arm/test/models/Qwen3_VL/test_qwen3_vl_model.py - Black 24.4.2 check for the changed Python files - git diff --check - verified pytorch-tokenizers 1.4.1 publishes wheels for Python 3.10-3.14 on Windows, Linux x86-64/aarch64, and macOS arm64 - verified the tokenizers and torchao gitlinks resolve exactly to v1.5.0-rc2 and v0.18.0 - verified torch 2.14.0, torchao 0.18.0, and TorchCodec 0.16.0 resolve from their configured indexes ## Release status This stays draft while pytorch/test-infra#8780 blocks persistence of the tokenizers RC2 wheel artifacts. Raise the package dependency to pytorch-tokenizers>=1.5.0 after the final tokenizer wheels are promoted, then verify that the final tag points to the RC2 commit or update the submodule gitlink. This PR was authored with Codex assistance. --- .ci/scripts/test-rocm-aoti.sh | 4 ++-- .ci/scripts/test-rocm-voxtral.sh | 8 ++++---- .ci/scripts/test_model_e2e.sh | 2 +- .ci/scripts/test_wheel_package_qnn.sh | 4 ++-- .../arm/test/models/Qwen3_VL/test_qwen3_vl_model.py | 6 ++++-- docs/README.md | 2 +- docs/source/getting-started.md | 4 ++-- docs/source/raspberry_pi_llama_tutorial.md | 2 +- docs/source/using-executorch-building-from-source.md | 2 +- docs/source/using-executorch-ios.md | 6 +++--- examples/models/moshi/mimi/install_requirements.sh | 2 +- examples/models/qwen3_5_moe/export.py | 12 +++++++----- extension/llm/tokenizers | 2 +- install_requirements.py | 2 +- setup.py | 11 ++++++----- third-party/ao | 2 +- 16 files changed, 38 insertions(+), 33 deletions(-) diff --git a/.ci/scripts/test-rocm-aoti.sh b/.ci/scripts/test-rocm-aoti.sh index 00592bc4bf6..f371d393cb9 100644 --- a/.ci/scripts/test-rocm-aoti.sh +++ b/.ci/scripts/test-rocm-aoti.sh @@ -9,8 +9,8 @@ set -euo pipefail ROCM_VERSION="${ROCM_VERSION:-7.2}" ROCM_PATH="${ROCM_PATH:-/opt/rocm}" -PYTORCH_ROCM_INDEX="${PYTORCH_ROCM_INDEX:-https://download.pytorch.org/whl/test/rocm${ROCM_VERSION}}" -TORCHAO_ROCM_WHEEL_BASE="${TORCHAO_ROCM_WHEEL_BASE:-https://download.pytorch.org/whl/nightly/rocm${ROCM_VERSION}}" +PYTORCH_ROCM_INDEX="${PYTORCH_ROCM_INDEX:-https://download.pytorch.org/whl/rocm${ROCM_VERSION}}" +TORCHAO_ROCM_WHEEL_BASE="${TORCHAO_ROCM_WHEEL_BASE:-https://download.pytorch.org/whl/rocm${ROCM_VERSION}}" ROCM_CI_TMP_ROOT="${RUNNER_TEMP:-/tmp}" mkdir -p "${ROCM_CI_TMP_ROOT}" 2>/dev/null || ROCM_CI_TMP_ROOT=/tmp ROCM_CI_TMPDIR="$(mktemp -d "${ROCM_CI_TMP_ROOT}/executorch-rocm-ci.XXXXXX")" diff --git a/.ci/scripts/test-rocm-voxtral.sh b/.ci/scripts/test-rocm-voxtral.sh index eb00c1efc73..332a4ad99cf 100644 --- a/.ci/scripts/test-rocm-voxtral.sh +++ b/.ci/scripts/test-rocm-voxtral.sh @@ -11,8 +11,8 @@ ROCM_VERSION="${ROCM_VERSION:-7.2}" ROCM_PATH="${ROCM_PATH:-/opt/rocm}" EXPECTED_ROCM_ARCH="${EXPECTED_ROCM_ARCH:-gfx950}" EXPECTED_WARP_SIZE="${EXPECTED_WARP_SIZE:-64}" -PYTORCH_ROCM_INDEX="${PYTORCH_ROCM_INDEX:-https://download.pytorch.org/whl/test/rocm${ROCM_VERSION}}" -TORCHAO_ROCM_WHEEL_BASE="${TORCHAO_ROCM_WHEEL_BASE:-https://download.pytorch.org/whl/nightly/rocm${ROCM_VERSION}}" +PYTORCH_ROCM_INDEX="${PYTORCH_ROCM_INDEX:-https://download.pytorch.org/whl/rocm${ROCM_VERSION}}" +TORCHAO_ROCM_WHEEL_BASE="${TORCHAO_ROCM_WHEEL_BASE:-https://download.pytorch.org/whl/rocm${ROCM_VERSION}}" VOXTRAL_CI_TMP_ROOT="${RUNNER_TEMP:-/tmp}" if ! mkdir -p "${VOXTRAL_CI_TMP_ROOT}" 2>/dev/null || [[ ! -w "${VOXTRAL_CI_TMP_ROOT}" ]]; then @@ -58,8 +58,8 @@ conda install -y -c conda-forge ffmpeg 'libstdcxx-ng>=12' python -m pip install 'fsspec[http]<=2025.3.0' python -m pip install datasets huggingface_hub librosa mistral-common safetensors soundfile -python -m pip install torchcodec==0.11.0 \ - --extra-index-url https://download.pytorch.org/whl/test/cpu +python -m pip install torchcodec==0.16.0 \ + --index-url https://download.pytorch.org/whl/cpu python - <<'PY' import os diff --git a/.ci/scripts/test_model_e2e.sh b/.ci/scripts/test_model_e2e.sh index c8c0cc9d706..f2ae92c20f0 100755 --- a/.ci/scripts/test_model_e2e.sh +++ b/.ci/scripts/test_model_e2e.sh @@ -345,7 +345,7 @@ elif [[ "$MODEL_NAME" == *whisper* ]] || [ "$MODEL_NAME" = "voxtral_realtime" ]; fi fi pip install datasets soundfile - pip install torchcodec==0.11.0 --extra-index-url https://download.pytorch.org/whl/test/cpu + pip install torchcodec==0.16.0 --index-url https://download.pytorch.org/whl/cpu python -c "from datasets import load_dataset;import soundfile as sf;sample = load_dataset('distil-whisper/librispeech_long', 'clean', split='validation')[0]['audio'];sf.write('${MODEL_DIR}/$AUDIO_FILE', sample['array'][:sample['sampling_rate']*30], sample['sampling_rate'])" fi diff --git a/.ci/scripts/test_wheel_package_qnn.sh b/.ci/scripts/test_wheel_package_qnn.sh index 763bd8733c1..72e7123da07 100644 --- a/.ci/scripts/test_wheel_package_qnn.sh +++ b/.ci/scripts/test_wheel_package_qnn.sh @@ -167,8 +167,8 @@ PY # ) echo "=== [$LABEL] Install torch==${TORCH_VERSION} ===" - # Install torch based on the pinned PyTorch version, preferring the PyTorch test index - "$PIPBIN" install torch=="${TORCH_VERSION}" --extra-index-url "https://download.pytorch.org/whl/test" + # Install torch based on the pinned PyTorch version. + "$PIPBIN" install --no-cache-dir torch=="${TORCH_VERSION}" --index-url "https://download.pytorch.org/whl/cpu" "$PIPBIN" install wheel # Install torchao based on the pinned commit from third-party/ao submodule diff --git a/backends/arm/test/models/Qwen3_VL/test_qwen3_vl_model.py b/backends/arm/test/models/Qwen3_VL/test_qwen3_vl_model.py index 2b83f75030a..2256dbeacb1 100644 --- a/backends/arm/test/models/Qwen3_VL/test_qwen3_vl_model.py +++ b/backends/arm/test/models/Qwen3_VL/test_qwen3_vl_model.py @@ -147,9 +147,11 @@ def __init__(self, config) -> None: with torch.no_grad(): grid_thw = _make_image_grid_thw(self.visual.pos_embed.weight.device) - pos_embeds = self.visual.fast_pos_embed_interpolate(grid_thw) + pos_embeds = self.visual.fast_pos_embed_interpolate( # type: ignore[operator] + grid_thw + ) - rotary_pos_emb = self.visual.rot_pos_emb(grid_thw) + rotary_pos_emb = self.visual.rot_pos_emb(grid_thw) # type: ignore[operator] emb = torch.cat((rotary_pos_emb, rotary_pos_emb), dim=-1) cos = emb.cos() sin = emb.sin() diff --git a/docs/README.md b/docs/README.md index da9e6a6a5df..9742863ad96 100644 --- a/docs/README.md +++ b/docs/README.md @@ -40,7 +40,7 @@ To build the documentation locally: 1. Clone the ExecuTorch repo to your machine. ```bash - git clone -b viable/strict https://github.com/pytorch/executorch.git && cd executorch + git clone -b release/1.5 https://github.com/pytorch/executorch.git && cd executorch ``` 1. If you don't have it already, start either a Python virtual environment: diff --git a/docs/source/getting-started.md b/docs/source/getting-started.md index 415445d3667..0a782f6d5b9 100644 --- a/docs/source/getting-started.md +++ b/docs/source/getting-started.md @@ -168,7 +168,7 @@ For a full example of running a model on Android, see the [DeepLabV3AndroidDemo] #### Installation ExecuTorch supports both iOS and macOS via C++, as well as hardware backends for CoreML and CPU. The iOS runtime library is provided as a collection of .xcframework targets and are made available as a Swift PM package. -To get started with Xcode, go to File > Add Package Dependencies. Paste the URL of the ExecuTorch repo into the search bar and select it. Make sure to change the branch name to the desired ExecuTorch version in format “swiftpm-”, (e.g. “swiftpm-0.6.0”). The ExecuTorch dependency can also be added to the package file manually. See [Using ExecuTorch on iOS](using-executorch-ios.md) for more information. +To get started with Xcode, go to File > Add Package Dependencies. Paste the URL of the ExecuTorch repo into the search bar and select it. Make sure to change the branch name to the desired ExecuTorch version in format “swiftpm-”, (e.g. “swiftpm-1.5.0”). The ExecuTorch dependency can also be added to the package file manually. See [Using ExecuTorch on iOS](using-executorch-ios.md) for more information. #### Runtime APIs Models can be loaded and run from Objective-C using the C++ APIs. @@ -199,7 +199,7 @@ For a platform the package does not cover, or to change build options, build fro CMake is the preferred build system for the ExecuTorch C++ runtime. To use with CMake, clone the ExecuTorch repository as a subdirectory of your project, and use CMake's `add_subdirectory("executorch")` to include the dependency. The `executorch` target, as well as kernel and backend targets will be made available to link against. The runtime can also be built standalone to support diverse toolchains. See [Using ExecuTorch with C++](using-executorch-cpp.md) and [Building from Source](using-executorch-building-from-source.md) for a detailed description of build integration, targets, and cross compilation. ``` -git clone -b viable/strict https://github.com/pytorch/executorch.git +git clone -b release/1.5 https://github.com/pytorch/executorch.git ``` ```cmake # Set CMAKE_CXX_STANDARD to 17 or above. diff --git a/docs/source/raspberry_pi_llama_tutorial.md b/docs/source/raspberry_pi_llama_tutorial.md index 9eb99711ef5..50226a10087 100644 --- a/docs/source/raspberry_pi_llama_tutorial.md +++ b/docs/source/raspberry_pi_llama_tutorial.md @@ -57,7 +57,7 @@ First, clone the ExecuTorch repository with the Raspberry Pi support: ```bash # Create project directory -mkdir ~/executorch-rpi && cd ~/executorch-rpi && git clone -b release/1.0 https://github.com/pytorch/executorch.git && +mkdir ~/executorch-rpi && cd ~/executorch-rpi && git clone -b release/1.5 https://github.com/pytorch/executorch.git && cd executorch ``` diff --git a/docs/source/using-executorch-building-from-source.md b/docs/source/using-executorch-building-from-source.md index e8c7f0e5a3b..77a8d11d8bc 100644 --- a/docs/source/using-executorch-building-from-source.md +++ b/docs/source/using-executorch-building-from-source.md @@ -50,7 +50,7 @@ portability details. ## Environment Setup Clone the ExecuTorch repository from GitHub and create a conda environment. Venv can be used in place of conda. ```bash - git clone -b viable/strict https://github.com/pytorch/executorch.git + git clone -b release/1.5 https://github.com/pytorch/executorch.git cd executorch conda create -yn executorch python=3.10 conda activate executorch diff --git a/docs/source/using-executorch-ios.md b/docs/source/using-executorch-ios.md index 7053c28fd76..1be079bfe13 100644 --- a/docs/source/using-executorch-ios.md +++ b/docs/source/using-executorch-ios.md @@ -31,7 +31,7 @@ The prebuilt ExecuTorch runtime, backend, and kernels are available as a [Swift #### Xcode -In Xcode, go to `File > Add Package Dependencies`. Paste the URL of the [ExecuTorch repo](https://github.com/pytorch/executorch) into the search bar and select it. Make sure to change the branch name to the desired ExecuTorch version in format "swiftpm-", (e.g. "swiftpm-1.0.0"), or a branch name in format "swiftpm-." (e.g. "swiftpm-1.1.0-20251101") for a [nightly build](https://ossci-ios.s3.amazonaws.com/list.html) on a specific date. +In Xcode, go to `File > Add Package Dependencies`. Paste the URL of the [ExecuTorch repo](https://github.com/pytorch/executorch) into the search bar and select it. Make sure to change the branch name to the desired ExecuTorch version in format "swiftpm-", (e.g. "swiftpm-1.5.0"), or a branch name in format "swiftpm-." (e.g. "swiftpm-1.5.0.20260910") for a [nightly build](https://ossci-ios.s3.amazonaws.com/list.html) on a specific date. ![](_static/img/swiftpm_xcode1.png) @@ -64,7 +64,7 @@ let package = Package( ], dependencies: [ // Use "swiftpm-." branch name for a nightly build. - .package(url: "https://github.com/pytorch/executorch.git", branch: "swiftpm-1.0.0") + .package(url: "https://github.com/pytorch/executorch.git", branch: "swiftpm-1.5.0") ], targets: [ .target( @@ -112,7 +112,7 @@ xcode-select --install 2. Clone ExecuTorch: ```bash -git clone -b viable/strict https://github.com/pytorch/executorch.git --depth 1 --recurse-submodules --shallow-submodules && cd executorch +git clone -b release/1.5 https://github.com/pytorch/executorch.git --depth 1 --recurse-submodules --shallow-submodules && cd executorch ``` 3. Set up [Python](https://www.python.org/downloads/macos/) 3.10+ and activate a virtual environment: diff --git a/examples/models/moshi/mimi/install_requirements.sh b/examples/models/moshi/mimi/install_requirements.sh index 93167ead9a1..a9b77f08bdf 100755 --- a/examples/models/moshi/mimi/install_requirements.sh +++ b/examples/models/moshi/mimi/install_requirements.sh @@ -15,7 +15,7 @@ if command -v apt-get >/dev/null 2>&1; then sudo apt-get update sudo apt-get install -y --no-install-recommends ffmpeg fi -pip install torchcodec==0.11.0 --extra-index-url https://download.pytorch.org/whl/test/cpu +pip install torchcodec==0.16.0 --index-url https://download.pytorch.org/whl/cpu pip install moshi==0.2.11 pip install bitsandbytes soundfile einops # Run llama2/install requirements for torchao deps diff --git a/examples/models/qwen3_5_moe/export.py b/examples/models/qwen3_5_moe/export.py index 92408a7ea71..055769be6bf 100644 --- a/examples/models/qwen3_5_moe/export.py +++ b/examples/models/qwen3_5_moe/export.py @@ -1138,9 +1138,11 @@ def _export_cuda(model, config, args): "enable_dynamic_shape": True, "get_mutable_buffer_metadata": _mutable_buffer_metadata_json(model), } - # Avoid PyTorch 2.13's fused cast/reduction compile-time autotune path. Keep - # max autotuning enabled so prefill GEMMs use the CUDA backend's Triton-only - # GEMM lowering instead of emitting libtorch fallback kernels. + # Keep compile-time autotuning enabled so AOTI embeds the selected Triton + # kernels in the shared object. PyTorch 2.14's lazy JIT path otherwise leaves + # runtime references to temporary kernel files that disappear after export. + # Max autotuning is also needed so prefill GEMMs use the CUDA backend's + # Triton-only GEMM lowering instead of emitting libtorch fallback kernels. et_prog = to_edge_transform_and_lower( {"decode": decode_ep, "prefill": prefill_ep}, partitioner={ @@ -1151,7 +1153,7 @@ def _export_cuda(model, config, args): CompileSpec("low_memory_mode", b"ON"), CompileSpec("emulate_precision_casts", b"OFF"), CompileSpec("max_autotune", b"ON"), - CompileSpec("autotune_at_compile_time", b"OFF"), + CompileSpec("autotune_at_compile_time", b"ON"), ] ) ], @@ -1162,7 +1164,7 @@ def _export_cuda(model, config, args): CompileSpec("low_memory_mode", b"ON"), CompileSpec("emulate_precision_casts", b"OFF"), CompileSpec("max_autotune", b"ON"), - CompileSpec("autotune_at_compile_time", b"OFF"), + CompileSpec("autotune_at_compile_time", b"ON"), ] ) ], diff --git a/extension/llm/tokenizers b/extension/llm/tokenizers index 1d7ca636008..61a10ac6124 160000 --- a/extension/llm/tokenizers +++ b/extension/llm/tokenizers @@ -1 +1 @@ -Subproject commit 1d7ca636008fdf88b0c64498ce9cf426041c8443 +Subproject commit 61a10ac61245a68374ace4900e49cd2aba65fb39 diff --git a/install_requirements.py b/install_requirements.py index c4a934a0180..d81cc0c58f3 100644 --- a/install_requirements.py +++ b/install_requirements.py @@ -17,7 +17,7 @@ # This will be dynamically set based on CUDA availability and CUDA backend enabled/disabled. TORCH_URL_BASE = "https://download.pytorch.org/whl/test" TORCHAO_URL_BASE = "https://download.pytorch.org/whl/nightly" -TORCHAO_NIGHTLY_VERSION = "0.18.0.dev20260729" +TORCHAO_NIGHTLY_VERSION = "0.18.0" # Since ExecuTorch often uses main-branch features of pytorch, only the nightly # pip versions will have the required features. diff --git a/setup.py b/setup.py index 8980d1bd94a..c8503e2318d 100644 --- a/setup.py +++ b/setup.py @@ -650,7 +650,7 @@ def _package_relative_depth(library: Path) -> int: def _torchao_requirement() -> str: """The torchao dependency, pinned to the series install_requirements.py installs. - Derived from that module rather than written out, so a nightly bump cannot move the + Derived from that module rather than written out, so a pin bump cannot move the pin without moving this bound with it. A bump into the next series would otherwise silently stop satisfying the lower bound, and installing this package over a development checkout would replace the torchao that was just installed. @@ -696,16 +696,17 @@ def _base_dependencies() -> List[str]: # scope. Neither is needed merely to import the backend. "py-cpuinfo", "requests", - "pytorch-tokenizers", + "pytorch-tokenizers>=1.5.0", # Shipped code imports torchao at module scope in many places, so a plain install cannot # lower a model without it. Among others: the XNNPACK utilities the partitioner uses # (backends/xnnpack/utils/utils.py), the Core ML quantizer, and executorch.export itself. # The MLX backend needs it too, though indirectly: it registers a torchao operator that # only exists once torchao has been imported. # - # The lower bound is the nightly install_requirements.py pins, so that installing this - # package over a development checkout leaves that pin in place. A bound at the stable - # release instead would evict it, because a dev release sorts below its own final. + # The lower bound is the version install_requirements.py pins, so that installing this + # package over a development checkout leaves that pin in place. When the pin is a + # development release, a bound at its eventual stable release would evict it because a + # dev release sorts below its own final. # # The upper bound is what makes naming a pre-release safe. A specifier that names one # accepts pre-releases for this requirement, so without the bound pip would resolve a diff --git a/third-party/ao b/third-party/ao index 03ca489dc62..5f2baf9d575 160000 --- a/third-party/ao +++ b/third-party/ao @@ -1 +1 @@ -Subproject commit 03ca489dc62776955c13999eb6b9d66453f4de7a +Subproject commit 5f2baf9d575cf732362594c998c399902942531f From 08a5956d9d7150964de4857c3408449e9791afca Mon Sep 17 00:00:00 2001 From: Jacob Szwejbka Date: Thu, 17 Sep 2026 13:20:05 -0700 Subject: [PATCH 3/8] [RELEASE ONLY CHANGES] Expose thread count through the parallel interface (#22911) Cherry-pick of #22908 onto `release/1.5`. This exposes the thread count through the installed public parallel interface, allowing downstream TorchAO code to avoid depending on the private `extension/threadpool/threadpool.h` header. Test plan: - Original PR #22908 CI - `python3 -m py_compile .ci/scripts/wheel/test_cpp_sdk.py` - `git diff --check` --- .ci/scripts/wheel/test_cpp_sdk.py | 3 ++- extension/threadpool/test/thread_parallel_test.cpp | 5 +++++ extension/threadpool/thread_parallel.cpp | 6 ++++++ runtime/kernel/thread_parallel_interface.h | 7 +++++++ 4 files changed, 20 insertions(+), 1 deletion(-) diff --git a/.ci/scripts/wheel/test_cpp_sdk.py b/.ci/scripts/wheel/test_cpp_sdk.py index 202a400e6c7..24e660e227b 100644 --- a/.ci/scripts/wheel/test_cpp_sdk.py +++ b/.ci/scripts/wheel/test_cpp_sdk.py @@ -1098,7 +1098,8 @@ def test_shipped_headers_have_implementations(work_dir: Path) -> None: "#define ET_USE_THREADPOOL\n" "#include \n" "using namespace executorch::extension;\n" - "int main() { return parallel_for(0, 1, 1, [](int64_t, int64_t) {}) ? 0 : 1; }\n" + "int main() { return get_thread_count() > 0 && " + "parallel_for(0, 1, 1, [](int64_t, int64_t) {}) ? 0 : 1; }\n" ), } diff --git a/extension/threadpool/test/thread_parallel_test.cpp b/extension/threadpool/test/thread_parallel_test.cpp index fd72211a789..34c57299808 100644 --- a/extension/threadpool/test/thread_parallel_test.cpp +++ b/extension/threadpool/test/thread_parallel_test.cpp @@ -15,6 +15,7 @@ #include using namespace ::testing; +using ::executorch::extension::get_thread_count; using ::executorch::extension::parallel_for; class ParallelTest : public ::testing::TestWithParam { @@ -205,6 +206,10 @@ TEST_P(ParallelTest, TestChunkSizeTooLarge) { } } +TEST(ThreadParallelInterfaceTest, GetThreadCount) { + EXPECT_GT(get_thread_count(), 0); +} + INSTANTIATE_TEST_SUITE_P( ParallelTestWithOrWithoutThreadpool, ParallelTest, diff --git a/extension/threadpool/thread_parallel.cpp b/extension/threadpool/thread_parallel.cpp index 0fd95019753..71daa053d1b 100644 --- a/extension/threadpool/thread_parallel.cpp +++ b/extension/threadpool/thread_parallel.cpp @@ -80,5 +80,11 @@ bool parallel_for( return true; } +int64_t get_thread_count() { + return static_cast( + ::executorch::extension::threadpool::get_threadpool() + ->get_thread_count()); +} + } // namespace extension } // namespace executorch diff --git a/runtime/kernel/thread_parallel_interface.h b/runtime/kernel/thread_parallel_interface.h index 8cce610dcb4..c620492423a 100644 --- a/runtime/kernel/thread_parallel_interface.h +++ b/runtime/kernel/thread_parallel_interface.h @@ -72,6 +72,9 @@ bool parallel_for( const int64_t grain_size, runtime::FunctionRef f); +/** Returns the number of threads available to parallel_for. */ +int64_t get_thread_count(); + int64_t get_thread_num(); void set_thread_num(int64_t thread_num); @@ -89,6 +92,10 @@ inline int64_t get_thread_num() { return 0; } +inline int64_t get_thread_count() { + return 1; +} + inline void set_thread_num(int64_t thread_num) { ET_DCHECK_MSG(false, "cannot set_thread_num without threading support!"); } From f8ce3d89f99434f930370f98fe105fa5ca40e468 Mon Sep 17 00:00:00 2001 From: pytorchbot Date: Thu, 17 Sep 2026 13:21:27 -0700 Subject: [PATCH 4/8] [Cortex-M] Update CMSIS-NN to v8.0.0 (#22910) ### Summary Update CMSIS-NN to v8.0.0 and align the CMSIS pack dependency and smoke-test versions. Stop Arm runner scripts from automatically substituting the Ethos-U SDK checkout, so fresh runner builds use the backend CMake pin. Remove the transpose-convolution ReLU and Hardtanh expected failures resolved by the release's padding-row alignment fix. ### Test plan The original update was validated with an explicitly selected v8.0.0 M55 runner: 51 passed, 7 xfailed in the focused Cortex-M suite. For the runner-script correction, shell syntax, ShellCheck error checks, and git diff checks pass. The CMake-selected v8 library cross-compiles for M55, M7, and M0+ with the expected MVE, DSP, and scalar paths. Complete runner builds and FVP tests have not been rerun for this correction. Authored with OpenAI Codex. cc @digantdesai @freddan80 @per @zingo @oscarandersson8218 @mansnils @Sebastian-Larsson @robell @rascani Co-authored-by: RJ Ascani --- backends/arm/cmsis_pack/config/executorch_config.yml | 2 +- backends/arm/cmsis_pack/test/smoke/smoke.csolution.yml | 2 +- backends/arm/scripts/build_executor_runner.sh | 5 ----- backends/cortex_m/CMakeLists.txt | 2 +- backends/cortex_m/test/ops/test_conv_transpose.py | 8 +------- examples/arduino/build_arduino_library.sh | 4 ++-- examples/arm/run.sh | 7 ------- 7 files changed, 6 insertions(+), 24 deletions(-) diff --git a/backends/arm/cmsis_pack/config/executorch_config.yml b/backends/arm/cmsis_pack/config/executorch_config.yml index 91bae4fca79..369756c8220 100644 --- a/backends/arm/cmsis_pack/config/executorch_config.yml +++ b/backends/arm/cmsis_pack/config/executorch_config.yml @@ -22,7 +22,7 @@ dependencies: - pack: ARM::CMSIS version: "6.0.0:6.99.99" - pack: ARM::CMSIS-NN - version: "7.0.0:7.99.99" + version: "8.0.0:8.99.99" optional: true # Only for CMSIS-NN optimized backend - pack: ARM::ethos-u-core-driver version: "1.0.0:1.99.99" diff --git a/backends/arm/cmsis_pack/test/smoke/smoke.csolution.yml b/backends/arm/cmsis_pack/test/smoke/smoke.csolution.yml index e861f16b13a..452eba70b40 100644 --- a/backends/arm/cmsis_pack/test/smoke/smoke.csolution.yml +++ b/backends/arm/cmsis_pack/test/smoke/smoke.csolution.yml @@ -10,7 +10,7 @@ solution: packs: - pack: ARM::CMSIS@>=6.0.0 - - pack: ARM::CMSIS-NN@7.0.0 + - pack: ARM::CMSIS-NN@8.0.0 - pack: ARM::Cortex_DFP@>=1.1.0 - pack: PyTorch::ExecuTorch diff --git a/backends/arm/scripts/build_executor_runner.sh b/backends/arm/scripts/build_executor_runner.sh index eeec9c14166..b83bcc531d3 100755 --- a/backends/arm/scripts/build_executor_runner.sh +++ b/backends/arm/scripts/build_executor_runner.sh @@ -146,10 +146,6 @@ ethosu_tools_dir=$(realpath ${ethosu_tools_dir}) ethos_u_root_dir="${ethosu_tools_dir}/ethos-u" mkdir -p "${ethos_u_root_dir}" ethos_u_root_dir=$(realpath ${ethos_u_root_dir}) -cmsis_nn_local_path="" -if [[ -d "${ethos_u_root_dir}/core_software/cmsis-nn" ]]; then - cmsis_nn_local_path=$(realpath "${ethos_u_root_dir}/core_software/cmsis-nn") -fi if [[ ${system_config} == "" ]] then @@ -246,7 +242,6 @@ cmake \ ${flatc_flags} \ -DEXECUTORCH_SELECT_OPS_LIST="${select_ops_list}" \ -DETHOS_SDK_PATH:PATH=${ethos_u_root_dir} \ - ${cmsis_nn_local_path:+-DCMSIS_NN_LOCAL_PATH:PATH=${cmsis_nn_local_path}} \ ${extra_build_flags} echo "[${BASH_SOURCE[0]}] Configured CMAKE" diff --git a/backends/cortex_m/CMakeLists.txt b/backends/cortex_m/CMakeLists.txt index 3c901b513de..a7c1d7c3ab2 100644 --- a/backends/cortex_m/CMakeLists.txt +++ b/backends/cortex_m/CMakeLists.txt @@ -23,7 +23,7 @@ include(FetchContent) # CMSIS-NN configuration with dynamic path detection set(CMSIS_NN_VERSION - "dbf45dbfcc515421dd6099037d3e2637b90748c8" + "v8.0.0" CACHE STRING "CMSIS-NN version to download" ) set(CMSIS_NN_LOCAL_PATH diff --git a/backends/cortex_m/test/ops/test_conv_transpose.py b/backends/cortex_m/test/ops/test_conv_transpose.py index 14bc8d98b5d..5388a130062 100644 --- a/backends/cortex_m/test/ops/test_conv_transpose.py +++ b/backends/cortex_m/test/ops/test_conv_transpose.py @@ -317,13 +317,7 @@ def test_dialect_conv_transpose2d(test_case, cortex_m_target): tester.check_not(test_case.model.ops_after_absent) -xfails_implementation: dict[str, xfail_type] = { - "conv_transpose2d_relu": "Fused transpose-conv + relu lowers correctly but current implementation is numerically incorrect.", - "conv_transpose2d_hardtanh": "Fused transpose-conv + hardtanh lowers correctly but current implementation is numerically incorrect.", -} - - -@parametrize("test_case", test_cases, xfails=xfails_implementation) +@parametrize("test_case", test_cases) def test_implementation_conv_transpose2d(test_case, cortex_m_target): tester = CortexMTester( test_case.model, test_case.example_inputs, target_config=cortex_m_target diff --git a/examples/arduino/build_arduino_library.sh b/examples/arduino/build_arduino_library.sh index 70b1135e485..7cc3aca9bd2 100755 --- a/examples/arduino/build_arduino_library.sh +++ b/examples/arduino/build_arduino_library.sh @@ -318,8 +318,8 @@ done # it with FetchContent at cmake time -- so fetch it at the revision that backend # pins. Without it the Cortex-M ops compile against headers that are not there. if [ -z "$CMSIS_NN" ]; then - CMSIS_NN_PIN=$(sed -n '/set(CMSIS_NN_VERSION/,/)/p' \ - "$ET_ROOT/backends/cortex_m/CMakeLists.txt" | grep -oE '"[0-9a-f]{40}"' | tr -d '"') + CMSIS_NN_PIN=$(sed -n '/set(CMSIS_NN_VERSION/,/)/s/^[[:space:]]*"\([^"]*\)".*/\1/p' \ + "$ET_ROOT/backends/cortex_m/CMakeLists.txt") if [ -z "$CMSIS_NN_PIN" ]; then echo "ERROR: could not read CMSIS_NN_VERSION from backends/cortex_m/CMakeLists.txt" exit 1 diff --git a/examples/arm/run.sh b/examples/arm/run.sh index b69f8f1c4a7..2475b218b99 100755 --- a/examples/arm/run.sh +++ b/examples/arm/run.sh @@ -164,10 +164,6 @@ arm_scratch_dir=$(realpath "${arm_scratch_dir}") ethos_u_root_dir="${arm_scratch_dir}/ethos-u" mkdir -p "${ethos_u_root_dir}" ethos_u_root_dir=$(realpath "${ethos_u_root_dir}") -cmsis_nn_local_path="" -if [[ -d "${ethos_u_root_dir}/core_software/cmsis-nn" ]]; then - cmsis_nn_local_path=$(realpath "${ethos_u_root_dir}/core_software/cmsis-nn") -fi setup_path_script=${arm_scratch_dir}/setup_path.sh _setup_msg="please refer to ${script_dir}/setup.sh to properly install necessary tools." @@ -331,9 +327,6 @@ configure_runner_build_dir() { -DETHOS_SDK_PATH:PATH="${ethos_u_root_dir}" -DEXECUTORCH_SELECT_OPS_LIST="${select_ops_list}" ) - if [[ -n "${cmsis_nn_local_path}" ]]; then - cmake_cmd+=(-DCMSIS_NN_LOCAL_PATH:PATH="${cmsis_nn_local_path}") - fi cmake_cmd+=(-DET_PTE_FILE_PATH:PATH="${pte_source}") if [[ "${pte_placement}" == "elf" ]]; then cmake_cmd+=(-DET_MODEL_PTE_ADDR=) From 65bfc9413d89353c326b1f0a2f43cfa05f057e9c Mon Sep 17 00:00:00 2001 From: pytorchbot Date: Thu, 17 Sep 2026 14:25:41 -0700 Subject: [PATCH 5/8] Arm backend: Relax SymbolicShapeSupport-checks (#22834) Allow TOSA support checks to distinguish between symbolic tensor metadata and symbolic shape materialization. Without the TOSA shape extension, symbolic tensor input/output shapes can still be accepted for ops whose lowering does not need materialized shape values. Reject symbolic SymInt arguments and the known shape- dependent edge cases instead. For targets that require fully resolved tensor shapes, add a separate partitioner-level check that rejects nodes with unresolved tensor input or output shapes. cc @digantdesai @freddan80 @per @zingo @oscarandersson8218 @mansnils @Sebastian-Larsson @robell @rascani Signed-off-by: Oscar Andersson Co-authored-by: Oscar Andersson <87121123+oscarandersson8218@users.noreply.github.com> --- backends/arm/ethosu/partitioner.py | 1 + .../tosa_supported_operators.py | 196 +++++++++++---- .../tosa_dialect/test_tosa_shape_support.py | 229 +++++++++++++++++- backends/arm/tosa/partitioner.py | 8 +- backends/arm/vgf/partitioner.py | 1 + 5 files changed, 381 insertions(+), 54 deletions(-) diff --git a/backends/arm/ethosu/partitioner.py b/backends/arm/ethosu/partitioner.py index eb04e69c8bf..0cf46cd59b0 100644 --- a/backends/arm/ethosu/partitioner.py +++ b/backends/arm/ethosu/partitioner.py @@ -37,6 +37,7 @@ def __init__( self.compile_spec = compile_spec self.additional_checks = additional_checks self.tosa_spec = compile_spec.tosa_spec + self._requires_resolved_tensor_shapes = True self._decomposable_resize_support = DecomposableResizeSupported(self.tosa_spec) self._custom_partition_ops: set[OpOverload] = set() self.intermediate_path = compile_spec._get_intermediate_path() diff --git a/backends/arm/operator_support/tosa_supported_operators.py b/backends/arm/operator_support/tosa_supported_operators.py index 9a6149b0a7f..7c386256047 100644 --- a/backends/arm/operator_support/tosa_supported_operators.py +++ b/backends/arm/operator_support/tosa_supported_operators.py @@ -505,7 +505,10 @@ def tosa_support_factory( if additional_positive_checks: positive_checks.extend(additional_positive_checks) negative_checks = _negative_checks( - tosa_spec, exported_program, reporter, additional_checks + tosa_spec, + exported_program, + reporter, + additional_checks, ) return chain( @@ -517,8 +520,44 @@ def tosa_support_factory( ) +def _has_symbolic_shape(node: fx.Node) -> bool: + val = node.meta.get("val") + vals = val if isinstance(val, (list, tuple)) else (val,) + for node_val in vals: + if isinstance(node_val, torch.SymInt): + return True + + shape = getattr(node_val, "shape", None) + if shape is not None and any(isinstance(dim, torch.SymInt) for dim in shape): + return True + + return False + + class SymbolicShapeSupportCheck(OperatorSupportBase): - """Reject symbolic tensor shapes for specs without the shape extension.""" + """Reject symbolic shape constructs that require the TOSA shape + extension. + """ + + _SYMBOLIC_SPATIAL_DIM_TARGETS = ( + exir_ops.edge.aten.convolution.default, + exir_ops.edge.aten.avg_pool2d.default, + exir_ops.edge.aten.max_pool2d.default, + exir_ops.edge.aten.max_pool2d_with_indices.default, + exir_ops.edge.aten._adaptive_avg_pool2d.default, + torch.ops.aten.conv_transpose2d.input, + ) + _SYMBOLIC_MEAN_TARGETS = ( + exir_ops.edge.aten.mean.dim, + exir_ops.edge.aten.mean.default, + ) + _SYMBOLIC_VIEW_SHAPE_TARGETS = ( + exir_ops.edge.aten.squeeze_copy.dim, + exir_ops.edge.aten.squeeze_copy.dims, + exir_ops.edge.aten.unsqueeze_copy.default, + ) + _SYMBOLIC_PAD_TARGETS = (exir_ops.edge.aten.constant_pad_nd.default,) + _SYMBOLIC_SLICE_TARGETS = (exir_ops.edge.aten.slice_copy.Tensor,) def __init__(self, reporter: WhyNoPartitionReporter): """Initialize the check with a reporter. @@ -530,88 +569,147 @@ def __init__(self, reporter: WhyNoPartitionReporter): self.reporter = reporter @staticmethod - def _has_symbolic_shape(node: fx.Node) -> bool: - val = node.meta.get("val") - vals = val if isinstance(val, (list, tuple)) else (val,) - for node_val in vals: - if isinstance(node_val, torch.SymInt): + def _has_symbolic_shape_argument(arg: object) -> bool: + if isinstance(arg, torch.SymInt): + return True + + if isinstance(arg, fx.Node): + return SymbolicShapeSupportCheck._has_symbolic_shape_argument( + arg.meta.get("val") + ) + + if isinstance(arg, (list, tuple)): + return any( + SymbolicShapeSupportCheck._has_symbolic_shape_argument(item) + for item in arg + ) + + return False + + @staticmethod + def _get_mean_reduction_dims(node: fx.Node, input_rank: int) -> tuple[int, ...]: + if node.target == exir_ops.edge.aten.mean.default: + return tuple(range(input_rank)) + + dims = node.kwargs.get("dim", node.args[1] if len(node.args) > 1 else None) + if dims is None: + return tuple(range(input_rank)) + if isinstance(dims, int): + return (dims % input_rank,) + return tuple(dim % input_rank for dim in typing.cast(Sequence[int], dims)) + + def _has_unsupported_symbolic_tensor_shape(self, node: fx.Node) -> bool: + if node.target not in ( + *self._SYMBOLIC_SPATIAL_DIM_TARGETS, + *self._SYMBOLIC_MEAN_TARGETS, + *self._SYMBOLIC_VIEW_SHAPE_TARGETS, + *self._SYMBOLIC_PAD_TARGETS, + *self._SYMBOLIC_SLICE_TARGETS, + ): + return False + if not node.all_input_nodes: + return False + + input_node = node.all_input_nodes[0] + input_fake_tensor = get_first_fake_tensor(input_node) + if not any(isinstance(s, torch.SymInt) for s in input_fake_tensor.shape): + return False + + if node.target in self._SYMBOLIC_SPATIAL_DIM_TARGETS: + if any(isinstance(s, torch.SymInt) for s in input_fake_tensor.shape[2:]): + self.reporter.report_reject(node, "Symbolic spatial dims unsupported") return True - shape = getattr(node_val, "shape", None) - if shape is not None and any( - isinstance(dim, torch.SymInt) for dim in shape + if node.target in self._SYMBOLIC_MEAN_TARGETS: + if any( + isinstance(input_fake_tensor.shape[dim], torch.SymInt) + for dim in self._get_mean_reduction_dims( + node, len(input_fake_tensor.shape) + ) ): + self.reporter.report_reject(node, "Symbolic mean dims unsupported") return True + if node.target in self._SYMBOLIC_VIEW_SHAPE_TARGETS: + self.reporter.report_reject(node, "Symbolic view dims unsupported") + return True + + if node.target in self._SYMBOLIC_PAD_TARGETS: + self.reporter.report_reject(node, "Symbolic pad dims unsupported") + return True + + if node.target in self._SYMBOLIC_SLICE_TARGETS: + self.reporter.report_reject(node, "Symbolic slices unsupported") + return True + return False - def _partition_dynamic_upmsample_nearest2d(self, node: fx.Node) -> bool: - """Check if the node is an upsample_nearest2d with symbolic shapes. + def is_node_supported( + self, submodules: typing.Mapping[str, torch.nn.Module], node: fx.Node + ) -> bool: + """Return False for symbolic shape uses needing shape extension. + + Without TOSA shape extension, symbolic input/output tensor dimensions + are generally allowed because they are tensor metadata. Symbolic shape + arguments and known shape-materialization edge cases are rejected. Args: + submodules (typing.Mapping[str, torch.nn.Module]): Exported modules. node (fx.Node): FX node to check. Returns: - bool: True if the node is an upsample_nearest2d with symbolic - shapes; otherwise, False. + bool: False if rejected by constraints; otherwise, True. """ - if node.target != exir_ops.edge.aten.upsample_nearest2d.vec: - return False + del submodules + if node.op in ("placeholder", "output"): + return True + if node.op == "call_function" and node.target in (*Q_OPS, *DQ_OPS): + return True - try: - input_tensor = get_first_fake_tensor(node.all_input_nodes[0]) - output_tensor = get_first_fake_tensor(node) - except Exception as exc: + if self._has_symbolic_shape_argument(node.args): self.reporter.report_reject( node, - f"upsample_nearest2d symbolic shapes need tensor metadata: {exc}", + "Node has symbolic shape arguments, has the TOSA spec shape extension support?", ) return False - input_size_xy = input_tensor.shape[2:4] - output_size_xy = output_tensor.shape[2:4] - if len(input_size_xy) != 2 or len(output_size_xy) != 2: - self.reporter.report_reject( - node, "upsample_nearest2d expects 2D spatial input/output." - ) + if self._has_unsupported_symbolic_tensor_shape(node): return False return True - def is_node_supported( - self, submodules: typing.Mapping[str, torch.nn.Module], node: fx.Node - ) -> bool: - """Return False for nodes with symbolic tensor input or output shapes. - Dynamic shapes require the TOSA shape extension. Reject nodes with - symbolic tensor dimensions before partitioning when the active spec - does not enable that extension. +class CheckResolvedTensorShapes(OperatorSupportBase): + """Reject nodes with unresolved tensor input or output shapes.""" - Args: - submodules (typing.Mapping[str, torch.nn.Module]): Exported modules. - node (fx.Node): FX node to check. + def __init__(self, reporter: WhyNoPartitionReporter): + """Initialize the check with a reporter. - Returns: - bool: False if rejected by constraints; otherwise, True. + Args: + reporter (WhyNoPartitionReporter): Reporter for rejection reasons. """ + self.reporter = reporter + + def is_node_supported( + self, submodules: typing.Mapping[str, torch.nn.Module], node: fx.Node + ) -> bool: + """Return False when the node depends on unresolved tensor shapes.""" + del submodules if node.op in ("placeholder", "output"): return True if node.op == "call_function" and node.target in (*Q_OPS, *DQ_OPS): return True - if self._has_symbolic_shape(node) or any( - self._has_symbolic_shape(input_node) for input_node in node.all_input_nodes + if _has_symbolic_shape(node) or any( + _has_symbolic_shape(input_node) for input_node in node.all_input_nodes ): - if node.target == exir_ops.edge.aten.upsample_nearest2d.vec: - return self._partition_dynamic_upmsample_nearest2d(node) - else: - self.reporter.report_reject( - node, - "Node has symbolic shape, has the TOSA spec shape extension support?", - ) - return False + self.reporter.report_reject( + node, + "Node has unresolved tensor shapes, which are not supported by this target.", + ) + return False return True diff --git a/backends/arm/test/misc/tosa_dialect/test_tosa_shape_support.py b/backends/arm/test/misc/tosa_dialect/test_tosa_shape_support.py index 36271dda018..6b9bc1f8db3 100644 --- a/backends/arm/test/misc/tosa_dialect/test_tosa_shape_support.py +++ b/backends/arm/test/misc/tosa_dialect/test_tosa_shape_support.py @@ -3,12 +3,18 @@ # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. +import executorch.backends.arm.operator_support.convolution_support # noqa: F401 +import executorch.backends.arm.operator_support.pool_2d_support # noqa: F401 +import executorch.backends.arm.operator_support.reduce_sum_support # noqa: F401 import executorch.backends.arm.operator_support.sym_size_int_support # noqa: F401 +import pytest import torch from executorch.backends.arm.operator_support.tosa_supported_operators import ( tosa_support_factory, ) +from executorch.backends.arm.test import common +from executorch.backends.arm.test.tester.arm_tester import ArmTester from executorch.backends.arm.tosa.compile_spec import TosaCompileSpec from executorch.backends.arm.tosa.partitioner import TOSAPartitioner from executorch.backends.arm.tosa.specification import TosaSpecification @@ -28,11 +34,60 @@ def forward(self, x: torch.Tensor, y: torch.Tensor) -> torch.Tensor: return torch.atan2(x, y) +class Conv2d(torch.nn.Module): + def __init__(self) -> None: + super().__init__() + self.conv = torch.nn.Conv2d(3, 4, 3, padding=1) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return self.conv(x) + + +class AvgPool2d(torch.nn.Module): + def forward(self, x: torch.Tensor) -> torch.Tensor: + return torch.nn.functional.avg_pool2d(x, kernel_size=2, stride=2) + + +class MeanDim(torch.nn.Module): + def forward(self, x: torch.Tensor) -> torch.Tensor: + return x.mean(dim=2) + + +class MeanDefault(torch.nn.Module): + def forward(self, x: torch.Tensor) -> torch.Tensor: + return x.mean() + + +class Squeeze(torch.nn.Module): + def forward(self, x: torch.Tensor) -> torch.Tensor: + return x.squeeze(2) + + +class Unsqueeze(torch.nn.Module): + def forward(self, x: torch.Tensor) -> torch.Tensor: + return x.unsqueeze(2) + + +class Slice(torch.nn.Module): + def forward(self, x: torch.Tensor) -> torch.Tensor: + return x[:, :, 1:, :] + + +class ScalarTensor(torch.nn.Module): + def forward(self) -> torch.Tensor: + return torch.scalar_tensor(1.0) + + class ReturnSymSize(torch.nn.Module): def forward(self, x: torch.Tensor) -> tuple[torch.Tensor, int]: return x, x.shape[0] +class ReshapeWithSymSize(torch.nn.Module): + def forward(self, x: torch.Tensor) -> torch.Tensor: + return x.reshape(x.shape[0], 6) + + def _exported_program( module: torch.nn.Module, inputs: tuple[torch.Tensor, ...], @@ -86,7 +141,7 @@ def test_shape_extension_accepts_supported_symbolic_tensor_op(): assert support.is_node_supported(exported_program.graph_module, add_node) is True -def test_without_shape_extension_rejects_supported_symbolic_tensor_op(): +def test_without_shape_extension_accepts_supported_symbolic_tensor_op(): inputs = (torch.randn(2, 3), torch.randn(2, 3)) batch = Dim("batch", min=1, max=4) exported_program = _exported_program( @@ -94,11 +149,177 @@ def test_without_shape_extension_rejects_supported_symbolic_tensor_op(): inputs, dynamic_shapes=({0: batch}, {0: batch}), ) - support, reporter = _support("TOSA-1.0+FP", exported_program) + support, _ = _support("TOSA-1.0+FP", exported_program) add_node = _find_node(exported_program, exir_ops.edge.aten.add.Tensor) - assert support.is_node_supported(exported_program.graph_module, add_node) is False - assert "Node has symbolic shape" in reporter.get_table_report() + assert support.is_node_supported(exported_program.graph_module, add_node) is True + + +def _assert_rejected_with_reason(exported_program, target, reason: str) -> None: + support, reporter = _support("TOSA-1.0+FP", exported_program) + node = _find_node(exported_program, target) + + assert support.is_node_supported(exported_program.graph_module, node) is False + assert reason in reporter.get_table_report() + + +@pytest.mark.parametrize( + "compile_spec", + (common.get_u55_compile_spec(), common.get_u85_compile_spec()), +) +def test_ethos_rejects_unresolved_tensor_shapes(compile_spec): + inputs = (torch.randn(2, 3), torch.randn(2, 3)) + batch = Dim("batch", min=1, max=4) + tester = ArmTester( + Add(), + inputs, + compile_spec, + dynamic_shapes=({0: batch}, {0: batch}), + ) + + tester.quantize().export().to_edge().partition() + targets = { + node.target + for node in tester.stages[tester.cur].artifact.exported_program().graph.nodes + } + + assert exir_ops.edge.aten.add.Tensor in targets + assert torch.ops.higher_order.executorch_call_delegate not in targets + + +def test_without_shape_extension_rejects_symbolic_spatial_conv2d(): + inputs = (torch.randn(2, 3, 8, 8),) + height = Dim("height", min=4, max=10) + exported_program = _exported_program( + Conv2d(), + inputs, + dynamic_shapes=({2: height},), + ) + + _assert_rejected_with_reason( + exported_program, + exir_ops.edge.aten.convolution.default, + "Symbolic spatial dims unsupported", + ) + + +def test_without_shape_extension_rejects_symbolic_spatial_pooling(): + inputs = (torch.randn(2, 3, 8, 8),) + height = Dim("height", min=2, max=5) * 2 + exported_program = _exported_program( + AvgPool2d(), + inputs, + dynamic_shapes=({2: height},), + ) + + _assert_rejected_with_reason( + exported_program, + exir_ops.edge.aten.avg_pool2d.default, + "Symbolic spatial dims unsupported", + ) + + +def test_without_shape_extension_rejects_symbolic_mean_reduction_dim(): + inputs = (torch.randn(2, 3, 8, 8),) + height = Dim("height", min=4, max=10) + exported_program = _exported_program( + MeanDim(), + inputs, + dynamic_shapes=({2: height},), + ) + + _assert_rejected_with_reason( + exported_program, + exir_ops.edge.aten.mean.dim, + "Symbolic mean dims unsupported", + ) + + +def test_without_shape_extension_rejects_symbolic_full_tensor_mean(): + inputs = (torch.randn(2, 3, 8, 8),) + height = Dim("height", min=4, max=10) + exported_program = _exported_program( + MeanDefault(), + inputs, + dynamic_shapes=({2: height},), + ) + + _assert_rejected_with_reason( + exported_program, + exir_ops.edge.aten.mean.default, + "Symbolic mean dims unsupported", + ) + + +def test_without_shape_extension_rejects_symbolic_squeeze(): + inputs = (torch.randn(2, 3, 1, 8),) + batch = Dim("batch", min=1, max=4) + exported_program = _exported_program( + Squeeze(), + inputs, + dynamic_shapes=({0: batch},), + ) + + _assert_rejected_with_reason( + exported_program, + exir_ops.edge.aten.squeeze_copy.dims, + "Symbolic view dims unsupported", + ) + + +def test_without_shape_extension_rejects_symbolic_unsqueeze(): + inputs = (torch.randn(2, 3, 8),) + batch = Dim("batch", min=1, max=4) + exported_program = _exported_program( + Unsqueeze(), + inputs, + dynamic_shapes=({0: batch},), + ) + + _assert_rejected_with_reason( + exported_program, + exir_ops.edge.aten.unsqueeze_copy.default, + "Symbolic view dims unsupported", + ) + + +def test_without_shape_extension_rejects_symbolic_slice(): + inputs = (torch.randn(2, 3, 8, 8),) + height = Dim("height", min=4, max=10) + exported_program = _exported_program( + Slice(), + inputs, + dynamic_shapes=({2: height},), + ) + + _assert_rejected_with_reason( + exported_program, + exir_ops.edge.aten.slice_copy.Tensor, + "Symbolic slices unsupported", + ) + + +def test_without_shape_extension_accepts_zero_input_supported_op(): + exported_program = _exported_program(ScalarTensor(), ()) + support, _ = _support("TOSA-1.0+FP", exported_program) + scalar_node = _find_node(exported_program, torch.ops.aten.scalar_tensor.default) + + assert support.is_node_supported(exported_program.graph_module, scalar_node) is True + + +def test_without_shape_extension_rejects_symbolic_shape_argument(): + inputs = (torch.randn(2, 2, 3),) + batch = Dim("batch", min=1, max=4) + exported_program = _exported_program( + ReshapeWithSymSize(), + inputs, + dynamic_shapes=({0: batch},), + ) + support, reporter = _support("TOSA-1.0+FP", exported_program) + view_node = _find_node(exported_program, exir_ops.edge.aten.view_copy.default) + + assert support.is_node_supported(exported_program.graph_module, view_node) is False + assert "Node has symbolic shape arguments" in reporter.get_table_report() def test_without_shape_extension_rejects_sym_size_int(): diff --git a/backends/arm/tosa/partitioner.py b/backends/arm/tosa/partitioner.py index 96c8286f664..2dd4e9c40f3 100644 --- a/backends/arm/tosa/partitioner.py +++ b/backends/arm/tosa/partitioner.py @@ -37,6 +37,7 @@ from executorch.backends.arm.common.type import ensure_type from executorch.backends.arm.constants import DQ_OPS, Q_OPS from executorch.backends.arm.operator_support.tosa_supported_operators import ( + CheckResolvedTensorShapes, tosa_support_factory, ) from executorch.backends.arm.tosa.backend import TOSABackend @@ -388,6 +389,7 @@ def __init__( self.compile_spec = compile_spec self.tosa_spec = compile_spec.tosa_spec self.additional_checks = additional_checks + self._requires_resolved_tensor_shapes = False self._decomposable_resize_support = DecomposableResizeSupported(self.tosa_spec) self._custom_partition_ops: set[torch._ops.OpOverload] = set() self.intermediate_path = compile_spec._get_intermediate_path() @@ -685,11 +687,15 @@ def _create_operator_support( containing_program: ExportedProgram, reporter: WhyNoPartitionReporter, ) -> OperatorSupportBase: + additional_checks = list(self.additional_checks or ()) + if self._requires_resolved_tensor_shapes: + additional_checks.append(CheckResolvedTensorShapes(reporter)) + return tosa_support_factory( self.tosa_spec, containing_program, reporter, - self.additional_checks, + additional_checks=additional_checks, additional_positive_checks=[ self._decomposable_resize_support, DecomposableLargeStrideMaxPool2dForU55Supported(self.tosa_spec), diff --git a/backends/arm/vgf/partitioner.py b/backends/arm/vgf/partitioner.py index be7de275af4..4d1b6c6f4b5 100644 --- a/backends/arm/vgf/partitioner.py +++ b/backends/arm/vgf/partitioner.py @@ -43,3 +43,4 @@ def __init__( self.intermediate_path = compile_spec._get_intermediate_path() # Preserve grid_sampler_2d for the VGF custom-lowering path only. self.register_custom_partition_op(exir_ops.edge.aten.grid_sampler_2d.default) + self._requires_resolved_tensor_shapes = False From 8e9da438c434c9c9b4deb9e632da62af7782ea7d Mon Sep 17 00:00:00 2001 From: pytorchbot Date: Thu, 17 Sep 2026 19:54:46 -0700 Subject: [PATCH 6/8] Promote NXP redundant Q/DQ cleanup to to_edge (#22913) Eval-mode dropout can disappear before shared no-op cleanup runs, leaving adjacent dequantize/quantize conversions. Promote NXP's `FoldRedundantDequantizeQuantizePass` from #21016 into the shared lowering sequence used by `to_edge` and `to_edge_transform_and_lower`. Cortex-M MLPerf Tiny DS-CNN then finishes with one quantize and one dequantize operation in both layouts, down from three of each. MobileNetV2 similarly drops from two to one of each. The shared `eliminate_dq_q` matcher remains unchanged, keeping this release patch focused on promoting the existing cleanup. Remove the stale dropout target from `RemoveNoopPass`. NXP retains a later invocation of the shared pass because its auxiliary-operator splitting can introduce new adjacent pairs. Update the Cortex-M model counts and correct the clone regression's obsolete Q/DQ signatures so the test also checks numerical equivalence. Validation on PyTorch 2.14 CPU: 77 EXIR and clone-transform tests passed, with 28 passing subtests and one expected failure. The dropout regression covers both edge APIs, per-tensor and per-channel quantization, and int8/uint8/int16/int32. All 25 selected Cortex-M DS-CNN, MobileNetV2, and explicit-layout tests passed, including Corstone-300 FVP implementation tests. Additional explicit-layout DS-CNN and legacy `ExirExportedProgram.to_edge` numerical checks passed. Formatting, flake8, and `git diff --check` passed. Authored with AI assistance from OpenAI Codex. Co-authored-by: RJ Ascani --- backends/cortex_m/test/models/test_ds_cnn.py | 8 +- .../cortex_m/test/models/test_mobilenet_v2.py | 8 +- backends/nxp/BUCK | 1 + .../edge_passes/neutron_edge_pass_manager.py | 15 +-- backends/nxp/tests/test_fold_redundant_qdq.py | 28 +---- .../transforms/test/test_remove_clone_ops.py | 30 +++--- exir/passes/BUCK | 13 +++ exir/passes/__init__.py | 4 + .../passes}/fold_redundant_qdq_pass.py | 18 ++-- exir/passes/remove_noop_pass.py | 1 - exir/tests/targets.bzl | 1 + exir/tests/test_passes.py | 102 +++++++++++++++++- 12 files changed, 157 insertions(+), 72 deletions(-) rename {backends/nxp/edge_passes => exir/passes}/fold_redundant_qdq_pass.py (55%) diff --git a/backends/cortex_m/test/models/test_ds_cnn.py b/backends/cortex_m/test/models/test_ds_cnn.py index ba5b2da6b78..7b578cb4177 100644 --- a/backends/cortex_m/test/models/test_ds_cnn.py +++ b/backends/cortex_m/test/models/test_ds_cnn.py @@ -16,15 +16,15 @@ "executorch_exir_dialects_edge__ops_aten_relu_default": 9, "executorch_exir_dialects_edge__ops_aten_view_copy_default": 1, "executorch_exir_dialects_edge__ops_quantized_decomposed_dequantize_per_channel_default": 18, - "executorch_exir_dialects_edge__ops_quantized_decomposed_dequantize_per_tensor_default": 17, - "executorch_exir_dialects_edge__ops_quantized_decomposed_quantize_per_tensor_default": 15, + "executorch_exir_dialects_edge__ops_quantized_decomposed_dequantize_per_tensor_default": 15, + "executorch_exir_dialects_edge__ops_quantized_decomposed_quantize_per_tensor_default": 13, } ops_after_transforms: dict[str, int] = { "executorch_exir_dialects_edge__ops_aten_view_copy_default": 1, - "executorch_exir_dialects_edge__ops_cortex_m_dequantize_per_tensor_default": 3, + "executorch_exir_dialects_edge__ops_cortex_m_dequantize_per_tensor_default": 1, "executorch_exir_dialects_edge__ops_cortex_m_pad_default": 1, - "executorch_exir_dialects_edge__ops_cortex_m_quantize_per_tensor_default": 3, + "executorch_exir_dialects_edge__ops_cortex_m_quantize_per_tensor_default": 1, "executorch_exir_dialects_edge__ops_cortex_m_quantized_avg_pool2d_default": 1, "executorch_exir_dialects_edge__ops_cortex_m_quantized_conv2d_default": 4, "executorch_exir_dialects_edge__ops_cortex_m_quantized_depthwise_conv2d_default": 5, diff --git a/backends/cortex_m/test/models/test_mobilenet_v2.py b/backends/cortex_m/test/models/test_mobilenet_v2.py index 9bc99e4bf2c..d6b2a6d368a 100644 --- a/backends/cortex_m/test/models/test_mobilenet_v2.py +++ b/backends/cortex_m/test/models/test_mobilenet_v2.py @@ -21,14 +21,14 @@ "executorch_exir_dialects_edge__ops_aten_linear_default": 1, "executorch_exir_dialects_edge__ops_aten_view_copy_default": 1, "executorch_exir_dialects_edge__ops_quantized_decomposed_dequantize_per_channel_default": 104, - "executorch_exir_dialects_edge__ops_quantized_decomposed_dequantize_per_tensor_default": 79, - "executorch_exir_dialects_edge__ops_quantized_decomposed_quantize_per_tensor_default": 67, + "executorch_exir_dialects_edge__ops_quantized_decomposed_dequantize_per_tensor_default": 78, + "executorch_exir_dialects_edge__ops_quantized_decomposed_quantize_per_tensor_default": 66, } ops_after_transforms: dict[str, int] = { "executorch_exir_dialects_edge__ops_aten_view_copy_default": 1, - "executorch_exir_dialects_edge__ops_cortex_m_dequantize_per_tensor_default": 2, - "executorch_exir_dialects_edge__ops_cortex_m_quantize_per_tensor_default": 2, + "executorch_exir_dialects_edge__ops_cortex_m_dequantize_per_tensor_default": 1, + "executorch_exir_dialects_edge__ops_cortex_m_quantize_per_tensor_default": 1, "executorch_exir_dialects_edge__ops_cortex_m_quantized_add_default": 10, "executorch_exir_dialects_edge__ops_cortex_m_quantized_avg_pool2d_default": 1, "executorch_exir_dialects_edge__ops_cortex_m_quantized_conv2d_default": 35, diff --git a/backends/nxp/BUCK b/backends/nxp/BUCK index 6dec42f04d7..ccbc303a0a2 100644 --- a/backends/nxp/BUCK +++ b/backends/nxp/BUCK @@ -31,6 +31,7 @@ fbcode_target(_kind = runtime.python_library, "//caffe2:torch", "//executorch/exir:lib", "//executorch/exir:pass_manager", + "//executorch/exir/passes:fold_redundant_qdq_pass", ], ) diff --git a/backends/nxp/edge_passes/neutron_edge_pass_manager.py b/backends/nxp/edge_passes/neutron_edge_pass_manager.py index 3a7fc3ffbfa..b0139662ca4 100644 --- a/backends/nxp/edge_passes/neutron_edge_pass_manager.py +++ b/backends/nxp/edge_passes/neutron_edge_pass_manager.py @@ -6,28 +6,29 @@ from executorch.backends.nxp.edge_passes.convert_reshaping_nodes_to_view import ( ConvertReshapingNodesToViewPass, ) -from executorch.backends.nxp.edge_passes.fold_redundant_qdq_pass import ( - FoldRedundantDequantizeQuantizePass, -) from executorch.backends.nxp.edge_passes.move_auxiliary_operator_into_separate_qdq_cluster_pass import ( MoveLeadingAuxiliaryOperatorIntoSeparateQDQClusterPass, MoveTrailingAuxiliaryOperatorIntoSeparateQDQClusterPass, ) -from executorch.backends.nxp.edge_passes.neutron_edge_pass import NeutronEdgePass from executorch.backends.nxp.edge_passes.remove_as_strided_copy_nodes import ( RemoveUselessAsStridedCopyNodes, ) -from torch.fx.passes.infra.pass_manager import PassManager +from executorch.exir.pass_base import ExportPass +from executorch.exir.pass_manager import PassManager +from executorch.exir.passes.fold_redundant_qdq_pass import ( + FoldRedundantDequantizeQuantizePass, +) class NeutronEdgePassManager(PassManager): - def __init__(self, passes: list[NeutronEdgePass] = None): - passes: list[NeutronEdgePass] = passes or [ + def __init__(self, passes: list[ExportPass] = None): + passes: list[ExportPass] = passes or [ MoveLeadingAuxiliaryOperatorIntoSeparateQDQClusterPass(), MoveTrailingAuxiliaryOperatorIntoSeparateQDQClusterPass(), RemoveUselessAsStridedCopyNodes(), ConvertReshapingNodesToViewPass(), + # Auxiliary-op splitting can introduce DQ -> Q on fanout branches. FoldRedundantDequantizeQuantizePass(), ] diff --git a/backends/nxp/tests/test_fold_redundant_qdq.py b/backends/nxp/tests/test_fold_redundant_qdq.py index e79d29b1166..b8dc946dd50 100644 --- a/backends/nxp/tests/test_fold_redundant_qdq.py +++ b/backends/nxp/tests/test_fold_redundant_qdq.py @@ -6,9 +6,6 @@ import torch -from executorch.backends.nxp.edge_passes.fold_redundant_qdq_pass import ( - FoldRedundantDequantizeQuantizePass, -) from executorch.backends.nxp.tests.executorch_pipeline import to_quantized_edge_program ExecutorchDelegateCall = torch.ops.higher_order.executorch_call_delegate @@ -54,33 +51,10 @@ def _count_delegates(edge_program) -> int: def test_fold_pass_present_merges_into_single_delegate(): - # The fold pass is part of the default NeutronEdgePassManager. + # The fold pass runs during to_edge, before Neutron partitioning. edge_program = to_quantized_edge_program(ConvDropoutConvModule(), INPUT_SHAPE) num_delegates = _count_delegates(edge_program) assert ( num_delegates == 1 ), f"expected a single delegate with the fold pass, got {num_delegates}" - - -def test_fold_pass_removes_redundant_qdq(): - graph = torch.fx.Graph() - quantized_input = graph.placeholder("quantized_input") - qparams = (0.25, 3, -128, 127, torch.int8) - dequantize = graph.call_function( - torch.ops.quantized_decomposed.dequantize_per_tensor.default, - args=(quantized_input, *qparams), - ) - quantize = graph.call_function( - torch.ops.quantized_decomposed.quantize_per_tensor.default, - args=(dequantize, *qparams), - ) - graph.output(quantize) - graph_module = torch.fx.GraphModule(torch.nn.Module(), graph) - - result = FoldRedundantDequantizeQuantizePass().run(graph_module) - - assert result.modified - remaining_nodes = list(result.graph_module.graph.nodes) - assert [node.op for node in remaining_nodes] == ["placeholder", "output"] - assert remaining_nodes[-1].args == (quantized_input,) diff --git a/backends/transforms/test/test_remove_clone_ops.py b/backends/transforms/test/test_remove_clone_ops.py index d34c522baaa..891364073a7 100644 --- a/backends/transforms/test/test_remove_clone_ops.py +++ b/backends/transforms/test/test_remove_clone_ops.py @@ -37,7 +37,7 @@ def test_dq_clone_q_linear(self): Test RemoveCloneOpsTransform on a graph with d/q -> clone -> q -> linear pattern Before: Should contain all nodes - After: Should only have the linear operation + After: Should only have the final dequantize and linear operations """ # Create a graph module directly with the pattern: quant -> clone -> dequant -> fp linear @@ -60,19 +60,13 @@ def forward(self, x): input_node = graph.placeholder("x") # Create nodes for our pattern: quant -> clone -> dequant -> fp linear - # Constants for quantization parameters - scale = graph.create_node( - "call_function", torch.tensor, args=([0.1],), kwargs={} - ) - zero_point = graph.create_node( - "call_function", torch.tensor, args=([0],), kwargs={} - ) + qparams = (0.1, 0, -128, 127, torch.int8) # Dequantize node dequant_node = graph.create_node( "call_function", torch.ops.quantized_decomposed.dequantize_per_tensor.default, - args=(input_node, scale, zero_point, torch.int8), + args=(input_node, *qparams), kwargs={}, ) @@ -89,19 +83,20 @@ def forward(self, x): quant_node = graph.create_node( "call_function", torch.ops.quantized_decomposed.quantize_per_tensor.default, - args=(clone_node, scale, zero_point, torch.int8), + args=(clone_node, *qparams), kwargs={}, ) - # Linear node (using the module's linear layer) - # Technically, should use quantized weight and bias - # but we are just inspecting graph patterns in this test + linear_input = graph.call_function( + torch.ops.quantized_decomposed.dequantize_per_tensor.default, + args=(quant_node, *qparams), + ) weight = graph.create_node("get_attr", "linear.weight") bias = graph.create_node("get_attr", "linear.bias") linear_node = graph.create_node( "call_function", torch.nn.functional.linear, - args=(quant_node, weight, bias), + args=(linear_input, weight, bias), kwargs={}, ) @@ -125,12 +120,16 @@ def forward(self, x): ) # Apply the transform + inputs = torch.randint(-128, 128, (2, 10), dtype=torch.int8) + expected = gm(inputs) transformed_gm = RemoveCloneOpsTransform()(gm).graph_module # Verify the dq -> clone -> q pattern is removed and linear op is still present using FileCheck FileCheck().check_not( "executorch_exir_dialects_edge__ops_aten_clone_default" - ).check_not("quantized_decomposed.dequantize_per_tensor.default").check_not( + ).check_count( + "quantized_decomposed.dequantize_per_tensor.default", 1, exactly=True + ).check_not( "quantized_decomposed.quantize_per_tensor.default" ).check_count( "torch._C._nn.linear", @@ -139,6 +138,7 @@ def forward(self, x): ).run( transformed_gm.code ) + torch.testing.assert_close(transformed_gm(inputs), expected, rtol=0, atol=0) def test_clone_non_identity_survives(self): """Verify clone ops that modify memory_format are preserved by RemoveCloneOpsTransform.""" diff --git a/exir/passes/BUCK b/exir/passes/BUCK index 6e7ca896932..6e47151614f 100644 --- a/exir/passes/BUCK +++ b/exir/passes/BUCK @@ -13,6 +13,7 @@ fbcode_target(_kind = runtime.python_library, ":convert_constant_dim_order_pass", ":debug_handle_generator_pass", ":external_constants_pass", + ":fold_redundant_qdq_pass", ":init_mutable_pass", ":insert_write_back_for_buffers_pass", ":legalize_portable_dim_order_pass", @@ -233,6 +234,18 @@ fbcode_target(_kind = runtime.python_library, ], ) +fbcode_target(_kind = runtime.python_library, + name = "fold_redundant_qdq_pass", + srcs = [ + "fold_redundant_qdq_pass.py", + ], + deps = [ + ":remove_noop_pass", + "//caffe2:torch", + "//executorch/exir:pass_base", + ], +) + fbcode_target(_kind = runtime.python_library, name = "remove_noop_pass", srcs = [ diff --git a/exir/passes/__init__.py b/exir/passes/__init__.py index 51ae9055ec7..57c4c313112 100644 --- a/exir/passes/__init__.py +++ b/exir/passes/__init__.py @@ -37,6 +37,9 @@ from executorch.exir.passes.debug_handle_generator_pass import DebugHandleGeneratorPass from executorch.exir.passes.executorch_prim_ops_registry import _EXECUTORCH_SYM_OPS +from executorch.exir.passes.fold_redundant_qdq_pass import ( + FoldRedundantDequantizeQuantizePass, +) from executorch.exir.passes.insert_write_back_for_buffers_pass import ( insert_write_back_for_buffers_pass, ) @@ -517,6 +520,7 @@ def dead_code_elimination_pass(graph_module: torch.fx.GraphModule) -> PassResult RemoveNoopPass(), PruneEmptyTensorsPass(), RemoveToCopyPass(), + FoldRedundantDequantizeQuantizePass(), ] ).passes ) diff --git a/backends/nxp/edge_passes/fold_redundant_qdq_pass.py b/exir/passes/fold_redundant_qdq_pass.py similarity index 55% rename from backends/nxp/edge_passes/fold_redundant_qdq_pass.py rename to exir/passes/fold_redundant_qdq_pass.py index 556b2851d9b..01c48ccf0b3 100644 --- a/backends/nxp/edge_passes/fold_redundant_qdq_pass.py +++ b/exir/passes/fold_redundant_qdq_pass.py @@ -6,22 +6,19 @@ import torch -from executorch.backends.nxp.edge_passes.neutron_edge_pass import NeutronEdgePass +from executorch.exir.pass_base import ExportPass from executorch.exir.passes.remove_noop_pass import _DEQUANT_OPS, eliminate_dq_q from torch.fx.passes.infra.pass_base import PassResult -class FoldRedundantDequantizeQuantizePass(NeutronEdgePass): - """Fold redundant ``dequantize -> quantize`` pairs with identical qparams. +class FoldRedundantDequantizeQuantizePass(ExportPass): + """Fold adjacent ``dequantize -> quantize`` pairs using the shared qparam matcher. - A dequantize immediately followed by a quantize at identical qparams is the - identity on the already-quantized value, so this pass reuses the shared - ``eliminate_dq_q`` helper to rewire each such quantize's consumers to the - dequantize's quantized input, removing the island and letting the neighboring - clusters delegate as a single subgraph. + Decomposition can erase a quantized no-op, such as eval-mode dropout, leaving + its surrounding dequantize and quantize nodes adjacent. """ - def run(self, graph_module: torch.fx.GraphModule) -> PassResult: + def call(self, graph_module: torch.fx.GraphModule) -> PassResult: dequant_nodes = [ node for node in graph_module.graph.nodes @@ -32,5 +29,8 @@ def run(self, graph_module: torch.fx.GraphModule) -> PassResult: eliminate_dq_q(graph_module, dequant_nodes) graph_module.graph.eliminate_dead_code() modified = len(graph_module.graph.nodes) != num_nodes_before + if modified: + graph_module.graph.lint() + graph_module.recompile() return PassResult(graph_module, modified) diff --git a/exir/passes/remove_noop_pass.py b/exir/passes/remove_noop_pass.py index e2d92909e53..aeb226443f0 100644 --- a/exir/passes/remove_noop_pass.py +++ b/exir/passes/remove_noop_pass.py @@ -61,7 +61,6 @@ def call(self, graph_module: GraphModule) -> PassResult: if node.target not in ( torch.ops.aten.to.dtype, - torch.ops.aten.dropout.default, torch.ops.aten.slice_copy.Tensor, ): continue diff --git a/exir/tests/targets.bzl b/exir/tests/targets.bzl index 4fce5dc0319..f68ee23cc16 100644 --- a/exir/tests/targets.bzl +++ b/exir/tests/targets.bzl @@ -231,6 +231,7 @@ def define_common_targets(is_fbcode = False): "//executorch/exir/passes:constant_prop_pass", "//executorch/exir/passes:cse_pass", "//executorch/exir/passes:debug_handle_generator_pass", + "//executorch/exir/passes:fold_redundant_qdq_pass", "//executorch/exir/passes:insert_write_back_for_buffers_pass", "//executorch/exir/passes:lib", "//executorch/exir/passes:memory_format_ops_pass", diff --git a/exir/tests/test_passes.py b/exir/tests/test_passes.py index 54211a490a3..3c9deb81df7 100644 --- a/exir/tests/test_passes.py +++ b/exir/tests/test_passes.py @@ -59,6 +59,9 @@ DebugHandleGeneratorPass, generate_missing_debug_handles, ) +from executorch.exir.passes.fold_redundant_qdq_pass import ( + FoldRedundantDequantizeQuantizePass, +) from executorch.exir.passes.insert_write_back_for_buffers_pass import ( insert_write_back_for_buffers_pass, ) @@ -2113,6 +2116,91 @@ def quantize_model( ) ) + def test_fold_redundant_dq_q_pass(self) -> None: + graph = torch.fx.Graph() + quantized_input = graph.placeholder("quantized_input") + qparams = (0.25, 3, -128, 127, torch.int8) + dequantize = graph.call_function( + torch.ops.quantized_decomposed.dequantize_per_tensor.default, + args=(quantized_input, *qparams), + ) + quantize = graph.call_function( + torch.ops.quantized_decomposed.quantize_per_tensor.default, + args=(dequantize, *qparams), + ) + graph.output(quantize) + graph_module = torch.fx.GraphModule(torch.nn.Module(), graph) + inputs = torch.arange(-128, 128, dtype=torch.int8) + expected = graph_module(inputs) + + result = FoldRedundantDequantizeQuantizePass()(graph_module) + + self.assertTrue(result.modified) + remaining_nodes = list(result.graph_module.graph.nodes) + self.assertEqual( + [node.op for node in remaining_nodes], ["placeholder", "output"] + ) + self.assertEqual(remaining_nodes[-1].args, (quantized_input,)) + torch.testing.assert_close(result.graph_module(inputs), expected) + self.assertFalse(FoldRedundantDequantizeQuantizePass()(graph_module).modified) + + def test_to_edge_quantized_dropout(self) -> None: + class QuantizedDropout(torch.nn.Module): + def __init__(self, dtype, per_channel): + super().__init__() + self.dtype = dtype + self.per_channel = per_channel + self.qmin = torch.iinfo(dtype).min + self.qmax = torch.iinfo(dtype).max + self.register_buffer("scales", torch.tensor([0.125, 0.25])) + self.register_buffer("zero_points", torch.tensor([3, 3])) + + def forward(self, x): + if self.per_channel: + q = torch.ops.quantized_decomposed.quantize_per_channel.default + dq = torch.ops.quantized_decomposed.dequantize_per_channel.default + params = ( + self.scales, + self.zero_points, + 0, + self.qmin, + self.qmax, + self.dtype, + ) + else: + q = torch.ops.quantized_decomposed.quantize_per_tensor.default + dq = torch.ops.quantized_decomposed.dequantize_per_tensor.default + params = (0.25, 3, self.qmin, self.qmax, self.dtype) + x = dq(q(x, *params), *params) + x = torch.nn.functional.dropout(x, p=0.5, training=False) + return dq(q(x, *params), *params) + + inputs = (torch.randn(2, 16),) + for lower, dtype, per_channel in itertools.product( + (to_edge, to_edge_transform_and_lower), + (torch.int8, torch.uint8, torch.int16, torch.int32), + (False, True), + ): + with self.subTest( + lower=lower.__name__, dtype=dtype, per_channel=per_channel + ): + model = QuantizedDropout(dtype, per_channel) + ep = export(model, inputs, strict=True) + self.assertTrue( + any( + n.target == torch.ops.aten.dropout.default + for n in ep.graph.nodes + ) + ) + edge = lower(ep).exported_program() + targets = [str(n.target) for n in edge.graph.nodes] + self.assertEqual(sum(".quantize_per_" in t for t in targets), 1) + self.assertEqual(sum(".dequantize_per_" in t for t in targets), 1) + self.assertFalse(any("dropout" in t for t in targets)) + torch.testing.assert_close( + edge.module()(*inputs), model(*inputs), rtol=0, atol=0 + ) + def test_dq_q_no_op_pass(self) -> None: class TestDqQ(torch.nn.Module): def __init__(self): @@ -2129,21 +2217,24 @@ def forward(self, x): model = TestDqQ() m_eager = model.eval() - ep = torch.export.export(m_eager, (torch.randn(9, 8),), strict=True) + inputs = (torch.randint(-128, 128, (9, 8), dtype=torch.int8),) + ep = torch.export.export(m_eager, inputs, strict=True) edge = to_edge(ep) - # Check that the dq and q nodes are not touched by the RemoveNoopPass. - self.assertTrue( + self.assertFalse( any( "dequantize" in str(node.target) for node in edge.exported_program().graph_module.graph.nodes ) ) - self.assertTrue( + self.assertFalse( any( "quantize" in str(node.target) for node in edge.exported_program().graph_module.graph.nodes ) ) + torch.testing.assert_close( + edge.exported_program().module()(*inputs), m_eager(*inputs) + ) def test_dq_q_different_qparams(self) -> None: class TestDqQDifferentQParam(torch.nn.Module): @@ -2162,7 +2253,8 @@ def forward(self, x): model = TestDqQDifferentQParam() m_eager = model.eval() - ep = torch.export.export(m_eager, (torch.randn(9, 8),), strict=True) + inputs = (torch.randint(-128, 128, (9, 8), dtype=torch.int8),) + ep = torch.export.export(m_eager, inputs, strict=True) edge = to_edge(ep) print(edge.exported_program().graph_module.graph) # Check that the dq and q nodes are not touched by the RemoveNoopPass. From b80d7ac0f5c1fb784ae047ff6b2bd4331ca8b2a8 Mon Sep 17 00:00:00 2001 From: Jacob Szwejbka Date: Thu, 17 Sep 2026 21:12:56 -0700 Subject: [PATCH 7/8] [RELEASE ONLY CHANGES] Bump ExecuTorch version to 1.5.1 (#22922) ## Summary Bump the ExecuTorch release version from 1.5.0 to 1.5.1 and update the stable SwiftPM documentation examples. This mirrors the version and documentation changes made for the 1.4.1 patch release. The dated nightly SwiftPM example remains unchanged. ## Validation `git diff --check` Authored with AI assistance from OpenAI Codex. --- docs/source/getting-started.md | 2 +- docs/source/using-executorch-ios.md | 4 ++-- version.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/source/getting-started.md b/docs/source/getting-started.md index 0a782f6d5b9..832c11641ba 100644 --- a/docs/source/getting-started.md +++ b/docs/source/getting-started.md @@ -168,7 +168,7 @@ For a full example of running a model on Android, see the [DeepLabV3AndroidDemo] #### Installation ExecuTorch supports both iOS and macOS via C++, as well as hardware backends for CoreML and CPU. The iOS runtime library is provided as a collection of .xcframework targets and are made available as a Swift PM package. -To get started with Xcode, go to File > Add Package Dependencies. Paste the URL of the ExecuTorch repo into the search bar and select it. Make sure to change the branch name to the desired ExecuTorch version in format “swiftpm-”, (e.g. “swiftpm-1.5.0”). The ExecuTorch dependency can also be added to the package file manually. See [Using ExecuTorch on iOS](using-executorch-ios.md) for more information. +To get started with Xcode, go to File > Add Package Dependencies. Paste the URL of the ExecuTorch repo into the search bar and select it. Make sure to change the branch name to the desired ExecuTorch version in format “swiftpm-”, (e.g. “swiftpm-1.5.1”). The ExecuTorch dependency can also be added to the package file manually. See [Using ExecuTorch on iOS](using-executorch-ios.md) for more information. #### Runtime APIs Models can be loaded and run from Objective-C using the C++ APIs. diff --git a/docs/source/using-executorch-ios.md b/docs/source/using-executorch-ios.md index 1be079bfe13..f2f952b662b 100644 --- a/docs/source/using-executorch-ios.md +++ b/docs/source/using-executorch-ios.md @@ -31,7 +31,7 @@ The prebuilt ExecuTorch runtime, backend, and kernels are available as a [Swift #### Xcode -In Xcode, go to `File > Add Package Dependencies`. Paste the URL of the [ExecuTorch repo](https://github.com/pytorch/executorch) into the search bar and select it. Make sure to change the branch name to the desired ExecuTorch version in format "swiftpm-", (e.g. "swiftpm-1.5.0"), or a branch name in format "swiftpm-." (e.g. "swiftpm-1.5.0.20260910") for a [nightly build](https://ossci-ios.s3.amazonaws.com/list.html) on a specific date. +In Xcode, go to `File > Add Package Dependencies`. Paste the URL of the [ExecuTorch repo](https://github.com/pytorch/executorch) into the search bar and select it. Make sure to change the branch name to the desired ExecuTorch version in format "swiftpm-", (e.g. "swiftpm-1.5.1"), or a branch name in format "swiftpm-." (e.g. "swiftpm-1.5.0.20260910") for a [nightly build](https://ossci-ios.s3.amazonaws.com/list.html) on a specific date. ![](_static/img/swiftpm_xcode1.png) @@ -64,7 +64,7 @@ let package = Package( ], dependencies: [ // Use "swiftpm-." branch name for a nightly build. - .package(url: "https://github.com/pytorch/executorch.git", branch: "swiftpm-1.5.0") + .package(url: "https://github.com/pytorch/executorch.git", branch: "swiftpm-1.5.1") ], targets: [ .target( diff --git a/version.txt b/version.txt index bc80560fad6..26ca594609a 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.5.0 +1.5.1 From 3b60683923245cf472b7323426920e15623ba361 Mon Sep 17 00:00:00 2001 From: pytorchbot Date: Fri, 18 Sep 2026 11:54:08 -0700 Subject: [PATCH 8/8] Arm backend: Partially support dynamic spatial ops (#22931) Support dynamic convolutions and pooling operations without the TOSA shape extension when partitioning proves lowering needs neither input- size adjustment nor dynamic padding. Recognize value-only max pooling in its exported with-indices form before backend preprocessing canonicalizes it. cc @digantdesai @freddan80 @per @zingo @oscarandersson8218 @mansnils @Sebastian-Larsson @robell @rascani Signed-off-by: Oscar Andersson Co-authored-by: Oscar Andersson <87121123+oscarandersson8218@users.noreply.github.com> --- .../arm/_passes/decompose_avg_pool2d_pass.py | 2 +- .../decompose_large_stride_maxpool2d_pass.py | 3 +- .../decompose_maxpool2d_with_dilation_pass.py | 2 +- .../arm/_passes/rewrite_avg_pool2d_pass.py | 4 +- .../arm/_passes/rewrite_max_pool2d_pass.py | 4 +- .../arm/_passes/size_adjust_input_pass.py | 105 ++++++++++++- .../arm/operator_support/pool_2d_support.py | 10 +- .../tosa_supported_operators.py | 49 ++++++ .../tosa_dialect/test_tosa_shape_support.py | 140 ++++++++++++++++-- .../passes/test_rewrite_max_pool2d_pass.py | 6 +- backends/arm/tosa/partitioner.py | 17 ++- 11 files changed, 311 insertions(+), 31 deletions(-) diff --git a/backends/arm/_passes/decompose_avg_pool2d_pass.py b/backends/arm/_passes/decompose_avg_pool2d_pass.py index a1774b256ef..e0c974598bc 100644 --- a/backends/arm/_passes/decompose_avg_pool2d_pass.py +++ b/backends/arm/_passes/decompose_avg_pool2d_pass.py @@ -111,7 +111,7 @@ def call_operator(self, op, args, kwargs, meta): kernel_h, kernel_w = args[1] kernel_size = kernel_h * kernel_w - if len(args) > 2 and args[2] is not None: + if len(args) > 2 and args[2]: stride_h, stride_w = args[2] else: stride_h, stride_w = kernel_h, kernel_w diff --git a/backends/arm/_passes/decompose_large_stride_maxpool2d_pass.py b/backends/arm/_passes/decompose_large_stride_maxpool2d_pass.py index f7a38d52b65..d8286d9f108 100644 --- a/backends/arm/_passes/decompose_large_stride_maxpool2d_pass.py +++ b/backends/arm/_passes/decompose_large_stride_maxpool2d_pass.py @@ -1,5 +1,6 @@ # Copyright (c) Meta Platforms, Inc. and affiliates. # All rights reserved. +# Copyright 2026 Arm Limited and/or its affiliates. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. @@ -120,7 +121,7 @@ def call_operator(self, op, args, kwargs, meta): x = args[0] kernel = args[1] - stride = args[2] if len(args) >= 3 else kernel + stride = args[2] if len(args) >= 3 and args[2] else kernel padding = args[3] if len(args) >= 4 else (0, 0) dilation = args[4] if len(args) >= 5 else (1, 1) ceil_mode = args[5] if len(args) >= 6 else False diff --git a/backends/arm/_passes/decompose_maxpool2d_with_dilation_pass.py b/backends/arm/_passes/decompose_maxpool2d_with_dilation_pass.py index 7729b755113..632778ff88d 100644 --- a/backends/arm/_passes/decompose_maxpool2d_with_dilation_pass.py +++ b/backends/arm/_passes/decompose_maxpool2d_with_dilation_pass.py @@ -68,7 +68,7 @@ def call_operator(self, op, args, kwargs, meta): # Normalize missing trailing args to their defaults x = args[0] kernel_size = args[1] - stride = args[2] + stride = args[2] if len(args) >= 3 and args[2] else kernel_size padding = args[3] if len(args) >= 4 else 0 dilation = args[4] if len(args) >= 5 else 1 ceil_mode = args[5] if len(args) == 6 else False diff --git a/backends/arm/_passes/rewrite_avg_pool2d_pass.py b/backends/arm/_passes/rewrite_avg_pool2d_pass.py index deda2572496..4887b60d77b 100644 --- a/backends/arm/_passes/rewrite_avg_pool2d_pass.py +++ b/backends/arm/_passes/rewrite_avg_pool2d_pass.py @@ -36,9 +36,7 @@ def call_operator(self, op, args, kwargs, meta, updated=False): x = args[0] kernel = to_2tuple(args[1]) - stride = to_2tuple(args[2]) if len(args) > 2 else () - if not stride: - stride = kernel # default to kernel_size + stride = to_2tuple(args[2]) if len(args) > 2 and args[2] else kernel pad_h, pad_w = to_2tuple(args[3]) if len(args) > 3 else (0, 0) # Make sure pad corresponds to TOSA diff --git a/backends/arm/_passes/rewrite_max_pool2d_pass.py b/backends/arm/_passes/rewrite_max_pool2d_pass.py index 47623b7dc2e..097bfdd2ce7 100644 --- a/backends/arm/_passes/rewrite_max_pool2d_pass.py +++ b/backends/arm/_passes/rewrite_max_pool2d_pass.py @@ -79,9 +79,7 @@ def call_operator(self, op, args, kwargs, meta, updated=False): x = args[0] kernel = args[1] - stride = to_2tuple(args[2]) if len(args) > 2 else () - if not stride: - stride = kernel # default to kernel_size + stride = to_2tuple(args[2]) if len(args) > 2 and args[2] else kernel padding = to_2tuple(args[3]) if len(args) > 3 else (0, 0) dilation = to_2tuple(args[4]) if len(args) > 4 else (1, 1) diff --git a/backends/arm/_passes/size_adjust_input_pass.py b/backends/arm/_passes/size_adjust_input_pass.py index 8056a770e99..1f71b2d046e 100644 --- a/backends/arm/_passes/size_adjust_input_pass.py +++ b/backends/arm/_passes/size_adjust_input_pass.py @@ -16,6 +16,9 @@ from executorch.backends.arm._passes.symbolic_value_range import ( evaluate_symbolic_expr_values, ) +from executorch.backends.arm.operators.operator_validation_utils import ( + adjust_pooling_pad_if_needed, +) from executorch.backends.arm.tosa.specification import get_context_shape_env from executorch.exir.dialects._ops import ops as exir_ops from executorch.exir.pass_base import ExportPass, PassResult @@ -40,6 +43,49 @@ def conv_remainder( return (input_length + 2 * pad - dilation * (weight - 1) - 1) % stride +def has_dynamic_conv_padding(conv_node: torch.fx.Node) -> bool: + """Return whether rewriting a convolution requires symbolic padding. + + Args: + conv_node (torch.fx.Node): Convolution node to inspect. + + Returns: + bool: Whether the convolution needs runtime padding. + + """ + input_node, weight, _, stride_hw, pad_hw, dilation_hw, transposed, _, _ = ( + conv_node.args + ) + if transposed: + return True + + input_shape = cast(torch.fx.Node, input_node).meta["val"].shape + weight_shape = cast(torch.fx.Node, weight).meta["val"].shape + spatial_rank = len(input_shape) - 2 + strides = expand_around_channel(cast(Sequence[int] | int, stride_hw), spatial_rank) + pads = expand_around_channel(cast(Sequence[int] | int, pad_hw), spatial_rank) + dilations = expand_around_channel( + cast(Sequence[int] | int, dilation_hw), spatial_rank + ) + shape_env = get_context_shape_env() + + for axis_index, (stride, pad, dilation) in enumerate(zip(strides, pads, dilations)): + remainder = conv_remainder( + input_shape[axis_index + 2], + pad, + dilation, + weight_shape[axis_index + 2], + stride, + ) + if not isinstance(remainder, torch.SymInt): + continue + exact_values = evaluate_symbolic_expr_values(remainder.node.expr, shape_env) + if exact_values is None or (len(exact_values) != 1 and max(exact_values) != 0): + return True + + return False + + def pooling_remainder( input_size: SymIntLike, pad: int, kernel_size: int, stride: int ) -> SymIntLike: @@ -49,6 +95,59 @@ def pooling_remainder( return (input_size + 2 * pad - kernel_size) % stride +def has_dynamic_pooling_padding(pooling_node: torch.fx.Node) -> bool: + """Return whether rewriting a pool requires symbolic padding. + + Args: + pooling_node (torch.fx.Node): Pooling node to inspect. + + Returns: + bool: Whether the pooling operation needs runtime padding. + + """ + input_node = cast(torch.fx.Node, pooling_node.args[0]) + kernel_size = pooling_node.args[1] + stride = ( + pooling_node.args[2] + if len(pooling_node.args) >= 3 and pooling_node.args[2] + else kernel_size + ) + padding = pooling_node.args[3] if len(pooling_node.args) >= 4 else 0 + ceil_mode_index = ( + 5 + if pooling_node.target + in ( + max_pooling_op, + exir_ops.edge.aten.max_pool2d_with_indices.default, + ) + else 4 + ) + ceil_mode = ( + pooling_node.args[ceil_mode_index] + if len(pooling_node.args) > ceil_mode_index + else False + ) + input_shape = input_node.meta["val"].shape + kernel_sizes = expand_around_channel(cast(Sequence[int] | int, kernel_size), 2) + strides = expand_around_channel(cast(Sequence[int] | int, stride), 2) + pads = expand_around_channel(cast(Sequence[int] | int, padding), 2) + shape_env = get_context_shape_env() + + for dim, (kernel, stride, pad) in enumerate( + zip(kernel_sizes, strides, pads), start=2 + ): + adjusted_pad = adjust_pooling_pad_if_needed( + input_shape[dim], kernel, stride, pad, bool(ceil_mode) + ) + if not isinstance(adjusted_pad, torch.SymInt): + continue + exact_values = evaluate_symbolic_expr_values(adjusted_pad.node.expr, shape_env) + if exact_values is None or len(exact_values) != 1: + return True + + return False + + def _greater_than(input: SymIntLike, other: int) -> bool | torch.SymBool: """Returns whether an int or SymInt is greater than another value.""" if isinstance(input, torch.SymInt): @@ -137,7 +236,11 @@ def get_slices_pooling(pooling_node: torch.fx.Node) -> Slices: input_node = pooling_node.args[0] kernel_size = pooling_node.args[1] - stride = pooling_node.args[2] + stride = ( + pooling_node.args[2] + if len(pooling_node.args) >= 3 and pooling_node.args[2] + else kernel_size + ) padding = pooling_node.args[3] if len(pooling_node.args) >= 4 else 0 input_shape = cast(torch.fx.Node, input_node).meta["val"].shape diff --git a/backends/arm/operator_support/pool_2d_support.py b/backends/arm/operator_support/pool_2d_support.py index 03b52fbb85f..b4e8094507f 100644 --- a/backends/arm/operator_support/pool_2d_support.py +++ b/backends/arm/operator_support/pool_2d_support.py @@ -142,7 +142,10 @@ def is_node_tosa_supported(self, node: fx.Node, tosa_spec: TosaSpecification): # Calculate padding used in the final TOSA operator kernel = cast(tuple[int, int], node.args[1]) - stride = cast(tuple[int, int], node.args[2]) + stride = cast( + tuple[int, int], + node.args[2] if len(node.args) >= 3 and node.args[2] else node.args[1], + ) padding = cast(tuple[int, int], node.args[3]) if len(node.args) > 3 else (0, 0) ceil_mode = cast(bool, node.args[4]) if len(node.args) > 4 else False count_include_pad = cast(bool, node.args[5]) if len(node.args) > 5 else True @@ -237,7 +240,10 @@ def is_node_tosa_supported(self, node: fx.Node, tosa_spec: TosaSpecification): return False kernel = cast(tuple[int, int], node.args[1]) - stride = cast(tuple[int, int], node.args[2]) + stride = cast( + tuple[int, int], + node.args[2] if len(node.args) >= 3 and node.args[2] else node.args[1], + ) padding = cast(tuple[int, int], node.args[3]) if len(node.args) >= 4 else (0, 0) dilation = ( cast(tuple[int, int], node.args[4]) if len(node.args) >= 5 else (1, 1) diff --git a/backends/arm/operator_support/tosa_supported_operators.py b/backends/arm/operator_support/tosa_supported_operators.py index 7c386256047..36844a02c57 100644 --- a/backends/arm/operator_support/tosa_supported_operators.py +++ b/backends/arm/operator_support/tosa_supported_operators.py @@ -30,6 +30,12 @@ FuseQuantizedActivationPass, ) from executorch.backends.arm._passes.insert_table_ops import TableOps +from executorch.backends.arm._passes.size_adjust_input_pass import ( + get_slices_convolution, + get_slices_pooling, + has_dynamic_conv_padding, + has_dynamic_pooling_padding, +) from executorch.backends.arm.common.annotation_meta import ArmAnnotationInfo from executorch.backends.arm.constants import DQ_OPS, MAX_RANK, Q_OPS from executorch.backends.arm.operator_support.control_flow_support import ( @@ -50,6 +56,7 @@ TOSA_PRO_MIXED_INT_SupportList, ) from executorch.backends.arm.tosa.specification import ( + get_context_shape_env, TosaSpecification, TosaSpecMapping, ) @@ -598,6 +605,46 @@ def _get_mean_reduction_dims(node: fx.Node, input_rank: int) -> tuple[int, ...]: return (dims % input_rank,) return tuple(dim % input_rank for dim in typing.cast(Sequence[int], dims)) + @staticmethod + def _symbolic_spatial_op_requires_shape_extension(node: fx.Node) -> bool: + """Return whether a symbolic spatial operation needs TOSA shape + operations. + + Args: + node (fx.Node): Spatial operation node to inspect. + + Returns: + bool: Whether the operation cannot use static input adjustment and + padding. + + """ + try: + get_context_shape_env() + except RuntimeError: + return True + + if node.target == exir_ops.edge.aten.convolution.default: + return ( + bool(node.args[6]) + or bool(get_slices_convolution(node)) + or has_dynamic_conv_padding(node) + ) + if node.target in ( + exir_ops.edge.aten.avg_pool2d.default, + exir_ops.edge.aten.max_pool2d.default, + exir_ops.edge.aten.max_pool2d_with_indices.default, + ): + if node.target == exir_ops.edge.aten.max_pool2d_with_indices.default: + users = list(node.users) + if ( + len(users) != 1 + or users[0].target != operator.getitem + or users[0].args[1] != 0 + ): + return True + return bool(get_slices_pooling(node)) or has_dynamic_pooling_padding(node) + return True + def _has_unsupported_symbolic_tensor_shape(self, node: fx.Node) -> bool: if node.target not in ( *self._SYMBOLIC_SPATIAL_DIM_TARGETS, @@ -617,6 +664,8 @@ def _has_unsupported_symbolic_tensor_shape(self, node: fx.Node) -> bool: if node.target in self._SYMBOLIC_SPATIAL_DIM_TARGETS: if any(isinstance(s, torch.SymInt) for s in input_fake_tensor.shape[2:]): + if not self._symbolic_spatial_op_requires_shape_extension(node): + return False self.reporter.report_reject(node, "Symbolic spatial dims unsupported") return True diff --git a/backends/arm/test/misc/tosa_dialect/test_tosa_shape_support.py b/backends/arm/test/misc/tosa_dialect/test_tosa_shape_support.py index 6b9bc1f8db3..f736fe523fb 100644 --- a/backends/arm/test/misc/tosa_dialect/test_tosa_shape_support.py +++ b/backends/arm/test/misc/tosa_dialect/test_tosa_shape_support.py @@ -35,17 +35,41 @@ def forward(self, x: torch.Tensor, y: torch.Tensor) -> torch.Tensor: class Conv2d(torch.nn.Module): - def __init__(self) -> None: + def __init__(self, stride: int = 1, padding: int = 1) -> None: super().__init__() - self.conv = torch.nn.Conv2d(3, 4, 3, padding=1) + self.conv = torch.nn.Conv2d(3, 4, 3, padding=padding, stride=stride) def forward(self, x: torch.Tensor) -> torch.Tensor: return self.conv(x) class AvgPool2d(torch.nn.Module): + def __init__( + self, kernel_size: int = 2, stride: int | None = None, padding: int = 0 + ) -> None: + super().__init__() + self.kernel_size = kernel_size + self.stride = stride + self.padding = padding + + def forward(self, x: torch.Tensor) -> torch.Tensor: + if self.stride is None: + return torch.nn.functional.avg_pool2d( + x, kernel_size=self.kernel_size, padding=self.padding + ) + return torch.nn.functional.avg_pool2d( + x, self.kernel_size, self.stride, self.padding + ) + + +class MaxPool2d(torch.nn.Module): def forward(self, x: torch.Tensor) -> torch.Tensor: - return torch.nn.functional.avg_pool2d(x, kernel_size=2, stride=2) + return torch.nn.functional.max_pool2d(x, kernel_size=2) + + +class MaxPool2dEmptyStride(torch.nn.Module): + def forward(self, x: torch.Tensor) -> torch.Tensor: + return torch.nn.functional.max_pool2d(x, kernel_size=2, stride=[]) class MeanDim(torch.nn.Module): @@ -187,7 +211,7 @@ def test_ethos_rejects_unresolved_tensor_shapes(compile_spec): assert torch.ops.higher_order.executorch_call_delegate not in targets -def test_without_shape_extension_rejects_symbolic_spatial_conv2d(): +def test_without_shape_extension_accepts_symbolic_spatial_conv2d_without_input_adjustment(): inputs = (torch.randn(2, 3, 8, 8),) height = Dim("height", min=4, max=10) exported_program = _exported_program( @@ -195,15 +219,49 @@ def test_without_shape_extension_rejects_symbolic_spatial_conv2d(): inputs, dynamic_shapes=({2: height},), ) + partition_result = TOSAPartitioner(TosaCompileSpec("TOSA-1.0+FP")).partition( + exported_program + ) + node = _find_node(exported_program, exir_ops.edge.aten.convolution.default) - _assert_rejected_with_reason( - exported_program, - exir_ops.edge.aten.convolution.default, - "Symbolic spatial dims unsupported", + assert node.meta.get("delegation_tag") in partition_result.partition_tags + + +def test_without_shape_extension_rejects_symbolic_spatial_conv2d_needing_input_adjustment(): + inputs = (torch.randn(2, 3, 8, 8),) + height = Dim("height", min=4, max=10) + exported_program = _exported_program( + Conv2d(stride=3), + inputs, + dynamic_shapes=({2: height},), ) + partition_result = TOSAPartitioner(TosaCompileSpec("TOSA-1.0+FP")).partition( + exported_program + ) + node = _find_node(exported_program, exir_ops.edge.aten.convolution.default) + + assert node.meta.get("delegation_tag") not in partition_result.partition_tags + -def test_without_shape_extension_rejects_symbolic_spatial_pooling(): +def test_without_shape_extension_rejects_symbolic_spatial_conv2d_needing_dynamic_padding(): + inputs = (torch.randn(2, 3, 8, 8),) + height = Dim("height", min=4, max=10) + exported_program = _exported_program( + Conv2d(stride=3, padding=2), + inputs, + dynamic_shapes=({2: height},), + ) + + partition_result = TOSAPartitioner(TosaCompileSpec("TOSA-1.0+FP")).partition( + exported_program + ) + node = _find_node(exported_program, exir_ops.edge.aten.convolution.default) + + assert node.meta.get("delegation_tag") not in partition_result.partition_tags + + +def test_without_shape_extension_accepts_symbolic_spatial_pooling_without_input_adjustment(): inputs = (torch.randn(2, 3, 8, 8),) height = Dim("height", min=2, max=5) * 2 exported_program = _exported_program( @@ -212,11 +270,67 @@ def test_without_shape_extension_rejects_symbolic_spatial_pooling(): dynamic_shapes=({2: height},), ) - _assert_rejected_with_reason( - exported_program, - exir_ops.edge.aten.avg_pool2d.default, - "Symbolic spatial dims unsupported", + partition_result = TOSAPartitioner(TosaCompileSpec("TOSA-1.0+FP")).partition( + exported_program ) + node = _find_node(exported_program, exir_ops.edge.aten.avg_pool2d.default) + + assert node.meta.get("delegation_tag") in partition_result.partition_tags + + +def test_without_shape_extension_accepts_value_only_symbolic_max_pooling(): + inputs = (torch.randn(2, 3, 8, 8),) + height = Dim("height", min=2, max=5) * 2 + exported_program = _exported_program( + MaxPool2d(), + inputs, + dynamic_shapes=({2: height},), + ) + + partition_result = TOSAPartitioner(TosaCompileSpec("TOSA-1.0+FP")).partition( + exported_program + ) + node = _find_node( + exported_program, exir_ops.edge.aten.max_pool2d_with_indices.default + ) + + assert node.meta.get("delegation_tag") in partition_result.partition_tags + + +def test_without_shape_extension_accepts_symbolic_max_pooling_with_empty_stride(): + inputs = (torch.randn(2, 3, 8, 8),) + height = Dim("height", min=2, max=5) * 2 + exported_program = _exported_program( + MaxPool2dEmptyStride(), + inputs, + dynamic_shapes=({2: height},), + ) + + partition_result = TOSAPartitioner(TosaCompileSpec("TOSA-1.0+FP")).partition( + exported_program + ) + node = _find_node( + exported_program, exir_ops.edge.aten.max_pool2d_with_indices.default + ) + + assert node.meta.get("delegation_tag") in partition_result.partition_tags + + +def test_without_shape_extension_rejects_symbolic_spatial_pooling_needing_dynamic_padding(): + inputs = (torch.randn(2, 3, 8, 8),) + height = Dim("height", min=4, max=10) + exported_program = _exported_program( + AvgPool2d(kernel_size=5, stride=3, padding=2), + inputs, + dynamic_shapes=({2: height},), + ) + + partition_result = TOSAPartitioner(TosaCompileSpec("TOSA-1.0+FP")).partition( + exported_program + ) + node = _find_node(exported_program, exir_ops.edge.aten.avg_pool2d.default) + + assert node.meta.get("delegation_tag") not in partition_result.partition_tags def test_without_shape_extension_rejects_symbolic_mean_reduction_dim(): diff --git a/backends/arm/test/passes/test_rewrite_max_pool2d_pass.py b/backends/arm/test/passes/test_rewrite_max_pool2d_pass.py index 9ba31f1e6e0..b4dd5313dff 100644 --- a/backends/arm/test/passes/test_rewrite_max_pool2d_pass.py +++ b/backends/arm/test/passes/test_rewrite_max_pool2d_pass.py @@ -6,6 +6,9 @@ from typing import cast, Dict, Protocol, Tuple import torch +from executorch.backends.arm._passes.decompose_maxpool2d_with_dilation_pass import ( + DecomposeMaxPool2dPass, +) from executorch.backends.arm._passes.remove_getitem_pass import RemoveGetItemPass from executorch.backends.arm._passes.rewrite_max_pool2d_pass import RewriteMaxPool2dPass from executorch.backends.arm.test import common @@ -77,6 +80,7 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: "max_pool2d_with_stride": MaxPool2dWithStride(), "max_pool2d_without_stride": MaxPool2dWithoutStride(), "max_pool2d_list_kernel": MaxPool2dListKernel(), + "max_pool2d_empty_stride": MaxPool2dWithEmptyStride(), } @@ -92,7 +96,7 @@ def test_rewrite_max_pool2d_tosa(module: ModuleWithInputs) -> None: ops_after_pass={ "executorch_exir_dialects_backend__ops_tosa_MAX_POOL2D_default": 1, }, - pass_list=[RemoveGetItemPass, RewriteMaxPool2dPass], + pass_list=[RemoveGetItemPass, DecomposeMaxPool2dPass, RewriteMaxPool2dPass], ) pipeline.run() diff --git a/backends/arm/tosa/partitioner.py b/backends/arm/tosa/partitioner.py index 2dd4e9c40f3..a75025b5872 100644 --- a/backends/arm/tosa/partitioner.py +++ b/backends/arm/tosa/partitioner.py @@ -42,7 +42,10 @@ ) from executorch.backends.arm.tosa.backend import TOSABackend from executorch.backends.arm.tosa.compile_spec import TosaCompileSpec -from executorch.backends.arm.tosa.specification import TosaSpecification +from executorch.backends.arm.tosa.specification import ( + TosaLoweringContext, + TosaSpecification, +) from executorch.exir.backend.partitioner import ( DelegationSpec, Partitioner, @@ -51,6 +54,7 @@ from executorch.exir.backend.utils import tag_constant_data, WhyNoPartitionReporter from executorch.exir.dialects._ops import ops as exir_ops from executorch.exir.graph_module import get_cond_while_submodules +from torch._export.utils import _get_shape_env_from_gm from torch.export.exported_program import ExportedProgram from torch.fx import GraphModule from torch.fx.experimental.symbolic_shapes import statically_known_true @@ -91,7 +95,7 @@ def is_node_supported( input_shape = get_first_fake_tensor(node.all_input_nodes[0]).shape return can_decompose_large_stride_maxpool2d( node.args[1], - node.args[2] if len(node.args) >= 3 else node.args[1], + node.args[2] if len(node.args) >= 3 and node.args[2] else node.args[1], node.args[3] if len(node.args) >= 4 else (0, 0), node.args[4] if len(node.args) >= 5 else (1, 1), node.args[5] if len(node.args) >= 6 else False, @@ -725,9 +729,12 @@ def partition(self, exported_program: ExportedProgram) -> PartitionResult: ) reporter = WhyNoPartitionReporter() - tags = self._tag_module( - exported_program.graph_module, exported_program, reporter - ) + with TosaLoweringContext( + self.tosa_spec, _get_shape_env_from_gm(exported_program.graph_module) + ): + tags = self._tag_module( + exported_program.graph_module, exported_program, reporter + ) partition_tags = {tag: self.delegation_spec for tag in tags} tag_constant_data(exported_program)