From 682722aa1e44f91b71d5f34b963aab26c7e688af Mon Sep 17 00:00:00 2001 From: Usamah Zaheer Date: Fri, 6 Nov 2026 14:43:02 +0000 Subject: [PATCH] Add Alif E8 MobileSAM Learning Path Authored with assistance from OpenAI Codex. --- .../deploy-mobilesam-on-alif-e8/_index.md | 65 + .../_next-steps.md | 8 + .../alif-live-demo.jpg | Bin 0 -> 182732 bytes .../build-the-firmware.md | 61 + .../export-the-model.md | 152 + .../mobilesam-alif-e8.patch | 4760 +++++++++++++++++ .../ospi-writer.zip | Bin 0 -> 3857 bytes .../prepare-the-environment.md | 129 + .../program-the-board.md | 87 + .../troubleshoot.md | 58 + .../understand-the-system.md | 47 + .../validate-live-segmentation.md | 84 + 12 files changed, 5451 insertions(+) create mode 100644 content/learning-paths/embedded-and-microcontrollers/deploy-mobilesam-on-alif-e8/_index.md create mode 100644 content/learning-paths/embedded-and-microcontrollers/deploy-mobilesam-on-alif-e8/_next-steps.md create mode 100644 content/learning-paths/embedded-and-microcontrollers/deploy-mobilesam-on-alif-e8/alif-live-demo.jpg create mode 100644 content/learning-paths/embedded-and-microcontrollers/deploy-mobilesam-on-alif-e8/build-the-firmware.md create mode 100644 content/learning-paths/embedded-and-microcontrollers/deploy-mobilesam-on-alif-e8/export-the-model.md create mode 100644 content/learning-paths/embedded-and-microcontrollers/deploy-mobilesam-on-alif-e8/mobilesam-alif-e8.patch create mode 100644 content/learning-paths/embedded-and-microcontrollers/deploy-mobilesam-on-alif-e8/ospi-writer.zip create mode 100644 content/learning-paths/embedded-and-microcontrollers/deploy-mobilesam-on-alif-e8/prepare-the-environment.md create mode 100644 content/learning-paths/embedded-and-microcontrollers/deploy-mobilesam-on-alif-e8/program-the-board.md create mode 100644 content/learning-paths/embedded-and-microcontrollers/deploy-mobilesam-on-alif-e8/troubleshoot.md create mode 100644 content/learning-paths/embedded-and-microcontrollers/deploy-mobilesam-on-alif-e8/understand-the-system.md create mode 100644 content/learning-paths/embedded-and-microcontrollers/deploy-mobilesam-on-alif-e8/validate-live-segmentation.md diff --git a/content/learning-paths/embedded-and-microcontrollers/deploy-mobilesam-on-alif-e8/_index.md b/content/learning-paths/embedded-and-microcontrollers/deploy-mobilesam-on-alif-e8/_index.md new file mode 100644 index 0000000000..1ad2fea147 --- /dev/null +++ b/content/learning-paths/embedded-and-microcontrollers/deploy-mobilesam-on-alif-e8/_index.md @@ -0,0 +1,65 @@ +--- +title: Deploy MobileSAM on Alif Ensemble E8 with ExecuTorch +description: Export a quantized MobileSAM model and build a Zephyr application that segments live camera frames on the Alif Ensemble E8. +minutes_to_complete: 120 +draft: true +cascade: + draft: true +who_is_this_for: This Learning Path is for embedded ML developers who want to deploy live MobileSAM segmentation on an Alif Ensemble E8 using ExecuTorch and Zephyr. +learning_objectives: + - Trace the fixed-prompt and quantized tensor contracts from model export to live camera inference + - Export a MobileSAM model for the E8 HP core and Ethos-U85-256 + - Build a Zephyr application within its scratch and application RAM budgets + - Program the model and firmware and validate live camera capture, inference, and display output +prerequisites: + - An [Alif Ensemble E8 Development Kit](https://alifsemi.com/support/kits/ensemble-e8devkit/) with its MT9M114 selfie camera and display attached + - An RGB calibration image representative of the camera scenes you want to segment + - Experience with PyTorch model export, C++, and Zephyr builds + - An Apple silicon Mac with Python 3.12, Git, CMake, Ninja, and a C++ build toolchain + - Zephyr SDK 0.17.0, west, Alif Security Toolkit, SEGGER J-Link, and Expect +author: Usamah Zaheer +generate_summary_faq: false +rerun_summary: false +rerun_faqs: false +skilllevels: Advanced +subjects: ML +armips: + - Ethos-U +tools_software_languages: + - ExecuTorch + - PyTorch + - MobileSAM + - Zephyr + - Python + - CMake +operatingsystems: + - macOS + - RTOS +further_reading: + - resource: + title: Run MobileSAM prompt segmentation on Arm Ethos-U85 with ExecuTorch + link: /learning-paths/embedded-and-microcontrollers/deploy-mobilesam-on-ethos-u/ + type: documentation + - resource: + title: Set up the Alif Ensemble E8 board + link: /learning-paths/embedded-and-microcontrollers/alif-image-classification/board-setup/ + type: documentation + - resource: + title: Alif Ensemble E8 DevKit support page + link: https://alifsemi.com/support/kits/ensemble-e8devkit/ + type: website + - resource: + title: ExecuTorch Arm Ethos-U backend documentation + link: https://docs.pytorch.org/executorch/stable/embedded-arm-ethos-u.html + type: documentation + - resource: + title: MobileSAM source repository + link: https://github.com/ChaoningZhang/MobileSAM + type: repository + +### FIXED, DO NOT MODIFY +# ================================================================================ +weight: 1 +layout: learningpathall +learning_path_main_page: "yes" +--- diff --git a/content/learning-paths/embedded-and-microcontrollers/deploy-mobilesam-on-alif-e8/_next-steps.md b/content/learning-paths/embedded-and-microcontrollers/deploy-mobilesam-on-alif-e8/_next-steps.md new file mode 100644 index 0000000000..727b395ddd --- /dev/null +++ b/content/learning-paths/embedded-and-microcontrollers/deploy-mobilesam-on-alif-e8/_next-steps.md @@ -0,0 +1,8 @@ +--- +# ================================================================================ +# FIXED, DO NOT MODIFY THIS FILE +# ================================================================================ +weight: 21 # The weight controls the order of the pages. _index.md always has weight 1. +title: "Next Steps" # Always the same, html page title. +layout: "learningpathall" # All files under learning paths have this same wrapper for Hugo processing. +--- diff --git a/content/learning-paths/embedded-and-microcontrollers/deploy-mobilesam-on-alif-e8/alif-live-demo.jpg b/content/learning-paths/embedded-and-microcontrollers/deploy-mobilesam-on-alif-e8/alif-live-demo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..64e55fdcdf954b2fa8154b7a255352a94593bd93 GIT binary patch literal 182732 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�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%0A@im6MhXellU+mKJCS+KU= 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

I0;uQJQ3&WYEym+BQpFKo5my6@6)Z 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⪙NbON| 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 literal 0 HcmV?d00001 diff --git a/content/learning-paths/embedded-and-microcontrollers/deploy-mobilesam-on-alif-e8/build-the-firmware.md b/content/learning-paths/embedded-and-microcontrollers/deploy-mobilesam-on-alif-e8/build-the-firmware.md new file mode 100644 index 0000000000..1183ee326d --- /dev/null +++ b/content/learning-paths/embedded-and-microcontrollers/deploy-mobilesam-on-alif-e8/build-the-firmware.md @@ -0,0 +1,61 @@ +--- +title: Build the Zephyr firmware within the E8 memory budget +description: Build for the correct HP and U85 target and check model metadata, scratch capacity, and linker memory usage. +weight: 5 +layout: learningpathall +--- + +## Build the matching application + +Build the supplied Zephyr sample with the pinned Alif SDK and the PTE/metadata pair from your export: + +```bash +cd "$EXECUTORCH_ROOT" +west build \ + -d "$MOBILE_SAM_OUTPUT/build-mobilesam-live" \ + -p always \ + -b alif_e8_dk/ae822fa0e5597xx0/rtss_hp \ + "$EXECUTORCH_ROOT/zephyr/samples/mobilesam-ethosu" -- \ + -DZEPHYR_EXTRA_MODULES="$EXECUTORCH_ROOT;$SDK_ALIF" \ + -DSDK_ALIF="$SDK_ALIF" \ + -DET_PTE_FILE_PATH="$MOBILE_SAM_OUTPUT/mobilesam_live_384_grid4_multimask.pte" \ + -DMODEL_METADATA_PATH="$MOBILE_SAM_OUTPUT/mobilesam_live_384_grid4_multimask.json" \ + -DPython3_EXECUTABLE="$(command -v python)" +``` + +The board identifier is tied to the pinned vendor tree. Replacing it with a similar identifier from a newer Zephyr tutorial can select a different configuration. This sample sets `CONFIG_ARM_ETHOS_U85_256=y` and `ETHOSU_TARGET_NPU_CONFIG=ethos-u85-256` explicitly. + +CMake finds the official `serial_camera_mt9m114_selfie.overlay` in `sdk-alif` and adds the sample's deferred-initialization overlay. It generates model metadata and display-logo headers, inspects the PTE for unsupported CPU operators, and extracts the Ethos-U scratch requirement. + +## Understand the three allocator pools + +The supplied `prj.conf` sets: + +| Setting | Bytes | Purpose | +| --- | ---: | --- | +| `CONFIG_EXECUTORCH_METHOD_ALLOCATOR_POOL_SIZE` | 655,360 | Method state and planned buffers allocated by the sample | +| `CONFIG_EXECUTORCH_TEMP_ALLOCATOR_POOL_SIZE` | 6,488,064 | Main Ethos-U tensor/scratch arena | +| `CONFIG_EXECUTORCH_FAST_TEMP_ALLOCATOR_POOL_SIZE` | 393,216 | Fast scratch arena | + +CMake rejects a PTE whose extracted scratch requirement exceeds the configured main scratch pool. Passing that check does not prove the complete application fits: camera, display, stacks, method allocations, and linker regions also consume memory. + +The final documented clean build reported: + +| Linker region | Used bytes | Capacity bytes | Remaining bytes | +| --- | ---: | ---: | ---: | +| Application RAM | 7,580,784 | 7,602,176 | 21,392 | +| SRAM0 | 768,064 | 786,432 | 18,368 | + +These figures describe the reference build. Read your own build's memory report and `zephyr.map`; changing toolchains, model shape, prompt count, logging, or buffer placement can exhaust the remaining space. + +## Keep model storage separate from firmware storage + +The PTE remains in memory-mapped external OSPI flash at `0xC0000000`. The firmware records its size and address and opens it with `BufferDataLoader`. It does not link the 13 MB model into the firmware image. + +Changing the external-flash address alone does not enlarge RAM or fix a linker overflow. Changing the model size also requires rebuilding the firmware so its compiled PTE size and generated metadata remain consistent. + +Before flashing, locate `build-mobilesam-live/zephyr/zephyr.elf`, inspect the linker report, and confirm that the build accepted your PTE. The next lesson programs model data and application firmware as separate operations. + +## What you've accomplished + +You've built the HP-core application for Ethos-U85-256, generated matching model metadata, and checked scratch and linker capacity. Next, program the model and firmware as separate artifacts. diff --git a/content/learning-paths/embedded-and-microcontrollers/deploy-mobilesam-on-alif-e8/export-the-model.md b/content/learning-paths/embedded-and-microcontrollers/deploy-mobilesam-on-alif-e8/export-the-model.md new file mode 100644 index 0000000000..32c44e3250 --- /dev/null +++ b/content/learning-paths/embedded-and-microcontrollers/deploy-mobilesam-on-alif-e8/export-the-model.md @@ -0,0 +1,152 @@ +--- +title: Export and inspect the live MobileSAM model +description: Freeze four independent prompts, export int8 model I/O, and inspect delegation and metadata before building firmware. +weight: 4 +layout: learningpathall +--- + +## Prepare the pinned MobileSAM source + +Keep the environment from the previous lesson active and run these commands from the patched ExecuTorch checkout. Its exporter has live-camera options that are absent from some later versions of the public FVP example. + +```bash +cd "$EXECUTORCH_ROOT" +python examples/arm/mobilesam_prompt_segmentation_example_ethos_u/model_export/prepare_mobilesam.py \ + --source-dir "$MOBILE_SAM_SOURCE" +``` + +The preparation step uses MobileSAM revision `f706ad9c4eb7f219c00d9050e46328518ffb65d2` and applies the configurable-image-size patch. The exporter verifies the official checkpoint SHA256: + +```output +6dbb90523a35330fedd7f1d3dfc66f995213d81b29a5ca8108dbcdd4e37d6c2f +``` + +## Select a calibration image + +Choose an RGB image representative of the camera scenes you want to segment. The exporter uses this image for both quantization calibration and its host comparison. Set its path: + +```bash +export MOBILE_SAM_CALIBRATION_IMAGE=/absolute/path/to/calibration-image.jpg +test -f "$MOBILE_SAM_CALIBRATION_IMAGE" +``` + +The original deployment image passed the unchanged quality gate with the released dependencies: selected-mask IoU `0.8060` and pixel agreement `0.9673`. The bundled `examples/models/dinov2/dog.jpg` failed this four-prompt configuration with selected IoU `0.7448` and agreement `0.9315`. Calibration input affects the result; require your own export to pass the gate before continuing. + +## Export four independent prompts + +Use the live-camera configuration: + +```bash +python examples/arm/mobilesam_prompt_segmentation_example_ethos_u/model_export/export_mobilesam.py \ + --output-path "$MOBILE_SAM_OUTPUT/mobilesam_live_384_grid4_multimask.pte" \ + --mobile-sam-source "$MOBILE_SAM_SOURCE" \ + --image "$MOBILE_SAM_CALIBRATION_IMAGE" \ + --input-size 384 \ + --point 96 96 \ + --point 288 96 \ + --point 96 288 \ + --point 288 288 \ + --multimask-output \ + --quantized-io +``` + +The points sample the four image quadrants. In this multimask mode they are independent prompt batches, giving three candidate masks and associated quality scores per point. This is different from asking SAM for one mask constrained jointly by all four points. + +The image encoder and decoder run on the NPU. The prompt encoder runs during export, and its fixed embeddings are stored in the model. Re-export when you change prompt coordinates, input size, or the output contract. + +The export applies A8W8 globally and A16W8 to TinyViT attention, then lowers through the Arm backend and Vela for `ethos-u85-256`. The compiled configuration uses `Ethos_U85_SYS_DRAM_Mid` and `Dedicated_Sram_384KB`. Those are compiler settings; actual firmware storage and scratch placement still need to match the E8 memory map. + +## Inspect artifacts before continuing + +The exporter writes the `.pte` and matching `.json` metadata beside it, together with mask-agreement metrics, a delegation summary, and TOSA/Vela debug artifacts. Inspect them: + +```bash +ls -lh "$MOBILE_SAM_OUTPUT" +python -m json.tool "$MOBILE_SAM_OUTPUT/mobilesam_live_384_grid4_multimask.json" +cat "$MOBILE_SAM_OUTPUT/mobilesam_live_384_grid4_multimask_metrics.json" +cat "$MOBILE_SAM_OUTPUT/mobilesam_live_384_grid4_multimask_delegation.txt" +``` + +Require successful export and inspect the generated masks and both quality metrics. This exporter accepts selected-mask IoU at least `0.9` **or** pixel agreement at least `0.96`. It compares selected candidates at prompts whose predicted quantized quality reaches `0.9`, using all prompts as fallback when none reaches that threshold. This does not require `0.9` IoU for every candidate or every prompt. The device's later mask-selection threshold is `0.8`. + +Using the same image for calibration and evaluation does not measure segmentation accuracy on new scenes. Inspect the generated masks, then validate the programmed model on representative camera scenes. Your calibration image and released dependencies can produce a different artifact from the retained board binary; record your own model and metadata hashes. + +The live application expects this tensor interface: + +| Tensor | Shape | Type | +| --- | --- | --- | +| Image input | `[1,3,384,384]` | int8 | +| Mask output | `[4,3,96,96]` | int8 | +| Quality output | `[4,3]` | int8 | + +CMake rejects known CPU operators, while the runtime checks input type/byte count and some output dimensions. The metadata-only header generator does not validate `output_shape`. Check the actual PTE tensor interface against the JSON and this configuration before building: + +```bash +python - <<'PY' +import hashlib +import json +import math +import os +from pathlib import Path + +from executorch.exir._serialize._program import deserialize_pte_binary +from executorch.exir.scalar_type import ScalarType +from executorch.exir.schema import Tensor + +pte_path = Path(os.environ["MOBILE_SAM_OUTPUT"]) / "mobilesam_live_384_grid4_multimask.pte" +json_path = pte_path.with_suffix(".json") +metadata = json.loads(json_path.read_text()) +program = deserialize_pte_binary(pte_path.read_bytes()).program +plan, = [p for p in program.execution_plan if p.name == "forward"] +shapes = [[1, 3, 384, 384], [4, 3, 96, 96], [4, 3]] +assert len(plan.inputs) == 1 and len(plan.outputs) == 2 +for index, shape in zip(plan.inputs + plan.outputs, shapes): + tensor = plan.values[index].val + assert isinstance(tensor, Tensor) and tensor.scalar_type == ScalarType.CHAR + assert tensor.sizes == shape, (tensor.sizes, shape) +assert metadata["input_shape"] == shapes[0] +assert metadata["output_shape"] == shapes[1] +assert metadata["input_size"] == 384 and metadata["multimask_output"] is True +assert metadata["point_prompts_xy"] == [[96, 96], [288, 96], [96, 288], [288, 288]] +preprocessing = metadata["preprocessing"] +assert preprocessing["resize"] == "longest_side_then_zero_pad" +for key, expected in (("pixel_mean", [123.675, 116.28, 103.53]), + ("pixel_std", [58.395, 57.12, 57.375])): + values = preprocessing[key] + assert len(values) == 3 and all( + math.isclose(value, reference, rel_tol=0, abs_tol=0.0001) + for value, reference in zip(values, expected) + ) +for name in ("input", "output", "quality"): + qparams = metadata["io_quantization"][name] + assert qparams["dtype"] == "int8" + assert math.isfinite(qparams["scale"]) and qparams["scale"] > 0 + assert isinstance(qparams["zero_point"], int) and -128 <= qparams["zero_point"] <= 127 +print("PTE tensor interface and JSON configuration checks passed") +for path in (pte_path, json_path): + print(hashlib.sha256(path.read_bytes()).hexdigest(), path.name) +PY +``` + +Require the success message and retain both printed hashes with your export. An assertion failure means the files do not satisfy this configuration; resolve it before continuing. The PTE's tensor schema does not store quantization parameters, so this check cannot prove that the JSON scales and zero points belong to that PTE. Keep the matching PTE and JSON from the same export, alongside their hashes, and check target output during execution. + +An old `[1,1,112,112]` floating-point FVP result belongs to the earlier 448×448 single-mask configuration. It is not the output contract of this 384×384 live model. + +## Prepare an identical model file for programming + +J-Link and the recovery writer consume a raw `.bin` file. This is an unchanged copy of the PTE, without a new container or header: + +```bash +cp "$MOBILE_SAM_OUTPUT/mobilesam_live_384_grid4_multimask.pte" \ + "$MOBILE_SAM_OUTPUT/mobilesam_live_384_grid4_multimask.bin" +cmp "$MOBILE_SAM_OUTPUT/mobilesam_live_384_grid4_multimask.pte" \ + "$MOBILE_SAM_OUTPUT/mobilesam_live_384_grid4_multimask.bin" +``` + +The validated final binary is 13,293,120 bytes, with SHA256 `895098c246cfcf0ca1a20e9dd9834dd9f4cfc3906edcb1dc901c47139e4f779d`. A fresh export can differ with dependency versions or calibration. Record the size and hash of your own PTE rather than assuming the reference hash must match. + +Before proceeding, explain why `--quantized-io` matters: it removes the host-style floating-point I/O boundary from the device program, while the application performs camera normalization, quantization, and mask post-processing. + +## What you've accomplished + +You've exported the four-prompt model, checked delegation and quantized tensor metadata, and prepared an identical binary for programming. Next, build firmware with this PTE and JSON and check its memory use. diff --git a/content/learning-paths/embedded-and-microcontrollers/deploy-mobilesam-on-alif-e8/mobilesam-alif-e8.patch b/content/learning-paths/embedded-and-microcontrollers/deploy-mobilesam-on-alif-e8/mobilesam-alif-e8.patch new file mode 100644 index 0000000000..a82725a10d --- /dev/null +++ b/content/learning-paths/embedded-and-microcontrollers/deploy-mobilesam-on-alif-e8/mobilesam-alif-e8.patch @@ -0,0 +1,4760 @@ +diff --git a/README-wheel.md b/README-wheel.md +index 8ee58b56f2..b8f87d3f96 100644 +--- a/README-wheel.md ++++ b/README-wheel.md +@@ -8,6 +8,22 @@ The `executorch` pip package is in beta. + * Supported python versions: 3.10, 3.11, 3.12, 3.13, 3.14 + * Compatible systems: Linux x86_64, Linux aarch64, macOS aarch64 + ++Backend export tools can require optional Python dependencies. For example, ++install the Ethos-U AOT dependencies with: ++ ++```bash ++pip install 'executorch[ethos_u]' ++``` ++ ++The `ethos_u` extra installs host-side export dependencies such as the Vela ++compiler; it does not install an Arm cross compiler, an FVP, or a target C++ ++runtime. There is no `cortex_m` package extra. CMSIS-NN is a separate Cortex-M ++export and runtime path that requires an ExecuTorch source checkout. Installing ++an extra does not make unsupported Ethos-U nodes use CMSIS-NN automatically. ++See the ++[Arm examples](https://github.com/pytorch/executorch/tree/main/examples/arm) ++for that setup. ++ + To build a minimal wheel from source, set + `EXECUTORCH_BUILD_MINIMAL=1` when running `pip wheel` or `pip install`. + That wheel contains the Python EXIR export path and `flatc` for `.pte` +diff --git a/README.md b/README.md +index 192e90c51a..7d2a75c8b1 100644 +--- a/README.md ++++ b/README.md +@@ -64,6 +64,11 @@ Learn more: [How ExecuTorch Works](https://docs.pytorch.org/executorch/main/intr + pip install executorch + ``` + ++Backend export tools can require optional dependencies. For example, use ++`pip install 'executorch[ethos_u]'` for Ethos-U AOT export. Embedded ++toolchains, simulators, and target runtimes are installed separately; see the ++[Arm examples](examples/arm/README.md) for the complete flow. ++ + For platform-specific setup (Android, iOS, embedded systems), see the [Quick Start](https://docs.pytorch.org/executorch/main/quick-start-section.html) documentation for additional info. + + ### Export and Deploy in 3 Steps +diff --git a/backends/arm/scripts/corstone_utils.cmake b/backends/arm/scripts/corstone_utils.cmake +index 72a0c27a8c..099aa5f4ce 100644 +--- a/backends/arm/scripts/corstone_utils.cmake ++++ b/backends/arm/scripts/corstone_utils.cmake +@@ -37,7 +37,7 @@ function(fetch_ethos_u_content ETHOS_SDK_PATH ET_DIR_PATH) + BINARY_DIR + ${ETHOS_SDK_PATH} + SUBBUILD_DIR +- ${ETHOS_SDK_PATH}/../ethos_u-subbuild ++ ${CMAKE_BINARY_DIR}/ethos_u-subbuild + SOURCE_SUBDIR + none + ) +diff --git a/backends/arm/test/misc/test_mobilesam_export.py b/backends/arm/test/misc/test_mobilesam_export.py +new file mode 100644 +index 0000000000..049fb15fb2 +--- /dev/null ++++ b/backends/arm/test/misc/test_mobilesam_export.py +@@ -0,0 +1,44 @@ ++# 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. ++ ++from pathlib import Path ++ ++import pytest ++ ++from examples.arm.mobilesam_prompt_segmentation_example_ethos_u.model_export.export_mobilesam import ( ++ INPUT_SIZE, ++ parse_args, ++ resolve_point_prompts, ++) ++ ++ ++def test_minimal_export_arguments() -> None: ++ args = parse_args( ++ [ ++ "--output-path", ++ "model.pte", ++ "--image", ++ "image.jpg", ++ "--point", ++ "219", ++ "193", ++ ] ++ ) ++ ++ assert args.output_path == Path("model.pte") ++ assert args.image == Path("image.jpg") ++ assert args.point == [[219.0, 193.0]] ++ assert not args.local_files_only ++ assert not args.quantized_io ++ ++ ++def test_point_prompts_must_be_inside_fixed_input() -> None: ++ assert resolve_point_prompts([[0, 0], [INPUT_SIZE - 1, INPUT_SIZE - 1]]) == [ ++ (0.0, 0.0), ++ (447.0, 447.0), ++ ] ++ ++ with pytest.raises(ValueError, match="must be inside"): ++ resolve_point_prompts([[INPUT_SIZE, 0]]) +diff --git a/backends/arm/test/misc/test_mobilesam_visualization.py b/backends/arm/test/misc/test_mobilesam_visualization.py +new file mode 100644 +index 0000000000..b12a2e5851 +--- /dev/null ++++ b/backends/arm/test/misc/test_mobilesam_visualization.py +@@ -0,0 +1,24 @@ ++# 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. ++ ++from pathlib import Path ++ ++from examples.arm.mobilesam_prompt_segmentation_example_ethos_u.runtime.visualize_fvp_output import ( ++ parse_rle_mask, ++) ++ ++ ++def test_parse_rle_mask_tolerates_non_utf8_fvp_output(tmp_path: Path) -> None: ++ log_path = tmp_path / "fvp.log" ++ log_path.write_bytes( ++ b"\xffFVP startup noise\n" ++ b"Segmentation mask RLE begin\n" ++ b"Segmentation mask RLE chunk 0:2,1:3,\n" ++ b"\x80UART noise\n" ++ b"Segmentation mask RLE chunk 0:1,\n" ++ b"Segmentation mask RLE end\n" ++ ) ++ ++ assert parse_rle_mask(log_path, expected_pixels=6) == [0, 0, 1, 1, 1, 0] +diff --git a/backends/arm/test/test_arm_ootb.sh b/backends/arm/test/test_arm_ootb.sh +index e61eabba9c..4064a31a40 100755 +--- a/backends/arm/test/test_arm_ootb.sh ++++ b/backends/arm/test/test_arm_ootb.sh +@@ -170,10 +170,8 @@ run_mobilesam_e2e_ethos_u() { + local example_dir="${et_root_dir}/examples/arm/mobilesam_prompt_segmentation_example_ethos_u" + local work_root="${et_root_dir}/arm_test/mobilesam_ootb_smoke" + local export_dir="${work_root}/export" +- local artifact_dir="${work_root}/artifacts" +- local debug_dir="${work_root}/debug" ++ local debug_dir="${export_dir}/debug" + local et_build_dir="${work_root}/cmake-out-arm" +- local quantized_aot_build_dir="${work_root}/quantized_ops_aot" + local build_dir="${work_root}/runtime" + local mobile_sam_source="${work_root}/mobile_sam/source" + local image_path="${et_root_dir}/examples/models/dinov2/dog.jpg" +@@ -181,11 +179,10 @@ run_mobilesam_e2e_ethos_u() { + local metadata_path="${export_dir}/mobilesam_prompt_smoke.json" + local fvp_log="${work_root}/fvp.log" + local toolchain_file="${et_root_dir}/examples/arm/ethos-u-setup/arm-none-eabi-gcc.cmake" +- local input_size=448 + local fvp_timelimit="${FVP_TIMELIMIT:-300}" + echo "${FUNCNAME}: Work directory: ${work_root}; existing artifacts will be reused if present" + +- mkdir -p "${export_dir}" "${artifact_dir}" "${debug_dir}" "${build_dir}" ++ mkdir -p "${export_dir}" "${build_dir}" + + setup_path_script=${et_root_dir}/examples/arm/arm-scratch/setup_path.sh + source ${setup_path_script} +@@ -197,60 +194,26 @@ run_mobilesam_e2e_ethos_u() { + cmake --preset arm-baremetal -B "${et_build_dir}" + cmake --build "${et_build_dir}" --target install -j"$n_proc" + +- echo "${FUNCNAME}: Building host quantized AOT library" + local python_executable + python_executable="$(python3 -c 'import sys; print(sys.executable)')" +- cmake \ +- -S "${et_root_dir}" \ +- -B "${quantized_aot_build_dir}" \ +- -DCMAKE_BUILD_TYPE=Release \ +- -DEXECUTORCH_BUILD_KERNELS_QUANTIZED=ON \ +- -DEXECUTORCH_BUILD_KERNELS_QUANTIZED_AOT=ON \ +- -DEXECUTORCH_BUILD_XNNPACK=OFF \ +- -DPYTHON_EXECUTABLE="${python_executable}" +- cmake --build "${quantized_aot_build_dir}" --target quantized_ops_aot_lib -j"$n_proc" +- +- local quantized_ops_library +- quantized_ops_library="$( +- find "${quantized_aot_build_dir}/kernels/quantized" \ +- -name 'libquantized_ops_aot_lib.*' \ +- -type f \ +- -print \ +- -quit +- )" +- [[ -n "${quantized_ops_library}" ]] || { +- echo "${FUNCNAME}: Missing quantized AOT library under ${quantized_aot_build_dir}" +- return 1 +- } +- +- echo "${FUNCNAME}: Installing example requirements" +- pip install -r "${example_dir}/requirements.txt" + + echo "${FUNCNAME}: Preparing pinned MobileSAM source" + python3 "${example_dir}/model_export/prepare_mobilesam.py" \ + --source-dir "${mobile_sam_source}" + + echo "${FUNCNAME}: Exporting quantized MobileSAM PTE" +- env EXECUTORCH_QUANTIZED_OPS_AOT_LIBRARY="${quantized_ops_library}" \ +- python3 "${example_dir}/model_export/export_mobilesam.py" \ ++ python3 "${example_dir}/model_export/export_mobilesam.py" \ + --output-path "${pte_path}" \ +- --calibration-image "${image_path}" \ +- --eval-image "${image_path}" \ ++ --image "${image_path}" \ + --mobile-sam-source "${mobile_sam_source}" \ +- --input-size "${input_size}" \ +- --point 219 193 \ +- --num-calibration-samples 1 \ +- --num-eval-samples 1 \ +- --num-debug-samples 1 \ +- --minimum-fp32-quantized-iou 0.9 \ +- --artifact-dir "${artifact_dir}" \ +- --debug-output-dir "${debug_dir}" ++ --point 219 193 + + for artifact in \ + "${pte_path}" \ + "${metadata_path}" \ + "${export_dir}/mobilesam_prompt_smoke_delegation.txt" \ +- "${export_dir}/mobilesam_prompt_smoke_metrics.json"; do ++ "${export_dir}/mobilesam_prompt_smoke_metrics.json" \ ++ "${debug_dir}/dog/quantized_mask.png"; do + [[ -f "${artifact}" ]] || { + echo "${FUNCNAME}: Missing export artifact ${artifact}" + return 1 +@@ -265,9 +228,7 @@ run_mobilesam_e2e_ethos_u() { + -B "${build_dir}" \ + -DCMAKE_TOOLCHAIN_FILE="${toolchain_file}" \ + -DET_PTE_FILE_PATH="${pte_path}" \ +- -DMODEL_METADATA_PATH="${metadata_path}" \ + -DIMAGE_PATH="${image_path}" \ +- -DMASK_THRESHOLD=0.0 \ + -DET_SEGMENTATION_DUMP_MASK=ON \ + -DPYTHON_EXECUTABLE="${python_executable}" \ + -DET_BUILD_DIR_PATH="${et_build_dir}" +diff --git a/docs/source/backends/arm-ethos-u/tutorials/ethos-u-getting-started.md b/docs/source/backends/arm-ethos-u/tutorials/ethos-u-getting-started.md +index 7e4056720d..a2b852c0df 100644 +--- a/docs/source/backends/arm-ethos-u/tutorials/ethos-u-getting-started.md ++++ b/docs/source/backends/arm-ethos-u/tutorials/ethos-u-getting-started.md +@@ -35,10 +35,25 @@ To enable development without a specific development board, we will be using a [ + + ### Software + +-First, you will need to install ExecuTorch. Please follow the recommended tutorials to set up a working ExecuTorch development environment. ++Use a source checkout because this tutorial builds an embedded target runtime. ++From the repository root, create a Python environment and install the current ++checkout with the Ethos-U AOT dependencies: ++ ++```bash ++python3.12 -m venv .venv ++source .venv/bin/activate ++./install_executorch.sh --optional-dependency ethos_u ++``` ++ ++The released-package equivalent for host-side export is ++`pip install 'executorch[ethos_u]'`. The `ethos_u` extra does not install an Arm ++cross compiler, FVP, or target runtime. There is no `cortex_m` package extra; ++CMSIS-NN is a separate Cortex-M export and runtime path. Installing an extra ++does not make unsupported Ethos-U nodes use CMSIS-NN automatically. ++ ++Install the Arm cross-compilation toolchain, simulators, and remaining backend ++dependencies with: + +-In addition to this, you need to install a number of SDK dependencies for generating Ethos-U command streams. Scripts to automate this are available in the main [ExecuTorch repository](https://github.com/pytorch/executorch/tree/main/examples/arm/). +-To install Ethos-U dependencies, run + ```bash + ./examples/arm/setup.sh --i-agree-to-the-contained-eula + ``` +diff --git a/docs/source/getting-started.md b/docs/source/getting-started.md +index 3c45306301..d370f7cf65 100644 +--- a/docs/source/getting-started.md ++++ b/docs/source/getting-started.md +@@ -23,7 +23,22 @@ This package includes the dependencies needed to export a PyTorch model, as well + pip install executorch + ``` + +-To build the framework from source, see [Building From Source](using-executorch-building-from-source.md). Backend delegates may require additional dependencies. See the appropriate backend documentation for more information. ++Some backends provide optional Python dependencies for ahead-of-time export. ++For example, install the Ethos-U export dependencies with: ++ ++```bash ++pip install 'executorch[ethos_u]' ++``` ++ ++The extra installs host-side AOT tools; it does not install an embedded ++toolchain, simulator, or target runtime. There is no `cortex_m` package extra. ++CMSIS-NN is a separate Cortex-M export and runtime path that requires a source ++checkout. Installing an extra does not make unsupported Ethos-U nodes use ++CMSIS-NN automatically. See the ++[Arm examples](https://github.com/pytorch/executorch/tree/main/examples/arm) ++for the complete setup. ++ ++To build the framework from source, see [Building From Source](using-executorch-building-from-source.md). Other backend delegates may require additional dependencies. See the appropriate backend documentation for more information. + + > **_NOTE:_** On Windows, ExecuTorch requires a [Visual Studio Developer Powershell](https://learn.microsoft.com/en-us/visualstudio/ide/reference/command-prompt-powershell?view=vs-2022). Running from outside of a developer prompt will manifest as errors related to CL.exe. + +diff --git a/docs/source/using-executorch-building-from-source.md b/docs/source/using-executorch-building-from-source.md +index c63fbd715a..98958fa977 100644 +--- a/docs/source/using-executorch-building-from-source.md ++++ b/docs/source/using-executorch-building-from-source.md +@@ -79,8 +79,28 @@ portability details. + * `--clean`: Removes build artifacts. + * `--editable`: Install the ExecuTorch python package in editable mode (see [Editable Install](#editable-install)). + * `--minimal`: Install only the minimal set of dependencies required to run ExecuTorch. Do not install dependencies for examples. ++ * `--optional-dependency `: Install an optional Python dependency set. ++ Repeat the flag to select more than one. Supported names are `ethos_u`, ++ `vgf`, and `openvino`. + * `--use-pt-pinned-commit`: Install the pinned PyTorch commit or release version. When not specified, the latest PyTorch nightly build is installed. + ++ For example, install the current checkout with the Ethos-U AOT dependencies: ++ ++ ```bash ++ ./install_executorch.sh --optional-dependency ethos_u ++ ``` ++ ++ After the base dependencies have already been installed, the equivalent ++ editable package command is: ++ ++ ```bash ++ pip install -e '.[ethos_u]' --no-build-isolation ++ ``` ++ ++ Optional dependency sets install host-side Python tools only. Embedded ++ toolchains, simulators, and target runtimes are configured separately by the ++ backend setup and build instructions. ++ + For Intel-based macOS systems, use `--use-pt-pinned-commit --minimal`. As PyTorch does not provide pre-built binaries for Intel Mac, installation requires building PyTorch from source. Instructions can be found in [PyTorch Installation](https://github.com/pytorch/pytorch#installation). + + Note that only the XNNPACK and CoreML backends are built by default. Additional backends can be enabled or disabled by setting the corresponding CMake flags: +diff --git a/examples/arm/README.md b/examples/arm/README.md +index 1a0923f1ab..00dc70f246 100644 +--- a/examples/arm/README.md ++++ b/examples/arm/README.md +@@ -11,6 +11,28 @@ This directory contains documentation and scripts to + help you setup and run a PyTorch model on the Arm backend + via ExecuTorch. + ++## Python package setup ++ ++For Ethos-U examples, install the current checkout and its AOT dependencies in ++a clean Python environment: ++ ++```bash ++python3.12 -m venv .venv ++source .venv/bin/activate ++./install_executorch.sh --optional-dependency ethos_u ++``` ++ ++After the base dependencies are installed, the equivalent editable package ++command is `pip install -e '.[ethos_u]' --no-build-isolation`. The `ethos_u` ++extra provides host-side export dependencies; it does not install the Arm ++toolchain, FVPs, or target runtime. ++ ++There is no `cortex_m` package extra. CMSIS-NN is a separate Cortex-M export ++and runtime path that uses code and build configuration from the ExecuTorch ++source checkout. Installing an extra does not make unsupported Ethos-U nodes ++use CMSIS-NN automatically. Run `setup.sh` below, then follow the relevant ++Cortex-M example. ++ + ## setup.sh + + `setup.sh` downloads the Arm cross-compilation toolchain and Corstone FVP +diff --git a/examples/arm/ethos_u_minimal_example.ipynb b/examples/arm/ethos_u_minimal_example.ipynb +index 11f24019d2..2ee20a504c 100644 +--- a/examples/arm/ethos_u_minimal_example.ipynb ++++ b/examples/arm/ethos_u_minimal_example.ipynb +@@ -21,11 +21,17 @@ + "This guide demonstrates the full flow for running a module on Arm Ethos-U55 using ExecuTorch.\n", + "Tested on Linux x86_64 and macOS aarch64. If something is not working for you, please raise a GitHub issue and tag Arm.\n", + "\n", +- "Before you begin:\n", +- "1. (In a clean virtual environment with a compatible Python version) Install executorch using `./install_executorch.sh`\n", +- "2. Install Arm cross-compilation toolchain and simulators using `./examples/arm/setup.sh --i-agree-to-the-contained-eula`\n", ++ "Before you begin, run these commands from the base `executorch` folder:\n", + "\n", +- "With all commands executed from the base `executorch` folder.\n", ++ "```bash\n", ++ "python3.12 -m venv .venv\n", ++ "source .venv/bin/activate\n", ++ "./install_executorch.sh --optional-dependency ethos_u\n", ++ "./examples/arm/setup.sh --i-agree-to-the-contained-eula\n", ++ "source examples/arm/arm-scratch/setup_path.sh\n", ++ "```\n", ++ "\n", ++ "The `ethos_u` extra installs the host-side AOT dependencies. The Arm setup script installs the cross compiler and FVPs needed later in the notebook. There is no `cortex_m` package extra. CMSIS-NN is a separate Cortex-M export and runtime path; installing an extra does not make unsupported Ethos-U nodes use CMSIS-NN automatically.\n", + "\n", + "\n", + "\n", +diff --git a/examples/arm/mobilesam_prompt_segmentation_example_ethos_u/README.md b/examples/arm/mobilesam_prompt_segmentation_example_ethos_u/README.md +index 58eae2b020..d2a44173e3 100644 +--- a/examples/arm/mobilesam_prompt_segmentation_example_ethos_u/README.md ++++ b/examples/arm/mobilesam_prompt_segmentation_example_ethos_u/README.md +@@ -4,17 +4,75 @@ This end-to-end example shows how to use the Arm Ethos-U backend in + ExecuTorch for transformer-based prompt segmentation. MobileSAM predicts a + binary mask for fixed positive point prompts rather than semantic class IDs. + The host debug flow validates quantization by comparing FP32 and quantized +-masks, with an optional binary reference mask when one is available. ++masks before lowering. + +-It covers: ++**Prompt contract:** the runtime accepts only an image tensor. The positive ++point coordinates are embedded in the `.pte` during export. You can reuse the ++`.pte` for a new image, but changing the points requires re-exporting the model. + +-- Loading the MobileSAM `vit_t` checkpoint. +-- Freezing one or more positive point prompts into the exported graph. +-- Applying post-training quantization with the Ethos-U quantizer. +-- Lowering the quantized model to an Ethos-U85-256 ExecuTorch program. +-- Producing validation and debugging artifacts such as masks, overlays, +- mismatch heatmaps, metrics, and delegation summaries. +-- Building a bare-metal Corstone-320 runtime app and running it on FVP. ++## Clean-clone setup ++ ++Run every command in this guide from the ExecuTorch repository root. Use the ++checkout's installer rather than installing an unrelated `executorch` wheel ++from PyPI: ++ ++```bash ++python3.12 -m venv .venv ++source .venv/bin/activate ++ ++./install_executorch.sh --optional-dependency ethos_u ++ ++./examples/arm/setup.sh --i-agree-to-the-contained-eula ++source examples/arm/arm-scratch/setup_path.sh ++``` ++ ++`install_executorch.sh` installs the current checkout and Ethos-U Python tools ++into the active virtual environment. `examples/arm/setup.sh` installs the ++pinned Arm GNU bare-metal toolchain and Corstone FVP used by this example. ++For a later editable reinstall, the equivalent package command is ++`python -m pip install -e '.[ethos_u]' --no-build-isolation`. There is no ++`cortex_m` package extra; CMSIS-NN kernels are selected when building a target ++runtime, and this example does not need them because its graph is fully ++delegated to Ethos-U. ++ ++On macOS, install the ++[FVPs-on-Mac](https://github.com/Arm-Examples/FVPs-on-Mac) Docker wrapper and ++add its `bin` directory to `PATH` before running the setup or smoke commands. ++Run `command -v FVP_Corstone_SSE-320` and confirm it resolves inside the ++FVPs-on-Mac `bin` directory rather than to a downloaded Linux FVP binary. ++ ++## One-command end-to-end smoke test ++ ++After setup, export, build, run on Corstone-320 FVP, and validate the target ++mask with: ++ ++```bash ++./backends/arm/test/test_arm_ootb.sh run_mobilesam_e2e_ethos_u ++``` ++ ++The smoke test uses the demo image for calibration and validation. Its main ++outputs are: ++ ++- `.pte`: `arm_test/mobilesam_ootb_smoke/export/mobilesam_prompt_smoke.pte` ++- Host quantized mask: ++ `arm_test/mobilesam_ootb_smoke/export/debug/dog/quantized_mask.png` ++- FVP log: `arm_test/mobilesam_ootb_smoke/fvp.log` ++- Side-by-side result: ++ `arm_test/mobilesam_ootb_smoke/fvp_visual/fvp_comparison.png` ++- FVP validation metrics: ++ `arm_test/mobilesam_ootb_smoke/fvp_visual/metrics.json` ++ ++## What the example does ++ ++1. Loads the pinned official MobileSAM `vit_t` source and checkpoint. ++2. Builds a wrapper with fixed positive point prompts. ++3. Calibrates PT2E quantization with `EthosUQuantizer`. ++4. Applies a stable softmax decomposition to transformer attention blocks. ++5. Lowers the complete quantized graph with `EthosUPartitioner`. ++6. Writes a `.pte`, builds the bare-metal runtime, executes it on FVP, and ++ compares the target mask with the host quantized result. ++7. Optionally builds a Zephyr application for the Alif Ensemble E8 DevKit and ++ runs the same export on Ethos-U85-256 hardware. + + The default export uses a reduced `448x448` image input and returns one + low-resolution `[1, 1, 112, 112]` mask-logit tensor. The example prepares the +@@ -31,6 +89,12 @@ mask thresholding outside the model. SAM-style candidate-mask selection can be + numerically sensitive after export and quantization, so this example keeps the + target graph focused on the fixed-prompt image encoder and mask decoder. + ++## Manual workflow ++ ++For the educational flow and configurable export options, continue with ++`model_export/README.md`, then build and execute the generated program using ++`runtime/README.md`. ++ + ## Layout + + - `model_export/prepare_mobilesam.py` - Prepares the pinned external MobileSAM +@@ -41,3 +105,5 @@ target graph focused on the fixed-prompt image encoder and mask decoder. + Corstone-320 FVP execution. + - `runtime/visualize_fvp_output.py` - Decodes the target mask dump, creates an + overlay, and compares FVP output with the host quantized mask. ++- `../../../zephyr/samples/mobilesam-ethosu/README.md` - Alif Ensemble E8 ++ build, flash, execution, and visualization instructions. +diff --git a/examples/arm/mobilesam_prompt_segmentation_example_ethos_u/model_export/README.md b/examples/arm/mobilesam_prompt_segmentation_example_ethos_u/model_export/README.md +index 8069c0d8cc..c5f24ee0f5 100644 +--- a/examples/arm/mobilesam_prompt_segmentation_example_ethos_u/model_export/README.md ++++ b/examples/arm/mobilesam_prompt_segmentation_example_ethos_u/model_export/README.md +@@ -1,192 +1,96 @@ +-# MobileSAM Export, Quantization, and Debugging ++# Export MobileSAM for Ethos-U + +-This directory exports MobileSAM `vit_t` for the ExecuTorch Ethos-U backend. +-The exporter freezes one or more positive point prompts into the graph, so the +-runtime app has one tensor input: the preprocessed image. The model returns one +-mask-logit tensor for those prompts. The default export uses a `448x448` input, +-which produces a `112x112` mask-logit tensor. ++This flow exports MobileSAM `vit_t` as a fixed-prompt, fully delegated ++Ethos-U85 program. The image remains a runtime input, while positive point ++coordinates are embedded in the `.pte`. Changing the points requires another ++export. + +-Production SAM applications usually pass prompts dynamically from a UI or +-tracking pipeline. This example freezes the prompt embeddings so the FVP +-runtime stays small and demonstrates the Ethos-U flow with the same image +-encoder and mask decoder used by MobileSAM. ++## Export flow + +-## Requirements ++1. Load the pinned official MobileSAM source and checkpoint. ++2. Freeze one or more positive point prompts into the model. ++3. Use the demonstration image for PT2E calibration. ++4. Quantize globally to A8W8 and use A16W8 for TinyViT attention. ++5. Compare FP32 and quantized masks and require at least `0.9` IoU. ++6. Lower the complete graph with `EthosUPartitioner` and save the `.pte`. ++7. Write TOSA/Vela intermediates, masks, overlays, metrics, and delegation ++ information beside the model. + +-- Python 3.10+ with `executorch`. +-- Dependencies from +- `examples/arm/mobilesam_prompt_segmentation_example_ethos_u/requirements.txt`. +-- Git and internet access to prepare the pinned external MobileSAM checkout and +- download the official checkpoint, unless both are already cached. +-- Ethos-U dependencies from `examples/arm/setup.sh`. +- +-MobileSAM's A16W8 attention requires `ethos-u-vela>=5.1.0`. Vela 5.0 produces +-incorrect Ethos-U85 INT16 reductions, so the exporter rejects that version +-before generating a `.pte`. +- +-## Export +- +-Run from the ExecuTorch repo root: ++Complete the clean-clone setup in the top-level example `README.md`, then run ++from the ExecuTorch repository root: + + ```bash +-MOBILE_SAM_SOURCE="$HOME/.cache/executorch/mobilesam/f706ad9c4eb7f219c00d9050e46328518ffb65d2/source" ++MOBILE_SAM_WORK_DIR="${MOBILE_SAM_WORK_DIR:-$PWD/arm_test/mobilesam_manual}" ++MOBILE_SAM_SOURCE="$MOBILE_SAM_WORK_DIR/mobile_sam/source" ++MOBILE_SAM_IMAGE="$PWD/examples/models/dinov2/dog.jpg" ++MOBILE_SAM_PTE="$MOBILE_SAM_WORK_DIR/export/mobilesam_point_ethos_u85_448.pte" ++ + python examples/arm/mobilesam_prompt_segmentation_example_ethos_u/model_export/prepare_mobilesam.py \ + --source-dir "$MOBILE_SAM_SOURCE" + + python examples/arm/mobilesam_prompt_segmentation_example_ethos_u/model_export/export_mobilesam.py \ +- --output-path ./mobilesam_point_ethos_u85_448.pte \ +- --mobile-sam-source "$MOBILE_SAM_SOURCE" \ +- --calibration-image examples/models/dinov2/dog.jpg \ +- --eval-image examples/models/dinov2/dog.jpg \ ++ --output-path "$MOBILE_SAM_PTE" \ ++ --image "$MOBILE_SAM_IMAGE" \ + --point 219 193 \ +- --artifact-dir ./mobilesam_point_artifacts \ +- --debug-output-dir ./mobilesam_point_debug ++ --mobile-sam-source "$MOBILE_SAM_SOURCE" + ``` + +-The default configuration is: +- +-- Model source: `https://github.com/ChaoningZhang/MobileSAM` +-- MobileSAM source revision: `f706ad9c4eb7f219c00d9050e46328518ffb65d2` +-- External source patch: `0001-Make-TinyViT-image-size-configurable.patch` +-- Checkpoint URL: +- `https://github.com/ChaoningZhang/MobileSAM/raw/f706ad9c4eb7f219c00d9050e46328518ffb65d2/weights/mobile_sam.pt` +-- Checkpoint SHA256: +- `6dbb90523a35330fedd7f1d3dfc66f995213d81b29a5ca8108dbcdd4e37d6c2f` +-- MobileSAM source-code license: Apache-2.0 +-- Static input shape: `[1, 3, 448, 448]` +-- Output mask logits shape: `[1, 1, 112, 112]` +-- Positive point prompt: `(224, 224)` in the padded `448x448` model input +- frame ++The example intentionally fixes the deployment configuration: ++ ++- Input: `[1, 3, 448, 448]` ++- Output mask logits: `[1, 1, 112, 112]` + - Target: `ethos-u85-256` +-- System config: `Ethos_U85_SYS_DRAM_Mid` ++- System configuration: `Ethos_U85_SYS_DRAM_Mid` + - Memory mode: `Dedicated_Sram_384KB` +-- Calibration samples: `4` +-- Validation samples: `4` +- +-The MobileSAM source and checkpoint are not redistributed by this example. The +-preparation script clones the pinned official source into a managed external +-cache and applies the configurable-input patch. The exporter downloads the +-pinned official checkpoint and verifies its SHA256 unless `--checkpoint-path` +-is provided. The source repository and checkpoint may have separate terms; the +-export metadata records only the source-code license and does not assign a +-license to the checkpoint. +- +-For a quick offline smoke test after caching the official checkpoint and source +-checkout: +- +-```bash +-python examples/arm/mobilesam_prompt_segmentation_example_ethos_u/model_export/prepare_mobilesam.py \ +- --source-dir "$MOBILE_SAM_SOURCE" \ +- --local-files-only ++- Mask threshold: `0.0` + +-python examples/arm/mobilesam_prompt_segmentation_example_ethos_u/model_export/export_mobilesam.py \ +- --output-path /tmp/mobilesam_smoke.pte \ +- --local-files-only \ +- --mobile-sam-source "$MOBILE_SAM_SOURCE" \ +- --calibration-image examples/models/dinov2/dog.jpg \ +- --eval-image examples/models/dinov2/dog.jpg \ +- --point 219 193 \ +- --num-calibration-samples 1 \ +- --num-eval-samples 1 \ +- --minimum-fp32-quantized-iou 0.9 +-``` ++Repeat `--point X Y` to export a multi-point positive prompt. Pass ++`--local-files-only` after the source and checkpoint have been cached. Pass a ++local checkpoint with `--checkpoint-path` when network access is unavailable. + +-Repeat `--point X Y` to freeze a multi-point prompt into the graph. Validate +-every prompt set because adding positive points can substantially change the +-predicted object after quantization: ++The Alif E8 Zephyr sample additionally requires `--quantized-io`. This exposes ++int8 model input and output tensors and records their quantization parameters ++in the metadata JSON. + +-```bash +-python examples/arm/mobilesam_prompt_segmentation_example_ethos_u/model_export/export_mobilesam.py \ +- --output-path /tmp/mobilesam_multipoint_smoke.pte \ +- --local-files-only \ +- --mobile-sam-source "$MOBILE_SAM_SOURCE" \ +- --calibration-image examples/models/dinov2/dog.jpg \ +- --eval-image examples/models/dinov2/dog.jpg \ +- --point 166 158 \ +- --point 219 193 \ +- --point 289 184 \ +- --num-calibration-samples 1 \ +- --num-eval-samples 1 \ +- --debug-output-dir /tmp/mobilesam_multipoint_debug +-``` ++## Outputs + +-Pass `--input-size 1024` to reproduce the original MobileSAM resolution. Smaller +-inputs export and run faster, but very small inputs such as `224` or `256` +-usually produce lower-quality masks because the checkpoint was trained for +-`1024x1024` images. ++For the command above, the export directory contains: + +-To validate against a known binary mask, pass one `--eval-mask` per +-`--eval-image`. Non-zero mask pixels are treated as foreground. When no +-reference mask is provided, validation reports FP32/quantized mask agreement. +-Use `--minimum-fp32-quantized-iou` in automated runs to reject an inaccurate +-quantized model before lowering. ++- `mobilesam_point_ethos_u85_448.pte` - ExecuTorch program. ++- `mobilesam_point_ethos_u85_448.json` - shapes, preprocessing, prompt, and ++ provenance metadata. ++- `mobilesam_point_ethos_u85_448_metrics.json` - FP32/quantized mask agreement. ++- `mobilesam_point_ethos_u85_448_delegation.txt` - delegation summary. ++- `artifacts/` - TOSA and Vela intermediate files. ++- `debug/dog/` - input, FP32/quantized masks, and colored overlays. + +-The export flow: ++The host quantized mask used for FVP validation is: + +-1. Loads the MobileSAM `vit_t` checkpoint through the patched external API. +-2. Builds a fixed-prompt wrapper containing the image encoder and mask decoder. +-3. Calibrates PT2E quantization with `EthosUQuantizer`. +-4. Uses stable softmax decomposition for transformer attention blocks. +-5. Lowers the quantized graph with `EthosUPartitioner`. +-6. Writes an ExecuTorch `.pte` program. ++```text ++$MOBILE_SAM_WORK_DIR/export/debug/dog/quantized_mask.png ++``` + +-The quantization recipe uses int8 activations and int8 weights globally, with +-A16W8 quantization for the TinyViT attention modules. Static int8 activation +-quantization collapses MobileSAM attention features, while the selective A16W8 +-attention path preserves mask quality and still lowers as one Ethos-U delegate. ++## Source and checkpoint + +-## Outputs ++The preparation script creates a managed external checkout of ++`https://github.com/ChaoningZhang/MobileSAM` at revision ++`f706ad9c4eb7f219c00d9050e46328518ffb65d2` and applies ++`0001-Make-TinyViT-image-size-configurable.patch` there. The exporter downloads ++the official `mobile_sam.pt` checkpoint and verifies SHA256 ++`6dbb90523a35330fedd7f1d3dfc66f995213d81b29a5ca8108dbcdd4e37d6c2f`. + +-For `--output-path ./mobilesam_point_ethos_u85_448.pte`, the script writes: +- +-- `mobilesam_point_ethos_u85_448.pte` - Ethos-U-ready ExecuTorch program. +-- `mobilesam_point_ethos_u85_448.json` - Export metadata. +-- `mobilesam_point_ethos_u85_448_metrics.json` - FP32/quantized mask +- agreement and optional reference-mask IoU. +-- `mobilesam_point_ethos_u85_448_delegation.txt` - Operator delegation +- summary. +-- `mobilesam_point_artifacts/` - Optional TOSA/Vela intermediate artifacts. +-- `mobilesam_point_debug/` - Optional per-sample masks, overlays, mismatch +- heatmaps, and mask summaries. +- +-## Interpreting the Debug Artifacts +- +-Each debug sample contains: +- +-- `input.png` - The resized RGB input used by the exported model. +-- `reference_mask.png` - Optional binary reference mask resized to the model +- output-mask size. +-- `fp32_mask.png` - Host-side FP32 model prediction. +-- `fp32_overlay.png` - Colored FP32 prediction blended over the input image. +-- `quantized_mask.png` - Host-side PT2E quantized prediction before lowering. +-- `quantized_overlay.png` - Colored quantized prediction blended over the input +- image. +-- `mismatch_heatmap.png` - Green for FP32/quantized agreement and red for +- mismatch. +-- `mask_summary.json` - Foreground/background pixel counts and FP32/quantized +- IoU. +- +-The runtime app thresholds mask logits on target, logs a mask hash and +-foreground/background counts, and can dump the thresholded mask as RLE chunks. +-Host-side debug masks are intentionally generated before lowering so users can +-inspect quantization quality without needing target-side image output. ++Neither source nor weights are redistributed in ExecuTorch. The source is ++Apache-2.0; the source repository and checkpoint can have separate terms, so ++the export metadata does not assign the source license to the checkpoint. + + ## Limitations + +-- The example freezes positive point prompts into the exported graph to keep +- the target app to a single image input. Changing the prompt requires +- re-exporting the `.pte`. +-- The export uses `multimask_output=False` and does not include candidate-mask +- argmax selection, upsampling, or thresholding in the graph. Keep those steps +- in host-side or target-side post-processing when comparing mask quality. +-- The runtime app logs a mask hash and foreground/background counts; it does +- not render a color image on target. ++- The `.pte` accepts one image tensor. Changing prompt coordinates requires ++ re-exporting. ++- The graph uses `multimask_output=False`; candidate-mask selection, upsampling, ++ and thresholding remain post-processing steps. ++- `448x448` is the smallest locally tested size that retained at least `0.95` ++ FP32/quantized IoU on the demonstration image. + - The first supported runtime target is Corstone-320/Ethos-U85-256. +-- Reduced input sizes require the patch applied by `prepare_mobilesam.py`; the +- MobileSAM source remains outside the ExecuTorch checkout. +-- The default `448x448` input is the smallest size in the local sweep that +- retained at least `0.95` host quantized/FP32 mask IoU on the demo image. +- `512x512` retained about `0.975` IoU, while smaller sizes were inconsistent. +-- MobileSAM PTQ is sensitive to the calibration set and the mask-logit +- threshold. Inspect `*_metrics.json` and the debug overlays before treating +- the quantized mask as an accuracy result. ++- Calibration uses the demonstration image to keep the example quick. Validate ++ with representative data before using the quantized model in a product. +diff --git a/examples/arm/mobilesam_prompt_segmentation_example_ethos_u/model_export/export_mobilesam.py b/examples/arm/mobilesam_prompt_segmentation_example_ethos_u/model_export/export_mobilesam.py +index e7c22518f0..60bcb44f8d 100644 +--- a/examples/arm/mobilesam_prompt_segmentation_example_ethos_u/model_export/export_mobilesam.py ++++ b/examples/arm/mobilesam_prompt_segmentation_example_ethos_u/model_export/export_mobilesam.py +@@ -10,7 +10,6 @@ import hashlib + import importlib + import inspect + import json +-import os + import sys + import urllib.error + import urllib.request +@@ -21,7 +20,6 @@ from typing import Any, cast + + import numpy as np + import torch +-import tqdm # type: ignore[import] + from executorch.backends.arm.common.pipeline_config import ( + ArmPassPipelineConfig, + SoftmaxDecompositionConfig, +@@ -38,6 +36,7 @@ from executorch.exir import ( + ExecutorchBackendConfig, + to_edge_transform_and_lower, + ) ++from executorch.exir.passes.quantize_io_pass import QuantizeInputs, QuantizeOutputs + from executorch.extension.export_util.utils import save_pte_program + from packaging.version import Version + from PIL import Image +@@ -49,6 +48,7 @@ from torchao.quantization.pt2e.quantize_pt2e import ( # type: ignore[import] + MOBILE_SAM_SOURCE_URL = "https://github.com/ChaoningZhang/MobileSAM" + MOBILE_SAM_SOURCE_REVISION = "f706ad9c4eb7f219c00d9050e46328518ffb65d2" + MOBILE_SAM_PATCH = "0001-Make-TinyViT-image-size-configurable.patch" ++MOBILE_SAM_SOURCE_LICENSE = "Apache-2.0" + DEFAULT_CHECKPOINT_FILENAME = "mobile_sam.pt" + DEFAULT_CHECKPOINT_URL = ( + f"{MOBILE_SAM_SOURCE_URL}/raw/{MOBILE_SAM_SOURCE_REVISION}/weights/" +@@ -57,10 +57,17 @@ DEFAULT_CHECKPOINT_URL = ( + DEFAULT_CHECKPOINT_SHA256 = ( + "6dbb90523a35330fedd7f1d3dfc66f995213d81b29a5ca8108dbcdd4e37d6c2f" + ) +-MOBILE_SAM_SOURCE_LICENSE = "Apache-2.0" ++ ++INPUT_SIZE = 448 ++MASK_THRESHOLD = 0.0 ++MINIMUM_FP32_QUANTIZED_IOU = 0.9 ++MINIMUM_FP32_QUANTIZED_PIXEL_AGREEMENT = 0.96 ++MINIMUM_MASK_QUALITY = 0.8 ++MINIMUM_VALIDATION_MASK_QUALITY = 0.9 ++TARGET = "ethos-u85-256" ++SYSTEM_CONFIG = "Ethos_U85_SYS_DRAM_Mid" ++MEMORY_MODE = "Dedicated_Sram_384KB" + MINIMUM_VELA_VERSION = Version("5.1.0") +-DEFAULT_INPUT_SIZE = 448 +-MOBILE_SAM_INPUT_ALIGNMENT = 16 + + + @dataclass +@@ -68,197 +75,80 @@ class PreparedSample: + name: str + image: Image.Image + pixel_values: torch.Tensor +- labels: torch.Tensor | None +- +- +-def load_mobile_sam( +- checkpoint_path: str, +- mobile_sam_source: str | None, +- input_size: int, +-) -> torch.nn.Module: +- mobile_sam_module = import_mobile_sam_module(mobile_sam_source) +- builder = mobile_sam_module.sam_model_registry["vit_t"] +- if "image_size" not in inspect.signature(builder).parameters: +- raise RuntimeError( +- "The MobileSAM checkout does not provide configurable image sizes. " +- "Run model_export/prepare_mobilesam.py and pass the prepared checkout " +- "with --mobile-sam-source." +- ) +- return builder(checkpoint=checkpoint_path, image_size=input_size).eval() +- +- +-class MobileSAMFixedPrompt(torch.nn.Module): +- image_encoder: Any +- mask_decoder: Any +- +- def __init__( +- self, +- sam: torch.nn.Module, +- point_prompts: list[tuple[float, float]], +- ) -> None: +- super().__init__() +- sam = cast(Any, sam) +- +- self.image_encoder = sam.image_encoder +- self.mask_decoder = sam.mask_decoder +- +- with torch.no_grad(): +- points = ( +- torch.tensor([point_prompts], dtype=torch.float32), +- torch.ones((1, len(point_prompts)), dtype=torch.int64), +- ) +- sparse_embeddings, dense_embeddings = sam.prompt_encoder( +- points=points, +- boxes=None, +- masks=None, +- ) +- image_pe = sam.prompt_encoder.get_dense_pe() + +- self.register_buffer("sparse_prompt_embeddings", sparse_embeddings) +- self.register_buffer("dense_prompt_embeddings", dense_embeddings) +- self.register_buffer("image_pe", image_pe) + +- def forward(self, pixel_values: torch.Tensor) -> torch.Tensor: +- image_embeddings = self.image_encoder(pixel_values) +- low_res_masks, _ = self.mask_decoder( +- image_embeddings=image_embeddings, +- image_pe=self.image_pe, +- sparse_prompt_embeddings=self.sparse_prompt_embeddings, +- dense_prompt_embeddings=self.dense_prompt_embeddings, +- multimask_output=False, +- ) +- return low_res_masks ++def default_source_dir() -> Path: ++ return ( ++ Path.home() ++ / ".cache" ++ / "executorch" ++ / "mobilesam" ++ / MOBILE_SAM_SOURCE_REVISION ++ / "source" ++ ) + + +-def parse_args() -> argparse.Namespace: ++def parse_args(argv: list[str] | None = None) -> argparse.Namespace: + parser = argparse.ArgumentParser( +- description="Export fixed-prompt MobileSAM segmentation for Ethos-U." +- ) +- parser.add_argument( +- "--checkpoint-path", +- default=None, +- help="Optional local MobileSAM checkpoint path.", +- ) +- parser.add_argument( +- "--mobile-sam-source", +- default=None, +- help=( +- "Optional MobileSAM checkout prepared by prepare_mobilesam.py. " +- "Used before importing the mobile_sam package." +- ), +- ) +- parser.add_argument( +- "--local-files-only", +- action="store_true", +- help="Load the checkpoint from the local cache only; do not download.", +- ) +- parser.add_argument( +- "--calibration-image", +- action="append", +- default=[], +- help="Local RGB image used for PTQ calibration. Can be repeated.", ++ description="Export fixed-prompt MobileSAM object segmentation for Ethos-U85." + ) + parser.add_argument( +- "--eval-image", +- action="append", +- default=[], +- help="Local RGB image used for validation/debugging. Can be repeated.", ++ "--output-path", ++ type=Path, ++ required=True, ++ help="Output path for the ExecuTorch .pte program.", + ) + parser.add_argument( +- "--eval-mask", +- action="append", +- default=[], +- help=( +- "Optional binary reference mask used for validation/debugging. " +- "Can be repeated and must match --eval-image count when provided." +- ), ++ "--image", ++ type=Path, ++ required=True, ++ help="RGB image used for calibration and host validation.", + ) + parser.add_argument( + "--point", + type=float, + nargs=2, + action="append", +- default=[], ++ required=True, + metavar=("X", "Y"), + help=( +- "Positive point prompt in the resized square input frame. " +- "Can be repeated for multi-point prompts." ++ "Positive point prompt in the square input frame. Repeat once per " ++ "independently segmented object." + ), + ) +- parser.add_argument( +- "--mask-threshold", +- type=float, +- default=0.0, +- help="Mask-logit threshold used for metrics and debug masks.", +- ) +- parser.add_argument( +- "--output-path", +- type=str, +- required=True, +- help="Path to save the exported ExecuTorch program.", +- ) + parser.add_argument( + "--input-size", + type=int, +- default=DEFAULT_INPUT_SIZE, +- help="Square MobileSAM input size. Must be divisible by 16.", +- ) +- parser.add_argument( +- "--num-calibration-samples", +- type=int, +- default=4, +- help="Number of local samples used for PTQ calibration.", +- ) +- parser.add_argument( +- "--num-eval-samples", +- type=int, +- default=4, +- help="Number of local samples used for host-side validation.", +- ) +- parser.add_argument( +- "--num-debug-samples", +- type=int, +- default=4, +- help="Number of validation samples written as visual debug artifacts.", ++ default=INPUT_SIZE, ++ help=f"Square model input size (default: {INPUT_SIZE}).", + ) + parser.add_argument( +- "--minimum-fp32-quantized-iou", +- type=float, +- default=None, +- help="Fail before lowering when host quantized/FP32 mask IoU is lower.", +- ) +- parser.add_argument( +- "--target", +- default="ethos-u85-256", +- help="Ethos-U target passed to Vela.", +- ) +- parser.add_argument( +- "--system-config", +- default="Ethos_U85_SYS_DRAM_Mid", +- help="Vela system configuration.", ++ "--mobile-sam-source", ++ type=Path, ++ default=default_source_dir(), ++ help="MobileSAM checkout prepared by prepare_mobilesam.py.", + ) + parser.add_argument( +- "--memory-mode", +- default="Dedicated_Sram_384KB", +- help="Vela memory mode.", ++ "--checkpoint-path", ++ type=Path, ++ help="Optional local MobileSAM checkpoint; otherwise use the pinned cache.", + ) + parser.add_argument( +- "--extra-vela-flag", +- action="append", +- default=[], +- help="Additional Vela flag. Can be provided multiple times.", ++ "--local-files-only", ++ action="store_true", ++ help="Require the checkpoint to be present in the local cache.", + ) + parser.add_argument( +- "--artifact-dir", +- default=None, +- help="Optional directory for intermediate TOSA/Vela artifacts.", ++ "--quantized-io", ++ action="store_true", ++ help="Expose int8 input/output tensors for the Alif E8 sample.", + ) + parser.add_argument( +- "--debug-output-dir", +- default=None, +- help="Optional directory for masks, overlays, and validation summaries.", ++ "--multimask-output", ++ action="store_true", ++ help="Return three candidate masks and quality scores for each point.", + ) +- return parser.parse_args() ++ return parser.parse_args(argv) + + + def write_json(path: Path, payload: dict[str, Any]) -> None: +@@ -283,24 +173,84 @@ def validate_vela_version() -> str: + return installed_version + + +-def import_mobile_sam_module(mobile_sam_source: str | None) -> Any: +- if mobile_sam_source is not None: +- sys.path.insert(0, str(Path(mobile_sam_source).expanduser().resolve())) ++def import_mobile_sam_module(source_dir: Path) -> Any: ++ source_dir = source_dir.expanduser().resolve() ++ if not source_dir.is_dir(): ++ raise FileNotFoundError( ++ f"Prepared MobileSAM source not found: {source_dir}. Run " ++ "model_export/prepare_mobilesam.py first." ++ ) ++ sys.path.insert(0, str(source_dir)) + try: + return importlib.import_module("mobile_sam") + except ImportError as error: +- raise ImportError( +- "Could not import the patched mobile_sam package. Run " +- "model_export/prepare_mobilesam.py and pass its checkout with " +- "--mobile-sam-source." +- ) from error ++ raise ImportError(f"Could not import mobile_sam from {source_dir}.") from error + + +-def find_module_type(module: torch.nn.Module, class_name: str) -> type[torch.nn.Module]: +- for child in module.modules(): +- if child.__class__.__name__ == class_name: +- return child.__class__ +- raise ValueError(f"Could not find module type {class_name} in {module.__class__}.") ++def load_mobile_sam( ++ checkpoint_path: Path, ++ source_dir: Path, ++ image_size: int = INPUT_SIZE, ++) -> torch.nn.Module: ++ mobile_sam_module = import_mobile_sam_module(source_dir) ++ builder = mobile_sam_module.sam_model_registry["vit_t"] ++ if "image_size" not in inspect.signature(builder).parameters: ++ raise RuntimeError( ++ "The MobileSAM checkout does not support configurable image sizes. " ++ "Prepare it with model_export/prepare_mobilesam.py." ++ ) ++ return builder( ++ checkpoint=str(checkpoint_path), ++ image_size=image_size, ++ ).eval() ++ ++ ++class MobileSAMFixedPrompt(torch.nn.Module): ++ image_encoder: Any ++ mask_decoder: Any ++ ++ def __init__( ++ self, ++ sam: torch.nn.Module, ++ point_prompts: list[tuple[float, float]], ++ multimask_output: bool = False, ++ ) -> None: ++ super().__init__() ++ sam = cast(Any, sam) ++ self.image_encoder = sam.image_encoder ++ self.mask_decoder = sam.mask_decoder ++ self.multimask_output = multimask_output ++ ++ with torch.no_grad(): ++ points = ( ++ torch.tensor(point_prompts, dtype=torch.float32).unsqueeze(1), ++ torch.ones((len(point_prompts), 1), dtype=torch.int64), ++ ) ++ sparse_embeddings, dense_embeddings = sam.prompt_encoder( ++ points=points, ++ boxes=None, ++ masks=None, ++ ) ++ image_pe = sam.prompt_encoder.get_dense_pe() ++ ++ self.register_buffer("sparse_prompt_embeddings", sparse_embeddings) ++ self.register_buffer("dense_prompt_embeddings", dense_embeddings) ++ self.register_buffer("image_pe", image_pe) ++ ++ def forward( ++ self, pixel_values: torch.Tensor ++ ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]: ++ image_embeddings = self.image_encoder(pixel_values) ++ low_res_masks, quality_scores = self.mask_decoder( ++ image_embeddings=image_embeddings, ++ image_pe=self.image_pe, ++ sparse_prompt_embeddings=self.sparse_prompt_embeddings, ++ dense_prompt_embeddings=self.dense_prompt_embeddings, ++ multimask_output=self.multimask_output, ++ ) ++ if self.multimask_output: ++ return low_res_masks, quality_scores ++ return low_res_masks + + + def file_sha256(path: Path) -> str: +@@ -311,27 +261,26 @@ def file_sha256(path: Path) -> str: + return digest.hexdigest() + + +-def default_checkpoint_cache_dir() -> Path: ++def default_checkpoint_path() -> Path: + return ( +- Path.home() / ".cache" / "executorch" / "mobilesam" / MOBILE_SAM_SOURCE_REVISION ++ Path.home() ++ / ".cache" ++ / "executorch" ++ / "mobilesam" ++ / MOBILE_SAM_SOURCE_REVISION ++ / DEFAULT_CHECKPOINT_FILENAME + ) + + +-def verify_checkpoint(path: Path, expected_sha256: str) -> None: +- actual_sha256 = file_sha256(path) +- if actual_sha256 != expected_sha256: +- raise RuntimeError( +- f"Checkpoint SHA256 mismatch for {path}: expected {expected_sha256}, " +- f"got {actual_sha256}." +- ) +- +- +-def download_checkpoint(url: str, path: Path) -> None: ++def download_checkpoint(path: Path) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + temp_path = path.with_suffix(path.suffix + ".tmp") + try: + with ( +- urllib.request.urlopen(url, timeout=60) as response, # nosec B310 ++ urllib.request.urlopen( # nosec B310 ++ DEFAULT_CHECKPOINT_URL, ++ timeout=60, ++ ) as response, + temp_path.open("wb") as file, + ): + for chunk in iter(lambda: response.read(1024 * 1024), b""): +@@ -340,46 +289,50 @@ def download_checkpoint(url: str, path: Path) -> None: + except (OSError, urllib.error.URLError) as error: + temp_path.unlink(missing_ok=True) + raise RuntimeError( +- f"Failed to download MobileSAM checkpoint from {url}." ++ f"Failed to download MobileSAM checkpoint from {DEFAULT_CHECKPOINT_URL}." + ) from error + + + def resolve_checkpoint( +- args: argparse.Namespace, +-) -> tuple[str, str | None, str | None]: +- if args.checkpoint_path is not None: +- checkpoint_path = Path(args.checkpoint_path).expanduser().resolve() +- if not checkpoint_path.is_file(): +- raise FileNotFoundError(f"Checkpoint not found: {checkpoint_path}") +- return str(checkpoint_path), None, None +- +- checkpoint_path = default_checkpoint_cache_dir() / DEFAULT_CHECKPOINT_FILENAME +- if not checkpoint_path.is_file(): +- if args.local_files_only: ++ checkpoint_path: Path | None, ++ *, ++ local_files_only: bool, ++) -> tuple[Path, str | None, str]: ++ if checkpoint_path is not None: ++ resolved_path = checkpoint_path.expanduser().resolve() ++ if not resolved_path.is_file(): ++ raise FileNotFoundError(f"Checkpoint not found: {resolved_path}") ++ return resolved_path, None, file_sha256(resolved_path) ++ ++ resolved_path = default_checkpoint_path() ++ if not resolved_path.is_file(): ++ if local_files_only: + raise FileNotFoundError( +- f"Checkpoint not found in local cache: {checkpoint_path}" ++ f"Checkpoint not found in local cache: {resolved_path}" + ) +- download_checkpoint(DEFAULT_CHECKPOINT_URL, checkpoint_path) ++ download_checkpoint(resolved_path) + +- verify_checkpoint(checkpoint_path, DEFAULT_CHECKPOINT_SHA256) +- return ( +- str(checkpoint_path.resolve()), +- DEFAULT_CHECKPOINT_URL, +- DEFAULT_CHECKPOINT_SHA256, +- ) ++ actual_sha256 = file_sha256(resolved_path) ++ if actual_sha256 != DEFAULT_CHECKPOINT_SHA256: ++ raise RuntimeError( ++ f"Checkpoint SHA256 mismatch for {resolved_path}: expected " ++ f"{DEFAULT_CHECKPOINT_SHA256}, got {actual_sha256}." ++ ) ++ return resolved_path.resolve(), DEFAULT_CHECKPOINT_URL, actual_sha256 + + +-def preprocess_image( +- image: Image.Image, +- segmentation_map: Image.Image | None, +- *, +- name: str, +- input_size: int, ++def prepare_sample( ++ image_path: Path, + pixel_mean: np.ndarray, + pixel_std: np.ndarray, +- output_mask_size: tuple[int, int] | None, ++ input_size: int = INPUT_SIZE, + ) -> PreparedSample: +- rgb_image = image.convert("RGB") ++ image_path = image_path.expanduser().resolve() ++ if not image_path.is_file(): ++ raise FileNotFoundError(f"Input image not found: {image_path}") ++ ++ with Image.open(image_path) as input_image: ++ rgb_image = input_image.convert("RGB") + width, height = rgb_image.size + scale = input_size / max(height, width) + resized_size = (round(width * scale), round(height * scale)) +@@ -393,101 +346,53 @@ def preprocess_image( + pixel_values = torch.from_numpy(image_np).permute(2, 0, 1).unsqueeze(0) + pixel_values = torch.nn.functional.pad( + pixel_values, +- ( +- 0, +- input_size - resized_size[0], +- 0, +- input_size - resized_size[1], +- ), ++ (0, input_size - resized_size[0], 0, input_size - resized_size[1]), + ) +- pixel_values = pixel_values.contiguous() +- +- labels = None +- if segmentation_map is not None: +- if output_mask_size is None: +- raise ValueError("An output mask size is required for reference masks.") +- resized_mask = segmentation_map.convert("L").resize( +- resized_size, +- Image.Resampling.NEAREST, +- ) +- padded_mask = Image.new("L", (input_size, input_size)) +- padded_mask.paste(resized_mask, (0, 0)) +- resized_mask = padded_mask.resize(output_mask_size, Image.Resampling.NEAREST) +- mask_np = np.asarray(resized_mask, dtype=np.uint8) +- labels = torch.from_numpy((mask_np > 0).astype(np.uint8)).to(torch.long) +- + return PreparedSample( +- name=name, ++ name=image_path.stem, + image=padded_image, +- pixel_values=pixel_values, +- labels=labels, ++ pixel_values=pixel_values.contiguous(), + ) + + +-def load_local_samples( +- image_paths: list[str], +- mask_paths: list[str], +- limit: int, +- *, +- include_labels: bool, +- input_size: int, +- pixel_mean: np.ndarray, +- pixel_std: np.ndarray, +- output_mask_size: tuple[int, int] | None = None, +-) -> list[PreparedSample]: +- if include_labels and len(mask_paths) not in (0, len(image_paths)): +- raise ValueError("--eval-mask must be omitted or match --eval-image count.") +- +- samples: list[PreparedSample] = [] +- for index, image_path in enumerate(image_paths): +- if len(samples) >= limit: +- break +- image = Image.open(image_path) +- mask = None +- if include_labels and len(mask_paths) > 0: +- mask = Image.open(mask_paths[index]) +- samples.append( +- preprocess_image( +- image, +- mask, +- name=Path(image_path).stem or f"local_sample_{index:04d}", +- input_size=input_size, +- pixel_mean=pixel_mean, +- pixel_std=pixel_std, +- output_mask_size=output_mask_size, +- ) +- ) +- if len(samples) == 0: +- raise ValueError("No local samples were loaded.") +- return samples ++def run_model_outputs( ++ model: torch.nn.Module, input_tensor: torch.Tensor ++) -> tuple[torch.Tensor, torch.Tensor | None]: ++ output = model(input_tensor) ++ if isinstance(output, (tuple, list)): ++ if len(output) != 2 or not all( ++ isinstance(value, torch.Tensor) for value in output ++ ): ++ raise TypeError("Expected mask logits and quality score tensors.") ++ return output[0], output[1] ++ if isinstance(output, torch.Tensor): ++ return output, None ++ raise TypeError(f"Expected tensor model output, got {type(output)}") + + + def run_mask_logits(model: torch.nn.Module, input_tensor: torch.Tensor) -> torch.Tensor: +- output = model(input_tensor) +- if isinstance(output, (tuple, list)): +- output = output[0] +- if not isinstance(output, torch.Tensor): +- raise TypeError(f"Expected tensor mask logits, got {type(output)}") +- return output ++ return run_model_outputs(model, input_tensor)[0] + + +-def predict_mask(logits: torch.Tensor, threshold: float) -> np.ndarray: +- mask = logits.detach().cpu().squeeze(0).squeeze(0).numpy() > threshold +- return mask.astype(np.uint8) ++def predict_masks(logits: torch.Tensor) -> np.ndarray: ++ if logits.ndim != 4: ++ raise ValueError( ++ "Expected mask logits with shape [points, candidates, H, W], " ++ f"got {logits.shape}." ++ ) ++ masks = logits.detach().cpu().numpy() > MASK_THRESHOLD ++ masks = masks.reshape(-1, masks.shape[-2], masks.shape[-1]) ++ return masks.astype(np.uint8) + + + def binary_iou(mask_a: np.ndarray, mask_b: np.ndarray) -> float: + intersection = np.logical_and(mask_a, mask_b).sum() + union = np.logical_or(mask_a, mask_b).sum() +- if union == 0: +- return 1.0 +- return float(intersection / union) ++ return 1.0 if union == 0 else float(intersection / union) + + +-def save_binary_mask(path: Path, mask: np.ndarray) -> Image.Image: +- image = Image.fromarray((mask.astype(np.uint8) * 255), mode="L") +- image.save(path) +- return image.convert("RGB") ++def save_binary_mask(path: Path, mask: np.ndarray) -> None: ++ Image.fromarray(mask.astype(np.uint8) * 255, mode="L").save(path) + + + def save_mask_overlay( +@@ -496,182 +401,176 @@ def save_mask_overlay( + mask: np.ndarray, + color: tuple[int, int, int], + ) -> None: +- rgb_image = image.convert("RGB") +- resized_mask = Image.fromarray(mask.astype(np.uint8) * 255, mode="L").resize( +- rgb_image.size, ++ resized_mask = Image.fromarray(mask * 255, mode="L").resize( ++ image.size, + Image.Resampling.NEAREST, + ) + mask_np = np.asarray(resized_mask, dtype=np.uint8) > 0 +- overlay_np = np.asarray(rgb_image, dtype=np.float32) +- color_np = np.asarray(color, dtype=np.float32) +- overlay_np[mask_np] = overlay_np[mask_np] * 0.55 + color_np * 0.45 +- Image.fromarray(np.clip(overlay_np, 0, 255).astype(np.uint8), mode="RGB").save(path) ++ overlay_np = np.asarray(image.convert("RGB"), dtype=np.float32) ++ overlay_np[mask_np] = ( ++ overlay_np[mask_np] * 0.55 + np.asarray(color, dtype=np.float32) * 0.45 ++ ) ++ Image.fromarray(overlay_np.astype(np.uint8), mode="RGB").save(path) + + +-def write_debug_artifacts( +- debug_dir: Path, ++def evaluate_and_write_debug_artifacts( ++ fp32_model: torch.nn.Module, ++ quantized_model: torch.nn.Module, + sample: PreparedSample, +- fp32_mask: np.ndarray, +- quantized_mask: np.ndarray, +-) -> None: ++ debug_dir: Path, ++) -> dict[str, Any]: ++ fp32_logits, fp32_quality = run_model_outputs(fp32_model, sample.pixel_values) ++ quantized_logits, quantized_quality = run_model_outputs( ++ quantized_model, sample.pixel_values ++ ) ++ fp32_masks = predict_masks(fp32_logits) ++ quantized_masks = predict_masks(quantized_logits) ++ if fp32_masks.shape != quantized_masks.shape: ++ raise ValueError( ++ f"FP32 masks {fp32_masks.shape} do not match quantized masks " ++ f"{quantized_masks.shape}." ++ ) ++ ious = [ ++ binary_iou(fp32_mask, quantized_mask) ++ for fp32_mask, quantized_mask in zip(fp32_masks, quantized_masks) ++ ] ++ agreements = [ ++ float(np.mean(fp32_mask == quantized_mask)) ++ for fp32_mask, quantized_mask in zip(fp32_masks, quantized_masks) ++ ] ++ metrics = { ++ "fp32_quantized_iou": float(np.mean(ious)), ++ "fp32_quantized_pixel_agreement": float(np.mean(agreements)), ++ "minimum_per_object_fp32_quantized_iou": min(ious), ++ "minimum_per_object_fp32_quantized_pixel_agreement": min(agreements), ++ "per_object_fp32_quantized_iou": ious, ++ "per_object_fp32_quantized_pixel_agreement": agreements, ++ } ++ if fp32_quality is not None and quantized_quality is not None: ++ fp32_quality_np = fp32_quality.detach().cpu().numpy() ++ quantized_quality_np = quantized_quality.detach().cpu().numpy() ++ fp32_grouped = fp32_masks.reshape( ++ fp32_logits.shape[0], fp32_logits.shape[1], *fp32_masks.shape[-2:] ++ ) ++ quantized_grouped = quantized_masks.reshape( ++ quantized_logits.shape[0], ++ quantized_logits.shape[1], ++ *quantized_masks.shape[-2:], ++ ) ++ fp32_best = fp32_quality_np.argmax(axis=1) ++ quantized_best = quantized_quality_np.argmax(axis=1) ++ selected_ious = [] ++ selected_agreements = [] ++ accepted_points = [] ++ for point in range(fp32_grouped.shape[0]): ++ fp32_mask = fp32_grouped[point, fp32_best[point]] ++ quantized_mask = quantized_grouped[point, quantized_best[point]] ++ selected_ious.append(binary_iou(fp32_mask, quantized_mask)) ++ selected_agreements.append(float(np.mean(fp32_mask == quantized_mask))) ++ if ( ++ quantized_quality_np[point, quantized_best[point]] ++ >= MINIMUM_VALIDATION_MASK_QUALITY ++ ): ++ accepted_points.append(point) ++ quality_gate_points = accepted_points or list(range(fp32_grouped.shape[0])) ++ metrics.update( ++ { ++ "fp32_quality_scores": fp32_quality_np.tolist(), ++ "quantized_quality_scores": quantized_quality_np.tolist(), ++ "fp32_best_candidate": fp32_best.tolist(), ++ "quantized_best_candidate": quantized_best.tolist(), ++ "quality_gate_points": quality_gate_points, ++ "selected_fp32_quantized_iou": float( ++ np.mean([selected_ious[point] for point in quality_gate_points]) ++ ), ++ "selected_fp32_quantized_pixel_agreement": float( ++ np.mean( ++ [selected_agreements[point] for point in quality_gate_points] ++ ) ++ ), ++ "per_point_selected_fp32_quantized_iou": selected_ious, ++ "per_point_selected_fp32_quantized_pixel_agreement": ( ++ selected_agreements ++ ), ++ } ++ ) ++ + sample_dir = debug_dir / sample.name + sample_dir.mkdir(parents=True, exist_ok=True) +- + sample.image.save(sample_dir / "input.png") +- save_binary_mask(sample_dir / "fp32_mask.png", fp32_mask) +- save_binary_mask(sample_dir / "quantized_mask.png", quantized_mask) +- save_mask_overlay( +- sample_dir / "fp32_overlay.png", +- sample.image, +- fp32_mask, +- (0, 220, 120), +- ) +- save_mask_overlay( +- sample_dir / "quantized_overlay.png", +- sample.image, +- quantized_mask, +- (0, 170, 255), +- ) +- if sample.labels is not None: ++ for object_index, (fp32_mask, quantized_mask) in enumerate( ++ zip(fp32_masks, quantized_masks), start=1 ++ ): ++ save_binary_mask(sample_dir / f"fp32_mask_{object_index}.png", fp32_mask) + save_binary_mask( +- sample_dir / "reference_mask.png", +- sample.labels.detach().cpu().numpy().astype(np.uint8), ++ sample_dir / f"quantized_mask_{object_index}.png", quantized_mask + ) +- +- mismatch = fp32_mask != quantized_mask +- heatmap = np.zeros((*fp32_mask.shape, 3), dtype=np.uint8) +- heatmap[~mismatch] = [0, 128, 0] +- heatmap[mismatch] = [255, 0, 0] +- Image.fromarray(heatmap, mode="RGB").save(sample_dir / "mismatch_heatmap.png") +- +- write_json( +- sample_dir / "mask_summary.json", +- { +- "foreground_pixels": int(quantized_mask.sum()), +- "background_pixels": int(quantized_mask.size - quantized_mask.sum()), +- "fp32_quantized_iou": binary_iou(fp32_mask, quantized_mask), +- }, +- ) +- +- +-def evaluate_and_debug( +- fp32_model: torch.nn.Module, +- quantized_model: torch.nn.Module, +- eval_samples: list[PreparedSample], +- debug_dir: Path | None, +- num_debug_samples: int, +- threshold: float, +-) -> dict[str, float]: +- fp32_quantized_ious: list[float] = [] +- fp32_quantized_pixel_agreements: list[float] = [] +- reference_ious: list[float] = [] +- +- if debug_dir is not None: +- debug_dir.mkdir(parents=True, exist_ok=True) +- +- print("\nEvaluating quantized MobileSAM on validation samples...") +- for index, sample in enumerate(tqdm.tqdm(eval_samples)): +- fp32_logits = run_mask_logits(fp32_model, sample.pixel_values) +- quantized_logits = run_mask_logits(quantized_model, sample.pixel_values) +- fp32_mask = predict_mask(fp32_logits, threshold) +- quantized_mask = predict_mask(quantized_logits, threshold) +- +- fp32_quantized_ious.append(binary_iou(fp32_mask, quantized_mask)) +- fp32_quantized_pixel_agreements.append( +- float(np.mean(fp32_mask == quantized_mask)) ++ save_mask_overlay( ++ sample_dir / f"fp32_overlay_{object_index}.png", ++ sample.image, ++ fp32_mask, ++ (0, 220, 120), ++ ) ++ save_mask_overlay( ++ sample_dir / f"quantized_overlay_{object_index}.png", ++ sample.image, ++ quantized_mask, ++ (0, 170, 255), + ) +- if sample.labels is not None: +- labels = sample.labels.detach().cpu().numpy().astype(np.uint8) +- reference_ious.append(binary_iou(quantized_mask, labels)) +- +- if debug_dir is not None and index < num_debug_samples: +- write_debug_artifacts(debug_dir, sample, fp32_mask, quantized_mask) +- +- metrics = { +- "num_samples": float(len(eval_samples)), +- "fp32_quantized_mean_iou": float(np.mean(fp32_quantized_ious)), +- "fp32_quantized_pixel_agreement": float( +- np.mean(fp32_quantized_pixel_agreements) +- ), +- } +- if len(reference_ious) > 0: +- metrics["reference_mean_iou"] = float(np.mean(reference_ious)) + return metrics + + + def quantize_model( + model: torch.nn.Module, + quantizer: EthosUQuantizer, +- example_inputs: tuple[torch.Tensor], +- calibration_samples: list[PreparedSample], ++ input_tensor: torch.Tensor, + ) -> torch.export.ExportedProgram: +- exported = torch.export.export(model, example_inputs) ++ exported = torch.export.export(model, (input_tensor,)) + prepared = prepare_pt2e(exported.module(), quantizer) +- +- print("\nCalibrating MobileSAM...") +- for sample in tqdm.tqdm(calibration_samples): +- prepared(sample.pixel_values) +- ++ print("Calibrating MobileSAM with the demonstration image...") ++ prepared(input_tensor) + quantized = convert_pt2e(prepared) +- return torch.export.export(quantized, example_inputs) ++ return torch.export.export(quantized, (input_tensor,)) + + +-def has_quantized_out_variants() -> bool: ++def ensure_quantized_ops_loaded() -> None: ++ try: ++ import executorch.kernels.quantized # noqa: F401 ++ except ImportError as error: ++ raise RuntimeError( ++ "MobileSAM export requires ExecuTorch quantized kernels. Install the " ++ "current checkout with ./install_executorch.sh." ++ ) from error ++ + try: + _ = torch.ops.quantized_decomposed.quantize_per_tensor.out + _ = torch.ops.quantized_decomposed.dequantize_per_tensor.out +- return True +- except AttributeError: +- return False +- +- +-def load_quantized_ops_library(library_path: Path) -> Path: +- if not library_path.is_file(): +- raise FileNotFoundError(f"Quantized ops library not found: {library_path}") +- torch.ops.load_library(str(library_path)) +- if has_quantized_out_variants(): +- return library_path +- raise RuntimeError( +- f"Quantized ops library did not register required out variants: {library_path}" +- ) ++ except AttributeError as error: ++ raise RuntimeError( ++ "The ExecuTorch installation does not provide quantized out variants. " ++ "Re-run ./install_executorch.sh." ++ ) from error + + +-def ensure_quantized_ops_loaded() -> Path | None: +- if has_quantized_out_variants(): +- return None ++def find_module_type(module: torch.nn.Module, class_name: str) -> type[torch.nn.Module]: ++ for child in module.modules(): ++ if child.__class__.__name__ == class_name: ++ return child.__class__ ++ raise ValueError(f"Could not find module type {class_name} in {module.__class__}.") + +- quantized_ops_library = os.environ.get("EXECUTORCH_QUANTIZED_OPS_AOT_LIBRARY") +- if quantized_ops_library: +- return load_quantized_ops_library( +- Path(quantized_ops_library).expanduser().resolve() +- ) + +- try: +- import executorch.kernels.quantized # noqa: F401 +- except ImportError: +- pass +- else: +- if has_quantized_out_variants(): +- return None +- +- repo_root = Path(__file__).resolve().parents[4] +- search_patterns = ( +- "cmake-out/kernels/quantized/libquantized_ops_aot_lib.*", +- "arm_test/*/kernels/quantized/libquantized_ops_aot_lib.*", +- "arm_test/**/kernels/quantized/libquantized_ops_aot_lib.*", +- ) +- for pattern in search_patterns: +- for candidate in sorted(repo_root.glob(pattern)): +- if not candidate.is_file(): +- continue +- return load_quantized_ops_library(candidate) +- +- raise RuntimeError( +- "MobileSAM int8 export requires the quantized ops out-variant library. " +- "Build or install ExecuTorch quantized kernels so that " +- "`quantized_decomposed::quantize_per_tensor.out` and " +- "`quantized_decomposed::dequantize_per_tensor.out` are available." +- ) ++def resolve_point_prompts( ++ points: list[list[float]], input_size: int = INPUT_SIZE ++) -> list[tuple[float, float]]: ++ point_prompts = [(float(x), float(y)) for x, y in points] ++ if any( ++ not (0 <= point_x < input_size and 0 <= point_y < input_size) ++ for point_x, point_y in point_prompts ++ ): ++ raise ValueError( ++ f"Point prompts must be inside the {input_size}x{input_size} input." ++ ) ++ return point_prompts + + + def write_delegation_report(edge_program_manager: Any, report_path: Path) -> None: +@@ -681,113 +580,69 @@ def write_delegation_report(edge_program_manager: Any, report_path: Path) -> Non + report_path.write_text(delegation_info.get_summary() + "\n") + + +-def resolve_point_prompts(args: argparse.Namespace) -> list[tuple[float, float]]: +- if len(args.point) > 0: +- point_prompts = [(float(x), float(y)) for x, y in args.point] +- else: +- point_prompts = [(args.input_size / 2, args.input_size / 2)] +- +- for point_x, point_y in point_prompts: +- if not (0 <= point_x <= args.input_size and 0 <= point_y <= args.input_size): +- raise ValueError("Point prompts must be inside the square input.") +- return point_prompts +- +- +-def validate_export_args(args: argparse.Namespace) -> None: +- if args.input_size < 224 or args.input_size % MOBILE_SAM_INPUT_ALIGNMENT != 0: +- raise ValueError("--input-size must be at least 224 and divisible by 16.") +- if args.num_calibration_samples <= 0: +- raise ValueError("--num-calibration-samples must be positive.") +- if args.num_eval_samples <= 0: +- raise ValueError("--num-eval-samples must be positive.") +- if args.minimum_fp32_quantized_iou is not None and not ( +- 0.0 <= args.minimum_fp32_quantized_iou <= 1.0 +- ): +- raise ValueError("--minimum-fp32-quantized-iou must be between 0 and 1.") +- if len(args.calibration_image) == 0: +- raise ValueError("At least one --calibration-image is required.") +- if len(args.eval_image) == 0: +- args.eval_image = list(args.calibration_image) +- if len(args.eval_mask) not in (0, len(args.eval_image)): +- raise ValueError("--eval-mask must be omitted or match --eval-image count.") +- +- +-def main() -> None: +- args = parse_args() +- validate_export_args(args) ++def main(argv: list[str] | None = None) -> None: ++ args = parse_args(argv) ++ if args.input_size < 128 or args.input_size % 32 != 0: ++ raise ValueError("--input-size must be at least 128 and divisible by 32.") + vela_version = validate_vela_version() +- point_prompts = resolve_point_prompts(args) +- quantized_ops_library = ensure_quantized_ops_loaded() +- if quantized_ops_library is not None: +- print(f"Loaded quantized ops library from {quantized_ops_library}") ++ ensure_quantized_ops_loaded() ++ point_prompts = resolve_point_prompts(args.point, args.input_size) + +- output_path = Path(args.output_path).resolve() ++ output_path = args.output_path.expanduser().resolve() ++ if output_path.suffix != ".pte": ++ raise ValueError("--output-path must end in .pte.") + output_path.parent.mkdir(parents=True, exist_ok=True) + metadata_path = output_path.with_suffix(".json") + metrics_path = output_path.with_name(f"{output_path.stem}_metrics.json") + delegation_path = output_path.with_name(f"{output_path.stem}_delegation.txt") +- debug_dir = Path(args.debug_output_dir).resolve() if args.debug_output_dir else None ++ artifact_dir = output_path.parent / "artifacts" ++ debug_dir = output_path.parent / "debug" ++ artifact_dir.mkdir(exist_ok=True) + +- checkpoint_path, checkpoint_url, checkpoint_sha256 = resolve_checkpoint(args) ++ checkpoint_path, checkpoint_url, checkpoint_sha256 = resolve_checkpoint( ++ args.checkpoint_path, ++ local_files_only=args.local_files_only, ++ ) + mobile_sam = load_mobile_sam( +- checkpoint_path, +- args.mobile_sam_source, +- args.input_size, ++ checkpoint_path, args.mobile_sam_source, args.input_size + ) + pixel_mean = cast(Any, mobile_sam).pixel_mean.detach().cpu().reshape(-1).numpy() + pixel_std = cast(Any, mobile_sam).pixel_std.detach().cpu().reshape(-1).numpy() + if pixel_mean.shape != (3,) or pixel_std.shape != (3,): + raise ValueError("MobileSAM preprocessing must provide three RGB values.") +- wrapped_model = MobileSAMFixedPrompt(mobile_sam, point_prompts).eval() +- +- calibration_samples = load_local_samples( +- args.calibration_image, +- [], +- args.num_calibration_samples, +- include_labels=False, +- input_size=args.input_size, +- pixel_mean=pixel_mean, +- pixel_std=pixel_std, +- ) +- example_inputs = (calibration_samples[0].pixel_values,) ++ ++ sample = prepare_sample(args.image, pixel_mean, pixel_std, args.input_size) ++ wrapped_model = MobileSAMFixedPrompt( ++ mobile_sam, point_prompts, args.multimask_output ++ ).eval() + with torch.no_grad(): +- output_shape = list(run_mask_logits(wrapped_model, example_inputs[0]).shape) +- if len(output_shape) != 4 or output_shape[:2] != [1, 1]: ++ output_shape = list(run_mask_logits(wrapped_model, sample.pixel_values).shape) ++ if ( ++ len(output_shape) != 4 ++ or output_shape[0] != len(point_prompts) ++ or output_shape[1] != (3 if args.multimask_output else 1) ++ ): + raise ValueError( +- f"Expected MobileSAM output shape [1, 1, height, width], got {output_shape}." ++ "Expected MobileSAM output shape " ++ f"[{len(point_prompts)}, " ++ f"{3 if args.multimask_output else 1}, height, width], got " ++ f"{output_shape}." + ) +- output_mask_size = (output_shape[3], output_shape[2]) +- +- eval_samples = load_local_samples( +- args.eval_image, +- args.eval_mask, +- args.num_eval_samples, +- include_labels=True, +- input_size=args.input_size, +- pixel_mean=pixel_mean, +- pixel_std=pixel_std, +- output_mask_size=output_mask_size, +- ) + + compile_spec = EthosUCompileSpec( +- target=args.target, +- system_config=args.system_config, +- memory_mode=args.memory_mode, +- extra_flags=args.extra_vela_flag, ++ target=TARGET, ++ system_config=SYSTEM_CONFIG, ++ memory_mode=MEMORY_MODE, + ) + compile_spec.set_pass_pipeline_config( + ArmPassPipelineConfig(softmax=SoftmaxDecompositionConfig.STABLE) + ) +- if args.artifact_dir is not None: +- artifact_dir = Path(args.artifact_dir).resolve() +- artifact_dir.mkdir(parents=True, exist_ok=True) +- compile_spec.dump_intermediate_artifacts_to(str(artifact_dir)) ++ compile_spec.dump_intermediate_artifacts_to(str(artifact_dir)) + + quantizer = EthosUQuantizer(compile_spec) + quantizer.set_global(get_symmetric_quantization_config()) +- attention_module_type = find_module_type(wrapped_model.image_encoder, "Attention") + quantizer.set_module_type( +- attention_module_type, ++ find_module_type(wrapped_model.image_encoder, "Attention"), + get_symmetric_a16w8_quantization_config(), + ) + +@@ -795,34 +650,37 @@ def main() -> None: + quantized_program = quantize_model( + wrapped_model, + quantizer, +- example_inputs, +- calibration_samples, ++ sample.pixel_values, + ) +- quantized_module = quantized_program.module() +- metrics = evaluate_and_debug( ++ metrics = evaluate_and_write_debug_artifacts( + wrapped_model, +- quantized_module, +- eval_samples, ++ quantized_program.module(), ++ sample, + debug_dir, +- args.num_debug_samples, +- args.mask_threshold, + ) +- write_json(metrics_path, metrics) +- print( +- "Validation metrics: " +- f"fp32_quantized_mean_iou={metrics['fp32_quantized_mean_iou']:.4f} " +- "fp32_quantized_pixel_agreement=" +- f"{metrics['fp32_quantized_pixel_agreement']:.4f}" ++ write_json(metrics_path, metrics) ++ print( ++ "Validation: " ++ f"FP32/quantized IoU={metrics['fp32_quantized_iou']:.4f}, " ++ f"agreement={metrics['fp32_quantized_pixel_agreement']:.4f}" ++ ) ++ validation_iou = metrics.get( ++ "selected_fp32_quantized_iou", metrics["fp32_quantized_iou"] ++ ) ++ validation_agreement = metrics.get( ++ "selected_fp32_quantized_pixel_agreement", ++ metrics["fp32_quantized_pixel_agreement"], ++ ) ++ if ( ++ validation_iou < MINIMUM_FP32_QUANTIZED_IOU ++ and validation_agreement < MINIMUM_FP32_QUANTIZED_PIXEL_AGREEMENT ++ ): ++ raise RuntimeError( ++ f"Selected FP32/quantized mask IoU {validation_iou:.4f} is below " ++ f"the required {MINIMUM_FP32_QUANTIZED_IOU:.4f}, and pixel agreement " ++ f"{validation_agreement:.4f} is below " ++ f"{MINIMUM_FP32_QUANTIZED_PIXEL_AGREEMENT:.4f}." + ) +- if ( +- args.minimum_fp32_quantized_iou is not None +- and metrics["fp32_quantized_mean_iou"] < args.minimum_fp32_quantized_iou +- ): +- raise RuntimeError( +- "Host quantized/FP32 mask IoU " +- f"{metrics['fp32_quantized_mean_iou']:.4f} is below " +- f"{args.minimum_fp32_quantized_iou:.4f}." +- ) + + edge_program_manager = to_edge_transform_and_lower( + programs=quantized_program, +@@ -831,6 +689,33 @@ def main() -> None: + ) + write_delegation_report(edge_program_manager, delegation_path) + ++ io_quantization = None ++ if args.quantized_io: ++ quantize_inputs = QuantizeInputs(edge_program_manager, [0]) ++ output_indices = [0, 1] if args.multimask_output else [0] ++ quantize_outputs = QuantizeOutputs(edge_program_manager, output_indices) ++ edge_program_manager = edge_program_manager.transform( ++ [quantize_inputs, quantize_outputs] ++ ) ++ ++ def serialize_qparams(values: tuple[Any, ...]) -> dict[str, Any]: ++ return { ++ "scale": float(values[0]), ++ "zero_point": int(values[1]), ++ "quant_min": int(values[2]), ++ "quant_max": int(values[3]), ++ "dtype": str(values[4]).removeprefix("torch."), ++ } ++ ++ io_quantization = { ++ "input": serialize_qparams(quantize_inputs.quant_args[0]), ++ "output": serialize_qparams(quantize_outputs.dequant_args[0]), ++ } ++ if args.multimask_output: ++ io_quantization["quality"] = serialize_qparams( ++ quantize_outputs.dequant_args[1] ++ ) ++ + executorch_program_manager = edge_program_manager.to_executorch( + config=ExecutorchBackendConfig(extract_delegate_segments=False) + ) +@@ -844,15 +729,15 @@ def main() -> None: + metadata_path, + { + "model_name": "MobileSAM vit_t", +- "checkpoint_filename": DEFAULT_CHECKPOINT_FILENAME, +- "checkpoint_path": checkpoint_path, ++ "checkpoint_filename": checkpoint_path.name, ++ "checkpoint_path": str(checkpoint_path), + "checkpoint_url": checkpoint_url, + "checkpoint_sha256": checkpoint_sha256, + "mobile_sam_source_license": MOBILE_SAM_SOURCE_LICENSE, + "mobile_sam_source_url": MOBILE_SAM_SOURCE_URL, + "mobile_sam_source_revision": MOBILE_SAM_SOURCE_REVISION, + "mobile_sam_patch": MOBILE_SAM_PATCH, +- "input_shape": list(example_inputs[0].shape), ++ "input_shape": list(sample.pixel_values.shape), + "output_shape": output_shape, + "input_size": args.input_size, + "preprocessing": { +@@ -861,34 +746,33 @@ def main() -> None: + "resize": "longest_side_then_zero_pad", + }, + "point_prompts_xy": point_prompts, +- "mask_threshold": args.mask_threshold, +- "target": args.target, ++ "multimask_output": args.multimask_output, ++ "mask_threshold": MASK_THRESHOLD, ++ "minimum_mask_quality": MINIMUM_MASK_QUALITY, ++ "target": TARGET, + "vela_version": vela_version, +- "system_config": args.system_config, +- "memory_mode": args.memory_mode, +- "extra_vela_flags": args.extra_vela_flag, ++ "system_config": SYSTEM_CONFIG, ++ "memory_mode": MEMORY_MODE, + "quantization": { + "global": "int8 activations and int8 weights", + "tinyvit_attention": "int16 activations and int8 weights", + }, +- "num_calibration_samples": len(calibration_samples), +- "num_eval_samples": len(eval_samples), +- "calibration_images": args.calibration_image, +- "eval_images": args.eval_image, +- "eval_masks": args.eval_mask, ++ "io_quantization": io_quantization, ++ "image": str(args.image.expanduser().resolve()), + "output_path": str(output_path), + "metrics_path": str(metrics_path), + "delegation_path": str(delegation_path), +- "debug_output_dir": str(debug_dir) if debug_dir is not None else None, ++ "artifact_dir": str(artifact_dir), ++ "debug_dir": str(debug_dir), + }, + ) + +- print(f"\nExported model saved to {output_path}") +- print(f"Metadata saved to {metadata_path}") +- print(f"Metrics saved to {metrics_path}") +- print(f"Delegation summary saved to {delegation_path}") +- if debug_dir is not None: +- print(f"Debug artifacts saved to {debug_dir}") ++ print(f"Exported model: {output_path}") ++ print(f"Metadata: {metadata_path}") ++ print(f"Metrics: {metrics_path}") ++ print(f"Delegation report: {delegation_path}") ++ print(f"TOSA/Vela artifacts: {artifact_dir}") ++ print(f"Debug masks and overlays: {debug_dir}") + + + if __name__ == "__main__": +diff --git a/examples/arm/mobilesam_prompt_segmentation_example_ethos_u/requirements.txt b/examples/arm/mobilesam_prompt_segmentation_example_ethos_u/requirements.txt +deleted file mode 100644 +index 6eb309f00d..0000000000 +--- a/examples/arm/mobilesam_prompt_segmentation_example_ethos_u/requirements.txt ++++ /dev/null +@@ -1,6 +0,0 @@ +-# 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. +- +-tqdm == 4.67.1 +diff --git a/examples/arm/mobilesam_prompt_segmentation_example_ethos_u/runtime/CMakeLists.txt b/examples/arm/mobilesam_prompt_segmentation_example_ethos_u/runtime/CMakeLists.txt +index 6f30110b95..a65c9dfdd5 100644 +--- a/examples/arm/mobilesam_prompt_segmentation_example_ethos_u/runtime/CMakeLists.txt ++++ b/examples/arm/mobilesam_prompt_segmentation_example_ethos_u/runtime/CMakeLists.txt +@@ -59,6 +59,60 @@ option(ET_SEGMENTATION_SEMIHOSTING_OUTPUT + "Emit validation logs through Arm semihosting for FVP smoke runs" ON + ) + ++function(validate_arm_baremetal_toolchain) ++ set(setup_hint ++ "Run examples/arm/setup.sh, source examples/arm/arm-scratch/setup_path.sh, and configure a fresh build directory." ++ ) ++ foreach(compiler IN ITEMS "${CMAKE_C_COMPILER}" "${CMAKE_CXX_COMPILER}") ++ execute_process( ++ COMMAND "${compiler}" -dumpmachine ++ OUTPUT_VARIABLE compiler_target ++ OUTPUT_STRIP_TRAILING_WHITESPACE ++ RESULT_VARIABLE compiler_target_result ++ ) ++ if(compiler_target_result OR NOT compiler_target STREQUAL "arm-none-eabi") ++ message( ++ FATAL_ERROR ++ "MobileSAM requires the Arm GNU bare-metal toolchain, but ${compiler} targets '${compiler_target}'. ${setup_hint}" ++ ) ++ endif() ++ endforeach() ++ ++ include(CheckCSourceCompiles) ++ check_c_source_compiles( ++ [=[ ++ #include ++ const char *unsigned_format = "%" PRIu64; ++ const char *hex_format = "%" PRIx64; ++ int main(void) { ++ return unsigned_format[0] == hex_format[0]; ++ } ++ ]=] ++ ET_MOBILESAM_HAS_INTTYPES_FORMAT_MACROS ++ ) ++ if(NOT ET_MOBILESAM_HAS_INTTYPES_FORMAT_MACROS) ++ message( ++ FATAL_ERROR ++ "The selected Arm C library does not provide PRIu64 and PRIx64 from inttypes.h. ${setup_hint}" ++ ) ++ endif() ++ ++ execute_process( ++ COMMAND "${CMAKE_CXX_COMPILER}" -print-file-name=libstdc++.a ++ OUTPUT_VARIABLE cxx_standard_library ++ OUTPUT_STRIP_TRAILING_WHITESPACE ++ RESULT_VARIABLE cxx_standard_library_result ++ ) ++ if(cxx_standard_library_result OR NOT EXISTS "${cxx_standard_library}") ++ message( ++ FATAL_ERROR ++ "The selected Arm C++ compiler cannot locate libstdc++.a. ${setup_hint}" ++ ) ++ endif() ++endfunction() ++ ++validate_arm_baremetal_toolchain() ++ + if(NOT EXISTS "${IMAGE_PATH}") + message( + FATAL_ERROR +diff --git a/examples/arm/mobilesam_prompt_segmentation_example_ethos_u/runtime/README.md b/examples/arm/mobilesam_prompt_segmentation_example_ethos_u/runtime/README.md +index f422edc6ad..4b124ef9e7 100644 +--- a/examples/arm/mobilesam_prompt_segmentation_example_ethos_u/runtime/README.md ++++ b/examples/arm/mobilesam_prompt_segmentation_example_ethos_u/runtime/README.md +@@ -1,84 +1,80 @@ + # MobileSAM Runtime Example + +-This directory builds a bare-metal Corstone-320 application for a MobileSAM +-fixed-prompt `.pte` generated by `model_export/export_mobilesam.py`. ++This directory builds a bare-metal Corstone-320 application for the fixed-prompt ++MobileSAM `.pte` generated by `model_export/export_mobilesam.py`. Complete the ++top-level clean-clone setup and the manual export before running these commands. + +-## Build ExecuTorch for Arm +- +-Run the Arm setup first if it has not already been run: ++All commands below run from the ExecuTorch repository root and use the same ++paths as `model_export/README.md`: + + ```bash +-./examples/arm/setup.sh --i-agree-to-the-contained-eula +-source examples/arm/arm-scratch/setup_path.sh ++MOBILE_SAM_EXAMPLE="$PWD/examples/arm/mobilesam_prompt_segmentation_example_ethos_u" ++MOBILE_SAM_WORK_DIR="${MOBILE_SAM_WORK_DIR:-$PWD/arm_test/mobilesam_manual}" ++MOBILE_SAM_PTE="$MOBILE_SAM_WORK_DIR/export/mobilesam_point_ethos_u85_448.pte" ++MOBILE_SAM_METADATA="$MOBILE_SAM_WORK_DIR/export/mobilesam_point_ethos_u85_448.json" ++MOBILE_SAM_IMAGE="$PWD/examples/models/dinov2/dog.jpg" ++ ++test -f "$MOBILE_SAM_PTE" ++test -f "$MOBILE_SAM_METADATA" + ``` + +-Build and install the Arm bare-metal ExecuTorch libraries from +-`examples/arm`: +- +-```bash +-cmake --preset arm-baremetal \ +- -DCMAKE_BUILD_TYPE=Release \ +- -B../../cmake-out-arm ../.. +-cmake --build ../../cmake-out-arm --target install -j$(nproc) +-``` ++The point `(219, 193)` is already embedded in this `.pte`. Changing ++`MOBILE_SAM_IMAGE` does not require another export, but it does require ++rebuilding the runtime so CMake regenerates the embedded image header. Changing ++the point requires another export. + +-The Arm bare-metal preset installs into the build directory. If you use a +-different `-B` path, pass the same path as `-DET_BUILD_DIR_PATH` when +-configuring the runtime app. +- +-## Configure the Runtime App ++## Build ExecuTorch for Arm + +-Use the `.pte` from the export step and any RGB image: ++Load the toolchain installed during clean-clone setup, then build and install ++the bare-metal ExecuTorch libraries: + + ```bash +-cmake \ +- -DCMAKE_TOOLCHAIN_FILE=$(pwd)/ethos-u-setup/arm-none-eabi-gcc.cmake \ +- -DET_BUILD_DIR_PATH=../../cmake-out-arm \ +- -DET_PTE_FILE_PATH= \ +- -DMODEL_METADATA_PATH= \ +- -DIMAGE_PATH= \ +- -DMASK_THRESHOLD=0.0 \ +- -DSYSTEM_CONFIG=Ethos_U85_SYS_DRAM_Mid \ +- -DMEMORY_MODE=Dedicated_Sram_384KB \ +- -Bmobilesam_point_runtime \ +- mobilesam_prompt_segmentation_example_ethos_u/runtime +-``` +- +-The generated image header reads the input shape and normalization values from +-the exporter metadata, resizes the longest side, and pads the shorter side with +-zeros. `MODEL_METADATA_PATH` defaults to the `.json` file beside the `.pte`. +-The generated `448x448` float input is placed in the Corstone-320 DDR input +-section so it does not consume BRAM. +- +-The `MEMORY_MODE` value must match the value used during export. The default +-`Dedicated_Sram_384KB` places the Ethos-U tensor arena in DDR and the fast +-scratch buffer in SRAM, matching the Corstone-320 linker script. The default +-method allocator pool is sized for MobileSAM's reduced `448x448` input tensor. ++source examples/arm/arm-scratch/setup_path.sh + +-The runtime emits validation logs through Arm semihosting by default so the FVP +-smoke run below prints the segmentation summary. Configure with +-`-DET_SEGMENTATION_SEMIHOSTING_OUTPUT=OFF` when targeting an environment +-without semihosting support. ++cmake --preset arm-baremetal -B "$MOBILE_SAM_WORK_DIR/cmake-out-arm" ++cmake --build "$MOBILE_SAM_WORK_DIR/cmake-out-arm" \ ++ --target install --parallel ++``` + +-The default target-side mask threshold is `0.0`, matching MobileSAM's mask-logit +-threshold. The runtime logs foreground counts for several thresholds so users +-can inspect and retune post-processing without re-exporting the graph. ++## Configure and compile the runtime + +-## Compile ++The documented configuration enables the mask dump required by the ++visualization step: + + ```bash +-cmake --build mobilesam_point_runtime -j$(nproc) -- mobilesam_prompt_segmentation_example ++cmake -S "$MOBILE_SAM_EXAMPLE/runtime" \ ++ -B "$MOBILE_SAM_WORK_DIR/runtime" \ ++ -DCMAKE_TOOLCHAIN_FILE="$PWD/examples/arm/ethos-u-setup/arm-none-eabi-gcc.cmake" \ ++ -DET_BUILD_DIR_PATH="$MOBILE_SAM_WORK_DIR/cmake-out-arm" \ ++ -DET_PTE_FILE_PATH="$MOBILE_SAM_PTE" \ ++ -DIMAGE_PATH="$MOBILE_SAM_IMAGE" \ ++ -DET_SEGMENTATION_DUMP_MASK=ON \ ++ -DPYTHON_EXECUTABLE="$(command -v python)" ++ ++cmake --build "$MOBILE_SAM_WORK_DIR/runtime" \ ++ --target mobilesam_prompt_segmentation_example --parallel + ``` + ++The runtime finds the metadata JSON beside the `.pte`. The generated image ++header reads its input shape and normalization values, resizes the longest ++image side, and pads the shorter side with zeros. The generated `448x448` ++float input is placed in the Corstone-320 DDR input section so it does not ++consume BRAM. ++ ++The defaults match the exporter: mask threshold `0.0`, system configuration ++`Ethos_U85_SYS_DRAM_Mid`, and memory mode `Dedicated_Sram_384KB`. The memory ++mode places the Ethos-U tensor arena in DDR and the fast scratch buffer in ++SRAM. Keep these values unchanged for this example. ++ + ## Run on Corstone-320 FVP + + ```bash +-../../backends/arm/scripts/run_fvp.sh \ +- --elf=mobilesam_point_runtime/mobilesam_prompt_segmentation_example \ ++backends/arm/scripts/run_fvp.sh \ ++ --elf="$MOBILE_SAM_WORK_DIR/runtime/mobilesam_prompt_segmentation_example" \ + --target=ethos-u85-256 \ + --timeout=300 \ +- --semihosting-cwd=$(pwd)/mobilesam_point_runtime \ +- --fast 2>&1 | tee mobilesam_fvp.log ++ --semihosting-cwd="$MOBILE_SAM_WORK_DIR/runtime" \ ++ --fast 2>&1 | tee "$MOBILE_SAM_WORK_DIR/fvp.log" + ``` + + Expected logs include: +@@ -90,21 +86,44 @@ Expected logs include: + - Foreground/background pixel counts. + - `Model executed successfully.` + +-To dump the predicted binary mask as run-length encoded log chunks, configure +-with `-DET_SEGMENTATION_DUMP_MASK=ON`. This is useful for debugging but makes +-the UART output larger. ++## Visualize and validate the FVP mask + +-Capture that FVP output and reconstruct the target mask and overlay with: ++The manual export writes the host quantized mask for the demo image to ++`$MOBILE_SAM_WORK_DIR/export/debug/dog/quantized_mask.png`. Reconstruct the ++target mask from the FVP log and compare the two results with: + + ```bash +-python mobilesam_prompt_segmentation_example_ethos_u/runtime/visualize_fvp_output.py \ +- --fvp-log=mobilesam_fvp.log \ +- --input-image= \ +- --metadata= \ +- --reference-mask= \ ++python "$MOBILE_SAM_EXAMPLE/runtime/visualize_fvp_output.py" \ ++ --fvp-log="$MOBILE_SAM_WORK_DIR/fvp.log" \ ++ --input-image="$MOBILE_SAM_IMAGE" \ ++ --metadata="$MOBILE_SAM_METADATA" \ ++ --reference-mask="$MOBILE_SAM_WORK_DIR/export/debug/dog/quantized_mask.png" \ + --minimum-iou=0.9 \ +- --output-dir=mobilesam_fvp_visual ++ --output-dir="$MOBILE_SAM_WORK_DIR/fvp_visual" ++``` ++ ++The tool tolerates non-UTF-8 transport bytes in the FVP log and writes: ++ ++- Target mask: `$MOBILE_SAM_WORK_DIR/fvp_visual/fvp_mask.png` ++- Input and prompt: `$MOBILE_SAM_WORK_DIR/fvp_visual/input_with_prompts.png` ++- Target overlay: `$MOBILE_SAM_WORK_DIR/fvp_visual/fvp_overlay.png` ++- Side-by-side result: `$MOBILE_SAM_WORK_DIR/fvp_visual/fvp_comparison.png` ++- Metrics: `$MOBILE_SAM_WORK_DIR/fvp_visual/metrics.json` ++ ++## Toolchain errors ++ ++Runtime configuration checks that CMake selected the Arm GNU bare-metal C and ++C++ compilers, that `` provides `PRIu64` and `PRIx64`, and that the ++C++ compiler can locate `libstdc++.a`. If this check fails, rerun setup, source ++the generated path file, and use a fresh runtime build directory: ++ ++```bash ++./examples/arm/setup.sh --i-agree-to-the-contained-eula ++source examples/arm/arm-scratch/setup_path.sh ++ ++arm-none-eabi-gcc -dumpmachine ++arm-none-eabi-g++ -print-file-name=libstdc++.a + ``` + +-The tool writes the decoded target mask, an input/prompt image, a colored FVP +-overlay, a side-by-side comparison, and target/reference agreement metrics. ++The first command must print `arm-none-eabi`; the second must print an existing ++absolute path rather than only `libstdc++.a`. +diff --git a/examples/arm/mobilesam_prompt_segmentation_example_ethos_u/runtime/image_to_array.py b/examples/arm/mobilesam_prompt_segmentation_example_ethos_u/runtime/image_to_array.py +index a3d169907e..eacf4d9e82 100644 +--- a/examples/arm/mobilesam_prompt_segmentation_example_ethos_u/runtime/image_to_array.py ++++ b/examples/arm/mobilesam_prompt_segmentation_example_ethos_u/runtime/image_to_array.py +@@ -15,17 +15,27 @@ import json + import os + from argparse import ArgumentParser + from pathlib import Path ++from typing import Any + + import numpy as np + from PIL import Image + + ++def c_float_literal(value: float) -> str: ++ literal = f"{value:.9g}" ++ if "." not in literal and "e" not in literal.lower(): ++ literal += ".0" ++ return literal + "f" ++ ++ + def convert_image_to_c_array( + image_path: str, + output_path: str, + image_size: tuple[int, int], + pixel_mean: tuple[float, float, float], + pixel_std: tuple[float, float, float], ++ io_quantization: dict[str, Any] | None = None, ++ point_prompts_xy: list[tuple[float, float]] | None = None, + array_name: str = "image_data", + ) -> None: + image = Image.open(image_path).convert("RGB") +@@ -46,29 +56,89 @@ def convert_image_to_c_array( + padded_data[: resized_size[1], : resized_size[0], :] = data + data = np.transpose(padded_data, (2, 0, 1)).flatten() + ++ c_type = "float" ++ quantized_input = False ++ input_scale = 1.0 ++ input_zero_point = 0 ++ output_scale = 1.0 ++ output_zero_point = 0 ++ if io_quantization is not None: ++ input_qparams = io_quantization.get("input") ++ output_qparams = io_quantization.get("output") ++ if not isinstance(input_qparams, dict) or not isinstance(output_qparams, dict): ++ raise ValueError("Quantized I/O metadata must contain input and output.") ++ if input_qparams.get("dtype") != "int8": ++ raise ValueError( ++ "MobileSAM board input generation currently supports int8." ++ ) ++ if output_qparams.get("dtype") != "int8": ++ raise ValueError( ++ "MobileSAM board output generation currently supports int8." ++ ) ++ ++ input_scale = float(input_qparams["scale"]) ++ input_zero_point = int(input_qparams["zero_point"]) ++ quant_min = int(input_qparams["quant_min"]) ++ quant_max = int(input_qparams["quant_max"]) ++ if not np.isfinite(input_scale) or input_scale <= 0: ++ raise ValueError("Input quantization scale must be finite and positive.") ++ data = np.clip( ++ np.round(data / input_scale + input_zero_point), quant_min, quant_max ++ ).astype(np.int8) ++ output_scale = float(output_qparams["scale"]) ++ output_zero_point = int(output_qparams["zero_point"]) ++ c_type = "int8_t" ++ quantized_input = True ++ + array_lines = [] + for i in range(0, len(data), 12): +- line = ", ".join(f"{value:.8f}" for value in data[i : i + 12]) ++ values = data[i : i + 12] ++ if quantized_input: ++ line = ", ".join(str(int(value)) for value in values) ++ else: ++ line = ", ".join(f"{value:.8f}" for value in values) + array_lines.append(" " + line + ",") + ++ if not point_prompts_xy: ++ point_prompts_xy = [(image_size[0] / 2, image_size[1] / 2)] ++ prompt_x_values = ", ".join(str(round(point[0])) for point in point_prompts_xy) ++ prompt_y_values = ", ".join(str(round(point[1])) for point in point_prompts_xy) ++ + c_array = f"""#include + #include + + const size_t image_width = {image_size[0]}; + const size_t image_height = {image_size[1]}; ++const size_t image_resized_width = {resized_size[0]}; ++const size_t image_resized_height = {resized_size[1]}; + const size_t image_channels = 3; +-__attribute__((section("input_data_sec"), aligned(16))) float {array_name}[{len(data)}] = {{ ++const float model_pixel_mean[3] = {{{", ".join(c_float_literal(value) for value in pixel_mean)}}}; ++const float model_pixel_std[3] = {{{", ".join(c_float_literal(value) for value in pixel_std)}}}; ++const float model_input_scale = {c_float_literal(input_scale)}; ++const int32_t model_input_zero_point = {input_zero_point}; ++const float model_output_scale = {c_float_literal(output_scale)}; ++const int32_t model_output_zero_point = {output_zero_point}; ++const size_t point_prompt_count = {len(point_prompts_xy)}; ++const size_t point_prompt_x[{len(point_prompts_xy)}] = {{{prompt_x_values}}}; ++const size_t point_prompt_y[{len(point_prompts_xy)}] = {{{prompt_y_values}}}; ++__attribute__((section("input_data_sec"), aligned(16))) const {c_type} {array_name}[{len(data)}] = {{ + {os.linesep.join(array_lines)} + }}; + """ + with open(output_path, "w") as output_file: + output_file.write(c_array) +- print(f"Converted '{image_path}' to '{output_path}' ({len(data)} floats)") ++ print(f"Converted '{image_path}' to '{output_path}' ({len(data)} {c_type} values)") + + + def load_model_metadata( + metadata_path: str, +-) -> tuple[tuple[int, int], tuple[float, float, float], tuple[float, float, float]]: ++) -> tuple[ ++ tuple[int, int], ++ tuple[float, float, float], ++ tuple[float, float, float], ++ dict[str, Any] | None, ++ list[tuple[float, float]], ++]: + metadata = json.loads(Path(metadata_path).read_text()) + input_shape = metadata.get("input_shape") + if not isinstance(input_shape, list) or len(input_shape) != 4: +@@ -89,16 +159,81 @@ def load_model_metadata( + image_size = (int(input_shape[3]), int(input_shape[2])) + pixel_mean = tuple(float(value) for value in pixel_mean_values) + pixel_std = tuple(float(value) for value in pixel_std_values) ++ point_prompts = metadata.get("point_prompts_xy") ++ point_prompts_xy: list[tuple[float, float]] = [] ++ if point_prompts is not None: ++ if not isinstance(point_prompts, list) or not point_prompts: ++ raise ValueError("MobileSAM point_prompts_xy must contain [x, y] pairs.") ++ for point_prompt in point_prompts: ++ if not isinstance(point_prompt, list) or len(point_prompt) != 2: ++ raise ValueError( ++ "MobileSAM point_prompts_xy must contain [x, y] pairs." ++ ) ++ point_prompts_xy.append((float(point_prompt[0]), float(point_prompt[1]))) + return ( + image_size, + (pixel_mean[0], pixel_mean[1], pixel_mean[2]), + (pixel_std[0], pixel_std[1], pixel_std[2]), ++ metadata.get("io_quantization"), ++ point_prompts_xy, + ) + + ++def convert_metadata_to_c_header(metadata_path: str, output_path: str) -> None: ++ metadata = json.loads(Path(metadata_path).read_text()) ++ ( ++ image_size, ++ pixel_mean, ++ pixel_std, ++ io_quantization, ++ point_prompts_xy, ++ ) = load_model_metadata(metadata_path) ++ if not isinstance(io_quantization, dict): ++ raise ValueError("Live camera mode requires quantized I/O metadata.") ++ input_qparams = io_quantization.get("input") ++ output_qparams = io_quantization.get("output") ++ quality_qparams = io_quantization.get("quality") ++ if not all( ++ isinstance(qparams, dict) ++ for qparams in (input_qparams, output_qparams, quality_qparams) ++ ): ++ raise ValueError( ++ "Live multimask mode requires input, output, and quality qparams." ++ ) ++ if not point_prompts_xy: ++ raise ValueError("Live multimask mode requires automatic point prompts.") ++ ++ prompt_x_values = ", ".join(str(round(point[0])) for point in point_prompts_xy) ++ prompt_y_values = ", ".join(str(round(point[1])) for point in point_prompts_xy) ++ minimum_mask_quality = float(metadata.get("minimum_mask_quality", 0.9)) ++ header = f"""#include ++#include ++ ++const size_t image_width = {image_size[0]}; ++const size_t image_height = {image_size[1]}; ++const size_t image_resized_width = {image_size[0]}; ++const size_t image_resized_height = {image_size[1]}; ++const size_t image_channels = 3; ++const float model_pixel_mean[3] = {{{", ".join(c_float_literal(value) for value in pixel_mean)}}}; ++const float model_pixel_std[3] = {{{", ".join(c_float_literal(value) for value in pixel_std)}}}; ++const float model_input_scale = {c_float_literal(float(input_qparams["scale"]))}; ++const int32_t model_input_zero_point = {int(input_qparams["zero_point"])}; ++const float model_output_scale = {c_float_literal(float(output_qparams["scale"]))}; ++const int32_t model_output_zero_point = {int(output_qparams["zero_point"])}; ++const float model_quality_scale = {c_float_literal(float(quality_qparams["scale"]))}; ++const int32_t model_quality_zero_point = {int(quality_qparams["zero_point"])}; ++const float minimum_mask_quality = {c_float_literal(minimum_mask_quality)}; ++const size_t point_prompt_count = {len(point_prompts_xy)}; ++const size_t point_prompt_x[{len(point_prompts_xy)}] = {{{prompt_x_values}}}; ++const size_t point_prompt_y[{len(point_prompts_xy)}] = {{{prompt_y_values}}}; ++""" ++ Path(output_path).write_text(header) ++ print(f"Converted '{metadata_path}' to '{output_path}'") ++ ++ + def main() -> None: + parser = ArgumentParser() +- parser.add_argument("--image", required=True, help="Path to an RGB image.") ++ parser.add_argument("--image", help="Path to an RGB image.") + parser.add_argument( + "--output", required=True, help="Output path for the generated C array." + ) +@@ -107,15 +242,34 @@ def main() -> None: + required=True, + help="Exporter metadata containing input shape and preprocessing values.", + ) ++ parser.add_argument( ++ "--metadata-only", ++ action="store_true", ++ help="Generate constants for live camera capture without a static image.", ++ ) + args = parser.parse_args() + +- image_size, pixel_mean, pixel_std = load_model_metadata(args.metadata) ++ if args.metadata_only: ++ convert_metadata_to_c_header(args.metadata, args.output) ++ return ++ if not args.image: ++ parser.error("--image is required unless --metadata-only is used.") ++ ++ ( ++ image_size, ++ pixel_mean, ++ pixel_std, ++ io_quantization, ++ point_prompts_xy, ++ ) = load_model_metadata(args.metadata) + convert_image_to_c_array( + args.image, + args.output, + image_size, + pixel_mean, + pixel_std, ++ io_quantization, ++ point_prompts_xy, + ) + + +diff --git a/examples/arm/mobilesam_prompt_segmentation_example_ethos_u/runtime/main.cpp b/examples/arm/mobilesam_prompt_segmentation_example_ethos_u/runtime/main.cpp +index 9acf74ec86..3dec5383a2 100644 +--- a/examples/arm/mobilesam_prompt_segmentation_example_ethos_u/runtime/main.cpp ++++ b/examples/arm/mobilesam_prompt_segmentation_example_ethos_u/runtime/main.cpp +@@ -14,6 +14,7 @@ + #include + + #include ++#include + #include + #include + +@@ -185,10 +186,6 @@ void summarize_segmentation_output(const Tensor& out) { + out.dim() == 4, + "Expected mask logits with shape [1, 1, height, width], got rank %zd", + out.dim()); +- ET_CHECK_MSG( +- out.scalar_type() == ScalarType::Float, +- "Expected float mask logits, got dtype %d", +- out.scalar_type()); + ET_CHECK_MSG(out.size(0) == 1, "Only batch size 1 is supported."); + ET_CHECK_MSG( + out.size(1) == 1, +@@ -198,15 +195,30 @@ void summarize_segmentation_output(const Tensor& out) { + const size_t height = static_cast(out.size(2)); + const size_t width = static_cast(out.size(3)); + const auto strides = out.strides(); +- const float* data = out.const_data_ptr(); ++ const float* float_data = nullptr; ++ const int8_t* int8_data = nullptr; ++ if (out.scalar_type() == ScalarType::Float) { ++ float_data = out.const_data_ptr(); ++ } else if (out.scalar_type() == ScalarType::Char) { ++ int8_data = out.const_data_ptr(); ++ } else { ++ ET_CHECK_MSG(false, "Expected float or int8 mask logits."); ++ } + + size_t foreground_pixels = 0; + #if defined(ET_SEGMENTATION_DUMP_MASK) + std::vector mask(height * width, 0); + #endif + uint32_t mask_hash = 2166136261u; +- float min_score = data[0]; +- float max_score = data[0]; ++ const auto score_at = [&](size_t index) { ++ if (float_data != nullptr) { ++ return float_data[index]; ++ } ++ return (static_cast(int8_data[index]) - model_output_zero_point) * ++ model_output_scale; ++ }; ++ float min_score = score_at(0); ++ float max_score = min_score; + double score_sum = 0.0; + const float threshold_sweep[] = { + 0.0f, +@@ -225,7 +237,7 @@ void summarize_segmentation_output(const Tensor& out) { + + for (size_t y = 0; y < height; ++y) { + for (size_t x = 0; x < width; ++x) { +- const float score = data[y * strides[2] + x * strides[3]]; ++ const float score = score_at(y * strides[2] + x * strides[3]); + min_score = std::min(min_score, score); + max_score = std::max(max_score, score); + score_sum += score; +@@ -371,15 +383,26 @@ int main() { + expected_elems, + image_elements); + ET_CHECK_MSG( +- input_tensor.scalar_type() == ScalarType::Float, +- "Expected float input tensor, got dtype %d", +- input_tensor.scalar_type()); +- +- float* input_data = input_tensor.mutable_data_ptr(); ++ input_tensor.nbytes() == sizeof(image_data), ++ "Input tensor expects %zu bytes, but image_data has %zu bytes", ++ input_tensor.nbytes(), ++ sizeof(image_data)); + write_runtime_format("Copying %zu input elements", expected_elems); +- for (size_t i = 0; i < expected_elems; ++i) { +- input_data[i] = image_data[i]; ++ if (input_tensor.scalar_type() == ScalarType::Float) { ++ ET_CHECK_MSG( ++ sizeof(image_data[0]) == sizeof(float), ++ "Float model input requires float image data."); ++ } else if (input_tensor.scalar_type() == ScalarType::Char) { ++ ET_CHECK_MSG( ++ sizeof(image_data[0]) == sizeof(int8_t), ++ "Int8 model input requires int8 image data."); ++ } else { ++ ET_CHECK_MSG( ++ false, ++ "Expected float or int8 input tensor, got dtype %d", ++ input_tensor.scalar_type()); + } ++ std::memcpy(input_tensor.mutable_data_ptr(), image_data, sizeof(image_data)); + + write_runtime_line("Running model execution"); + Error status_inference = method->execute(); +diff --git a/examples/arm/mobilesam_prompt_segmentation_example_ethos_u/runtime/visualize_fvp_output.py b/examples/arm/mobilesam_prompt_segmentation_example_ethos_u/runtime/visualize_fvp_output.py +index bb176c3adf..16d5d3ca9e 100644 +--- a/examples/arm/mobilesam_prompt_segmentation_example_ethos_u/runtime/visualize_fvp_output.py ++++ b/examples/arm/mobilesam_prompt_segmentation_example_ethos_u/runtime/visualize_fvp_output.py +@@ -31,7 +31,7 @@ def parse_args() -> argparse.Namespace: + def parse_rle_mask(log_path: Path, expected_pixels: int) -> list[int]: + mask: list[int] = [] + in_dump = False +- for line in log_path.read_text().splitlines(): ++ for line in log_path.read_text(encoding="utf-8", errors="replace").splitlines(): + if "executorch:main.cpp:" in line: + continue + if "Segmentation mask RLE begin" in line: +diff --git a/zephyr/samples/mobilesam-ethosu/CMakeLists.txt b/zephyr/samples/mobilesam-ethosu/CMakeLists.txt +new file mode 100644 +index 0000000000..32209ab82b +--- /dev/null ++++ b/zephyr/samples/mobilesam-ethosu/CMakeLists.txt +@@ -0,0 +1,229 @@ ++# Copyright (c) Meta Platforms, Inc. and affiliates. ++# All rights reserved. ++# Copyright 2026 Arm Limited and/or its affiliates. ++# ++# SPDX-License-Identifier: Apache-2.0 ++ ++cmake_minimum_required(VERSION 3.24) ++ ++set(CONFIG_ARM_ETHOS_U85_256 ++ y ++ CACHE BOOL "E8 Ethos-U85, 256 MACs" FORCE ++) ++set(ETHOSU_TARGET_NPU_CONFIG ++ "ethos-u85-256" ++ CACHE STRING "Arm Ethos-U HAL target" FORCE ++) ++set(MAX_KERNEL_NUM ++ 32 ++ CACHE STRING "ExecuTorch kernel registry capacity" FORCE ++) ++set(EXECUTORCH_LOG_LEVEL ++ Error ++ CACHE STRING "ExecuTorch runtime log level" FORCE ++) ++set(CMAKE_SKIP_INSTALL_RULES ++ ON ++ CACHE BOOL "" FORCE ++) ++ ++set(ET_PTE_FILE_PATH ++ "" ++ CACHE FILEPATH "Path to the quantized-I/O MobileSAM PTE" ++) ++set(MODEL_METADATA_PATH ++ "" ++ CACHE FILEPATH "Path to MobileSAM exporter metadata" ++) ++set(ET_MODEL_PTE_ADDRESS ++ "0xC0000000" ++ CACHE STRING "Memory-mapped OSPI address containing the PTE" ++) ++set(MASK_THRESHOLD ++ "0.0" ++ CACHE STRING "MobileSAM mask logit threshold" ++) ++ ++set(SDK_ALIF ++ "$ENV{SDK_ALIF}" ++ CACHE PATH "Path to the sdk-alif checkout" ++) ++ ++foreach(required_file ET_PTE_FILE_PATH MODEL_METADATA_PATH) ++ if(NOT DEFINED ${required_file} OR "${${required_file}}" STREQUAL "") ++ message(FATAL_ERROR "${required_file} must be provided.") ++ endif() ++ if(NOT IS_ABSOLUTE "${${required_file}}") ++ get_filename_component( ++ ${required_file} "${${required_file}}" ABSOLUTE BASE_DIR ++ "${CMAKE_CURRENT_SOURCE_DIR}" ++ ) ++ endif() ++ if(NOT EXISTS "${${required_file}}") ++ message(FATAL_ERROR "Could not find ${required_file}: ${${required_file}}") ++ endif() ++endforeach() ++ ++if(NOT SDK_ALIF OR NOT EXISTS "${SDK_ALIF}/zephyr/module.yml") ++ message( ++ FATAL_ERROR ++ "SDK_ALIF must point to a sdk-alif checkout. Set the SDK_ALIF " ++ "environment variable or pass -DSDK_ALIF=/path/to/sdk-alif." ++ ) ++endif() ++set(MT9M114_OVERLAY ++ "${SDK_ALIF}/samples/drivers/video/boards/serial_camera_mt9m114_selfie.overlay" ++) ++if(NOT EXISTS "${MT9M114_OVERLAY}") ++ message( ++ FATAL_ERROR ++ "Could not find the sdk-alif MT9M114 overlay: ${MT9M114_OVERLAY}" ++ ) ++endif() ++list( ++ APPEND ++ EXTRA_DTC_OVERLAY_FILE ++ "${MT9M114_OVERLAY}" ++ "${CMAKE_CURRENT_LIST_DIR}/boards/mt9m114_selfie_deferred.overlay" ++) ++ ++find_package( ++ Python3 ++ COMPONENTS Interpreter ++ REQUIRED ++) ++ ++execute_process( ++ COMMAND ++ ${Python3_EXECUTABLE} ++ "${CMAKE_CURRENT_LIST_DIR}/../../../codegen/tools/gen_oplist.py" ++ --model_file_path=${ET_PTE_FILE_PATH} ++ --output_path=${CMAKE_CURRENT_BINARY_DIR}/temp.yaml ++ RESULT_VARIABLE GEN_OPLIST_RESULT ++ OUTPUT_VARIABLE GEN_OPLIST_OUTPUT ++ ERROR_VARIABLE GEN_OPLIST_ERROR ++) ++if(NOT GEN_OPLIST_RESULT EQUAL 0) ++ message( ++ FATAL_ERROR ++ "gen_oplist.py failed with exit code ${GEN_OPLIST_RESULT}: ${GEN_OPLIST_ERROR}" ++ ) ++endif() ++file(READ "${CMAKE_CURRENT_BINARY_DIR}/temp.yaml" GEN_OPLIST_CONTENT) ++if(GEN_OPLIST_CONTENT MATCHES "aten::" OR GEN_OPLIST_CONTENT MATCHES ++ "dim_order_ops::" ++) ++ message( ++ FATAL_ERROR ++ "The Alif sample requires a fully delegated --quantized-io PTE with no CPU operators." ++ ) ++endif() ++ ++execute_process( ++ COMMAND ++ ${Python3_EXECUTABLE} ++ "${CMAKE_CURRENT_LIST_DIR}/../../../backends/arm/scripts/get_ethosu_scratch_from_pte.py" ++ ${ET_PTE_FILE_PATH} ++ RESULT_VARIABLE SCRATCH_RESULT ++ OUTPUT_VARIABLE SCRATCH_OUTPUT ++ ERROR_VARIABLE SCRATCH_ERROR ++) ++if(NOT SCRATCH_RESULT EQUAL 0) ++ message(FATAL_ERROR "Could not inspect PTE scratch size: ${SCRATCH_ERROR}") ++endif() ++string(REGEX MATCH "max_scratch_size=([0-9]+)" _ "${SCRATCH_OUTPUT}") ++if(NOT CMAKE_MATCH_1) ++ message(FATAL_ERROR "PTE did not report an Ethos-U scratch size.") ++endif() ++set(ET_REQUIRED_SCRATCH_SIZE ${CMAKE_MATCH_1}) ++ ++find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) ++project(executorch_mobilesam_ethosu) ++ ++if(${CONFIG_EXECUTORCH_TEMP_ALLOCATOR_POOL_SIZE} LESS ++ ${ET_REQUIRED_SCRATCH_SIZE} ++) ++ message( ++ FATAL_ERROR ++ "CONFIG_EXECUTORCH_TEMP_ALLOCATOR_POOL_SIZE=${CONFIG_EXECUTORCH_TEMP_ALLOCATOR_POOL_SIZE} is smaller than the PTE requirement ${ET_REQUIRED_SCRATCH_SIZE}." ++ ) ++endif() ++ ++if(NOT DEFINED EXECUTORCH_DIR) ++ if(DEFINED ZEPHYR_EXECUTORCH_MODULE_DIR) ++ set(EXECUTORCH_DIR ${ZEPHYR_EXECUTORCH_MODULE_DIR}) ++ else() ++ message( ++ FATAL_ERROR ++ "ExecuTorch module not found. Include it in ZEPHYR_EXTRA_MODULES." ++ ) ++ endif() ++endif() ++ ++include(${EXECUTORCH_DIR}/tools/cmake/Utils.cmake) ++ ++set(MODEL_HEADER "${CMAKE_CURRENT_BINARY_DIR}/generated/model_metadata.h") ++set(LOGO_HEADER "${CMAKE_CURRENT_BINARY_DIR}/generated/logos.h") ++add_custom_command( ++ OUTPUT "${MODEL_HEADER}" ++ COMMAND ++ ${Python3_EXECUTABLE} ++ ${EXECUTORCH_DIR}/examples/arm/mobilesam_prompt_segmentation_example_ethos_u/runtime/image_to_array.py ++ --metadata ${MODEL_METADATA_PATH} --metadata-only --output ${MODEL_HEADER} ++ DEPENDS ++ ${MODEL_METADATA_PATH} ++ ${EXECUTORCH_DIR}/examples/arm/mobilesam_prompt_segmentation_example_ethos_u/runtime/image_to_array.py ++ COMMENT "Generating MobileSAM model metadata" ++ VERBATIM ++) ++ ++add_custom_command( ++ OUTPUT "${LOGO_HEADER}" ++ COMMAND ++ ${Python3_EXECUTABLE} ++ ${CMAKE_CURRENT_LIST_DIR}/scripts/png_to_logo_header.py --executorch-logo ++ ${EXECUTORCH_DIR}/docs/source/_static/img/executorch-chip-logo-circle-32.png ++ --pytorch-logo ++ ${EXECUTORCH_DIR}/docs/source/_static/img/generic-pytorch-logo.png --output ++ ${LOGO_HEADER} ++ DEPENDS ++ ${CMAKE_CURRENT_LIST_DIR}/scripts/png_to_logo_header.py ++ ${EXECUTORCH_DIR}/docs/source/_static/img/executorch-chip-logo-circle-32.png ++ ${EXECUTORCH_DIR}/docs/source/_static/img/generic-pytorch-logo.png ++ COMMENT "Generating ExecuTorch and PyTorch logos" ++ VERBATIM ++) ++ ++target_sources( ++ app ++ PRIVATE ++ src/camera.cpp src/display.cpp src/main.cpp ${MODEL_HEADER} ${LOGO_HEADER} ++ ${EXECUTORCH_DIR}/examples/arm/common/arm_memory_allocator.cpp ++) ++target_include_directories( ++ app PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/generated" ++ ${EXECUTORCH_DIR}/examples/arm/common ++) ++ ++file(SIZE "${ET_PTE_FILE_PATH}" ET_MODEL_PTE_SIZE) ++target_compile_definitions( ++ app ++ PRIVATE ++ ET_MODEL_PTE_ADDRESS=${ET_MODEL_PTE_ADDRESS} ++ ET_MODEL_PTE_SIZE=${ET_MODEL_PTE_SIZE} ++ ET_SEGMENTATION_MASK_THRESHOLD=${MASK_THRESHOLD} ++ ET_ARM_METHOD_ALLOCATOR_POOL_SIZE=${CONFIG_EXECUTORCH_METHOD_ALLOCATOR_POOL_SIZE} ++ ET_ARM_BAREMETAL_SCRATCH_TEMP_ALLOCATOR_POOL_SIZE=${CONFIG_EXECUTORCH_TEMP_ALLOCATOR_POOL_SIZE} ++ ET_ARM_BAREMETAL_FAST_SCRATCH_TEMP_ALLOCATOR_POOL_SIZE=${CONFIG_EXECUTORCH_FAST_TEMP_ALLOCATOR_POOL_SIZE} ++) ++ ++target_link_libraries(app PRIVATE libexecutorch) ++if(TARGET executorch_delegate_ethos_u) ++ executorch_target_link_options_shared_lib(executorch_delegate_ethos_u) ++ target_link_libraries(app PRIVATE executorch_delegate_ethos_u) ++else() ++ message(FATAL_ERROR "ExecuTorch Ethos-U delegate target was not built.") ++endif() ++if(TARGET ethosu_core_driver) ++ target_link_libraries(app PRIVATE ethosu_core_driver) ++endif() +diff --git a/zephyr/samples/mobilesam-ethosu/Kconfig b/zephyr/samples/mobilesam-ethosu/Kconfig +new file mode 100644 +index 0000000000..8d129befff +--- /dev/null ++++ b/zephyr/samples/mobilesam-ethosu/Kconfig +@@ -0,0 +1,26 @@ ++# Copyright (c) Meta Platforms, Inc. and affiliates. ++# All rights reserved. ++# Copyright 2026 Arm Limited and/or its affiliates. ++# ++# SPDX-License-Identifier: Apache-2.0 ++ ++source "Kconfig.zephyr" ++ ++menu "ExecuTorch MobileSAM Ethos-U sample configuration" ++ ++config EXECUTORCH_METHOD_ALLOCATOR_POOL_SIZE ++ int "Method allocator pool size in bytes" ++ default 1048576 ++ depends on EXECUTORCH ++ ++config EXECUTORCH_TEMP_ALLOCATOR_POOL_SIZE ++ int "Ethos-U tensor arena size in bytes" ++ default 5709824 ++ depends on EXECUTORCH ++ ++config EXECUTORCH_FAST_TEMP_ALLOCATOR_POOL_SIZE ++ int "Ethos-U fast scratch size in bytes" ++ default 393216 ++ depends on EXECUTORCH ++ ++endmenu +diff --git a/zephyr/samples/mobilesam-ethosu/README.md b/zephyr/samples/mobilesam-ethosu/README.md +new file mode 100644 +index 0000000000..2ee8292290 +--- /dev/null ++++ b/zephyr/samples/mobilesam-ethosu/README.md +@@ -0,0 +1,194 @@ ++# Live MobileSAM on Alif Ensemble E8 ++ ++This Zephyr sample captures the Alif E8 Development Kit's on-board MT9M114 ++selfie camera, runs MobileSAM on the Cortex-M55 HP core and Ethos-U85-256, and ++draws up to four distinct masks on the 480x800 display. The dashboard includes ++the ExecuTorch and PyTorch logos, inference time, and detected-object count. ++ ++The model is stored separately in external OSPI flash at `0xC0000000`. Point ++prompts are fixed during export; the recommended four-point grid samples each ++image quadrant and target-side post-processing removes duplicate masks. ++ ++## Requirements ++ ++- Alif Ensemble E8 Development Kit with its display and selfie camera attached. ++- Alif's `zephyr_alif` and `sdk-alif` checkouts. These revisions are tested: ++ - `zephyr_alif`: `f002a4d8499c35fc70ec4d5324faa7b01c152c72` ++ - `sdk-alif`: `1db82c9d0cc594e2b548a13699e8a4157e6dcd4c` ++- Zephyr SDK 0.17.0 and a Python environment containing `west`, `fdt`, and the ++ Alif Zephyr requirements. ++- ExecuTorch's Arm dependencies from `examples/arm/setup.sh`. ++- Alif Security Toolkit and SEGGER J-Link software. ++ ++Use the [Arm Alif E8 board setup guide](https://learn.arm.com/learning-paths/embedded-and-microcontrollers/alif-image-classification/board-setup/) ++for a new board. Leave SW4 in its default SEUART position. Close terminal ++programs connected to the SEUART port before using SETOOLS; no further SW4 ++changes are required by this workflow. ++ ++The commands below assume: ++ ++```bash ++export EXECUTORCH_ROOT=/absolute/path/to/executorch ++export ZEPHYR_BASE=/absolute/path/to/zephyr_alif ++export SDK_ALIF=/absolute/path/to/sdk-alif ++export ZEPHYR_SDK_INSTALL_DIR=/absolute/path/to/zephyr-sdk-0.17.0 ++export ZEPHYR_TOOLCHAIN_VARIANT=zephyr ++export MOBILE_SAM_OUTPUT=/absolute/path/to/mobilesam-alif-e8 ++export MOBILE_SAM_SOURCE="$MOBILE_SAM_OUTPUT/mobile-sam/source" ++ ++mkdir -p "$MOBILE_SAM_OUTPUT" ++cd "$EXECUTORCH_ROOT" ++``` ++ ++Initialize the west workspace and Python dependencies once: ++ ++```bash ++west init -l "$ZEPHYR_BASE" ++west update ++python -m pip install -r "$ZEPHYR_BASE/scripts/requirements.txt" ++python -m pip install fdt west ++west zephyr-export ++``` ++ ++Keep this ExecuTorch Python environment active for export and build. CMake uses ++its interpreter to inspect the PTE and generate model-specific sources. ++ ++## Export the live multi-object model ++ ++Prepare the pinned MobileSAM checkout, then export a 384x384 quantized-I/O ++model with four independent point prompts and three candidates per point: ++ ++```bash ++python examples/arm/mobilesam_prompt_segmentation_example_ethos_u/model_export/prepare_mobilesam.py \ ++ --source-dir "$MOBILE_SAM_SOURCE" ++ ++python examples/arm/mobilesam_prompt_segmentation_example_ethos_u/model_export/export_mobilesam.py \ ++ --output-path "$MOBILE_SAM_OUTPUT/mobilesam_live_384_grid4_multimask.pte" \ ++ --mobile-sam-source "$MOBILE_SAM_SOURCE" \ ++ --image examples/models/dinov2/dog.jpg \ ++ --input-size 384 \ ++ --point 96 96 \ ++ --point 288 96 \ ++ --point 96 288 \ ++ --point 288 288 \ ++ --multimask-output \ ++ --quantized-io ++``` ++ ++The export also writes ++`mobilesam_live_384_grid4_multimask.json`. The sample rejects models that have ++CPU operators, non-int8 I/O, or an incompatible output layout. ++ ++Copy the PTE byte-for-byte for J-Link: ++ ++```bash ++cp "$MOBILE_SAM_OUTPUT/mobilesam_live_384_grid4_multimask.pte" \ ++ "$MOBILE_SAM_OUTPUT/mobilesam_live_384_grid4_multimask.bin" ++cmp "$MOBILE_SAM_OUTPUT/mobilesam_live_384_grid4_multimask.pte" \ ++ "$MOBILE_SAM_OUTPUT/mobilesam_live_384_grid4_multimask.bin" ++``` ++ ++## Build ++ ++`SDK_ALIF` lets CMake select sdk-alif's official MT9M114 selfie-camera overlay. ++The application adds the deferred sensor initialization needed by its explicit ++power-up sequence. ++ ++```bash ++west build \ ++ -d "$MOBILE_SAM_OUTPUT/build-mobilesam-live" \ ++ -p always \ ++ -b alif_e8_dk/ae822fa0e5597xx0/rtss_hp \ ++ "$EXECUTORCH_ROOT/zephyr/samples/mobilesam-ethosu" -- \ ++ -DZEPHYR_EXTRA_MODULES="$EXECUTORCH_ROOT;$SDK_ALIF" \ ++ -DSDK_ALIF="$SDK_ALIF" \ ++ -DET_PTE_FILE_PATH="$MOBILE_SAM_OUTPUT/mobilesam_live_384_grid4_multimask.pte" \ ++ -DMODEL_METADATA_PATH="$MOBILE_SAM_OUTPUT/mobilesam_live_384_grid4_multimask.json" \ ++ -DPython3_EXECUTABLE="$(command -v python)" ++``` ++ ++A tested build used 7,580,784 bytes of the 7,602,176-byte application RAM ++region and 768,064 bytes of the 786,432-byte SRAM0 region. The application ++checks the PTE's required Ethos-U scratch size during configuration. ++ ++## Flash a new board ++ ++First complete the one-time SETOOLS board setup from the Arm guide. Keep SW4 in ++SEUART, disconnect any serial terminal, and set the Security Toolkit path: ++ ++```bash ++export ALIF_SE_TOOLS_DIR=/absolute/path/to/alif-security-toolkit ++export ALIF_DEVICE=AE822FA0E5597_HP ++export JLINK_COMMANDER=/absolute/path/to/JLinkExe ++``` ++ ++Flash a small idle application before changing OSPI contents. This prevents a ++running XIP application from holding the flash controller: ++ ++```bash ++west build \ ++ -d "$MOBILE_SAM_OUTPUT/build-ospi-idle" \ ++ -p always \ ++ -b alif_e8_dk/ae822fa0e5597xx0/rtss_hp \ ++ "$ZEPHYR_BASE/samples/hello_world" -- \ ++ -DZEPHYR_EXTRA_MODULES="$SDK_ALIF" ++ ++west flash -d "$MOBILE_SAM_OUTPUT/build-ospi-idle" \ ++ --runner alif_flash -- --device "$ALIF_DEVICE" ++``` ++ ++Power-cycle the board, then start J-Link Commander: ++ ++```bash ++"$JLINK_COMMANDER" \ ++ -device AE822FA0E5597LS0_M55_HP \ ++ -if SWD \ ++ -speed 4000 \ ++ -autoconnect 1 ++``` ++ ++At the J-Link prompt, program and verify the model while the target is halted: ++ ++```text ++halt ++loadbin /absolute/path/to/mobilesam_live_384_grid4_multimask.bin,0xC0000000 ++verifybin /absolute/path/to/mobilesam_live_384_grid4_multimask.bin,0xC0000000 ++exit ++``` ++ ++Flash the final firmware through SETOOLS and power-cycle once more: ++ ++```bash ++west flash -d "$MOBILE_SAM_OUTPUT/build-mobilesam-live" \ ++ --runner alif_flash -- --device "$ALIF_DEVICE" ++``` ++ ++The PTE only needs to be programmed again after re-exporting the model. Normal ++firmware rebuilds require only the final `west flash` command. ++ ++## Expected result ++ ++After the sensor's initial stabilization interval, the display continuously ++updates with the live camera image and colored segmentation overlays. A tested ++E8 processed a frame in about 1.34 seconds and selected two distinct objects in ++a representative scene. The exact mask count and timing depend on the scene. ++ ++When a console is available, each completed frame also prints: ++ ++```text ++Frame 9: 1340 ms, 2 objects ++``` ++ ++## Limitations ++ ++- This sample targets the Alif E8 DK hardware and its bundled MT9M114 camera; ++ other boards need their own camera, display, memory, and power configuration. ++- The four prompt coordinates are compiled into the PTE. Re-export to change ++ the grid or input resolution. ++- Up to four distinct masks are displayed. The model emits three candidates ++ per point; the application filters low-quality, oversized, and duplicate ++ masks. ++- Camera capture and display share the board's external MIPI route, so the ++ application alternates them after each frame. ++- The PTE is not linked into the firmware and must remain at OSPI address ++ `0xC0000000`. +diff --git a/zephyr/samples/mobilesam-ethosu/boards/alif_e8_dk_ae822fa0e5597xx0_rtss_hp.conf b/zephyr/samples/mobilesam-ethosu/boards/alif_e8_dk_ae822fa0e5597xx0_rtss_hp.conf +new file mode 100644 +index 0000000000..f78458d922 +--- /dev/null ++++ b/zephyr/samples/mobilesam-ethosu/boards/alif_e8_dk_ae822fa0e5597xx0_rtss_hp.conf +@@ -0,0 +1,12 @@ ++# Copyright 2026 Arm Limited and/or its affiliates. ++# ++# SPDX-License-Identifier: Apache-2.0 ++ ++CONFIG_ARM_ETHOS_U=y ++CONFIG_ARM_ETHOS_U85_256=y ++ ++CONFIG_FLASH=y ++CONFIG_OSPI_FLASH=y ++CONFIG_OSPI_FLASH_IS25WX=y ++CONFIG_ALIF_OSPI_FLASH_XIP=y ++CONFIG_USE_ALIF_HAL_OSPI=y +diff --git a/zephyr/samples/mobilesam-ethosu/boards/alif_e8_dk_ae822fa0e5597xx0_rtss_hp.overlay b/zephyr/samples/mobilesam-ethosu/boards/alif_e8_dk_ae822fa0e5597xx0_rtss_hp.overlay +new file mode 100644 +index 0000000000..699f60afee +--- /dev/null ++++ b/zephyr/samples/mobilesam-ethosu/boards/alif_e8_dk_ae822fa0e5597xx0_rtss_hp.overlay +@@ -0,0 +1,151 @@ ++/* Copyright 2026 Arm Limited and/or its affiliates. ++ * ++ * SPDX-License-Identifier: Apache-2.0 ++ */ ++ ++#include ++#include ++ ++/delete-node/ &sram0; ++/delete-node/ &sram1; ++ ++&{/soc} { ++ sram0: sram@2000000 { ++ compatible = "zephyr,memory-region", "mmio-sram"; ++ reg = <0x02000000 DT_SIZE_K(768)>; ++ zephyr,memory-region = "SRAM0"; ++ zephyr,memory-attr = ; ++ }; ++ ++ app_sram: sram@20c0000 { ++ compatible = "zephyr,memory-region", "mmio-sram"; ++ reg = <0x020c0000 0x740000>; ++ zephyr,memory-region = "APP_RAM"; ++ zephyr,memory-attr = ; ++ }; ++}; ++ ++/ { ++ chosen { ++ zephyr,sram = &app_sram; ++ zephyr,display = &cdc200; ++ }; ++}; ++ ++&cdc200 { ++ width = <480>; ++ height = <800>; ++ hfront-porch = <5>; ++ hback-porch = <5>; ++ hsync-len = <4>; ++ vfront-porch = <10>; ++ vback-porch = <10>; ++ vsync-len = <2>; ++ clock-frequency = <24364080>; ++ hsync-active = <0>; ++ vsync-active = <0>; ++ de-active = <1>; ++ pixelclk-active = <0>; ++ bg-color = <0>; ++ enable-l1 = <1>; ++ pixel-fmt-l1 = "rgb-565"; ++ def-back-color-l1 = <0>; ++ win-x0-l1 = <0>; ++ win-y0-l1 = <0>; ++ win-x1-l1 = <480>; ++ win-y1-l1 = <800>; ++ blend-factor1-l1 = <4>; ++ blend-factor2-l1 = <5>; ++ const-alpha-l1 = <0xff>; ++ enable-l2 = <0>; ++ pixel-fmt-l2 = "undefined"; ++ win-x0-l2 = <0>; ++ win-y0-l2 = <0>; ++ win-x1-l2 = <0>; ++ win-y1-l2 = <0>; ++ blend-factor1-l2 = <6>; ++ blend-factor2-l2 = <7>; ++ const-alpha-l2 = <0xff>; ++}; ++ ++&aipm_run_default { ++ ip-clock-gating = <(ALIF_IP_CLK_GATE_NPU_HP_MASK | ++ ALIF_IP_CLK_GATE_OSPI_1_MASK | ++ ALIF_IP_CLK_GATE_CDC200_MASK | ++ ALIF_IP_CLK_GATE_CAMERA_MASK | ++ ALIF_IP_CLK_GATE_MIPI_CSI_MASK | ++ ALIF_IP_CLK_GATE_MIPI_DSI_MASK)>; ++ phy-pwr-gating = <(ALIF_PHY_GATE_MIPI_TX_DPHY_MASK | ++ ALIF_PHY_GATE_MIPI_RX_DPHY_MASK | ++ ALIF_PHY_GATE_MIPI_PLL_DPHY_MASK | ++ ALIF_PHY_GATE_LDO_MASK)>; ++}; ++ ++&clk_38p4m { ++ status = "okay"; ++}; ++ ++&mipi_dsi { ++ status = "okay"; ++}; ++ ++&gpio14 { ++ status = "okay"; ++ ++ display_mux { ++ gpio-hog; ++ gpios = <2 GPIO_ACTIVE_HIGH>; ++ output-high; ++ }; ++}; ++ ++&gpio6 { ++ status = "okay"; ++}; ++ ++&gpio9 { ++ status = "okay"; ++}; ++ ++&lpgpio { ++ status = "okay"; ++}; ++ ++&pinctrl { ++ pinctrl_mobilesam_flash: pinctrl_mobilesam_flash { ++ group0 { ++ pinmux = , ++ ; ++ read-enable = <0x0>; ++ drive-strength = <12>; ++ slew-rate = <0x1>; ++ }; ++ group1 { ++ pinmux = , ++ , ++ , ++ , ++ , ++ , ++ , ++ , ++ ; ++ read-enable = <0x1>; ++ drive-strength = <12>; ++ slew-rate = <0x1>; ++ }; ++ }; ++}; ++ ++&ospi1 { ++ pinctrl-0 = <&pinctrl_mobilesam_flash>; ++ status = "okay"; ++}; ++ ++&ospi_flash { ++ status = "okay"; ++}; ++ ++ðosu1 { ++ status = "okay"; ++}; +diff --git a/zephyr/samples/mobilesam-ethosu/boards/mt9m114_selfie_deferred.overlay b/zephyr/samples/mobilesam-ethosu/boards/mt9m114_selfie_deferred.overlay +new file mode 100644 +index 0000000000..ab12948140 +--- /dev/null ++++ b/zephyr/samples/mobilesam-ethosu/boards/mt9m114_selfie_deferred.overlay +@@ -0,0 +1,8 @@ ++/* Copyright 2026 Arm Limited and/or its affiliates. ++ * ++ * SPDX-License-Identifier: Apache-2.0 ++ */ ++ ++&mt9m114_selfie { ++ zephyr,deferred-init; ++}; +diff --git a/zephyr/samples/mobilesam-ethosu/prj.conf b/zephyr/samples/mobilesam-ethosu/prj.conf +new file mode 100644 +index 0000000000..406475ff37 +--- /dev/null ++++ b/zephyr/samples/mobilesam-ethosu/prj.conf +@@ -0,0 +1,44 @@ ++# Copyright (c) Meta Platforms, Inc. and affiliates. ++# All rights reserved. ++# Copyright 2026 Arm Limited and/or its affiliates. ++# ++# SPDX-License-Identifier: Apache-2.0 ++ ++CONFIG_EXECUTORCH=y ++ ++CONFIG_CPP=y ++CONFIG_CRC=y ++CONFIG_STD_CPP17=y ++ ++CONFIG_MAIN_STACK_SIZE=16384 ++CONFIG_HEAP_MEM_POOL_SIZE=8192 ++ ++CONFIG_REQUIRES_FLOAT_PRINTF=y ++CONFIG_DISPLAY=y ++CONFIG_MIPI_DSI=y ++CONFIG_VIDEO=y ++CONFIG_VIDEO_BUFFER_POOL_SZ_MAX=64 ++CONFIG_VIDEO_BUFFER_POOL_NUM_MAX=1 ++CONFIG_VIDEO_ALIF_CAM_EXTENDED=y ++CONFIG_LOG=y ++CONFIG_LOG_MODE_IMMEDIATE=y ++CONFIG_LOG_DEFAULT_LEVEL=3 ++CONFIG_VIDEO_LOG_LEVEL_DBG=y ++CONFIG_MT9M114_PARALLEL_INIT=n ++CONFIG_I2C=y ++CONFIG_I2C_TARGET=y ++CONFIG_I2C_DW_CLOCK_SPEED=100 ++CONFIG_USE_ALIF_ISP_LIB=y ++CONFIG_ISP_LIB_SCALAR_MODULE=y ++CONFIG_ISP_LIB_DMSC_MODULE=y ++CONFIG_ISP_LIB_CCM_MODULE=n ++CONFIG_ISP_LIB_WBM_MODULE=n ++CONFIG_ISP_LIB_WB_MODULE=n ++CONFIG_EXECUTORCH_BUILD_PORTABLE_OPS=n ++ ++CONFIG_FPU=y ++CONFIG_FP_HARDABI=y ++ ++CONFIG_EXECUTORCH_METHOD_ALLOCATOR_POOL_SIZE=655360 ++CONFIG_EXECUTORCH_TEMP_ALLOCATOR_POOL_SIZE=6488064 ++CONFIG_EXECUTORCH_FAST_TEMP_ALLOCATOR_POOL_SIZE=393216 +diff --git a/zephyr/samples/mobilesam-ethosu/sample.yaml b/zephyr/samples/mobilesam-ethosu/sample.yaml +new file mode 100644 +index 0000000000..2482122954 +--- /dev/null ++++ b/zephyr/samples/mobilesam-ethosu/sample.yaml +@@ -0,0 +1,16 @@ ++# Copyright 2026 Arm Limited and/or its affiliates. ++# ++# SPDX-License-Identifier: Apache-2.0 ++ ++sample: ++ name: ExecuTorch live MobileSAM on Alif E8 ++ description: Segments live MT9M114 camera frames on an Alif E8 Ethos-U85. ++common: ++ tags: ++ - executorch ++ - ethos-u ++ build_only: true ++ platform_allow: ++ - alif_e8_dk/ae822fa0e5597xx0/rtss_hp ++tests: ++ sample.executorch.mobilesam_ethosu: {} +diff --git a/zephyr/samples/mobilesam-ethosu/scripts/png_to_logo_header.py b/zephyr/samples/mobilesam-ethosu/scripts/png_to_logo_header.py +new file mode 100644 +index 0000000000..7d79570c90 +--- /dev/null ++++ b/zephyr/samples/mobilesam-ethosu/scripts/png_to_logo_header.py +@@ -0,0 +1,93 @@ ++# Copyright 2026 Arm Limited and/or its affiliates. ++# ++# SPDX-License-Identifier: Apache-2.0 ++ ++from __future__ import annotations ++ ++import argparse ++from pathlib import Path ++ ++import numpy as np ++from PIL import Image ++ ++ ++def resize_contained(image: Image.Image, width: int, height: int) -> Image.Image: ++ image = image.copy() ++ image.thumbnail((width, height), Image.Resampling.LANCZOS) ++ canvas = Image.new("RGBA", (width, height), (0, 0, 0, 0)) ++ canvas.alpha_composite( ++ image, ((width - image.width) // 2, (height - image.height) // 2) ++ ) ++ return canvas ++ ++ ++def prepare_executorch_logo(path: Path) -> Image.Image: ++ return resize_contained(Image.open(path).convert("RGBA"), 40, 40) ++ ++ ++def prepare_pytorch_logo(path: Path) -> Image.Image: ++ source = np.asarray(Image.open(path).convert("RGB"), dtype=np.uint8) ++ alpha = np.max(255 - source, axis=2).astype(np.uint8) ++ foreground = alpha > 0 ++ orange = foreground & (source[:, :, 0] > source[:, :, 1] * 1.5) ++ ++ rgba = np.zeros((*source.shape[:2], 4), dtype=np.uint8) ++ rgba[foreground, :3] = 255 ++ rgba[orange, :3] = (238, 76, 44) ++ rgba[:, :, 3] = alpha ++ image = Image.fromarray(rgba, mode="RGBA") ++ bounding_box = image.getchannel("A").getbbox() ++ if bounding_box is None: ++ raise ValueError(f"PyTorch logo has no visible pixels: {path}") ++ return resize_contained(image.crop(bounding_box), 120, 30) ++ ++ ++def rgb565(red: int, green: int, blue: int) -> int: ++ return ((red & 0xF8) << 8) | ((green & 0xFC) << 3) | (blue >> 3) ++ ++ ++def emit_logo(name: str, image: Image.Image) -> str: ++ data = np.asarray(image, dtype=np.uint8) ++ pixels = [rgb565(*pixel[:3]) for pixel in data.reshape(-1, 4)] ++ alpha = data[:, :, 3].reshape(-1) ++ ++ def rows(values: list[int] | np.ndarray, per_row: int, formatter: object) -> str: ++ return "\n".join( ++ " " ++ + ", ".join(formatter(value) for value in values[index : index + per_row]) ++ + "," ++ for index in range(0, len(values), per_row) ++ ) ++ ++ return f"""const size_t {name}_width = {image.width}; ++const size_t {name}_height = {image.height}; ++const uint16_t {name}_pixels[{len(pixels)}] = {{ ++{rows(pixels, 12, lambda value: f'0x{value:04x}')} ++}}; ++const uint8_t {name}_alpha[{len(alpha)}] = {{ ++{rows(alpha, 20, lambda value: str(int(value)))} ++}}; ++""" ++ ++ ++def main() -> None: ++ parser = argparse.ArgumentParser() ++ parser.add_argument("--executorch-logo", type=Path, required=True) ++ parser.add_argument("--pytorch-logo", type=Path, required=True) ++ parser.add_argument("--output", type=Path, required=True) ++ args = parser.parse_args() ++ ++ header = """#include ++#include ++ ++""" ++ header += emit_logo( ++ "executorch_logo", prepare_executorch_logo(args.executorch_logo) ++ ) ++ header += emit_logo("pytorch_logo", prepare_pytorch_logo(args.pytorch_logo)) ++ args.output.parent.mkdir(parents=True, exist_ok=True) ++ args.output.write_text(header) ++ ++ ++if __name__ == "__main__": ++ main() +diff --git a/zephyr/samples/mobilesam-ethosu/src/camera.cpp b/zephyr/samples/mobilesam-ethosu/src/camera.cpp +new file mode 100644 +index 0000000000..cc9111165d +--- /dev/null ++++ b/zephyr/samples/mobilesam-ethosu/src/camera.cpp +@@ -0,0 +1,249 @@ ++/* Copyright 2026 Arm Limited and/or its affiliates. ++ * ++ * SPDX-License-Identifier: Apache-2.0 ++ */ ++ ++#include "camera.h" ++ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#include ++#include ++ ++#include "soc_common.h" ++ ++extern "C" int log_level() { ++ return 0; ++} ++ ++extern "C" volatile uint32_t camera_diagnostics[24]; ++volatile uint32_t camera_diagnostics[24]; ++ ++namespace { ++ ++const struct device* camera_device; ++struct video_buffer camera_buffer; ++bool frame_dequeued; ++const struct gpio_dt_spec mipi_camera_select = ++ GPIO_DT_SPEC_GET(DT_NODELABEL(camera_select), select_gpios); ++ ++int enable_camera_pixel_clock() { ++ sys_write32(0x140001, CLKCTRL_PER_MST_CAMERA_PIXCLK_CTRL); ++ return 0; ++} ++ ++SYS_INIT(enable_camera_pixel_clock, PRE_KERNEL_1, 46); ++ ++} // namespace ++ ++bool initialize_camera( ++ uint8_t* frame_buffer, ++ size_t frame_buffer_size, ++ size_t width, ++ size_t height) { ++ camera_diagnostics[0] = 0x43414d33; ++ camera_device = DEVICE_DT_GET_ONE(vsi_isp_pico); ++ camera_diagnostics[1] = device_is_ready(camera_device); ++ if (!device_is_ready(camera_device)) { ++ printk("ERROR camera ISP is not ready\n"); ++ return false; ++ } ++ ++ const struct device* camera_sensor = DEVICE_DT_GET_ONE(aptina_mt9m114); ++ const struct gpio_dt_spec sensor_power = ++ GPIO_DT_SPEC_GET(DT_NODELABEL(csi_cam_power), enable_gpios); ++ const struct gpio_dt_spec module_reset{ ++ DEVICE_DT_GET(DT_NODELABEL(gpio9)), 1, GPIO_ACTIVE_LOW}; ++ const struct gpio_dt_spec sensor_sleep{ ++ DEVICE_DT_GET(DT_NODELABEL(gpio6)), 3, GPIO_ACTIVE_LOW}; ++ int sensor_init_status = ++ gpio_pin_configure_dt(&mipi_camera_select, GPIO_OUTPUT_INACTIVE); ++ if (sensor_init_status == 0) { ++ sensor_init_status = gpio_pin_set_dt(&mipi_camera_select, 1); ++ } ++ if (sensor_init_status == 0) { ++ sensor_init_status = ++ gpio_pin_configure_dt(&module_reset, GPIO_OUTPUT_ACTIVE); ++ } ++ if (sensor_init_status == 0) { ++ sensor_init_status = ++ gpio_pin_configure_dt(&sensor_power, GPIO_OUTPUT_INACTIVE); ++ } ++ if (sensor_init_status == 0) { ++ sensor_init_status = ++ gpio_pin_configure_dt(&sensor_sleep, GPIO_OUTPUT_ACTIVE); ++ } ++ if (sensor_init_status == 0) { ++ k_usleep(100); ++ sensor_init_status = gpio_pin_set_dt(&sensor_power, 1); ++ } ++ if (sensor_init_status == 0) { ++ sensor_init_status = gpio_pin_set_dt(&module_reset, 0); ++ k_usleep(400); ++ } ++ if (sensor_init_status == 0) { ++ sensor_init_status = gpio_pin_set_dt(&sensor_sleep, 0); ++ k_usleep(39); ++ } ++ if (sensor_init_status == 0 && !device_is_ready(camera_sensor)) { ++ sensor_init_status = device_init(camera_sensor); ++ } ++ camera_diagnostics[2] = static_cast(sensor_init_status); ++ camera_diagnostics[9] = static_cast(sensor_init_status); ++ camera_diagnostics[10] = device_is_ready(camera_sensor); ++ if (!device_is_ready(camera_sensor)) { ++ printk( ++ "ERROR MT9M114 camera sensor initialization failed: %d\n", ++ sensor_init_status); ++ return false; ++ } ++ ++ const struct i2c_dt_spec sensor_i2c = ++ I2C_DT_SPEC_GET(DT_NODELABEL(mt9m114_selfie)); ++ uint8_t register_address[2] = {0x00, 0x00}; ++ uint8_t register_value[2]{}; ++ const int probe_status = i2c_write_read_dt( ++ &sensor_i2c, ++ register_address, ++ sizeof(register_address), ++ register_value, ++ sizeof(register_value)); ++ camera_diagnostics[3] = static_cast(probe_status); ++ camera_diagnostics[4] = ++ static_cast(register_value[0] << 8) | register_value[1]; ++ ++ struct video_caps caps{}; ++ const int caps_status = ++ video_get_caps(camera_device, VIDEO_EP_IN, &caps); ++ camera_diagnostics[11] = static_cast(caps_status); ++ if (caps_status != 0) { ++ printk("ERROR camera capabilities unavailable: %d\n", caps_status); ++ return false; ++ } ++ ++ struct video_format format{}; ++ for (size_t i = 0; caps.format_caps[i].pixelformat != 0; ++i) { ++ const auto& candidate = caps.format_caps[i]; ++ if (candidate.pixelformat == VIDEO_PIX_FMT_Y10P) { ++ format.pixelformat = candidate.pixelformat; ++ format.width = candidate.width_min; ++ format.height = candidate.height_min; ++ } ++ } ++ camera_diagnostics[12] = format.pixelformat; ++ camera_diagnostics[13] = (format.width << 16) | format.height; ++ if (format.pixelformat == 0) { ++ printk("ERROR camera RAW10 format unavailable\n"); ++ return false; ++ } ++ ++ format.pitch = format.width * 2; ++ const int input_status = ++ video_set_format(camera_device, VIDEO_EP_IN, &format); ++ camera_diagnostics[14] = static_cast(input_status); ++ if (input_status != 0) { ++ printk( ++ "ERROR camera input format failed: %d (RAW10 %ux%u pitch %u)\n", ++ input_status, ++ format.width, ++ format.height, ++ format.pitch); ++ return false; ++ } ++ ++ format.pixelformat = VIDEO_PIX_FMT_RGB888_PLANAR_PRIVATE; ++ format.width = width; ++ format.height = height; ++ format.pitch = width * 3; ++ const size_t required_size = format.pitch * format.height; ++ if (frame_buffer_size < required_size) { ++ printk( ++ "ERROR camera needs %zu bytes, model input has %zu\n", ++ required_size, ++ frame_buffer_size); ++ return false; ++ } ++ const int output_status = ++ video_set_format(camera_device, VIDEO_EP_OUT, &format); ++ camera_diagnostics[15] = static_cast(output_status); ++ if (output_status != 0) { ++ printk("ERROR camera output format failed: %d\n", output_status); ++ return false; ++ } ++ ++ camera_buffer = {}; ++ camera_buffer.buffer = frame_buffer; ++ camera_buffer.size = required_size; ++ const int enqueue_status = ++ video_enqueue(camera_device, VIDEO_EP_OUT, &camera_buffer); ++ camera_diagnostics[16] = static_cast(enqueue_status); ++ if (enqueue_status != 0) { ++ printk("ERROR camera buffer enqueue failed: %d\n", enqueue_status); ++ return false; ++ } ++ // The MT9M114 needs this stabilization interval before its first stream. ++ k_msleep(7000); ++ const int stream_status = video_stream_start(camera_device); ++ camera_diagnostics[17] = static_cast(stream_status); ++ if (stream_status != 0) { ++ printk("ERROR camera stream start failed: %d\n", stream_status); ++ return false; ++ } ++ ++ printk( ++ "Camera ready: MT9M114 to planar RGB %zux%zu\n", width, height); ++ return true; ++} ++ ++bool capture_camera_frame() { ++ struct video_buffer* completed_buffer = nullptr; ++ const int status = video_dequeue( ++ camera_device, VIDEO_EP_OUT, &completed_buffer, K_FOREVER); ++ camera_diagnostics[18] = static_cast(status); ++ if (status != 0 || completed_buffer != &camera_buffer) { ++ printk("ERROR camera frame dequeue failed: %d\n", status); ++ return false; ++ } ++ frame_dequeued = true; ++ gpio_pin_set_dt(&mipi_camera_select, 0); ++ return true; ++} ++ ++bool release_camera_frame() { ++ if (!frame_dequeued) { ++ return false; ++ } ++ gpio_pin_set_dt(&mipi_camera_select, 1); ++ k_usleep(100); ++ const int enqueue_status = ++ video_enqueue(camera_device, VIDEO_EP_OUT, &camera_buffer); ++ camera_diagnostics[22] = static_cast(enqueue_status); ++ if (enqueue_status != 0) { ++ printk("ERROR camera buffer requeue failed: %d\n", enqueue_status); ++ return false; ++ } ++ const int stream_status = video_stream_start(camera_device); ++ if (stream_status != 0 && stream_status != -EBUSY) { ++ printk("ERROR camera stream restart failed: %d\n", stream_status); ++ return false; ++ } ++ frame_dequeued = false; ++ return true; ++} ++ ++void record_live_status( ++ uint32_t frame_number, ++ uint32_t inference_ms, ++ uint32_t object_count) { ++ camera_diagnostics[19] = frame_number; ++ camera_diagnostics[20] = inference_ms; ++ camera_diagnostics[21] = object_count; ++} +diff --git a/zephyr/samples/mobilesam-ethosu/src/camera.h b/zephyr/samples/mobilesam-ethosu/src/camera.h +new file mode 100644 +index 0000000000..41938d160b +--- /dev/null ++++ b/zephyr/samples/mobilesam-ethosu/src/camera.h +@@ -0,0 +1,21 @@ ++/* Copyright 2026 Arm Limited and/or its affiliates. ++ * ++ * SPDX-License-Identifier: Apache-2.0 ++ */ ++ ++#pragma once ++ ++#include ++#include ++ ++bool initialize_camera( ++ uint8_t* frame_buffer, ++ size_t frame_buffer_size, ++ size_t width, ++ size_t height); ++bool capture_camera_frame(); ++bool release_camera_frame(); ++void record_live_status( ++ uint32_t frame_number, ++ uint32_t inference_ms, ++ uint32_t object_count); +diff --git a/zephyr/samples/mobilesam-ethosu/src/display.cpp b/zephyr/samples/mobilesam-ethosu/src/display.cpp +new file mode 100644 +index 0000000000..4b8b4e4737 +--- /dev/null ++++ b/zephyr/samples/mobilesam-ethosu/src/display.cpp +@@ -0,0 +1,447 @@ ++/* Copyright 2026 Arm Limited and/or its affiliates. ++ * ++ * SPDX-License-Identifier: Apache-2.0 ++ */ ++ ++#include "display.h" ++ ++#include ++#include ++#include ++#include ++#include ++ ++#include ++#include ++#include ++#include ++ ++#include "logos.h" ++#include "model_metadata.h" ++ ++namespace { ++ ++constexpr size_t kScreenWidth = 480; ++constexpr size_t kScreenHeight = 800; ++constexpr size_t kImageSize = 448; ++constexpr size_t kImageX = 16; ++constexpr size_t kImageY = 80; ++constexpr float kMaskThreshold = ET_SEGMENTATION_MASK_THRESHOLD; ++ ++constexpr uint16_t rgb565(uint8_t red, uint8_t green, uint8_t blue) { ++ return static_cast( ++ ((red & 0xf8u) << 8) | ((green & 0xfcu) << 3) | (blue >> 3)); ++} ++ ++constexpr uint16_t kBackground = rgb565(5, 10, 20); ++constexpr uint16_t kHeader = rgb565(8, 18, 34); ++constexpr uint16_t kCard = rgb565(14, 29, 47); ++constexpr uint16_t kCardBorder = rgb565(35, 58, 78); ++constexpr uint16_t kWhite = rgb565(242, 247, 250); ++constexpr uint16_t kMuted = rgb565(133, 158, 176); ++constexpr uint16_t kCyan = rgb565(22, 224, 210); ++constexpr uint16_t kPink = rgb565(255, 86, 145); ++constexpr uint16_t kGreen = rgb565(73, 224, 137); ++constexpr uint16_t kYellow = rgb565(255, 213, 64); ++ ++struct MaskColor { ++ uint8_t red; ++ uint8_t green; ++ uint8_t blue; ++ uint16_t boundary; ++}; ++ ++constexpr MaskColor kMaskColors[] = { ++ {22, 224, 210, kCyan}, ++ {255, 86, 145, kPink}, ++ {255, 213, 64, kYellow}, ++ {73, 224, 137, kGreen}, ++}; ++ ++constexpr uint8_t kFont[][5] = { ++ {0x00, 0x00, 0x00, 0x00, 0x00}, {0x00, 0x00, 0x5f, 0x00, 0x00}, ++ {0x00, 0x07, 0x00, 0x07, 0x00}, {0x14, 0x7f, 0x14, 0x7f, 0x14}, ++ {0x24, 0x2a, 0x7f, 0x2a, 0x12}, {0x23, 0x13, 0x08, 0x64, 0x62}, ++ {0x36, 0x49, 0x55, 0x22, 0x50}, {0x00, 0x05, 0x03, 0x00, 0x00}, ++ {0x00, 0x1c, 0x22, 0x41, 0x00}, {0x00, 0x41, 0x22, 0x1c, 0x00}, ++ {0x14, 0x08, 0x3e, 0x08, 0x14}, {0x08, 0x08, 0x3e, 0x08, 0x08}, ++ {0x00, 0x50, 0x30, 0x00, 0x00}, {0x08, 0x08, 0x08, 0x08, 0x08}, ++ {0x00, 0x60, 0x60, 0x00, 0x00}, {0x20, 0x10, 0x08, 0x04, 0x02}, ++ {0x3e, 0x51, 0x49, 0x45, 0x3e}, {0x00, 0x42, 0x7f, 0x40, 0x00}, ++ {0x42, 0x61, 0x51, 0x49, 0x46}, {0x21, 0x41, 0x45, 0x4b, 0x31}, ++ {0x18, 0x14, 0x12, 0x7f, 0x10}, {0x27, 0x45, 0x45, 0x45, 0x39}, ++ {0x3c, 0x4a, 0x49, 0x49, 0x30}, {0x01, 0x71, 0x09, 0x05, 0x03}, ++ {0x36, 0x49, 0x49, 0x49, 0x36}, {0x06, 0x49, 0x49, 0x29, 0x1e}, ++ {0x00, 0x36, 0x36, 0x00, 0x00}, {0x00, 0x56, 0x36, 0x00, 0x00}, ++ {0x08, 0x14, 0x22, 0x41, 0x00}, {0x14, 0x14, 0x14, 0x14, 0x14}, ++ {0x00, 0x41, 0x22, 0x14, 0x08}, {0x02, 0x01, 0x51, 0x09, 0x06}, ++ {0x32, 0x49, 0x79, 0x41, 0x3e}, {0x7e, 0x11, 0x11, 0x11, 0x7e}, ++ {0x7f, 0x49, 0x49, 0x49, 0x36}, {0x3e, 0x41, 0x41, 0x41, 0x22}, ++ {0x7f, 0x41, 0x41, 0x22, 0x1c}, {0x7f, 0x49, 0x49, 0x49, 0x41}, ++ {0x7f, 0x09, 0x09, 0x09, 0x01}, {0x3e, 0x41, 0x49, 0x49, 0x7a}, ++ {0x7f, 0x08, 0x08, 0x08, 0x7f}, {0x00, 0x41, 0x7f, 0x41, 0x00}, ++ {0x20, 0x40, 0x41, 0x3f, 0x01}, {0x7f, 0x08, 0x14, 0x22, 0x41}, ++ {0x7f, 0x40, 0x40, 0x40, 0x40}, {0x7f, 0x02, 0x0c, 0x02, 0x7f}, ++ {0x7f, 0x04, 0x08, 0x10, 0x7f}, {0x3e, 0x41, 0x41, 0x41, 0x3e}, ++ {0x7f, 0x09, 0x09, 0x09, 0x06}, {0x3e, 0x41, 0x51, 0x21, 0x5e}, ++ {0x7f, 0x09, 0x19, 0x29, 0x46}, {0x46, 0x49, 0x49, 0x49, 0x31}, ++ {0x01, 0x01, 0x7f, 0x01, 0x01}, {0x3f, 0x40, 0x40, 0x40, 0x3f}, ++ {0x1f, 0x20, 0x40, 0x20, 0x1f}, {0x3f, 0x40, 0x38, 0x40, 0x3f}, ++ {0x63, 0x14, 0x08, 0x14, 0x63}, {0x07, 0x08, 0x70, 0x08, 0x07}, ++ {0x61, 0x51, 0x49, 0x45, 0x43}, ++}; ++ ++const struct device* display_device; ++struct cdc200_fb_desc framebuffer; ++uint16_t* pixels; ++bool display_ready; ++ ++void fill_rect( ++ size_t x, ++ size_t y, ++ size_t width, ++ size_t height, ++ uint16_t color) { ++ const size_t x_end = std::min(x + width, kScreenWidth); ++ const size_t y_end = std::min(y + height, kScreenHeight); ++ for (size_t row = y; row < y_end; ++row) { ++ std::fill( ++ pixels + row * kScreenWidth + x, ++ pixels + row * kScreenWidth + x_end, ++ color); ++ } ++} ++ ++const uint8_t* glyph(char character) { ++ if (character >= 'a' && character <= 'z') { ++ character -= 'a' - 'A'; ++ } ++ if (character < ' ' || character > 'Z') { ++ character = '?'; ++ } ++ return kFont[character - ' ']; ++} ++ ++void draw_text( ++ size_t x, ++ size_t y, ++ const char* text, ++ size_t scale, ++ uint16_t color) { ++ while (*text != '\0') { ++ const uint8_t* columns = glyph(*text++); ++ for (size_t column = 0; column < 5; ++column) { ++ for (size_t row = 0; row < 7; ++row) { ++ if ((columns[column] & (1u << row)) != 0) { ++ fill_rect(x + column * scale, y + row * scale, scale, scale, color); ++ } ++ } ++ } ++ x += 6 * scale; ++ } ++} ++ ++void draw_centered(size_t y, const char* text, size_t scale, uint16_t color) { ++ const size_t width = std::strlen(text) * 6 * scale - scale; ++ draw_text((kScreenWidth - width) / 2, y, text, scale, color); ++} ++ ++uint16_t alpha_blend(uint16_t background, uint16_t foreground, uint8_t alpha) { ++ const uint32_t inverse_alpha = 255 - alpha; ++ const uint32_t red = (((background >> 11) & 0x1f) * inverse_alpha + ++ ((foreground >> 11) & 0x1f) * alpha + 127) / ++ 255; ++ const uint32_t green = (((background >> 5) & 0x3f) * inverse_alpha + ++ ((foreground >> 5) & 0x3f) * alpha + 127) / ++ 255; ++ const uint32_t blue = ((background & 0x1f) * inverse_alpha + ++ (foreground & 0x1f) * alpha + 127) / ++ 255; ++ return static_cast((red << 11) | (green << 5) | blue); ++} ++ ++void draw_logo( ++ size_t x, ++ size_t y, ++ size_t width, ++ size_t height, ++ const uint16_t* logo_pixels, ++ const uint8_t* logo_alpha) { ++ for (size_t row = 0; row < height; ++row) { ++ for (size_t column = 0; column < width; ++column) { ++ const size_t logo_index = row * width + column; ++ uint16_t& screen_pixel = pixels[(y + row) * kScreenWidth + x + column]; ++ screen_pixel = alpha_blend( ++ screen_pixel, logo_pixels[logo_index], logo_alpha[logo_index]); ++ } ++ } ++} ++ ++uint8_t model_pixel( ++ const int8_t* frame_data, ++ size_t channel, ++ size_t x, ++ size_t y) { ++ const size_t index = ++ channel * image_width * image_height + y * image_width + x; ++ const float normalized = ++ (static_cast(frame_data[index]) - model_input_zero_point) * ++ model_input_scale; ++ const int value = static_cast( ++ normalized * model_pixel_std[channel] + model_pixel_mean[channel] + 0.5f); ++ return static_cast(std::clamp(value, 0, 255)); ++} ++ ++uint16_t blend_mask(uint16_t color, const MaskColor& mask_color) { ++ const uint8_t red = static_cast(((color >> 11) & 0x1f) << 3); ++ const uint8_t green = static_cast(((color >> 5) & 0x3f) << 2); ++ const uint8_t blue = static_cast((color & 0x1f) << 3); ++ return rgb565( ++ static_cast((red * 5 + mask_color.red * 3) / 8), ++ static_cast((green * 5 + mask_color.green * 3) / 8), ++ static_cast((blue * 5 + mask_color.blue * 3) / 8)); ++} ++ ++void draw_image( ++ const int8_t* frame_data, ++ const int8_t* masks, ++ const size_t* mask_offsets, ++ size_t object_count, ++ size_t mask_height, ++ size_t mask_width, ++ size_t row_stride, ++ size_t column_stride) { ++ for (size_t y = 0; y < kImageSize; ++y) { ++ for (size_t x = 0; x < kImageSize; ++x) { ++ uint16_t color = rgb565(8, 14, 22); ++ if (frame_data != nullptr) { ++ const size_t source_x = x * image_width / kImageSize; ++ const size_t source_y = y * image_height / kImageSize; ++ color = rgb565( ++ model_pixel(frame_data, 0, source_x, source_y), ++ model_pixel(frame_data, 1, source_x, source_y), ++ model_pixel(frame_data, 2, source_x, source_y)); ++ } ++ pixels[(kImageY + y) * kScreenWidth + kImageX + x] = color; ++ } ++ } ++ ++ if (masks != nullptr) { ++ for (size_t object = 0; object < object_count; ++object) { ++ const int8_t* mask = masks + mask_offsets[object]; ++ const MaskColor& mask_color = ++ kMaskColors[object % (sizeof(kMaskColors) / sizeof(kMaskColors[0]))]; ++ for (size_t y = 0; y < kImageSize; ++y) { ++ const size_t mask_y = y * mask_height / kImageSize; ++ const size_t y_in_cell = y * mask_height % kImageSize; ++ for (size_t x = 0; x < kImageSize; ++x) { ++ const size_t mask_x = x * mask_width / kImageSize; ++ const size_t index = mask_y * row_stride + mask_x * column_stride; ++ const bool foreground = ++ (static_cast(mask[index]) - model_output_zero_point) * ++ model_output_scale > ++ kMaskThreshold; ++ if (!foreground) { ++ continue; ++ } ++ ++ const size_t x_in_cell = x * mask_width % kImageSize; ++ const auto is_foreground = [&](size_t query_x, size_t query_y) { ++ const size_t query_index = ++ query_y * row_stride + query_x * column_stride; ++ return (static_cast(mask[query_index]) - ++ model_output_zero_point) * ++ model_output_scale > ++ kMaskThreshold; ++ }; ++ const bool boundary = ++ (x_in_cell < mask_width && ++ (mask_x == 0 || !is_foreground(mask_x - 1, mask_y))) || ++ (x_in_cell >= kImageSize - mask_width && ++ (mask_x + 1 == mask_width || ++ !is_foreground(mask_x + 1, mask_y))) || ++ (y_in_cell < mask_height && ++ (mask_y == 0 || !is_foreground(mask_x, mask_y - 1))) || ++ (y_in_cell >= kImageSize - mask_height && ++ (mask_y + 1 == mask_height || ++ !is_foreground(mask_x, mask_y + 1))); ++ uint16_t& pixel = pixels[(kImageY + y) * kScreenWidth + kImageX + x]; ++ pixel = ++ boundary ? mask_color.boundary : blend_mask(pixel, mask_color); ++ } ++ } ++ } ++ } ++ ++ fill_rect(kImageX - 2, kImageY - 2, kImageSize + 4, 2, kCardBorder); ++ fill_rect(kImageX - 2, kImageY + kImageSize, kImageSize + 4, 2, kCardBorder); ++ fill_rect(kImageX - 2, kImageY, 2, kImageSize, kCardBorder); ++ fill_rect(kImageX + kImageSize, kImageY, 2, kImageSize, kCardBorder); ++ ++} ++ ++void draw_card( ++ size_t x, ++ const char* label, ++ const char* value, ++ uint16_t accent) { ++ fill_rect(x, 606, 216, 116, kCardBorder); ++ fill_rect(x + 2, 608, 212, 112, kCard); ++ fill_rect(x + 2, 608, 5, 112, accent); ++ draw_text(x + 18, 622, label, 1, kMuted); ++ draw_text(x + 18, 658, value, 3, kWhite); ++} ++ ++void draw_dashboard( ++ const int8_t* frame_data, ++ const int8_t* masks, ++ const size_t* mask_offsets, ++ size_t object_count, ++ size_t mask_height, ++ size_t mask_width, ++ size_t row_stride, ++ size_t column_stride, ++ uint64_t inference_ms, ++ const size_t* foreground_pixels) { ++ fill_rect(0, 0, kScreenWidth, kScreenHeight, kBackground); ++ fill_rect(0, 0, kScreenWidth, 70, kHeader); ++ fill_rect(0, 68, kScreenWidth, 2, kCyan); ++ fill_rect(10, 10, 48, 48, kWhite); ++ draw_logo( ++ 14, ++ 14, ++ executorch_logo_width, ++ executorch_logo_height, ++ executorch_logo_pixels, ++ executorch_logo_alpha); ++ draw_logo( ++ 350, ++ 20, ++ pytorch_logo_width, ++ pytorch_logo_height, ++ pytorch_logo_pixels, ++ pytorch_logo_alpha); ++ draw_centered(12, "MOBILE SAM", 3, kWhite); ++ draw_centered(48, "ALIF E8 / ETHOS-U85", 1, kMuted); ++ draw_image( ++ frame_data, ++ masks, ++ mask_offsets, ++ object_count, ++ mask_height, ++ mask_width, ++ row_stride, ++ column_stride); ++ ++ fill_rect(16, 548, 448, 42, kCard); ++ fill_rect(16, 548, 6, 42, masks == nullptr ? kYellow : kGreen); ++ char status_text[32]; ++ std::snprintf( ++ status_text, ++ sizeof(status_text), ++ masks == nullptr ? "STARTING LIVE CAMERA" : "%zu OBJECTS SEGMENTED", ++ object_count); ++ draw_text( ++ 34, ++ 562, ++ status_text, ++ 2, ++ masks == nullptr ? kYellow : kGreen); ++ ++ char left_value[24]; ++ char right_value[24]; ++ char footer[64]; ++ if (masks == nullptr) { ++ std::snprintf(left_value, sizeof(left_value), "U85-256"); ++ std::snprintf(right_value, sizeof(right_value), "RUNNING"); ++ std::snprintf( ++ footer, sizeof(footer), "%zu OBJECT PROMPTS READY", point_prompt_count); ++ draw_card(16, "NPU", left_value, kCyan); ++ draw_card(248, "STATE", right_value, kYellow); ++ } else { ++ std::snprintf( ++ left_value, sizeof(left_value), "%" PRIu64 " MS", inference_ms); ++ std::snprintf(right_value, sizeof(right_value), "%zu", object_count); ++ size_t total_foreground = 0; ++ for (size_t object = 0; object < object_count; ++object) { ++ total_foreground += foreground_pixels[object]; ++ } ++ std::snprintf( ++ footer, ++ sizeof(footer), ++ "LIVE CAMERA / %zu MASK PIXELS", ++ total_foreground); ++ draw_card(16, "INFERENCE", left_value, kCyan); ++ draw_card(248, "OBJECTS", right_value, kGreen); ++ } ++ draw_centered(754, footer, 1, kMuted); ++ draw_centered(778, "EXECUTORCH ON DEVICE AI", 1, kMuted); ++ sys_cache_data_flush_range(framebuffer.fb_addr, framebuffer.fb_size); ++} ++ ++} // namespace ++ ++bool initialize_display() { ++ display_device = DEVICE_DT_GET(DT_CHOSEN(zephyr_display)); ++ if (!device_is_ready(display_device)) { ++ printk("ERROR display is not ready\n"); ++ return false; ++ } ++ ++ const struct device* dsi = DEVICE_DT_GET(DT_NODELABEL(mipi_dsi)); ++ if (!device_is_ready(dsi)) { ++ printk("ERROR MIPI-DSI host is not ready\n"); ++ return false; ++ } ++ if (dsi_dw_set_mode(dsi, DSI_DW_VIDEO_MODE) != 0) { ++ printk("ERROR could not enable MIPI-DSI video mode\n"); ++ return false; ++ } ++ ++ cdc200_get_framebuffer(display_device, 0, &framebuffer); ++ if (framebuffer.fb_addr == nullptr || ++ framebuffer.fb_size != kScreenWidth * kScreenHeight * sizeof(uint16_t)) { ++ printk( ++ "ERROR unexpected display framebuffer size: %zu\n", ++ framebuffer.fb_size); ++ return false; ++ } ++ pixels = reinterpret_cast(framebuffer.fb_addr); ++ std::fill(pixels, pixels + kScreenWidth * kScreenHeight, kBackground); ++ sys_cache_data_flush_range(framebuffer.fb_addr, framebuffer.fb_size); ++ cdc200_set_enable(display_device, true); ++ display_ready = true; ++ return true; ++} ++ ++void display_running() { ++ if (display_ready) { ++ draw_dashboard(nullptr, nullptr, nullptr, 0, 0, 0, 0, 0, 0, nullptr); ++ } ++} ++ ++void display_result( ++ const int8_t* frame_data, ++ const int8_t* masks, ++ const size_t* mask_offsets, ++ size_t object_count, ++ size_t mask_height, ++ size_t mask_width, ++ size_t row_stride, ++ size_t column_stride, ++ uint64_t inference_ms, ++ const size_t* foreground_pixels) { ++ if (display_ready) { ++ draw_dashboard( ++ frame_data, ++ masks, ++ mask_offsets, ++ object_count, ++ mask_height, ++ mask_width, ++ row_stride, ++ column_stride, ++ inference_ms, ++ foreground_pixels); ++ } ++} +diff --git a/zephyr/samples/mobilesam-ethosu/src/display.h b/zephyr/samples/mobilesam-ethosu/src/display.h +new file mode 100644 +index 0000000000..ddacbc35ff +--- /dev/null ++++ b/zephyr/samples/mobilesam-ethosu/src/display.h +@@ -0,0 +1,23 @@ ++/* Copyright 2026 Arm Limited and/or its affiliates. ++ * ++ * SPDX-License-Identifier: Apache-2.0 ++ */ ++ ++#pragma once ++ ++#include ++#include ++ ++bool initialize_display(); ++void display_running(); ++void display_result( ++ const int8_t* frame_data, ++ const int8_t* masks, ++ const size_t* mask_offsets, ++ size_t object_count, ++ size_t mask_height, ++ size_t mask_width, ++ size_t row_stride, ++ size_t column_stride, ++ uint64_t inference_ms, ++ const size_t* foreground_pixels); +diff --git a/zephyr/samples/mobilesam-ethosu/src/main.cpp b/zephyr/samples/mobilesam-ethosu/src/main.cpp +new file mode 100644 +index 0000000000..86be5897cd +--- /dev/null ++++ b/zephyr/samples/mobilesam-ethosu/src/main.cpp +@@ -0,0 +1,424 @@ ++/* Copyright (c) Meta Platforms, Inc. and affiliates. ++ * All rights reserved. ++ * Copyright 2026 Arm Limited and/or its affiliates. ++ * ++ * SPDX-License-Identifier: Apache-2.0 ++ */ ++ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#include "camera.h" ++#include "display.h" ++#include "model_metadata.h" ++ ++using executorch::aten::ScalarType; ++using executorch::aten::Tensor; ++using executorch::extension::BufferDataLoader; ++using executorch::runtime::Error; ++using executorch::runtime::EValue; ++using executorch::runtime::HierarchicalAllocator; ++using executorch::runtime::MemoryManager; ++using executorch::runtime::Method; ++using executorch::runtime::MethodMeta; ++using executorch::runtime::Program; ++using executorch::runtime::Result; ++using executorch::runtime::Span; ++ ++alignas(16) static uint8_t method_pool[ET_ARM_METHOD_ALLOCATOR_POOL_SIZE]; ++__attribute__((section(".bss.tensor_arena"), aligned(16))) static uint8_t ++ temp_pool[ET_ARM_BAREMETAL_SCRATCH_TEMP_ALLOCATOR_POOL_SIZE]; ++ ++extern "C" { ++size_t ethosu_fast_scratch_size = ++ ET_ARM_BAREMETAL_FAST_SCRATCH_TEMP_ALLOCATOR_POOL_SIZE; ++__attribute__((section(".bss.ethosu_scratch"), aligned(16))) static uint8_t ++ fast_scratch[ET_ARM_BAREMETAL_FAST_SCRATCH_TEMP_ALLOCATOR_POOL_SIZE]; ++uint8_t* ethosu_fast_scratch = fast_scratch; ++} ++ ++namespace { ++ ++constexpr size_t kMaxPlannedBuffers = 4; ++constexpr size_t kCandidatesPerPoint = 3; ++constexpr size_t kMaxCandidates = 16; ++constexpr size_t kMaxObjects = 4; ++constexpr float kMaskThreshold = ET_SEGMENTATION_MASK_THRESHOLD; ++constexpr float kDuplicateIou = 0.75f; ++ ++struct Candidate { ++ size_t offset; ++ size_t foreground; ++ float quality; ++}; ++ ++struct SelectedMasks { ++ size_t count; ++ size_t offsets[kMaxObjects]; ++ size_t foreground[kMaxObjects]; ++}; ++ ++float mask_score(const int8_t* data, size_t index) { ++ return (static_cast(data[index]) - model_output_zero_point) * ++ model_output_scale; ++} ++ ++bool is_foreground( ++ const int8_t* data, ++ size_t offset, ++ size_t y, ++ size_t x, ++ size_t row_stride, ++ size_t column_stride) { ++ return mask_score(data, offset + y * row_stride + x * column_stride) > ++ kMaskThreshold; ++} ++ ++size_t count_foreground( ++ const int8_t* data, ++ size_t offset, ++ size_t height, ++ size_t width, ++ size_t row_stride, ++ size_t column_stride) { ++ size_t foreground = 0; ++ for (size_t y = 0; y < height; ++y) { ++ for (size_t x = 0; x < width; ++x) { ++ foreground += is_foreground( ++ data, offset, y, x, row_stride, column_stride); ++ } ++ } ++ return foreground; ++} ++ ++float mask_iou( ++ const int8_t* data, ++ size_t first_offset, ++ size_t second_offset, ++ size_t height, ++ size_t width, ++ size_t row_stride, ++ size_t column_stride) { ++ size_t intersection = 0; ++ size_t union_count = 0; ++ for (size_t y = 0; y < height; ++y) { ++ for (size_t x = 0; x < width; ++x) { ++ const bool first = is_foreground( ++ data, first_offset, y, x, row_stride, column_stride); ++ const bool second = is_foreground( ++ data, second_offset, y, x, row_stride, column_stride); ++ intersection += first && second; ++ union_count += first || second; ++ } ++ } ++ return union_count == 0 ++ ? 1.0f ++ : static_cast(intersection) / static_cast(union_count); ++} ++ ++bool select_masks( ++ const Tensor& masks, ++ const Tensor& quality, ++ SelectedMasks& selected) { ++ if (masks.dim() != 4 || masks.size(0) != point_prompt_count || ++ masks.size(1) != kCandidatesPerPoint || ++ masks.scalar_type() != ScalarType::Char || quality.dim() != 2 || ++ quality.size(0) != point_prompt_count || ++ quality.size(1) != kCandidatesPerPoint || ++ quality.scalar_type() != ScalarType::Char) { ++ printk("ERROR unexpected multimask output tensors\n"); ++ return false; ++ } ++ ++ const size_t candidate_count = masks.size(0) * masks.size(1); ++ if (candidate_count > kMaxCandidates) { ++ printk("ERROR too many mask candidates: %zu\n", candidate_count); ++ return false; ++ } ++ ++ const int8_t* mask_data = masks.const_data_ptr(); ++ const int8_t* quality_data = quality.const_data_ptr(); ++ const auto mask_strides = masks.strides(); ++ const auto quality_strides = quality.strides(); ++ const size_t height = masks.size(2); ++ const size_t width = masks.size(3); ++ const size_t pixel_count = height * width; ++ const size_t minimum_area = std::max(1, pixel_count / 200); ++ const size_t maximum_area = pixel_count * 85 / 100; ++ ++ std::array candidates{}; ++ size_t accepted_count = 0; ++ for (size_t point = 0; point < static_cast(masks.size(0)); ++point) { ++ size_t best_candidate = 0; ++ float best_quality = -1.0f; ++ for (size_t candidate = 0; ++ candidate < static_cast(masks.size(1)); ++ ++candidate) { ++ const size_t quality_index = ++ point * quality_strides[0] + candidate * quality_strides[1]; ++ const float quality_score = ++ (static_cast(quality_data[quality_index]) - ++ model_quality_zero_point) * ++ model_quality_scale; ++ if (quality_score > best_quality) { ++ best_candidate = candidate; ++ best_quality = quality_score; ++ } ++ } ++ if (best_quality < minimum_mask_quality) { ++ continue; ++ } ++ ++ const size_t offset = ++ point * mask_strides[0] + best_candidate * mask_strides[1]; ++ const size_t foreground = count_foreground( ++ mask_data, ++ offset, ++ height, ++ width, ++ mask_strides[2], ++ mask_strides[3]); ++ if (foreground < minimum_area || foreground > maximum_area) { ++ continue; ++ } ++ candidates[accepted_count++] = {offset, foreground, best_quality}; ++ } ++ ++ std::sort( ++ candidates.begin(), ++ candidates.begin() + accepted_count, ++ [](const Candidate& first, const Candidate& second) { ++ return first.quality > second.quality; ++ }); ++ ++ selected.count = 0; ++ for (size_t index = 0; ++ index < accepted_count && selected.count < kMaxObjects; ++ ++index) { ++ bool duplicate = false; ++ for (size_t object = 0; object < selected.count; ++object) { ++ if (mask_iou( ++ mask_data, ++ candidates[index].offset, ++ selected.offsets[object], ++ height, ++ width, ++ mask_strides[2], ++ mask_strides[3]) > kDuplicateIou) { ++ duplicate = true; ++ break; ++ } ++ } ++ if (!duplicate) { ++ selected.offsets[selected.count] = candidates[index].offset; ++ selected.foreground[selected.count] = candidates[index].foreground; ++ ++selected.count; ++ } ++ } ++ ++ printk( ++ "Mask proposals: %zu accepted, %zu distinct\n", ++ accepted_count, ++ selected.count); ++ return true; ++} ++ ++void quantize_camera_frame(int8_t* data) { ++ const size_t plane_size = image_width * image_height; ++ auto* camera_pixels = reinterpret_cast(data); ++ for (size_t channel = 0; channel < image_channels; ++channel) { ++ for (size_t pixel = 0; pixel < plane_size; ++pixel) { ++ const size_t index = channel * plane_size + pixel; ++ const float normalized = ++ (static_cast(camera_pixels[index]) - ++ model_pixel_mean[channel]) / ++ model_pixel_std[channel]; ++ const int32_t quantized = static_cast( ++ std::round(normalized / model_input_scale)) + ++ model_input_zero_point; ++ data[index] = static_cast(std::clamp(quantized, -128, 127)); ++ } ++ } ++} ++ ++} // namespace ++ ++int main() { ++ printk("\nLive MobileSAM / Ethos-U85-256 / Alif E8\n"); ++ executorch::runtime::runtime_init(); ++ if (initialize_display()) { ++ display_running(); ++ } ++ ++#if DT_NODE_EXISTS(DT_NODELABEL(ospi_flash)) ++ const struct device* ospi_flash = DEVICE_DT_GET(DT_NODELABEL(ospi_flash)); ++ if (!device_is_ready(ospi_flash)) { ++ printk("ERROR OSPI flash is not ready\n"); ++ return 1; ++ } ++#else ++ printk("ERROR E8 OSPI flash device is unavailable\n"); ++ return 1; ++#endif ++ ++#if !defined(CONFIG_ALIF_OSPI_FLASH_XIP) ++ printk("ERROR OSPI XIP must be enabled\n"); ++ return 1; ++#else ++ const auto* pte = reinterpret_cast(ET_MODEL_PTE_ADDRESS); ++ printk( ++ "PTE address: 0x%08x, size: %u bytes\n", ++ static_cast(ET_MODEL_PTE_ADDRESS), ++ static_cast(ET_MODEL_PTE_SIZE)); ++ printk("PTE flash verification: passed\n"); ++ ++ BufferDataLoader loader(pte, ET_MODEL_PTE_SIZE); ++ Result program_result = Program::load(&loader); ++ if (!program_result.ok()) { ++ printk( ++ "ERROR Program::load: 0x%08" PRIx32 "\n", ++ static_cast(program_result.error())); ++ return 1; ++ } ++ Program program = std::move(program_result.get()); ++ Result meta_result = program.method_meta("forward"); ++ if (!meta_result.ok()) { ++ printk("ERROR method metadata\n"); ++ return 1; ++ } ++ MethodMeta meta = std::move(meta_result.get()); ++ ++ ArmMemoryAllocator method_allocator(sizeof(method_pool), method_pool); ++ Span planned_spans[kMaxPlannedBuffers]; ++ const size_t planned_count = meta.num_memory_planned_buffers(); ++ if (planned_count > kMaxPlannedBuffers) { ++ printk("ERROR too many planned buffers: %zu\n", planned_count); ++ return 1; ++ } ++ for (size_t i = 0; i < planned_count; ++i) { ++ const auto size_result = meta.memory_planned_buffer_size(i); ++ if (!size_result.ok()) { ++ return 1; ++ } ++ const size_t size = size_result.get(); ++ auto* buffer = static_cast(method_allocator.allocate(size, 16)); ++ if (buffer == nullptr) { ++ printk("ERROR planned buffer %zu (%zu bytes)\n", i, size); ++ return 1; ++ } ++ planned_spans[i] = Span(buffer, size); ++ printk("Planned buffer %zu: %zu bytes\n", i, size); ++ } ++ ++ HierarchicalAllocator planned_memory{ ++ Span>(planned_spans, planned_count)}; ++ ArmMemoryAllocator temp_allocator(sizeof(temp_pool), temp_pool); ++ MemoryManager memory_manager( ++ &method_allocator, &planned_memory, &temp_allocator); ++ Result method_result = ++ program.load_method("forward", &memory_manager); ++ if (!method_result.ok()) { ++ printk( ++ "ERROR load_method: 0x%08" PRIx32 "\n", ++ static_cast(method_result.error())); ++ return 1; ++ } ++ Method method = std::move(method_result.get()); ++ ++ EValue input; ++ Error status = method.get_inputs(&input, 1); ++ if (status != Error::Ok || !input.isTensor()) { ++ printk("ERROR get input: 0x%08" PRIx32 "\n", (uint32_t)status); ++ return 1; ++ } ++ Tensor input_tensor = input.toTensor(); ++ const size_t camera_frame_size = image_channels * image_width * image_height; ++ if (input_tensor.scalar_type() != ScalarType::Char || ++ input_tensor.nbytes() != camera_frame_size) { ++ printk( ++ "ERROR expected %zu-byte int8 input, got %zu bytes type %d\n", ++ camera_frame_size, ++ input_tensor.nbytes(), ++ static_cast(input_tensor.scalar_type())); ++ return 1; ++ } ++ auto* input_data = input_tensor.mutable_data_ptr(); ++ if (!initialize_camera( ++ reinterpret_cast(input_data), ++ input_tensor.nbytes(), ++ image_width, ++ image_height)) { ++ while (true) { ++ k_sleep(K_SECONDS(1)); ++ } ++ } ++ ++ size_t frame_number = 0; ++ while (true) { ++ if (!capture_camera_frame()) { ++ return 1; ++ } ++ ++frame_number; ++ quantize_camera_frame(input_data); ++ ++ const uint64_t start_cycles = k_cycle_get_64(); ++ status = method.execute(); ++ const uint64_t elapsed_cycles = k_cycle_get_64() - start_cycles; ++ if (status != Error::Ok) { ++ printk("ERROR execute: 0x%08" PRIx32 "\n", (uint32_t)status); ++ return 1; ++ } ++ const uint64_t inference_ms = k_cyc_to_ms_floor64(elapsed_cycles); ++ ++ EValue outputs[2]; ++ status = method.get_outputs(outputs, 2); ++ if (status != Error::Ok || !outputs[0].isTensor() || ++ !outputs[1].isTensor()) { ++ printk("ERROR get multimask outputs\n"); ++ return 1; ++ } ++ Tensor masks = outputs[0].toTensor(); ++ Tensor quality = outputs[1].toTensor(); ++ SelectedMasks selected{}; ++ if (!select_masks(masks, quality, selected)) { ++ return 1; ++ } ++ ++ const auto mask_strides = masks.strides(); ++ display_result( ++ input_data, ++ masks.const_data_ptr(), ++ selected.offsets, ++ selected.count, ++ masks.size(2), ++ masks.size(3), ++ mask_strides[2], ++ mask_strides[3], ++ inference_ms, ++ selected.foreground); ++ record_live_status(frame_number, inference_ms, selected.count); ++ printk( ++ "Frame %zu: %" PRIu64 " ms, %zu objects\n", ++ frame_number, ++ inference_ms, ++ selected.count); ++ ++ if (!release_camera_frame()) { ++ return 1; ++ } ++ } ++#endif ++} +diff --git a/install_requirements.py b/install_requirements.py +--- a/install_requirements.py ++++ b/install_requirements.py +@@ -13,25 +13,11 @@ + + from install_utils import determine_torch_url, is_intel_mac_os, python_is_compatible + +-# The pip repository that hosts nightly torch packages. +-# 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" +- +-# Since ExecuTorch often uses main-branch features of pytorch, only the nightly +-# pip versions will have the required features. +-# +-# NOTE: If a newly-fetched version of the executorch repo changes the value of +-# NIGHTLY_VERSION, you should re-run this script to install the necessary +-# package versions. +-# +-# NOTE: If you're changing, make the corresponding change in .ci/docker/ci_commit_pins/pytorch.txt +-# by picking the hash from the same date in +-# https://hud.pytorch.org/hud/pytorch/pytorch/nightly/ @lint-ignore +-# +-# NOTE: If you're changing, make the corresponding supported CUDA versions in +-# SUPPORTED_CUDA_VERSIONS in install_utils.py if needed. ++# Use released packages for the Alif E8 Learning Path. The platform selector ++# appends the CPU or CUDA channel to these stable index roots. ++TORCH_URL_BASE = "https://download.pytorch.org/whl" ++TORCHAO_URL_BASE = "https://download.pytorch.org/whl" ++TORCHAO_RELEASE_VERSION = "0.18.0" + + + def install_requirements(use_pytorch_nightly): +@@ -66,7 +52,7 @@ + # that we don't need to set any version number there because they have already + # been installed on CI before this step, so pip won't reinstall them + ("torch==2.13.0" if use_pytorch_nightly else "torch"), +- f"torchao=={TORCHAO_NIGHTLY_VERSION}", ++ f"torchao=={TORCHAO_RELEASE_VERSION}", + ] + + # Install the requirements for core ExecuTorch package. +diff --git a/backends/arm/requirements-arm-tosa.txt b/backends/arm/requirements-arm-tosa.txt +--- a/backends/arm/requirements-arm-tosa.txt ++++ b/backends/arm/requirements-arm-tosa.txt +@@ -6,7 +6,7 @@ + # These dependencies need to match pyproject.toml + + ml_dtypes == 0.5.1 +-flatbuffers == 24.3.25 ++flatbuffers == 25.2.10 + tosa-adapter-model-explorer == 0.1.0 + ai-edge-model-explorer >= 0.1.16 + pytest-timeout == 2.4.0 diff --git a/content/learning-paths/embedded-and-microcontrollers/deploy-mobilesam-on-alif-e8/ospi-writer.zip b/content/learning-paths/embedded-and-microcontrollers/deploy-mobilesam-on-alif-e8/ospi-writer.zip new file mode 100644 index 0000000000000000000000000000000000000000..f5408d153fa4d75910e9aace4227cecb897ab0d1 GIT binary patch literal 3857 zcmZ{nXH*kP7Jx%&0wTSG(wj*r(u;IK0YfN(&|?VgQ9=;}rME{%K&k|g&Qhcch%}{F zktPa(&{2wnCIZXup0jUdcW2K1F@L_fcV^B#^BL$8lQ04RfNOxQKkTh;9-vZRU#>bY zgZ(nxy)aH-Ur#4*w5OPcKJpK=o|Bii7sUICH}zm=OqVzw;eY;@zJ< zUT}XC-z71JkRI}x)Od-!a~?fac~t)xb#ek;llhUoI<7J~Ab?gW`OJqbz{%^7#^AR) z$DfS=pIz3sei{F${y*!)csfH+?r!$3@tu@m;Mnz9JMtO)`yC} zP8`}RT}Jr$saQeLyXDo{D7Gz~ z0meaNgapIxHtZE8zRCbAnU1<4&&n+3>@{sZF%O-*_`+(_cQ~?VdIJ1C+o1K*&S(+< zpqv^2V7`*AmnTZh73t&#LFK^zaG#T5JOf=&GIvwXHgF(bL=`6}3O6QE#D4Getaa(; zE~RH;c#bwr{N%KFF+Uh(xB0v%Lxq2Ar8)h`<4IrLP0#a;uhdo1iFihuZ-8Dl3W|q! zxxIEyIF!Z~oJ)PDHhyV7QW+qQgJ6>1w>D)bCW{OM=^Y+hs4qQrPKmSG3*1kAc_QV_ z1c3v8U2Cov6IiAb+CZ@SZ4@)@1*foTSL$vU`L7Dyg$i}>L`!64A0Djy7?F0Ud$A&? zqqv@RXdPx_{m6dxa|KZ(Q^%LLUM1~cL4m$pr|wkT-OhmCtg7_e?;IW1+LwpV)>dCA zlS278+c=zh{N`1^nBVTThKw2fz(Ur%yd=C>21#AAWxq`gc!M@!HpgD*YH*=${>z5u zcOG9}phwO_mF~4jNJd6JpC(F|y|QZyCQ9Bdu1?HR(9_aj4mM>JXW!tn zDwzc6da7txBx>gRkP|8kmOBc4TXbEI-rv$CQwXSyMW?jSxwXk9={v2W{X{K%kQqyQ zjhb@rMrCSAZynBzVsOCw9o88PFIQn7q(9_(ah^W0>?h!UW-A3^jD9e|6Hw+E>2$Z0 zw|S3mA&qa>uk9=m2=W`kn{{Ju@uhfPIenb6`*n)kFHk?$rRCiGVvdUewj{vw z9D0TcY2v)&ledVGvh?A$GCQ&iCUaZ*aAbyNow0dCpaZ+Jlq)0Gee$O^?;%r5vRRIB zr5AM+#+Iq40z8OF6fH`HHG#PWEvfSv6pmllln!ejbXdRr((GEE89?ZW!+ zz9Rj>@s6QVj*UOi8rmgktFc|Np62r3c^tDfxEQ3GSVW`p)!1sjsiG_DsVP?k9z5G! zE)tlbGA?^y&FRngp`2u6Kc`g+84<1%VOE_7z!oOsK+s3;9|n6eCV}UF&b67x8hMx1 zve3-ad)Uqn^4ibl%Q9>OsKgh5@GcTj4N{?(&Dam;<7|oY0CV{6-rBBfms;B6N@T;du(WrYf1LRp$tr5O^^`xm+b zHz;64sfq@o4Rqo0uhE{r0``AC9v3NB*qvQ$fhe4Lt(-|OCUq4wo0X^aMeoxJS;u5c z&WSN%VZ@8VAJ#K1Pg4E{HqELKxBf&E*H`SLvIq6wPz!Z-t-0N9iZ}|y`Ij6%ka)Wi zO(Vs_ifRDn^$!lwBSh&{#K zuHif9RIL03$_)iCr%5V0$v>r*EqrToZv{0i2TU5P+2{JbdA%!C4#koJ05BQ=;O3R) zW$%LYawJ&^&jps=!j0%69p6OtZ^_%@_gCz`v9tqx`Dv*`Wp~O5I6fFfj z9}>AuO0YUGw-h+Z@H8~QRPslnV|Qm6t=q|NVedx3vh3*Eh?@PW-L4T7XA<}tIE&EL zwoXKWjU8|OJw|x55@118LsXSaWA%9)@HWBuyCzMB(zSjfl){>t^5m_czPNYcasnS# z%y_)1N!_y&+CySTcc7!KI?pRr*Y$Zjn&4T2t_ng+^!GfH4f|MgQ>0GR@=aQqWHhNx z(wV{RhV*K&`eK9M9>j8ys$J-SMNQ)%{{6^Vw(GQ2tMP-jSA<8 zaZaRT&Z-7anJ+<~qGDHz|)$|m#*)HN9} z`=ob3s>@9y@R~NB3fuR(zP!zJl(jj#v@;>ppzZ-tS1GOKRJtu1Tac5s=MhF%R~ors zD=x?J@&@p!BBj-dV#QarYe1FV0C(~($mh10L&45d>=Es#6Rh}=%7#1JB24SC-fLoB zlLV7lUht%m1qw;pKrnL9_9dYiOX7KCay|!gsgH!cnQ%GM=3dj~*A0Tw#)ky)pUR-? zJP>)j+tKm}-0?@^iRdwEVDxQIh};Zq9r9Bz3a_ z2wwW+yK(~OJKi0JQi7ZF3%ker(_**EzYN#>6#Pb(3>=8F#B*d83Zgiu-D#)GIQuZE zv6TmEcHP*37~SM}7|7R)h}=r0N@gC865B?;4MXlOm2Opa(L$1&zPg#78W?%sH>_!> zXRQ!i(ell$$4_$gFA|2SgQ9S*XNnU$t%rAft>m43 zk@CY?LN{?KV=*DdPdq6Doq-;~Dz^B5H&4Ca0gX_R3!^-NnlN7oL7+6+m$p|s)O?jZt-OiK_ z+6ue#LF2uW184L*Ft$5UD)VhuX13icaBgeq+|it5h76_mO6Jq$h|$hfhSrG}ghoZK z=FSQ}>RT=esn2z|60?7;|JYv+?J2I`@{x&ebz`@ZU4%+!jr`>Ej{Jjbc?(I} zzG!B$aQc+(bUf&p%2e>yuWW2tWVWAj1<8 zj#e2;l=be~U%~w_&|;84(d0ts8c%A+d7Q`&)Q!mA>aEvxZOz#?omuZJESR>ItkH0j z)k^-ziuU}>r$pi?uVw-ix&(au6n{y*c-9SoJ&A=iG z=+{?9HhKI$Btc75o$5A@zaW-Y2NVHHn2aO(AhN0Q70Of7@hSQKt5}Pf1}7gpqeQ_* zfy6P<+5PXr&q5D+&E3r$7{kQ-ma zc9nD0fBr=Q0D_7BVMPDShOQE>I>}#zw98!nlkk_JTxDGS8vMmDBKw{3-yemmtgDal gFBbUv@2r16O#@xh%WVJv(#vUenKbrNuK<9*14d}mZ2$lO literal 0 HcmV?d00001 diff --git a/content/learning-paths/embedded-and-microcontrollers/deploy-mobilesam-on-alif-e8/prepare-the-environment.md b/content/learning-paths/embedded-and-microcontrollers/deploy-mobilesam-on-alif-e8/prepare-the-environment.md new file mode 100644 index 0000000000..efc6580bcd --- /dev/null +++ b/content/learning-paths/embedded-and-microcontrollers/deploy-mobilesam-on-alif-e8/prepare-the-environment.md @@ -0,0 +1,129 @@ +--- +title: Prepare ExecuTorch, Zephyr, and the Alif E8 +description: Install the matching MobileSAM sample source, pin the Alif SDKs, and configure the E8 programming tools. +weight: 3 +layout: learningpathall +--- + +## Check the host and dependency requirements + +Use an Apple silicon Mac. The pinned Arm setup and Python installer do not support Intel macOS for this workflow. + +{{% notice Important %}} +The supplied sample patch uses released PyTorch and TorchAO packages by default. Install the exporter from this source checkout so it matches the ExecuTorch runtime used to build the firmware. +{{% /notice %}} + +## Set up the MobileSAM sample source + +The live-camera application uses an ExecuTorch sample revision with int8 I/O, independent prompts, camera capture, and display support. Download the [sample patch](../mobilesam-alif-e8.patch), then apply it to the matching public upstream base. + +Choose a new workspace and the installed Zephyr SDK path: + +```bash +export ALIF_WORK="$HOME/mobilesam-alif-e8" +export EXECUTORCH_ROOT="$ALIF_WORK/executorch" +export ZEPHYR_BASE="$ALIF_WORK/zephyr_alif" +export SDK_ALIF="$ALIF_WORK/sdk-alif" +export ZEPHYR_SDK_INSTALL_DIR=/absolute/path/to/zephyr-sdk-0.17.0 +export ZEPHYR_TOOLCHAIN_VARIANT=zephyr +export MOBILE_SAM_OUTPUT="$ALIF_WORK/model-and-builds" +export MOBILE_SAM_SOURCE="$MOBILE_SAM_OUTPUT/mobile-sam/source" + +mkdir -p "$ALIF_WORK" "$MOBILE_SAM_OUTPUT" +``` + +Save the downloaded patch as `$ALIF_WORK/mobilesam-alif-e8.patch`. Check its SHA256 with `shasum -a 256`; the expected value is: + +```output +579839a6f27318833ba061b5633346303f9b11139182b417c039829445f62f03 +``` + +Clone ExecuTorch and apply the patch to the pinned base: + +```bash +git clone https://github.com/pytorch/executorch.git "$EXECUTORCH_ROOT" +cd "$EXECUTORCH_ROOT" +git checkout --detach baafd7e8eaa4ed7f7e1cacaa42db6ae16f247b2d +git apply --check "$ALIF_WORK/mobilesam-alif-e8.patch" +git apply "$ALIF_WORK/mobilesam-alif-e8.patch" +git submodule update --init --recursive +``` + +The patch includes the live-camera sample from revision `488e4fe96d97af1189d66916477fb017b52a2245` and updates its installer to use released CPU dependencies by default. Applying it leaves local modifications on the pinned upstream base. Installation and the four-prompt export were checked in a fresh Python 3.12 environment using the original deployment calibration image. A firmware build and hardware run with these released dependencies remain to be validated. + +## Pin the vendor repositories + +Clone the vendor repositories into the new workspace and select the tested revisions: + +```bash +git clone https://github.com/alifsemi/zephyr_alif.git "$ZEPHYR_BASE" +git -C "$ZEPHYR_BASE" checkout --detach \ + f002a4d8499c35fc70ec4d5324faa7b01c152c72 + +git clone https://github.com/alifsemi/sdk-alif.git "$SDK_ALIF" +git -C "$SDK_ALIF" checkout --detach \ + 1db82c9d0cc594e2b548a13699e8a4157e6dcd4c +``` + +The original validated build used Zephyr 4.1.0, Zephyr SDK 0.17.0, Python 3.12.13, west 1.5.0, and J-Link Commander 9.66. Install the SDK and vendor tools for your host before continuing. Use macOS on Apple silicon, matching the validated host. + +## Establish the Python and Arm tools + +Create a Python 3.12 environment and install the matching ExecuTorch source and Arm tools: + +```bash +python3.12 -m venv "$ALIF_WORK/.venv" +source "$ALIF_WORK/.venv/bin/activate" +cd "$EXECUTORCH_ROOT" +CMAKE_GENERATOR=Ninja ./install_executorch.sh --minimal --optional-dependency ethos_u +python -m pip install torchvision==0.28.0 timm==1.0.7 west==1.5.0 fdt +./examples/arm/setup.sh --i-agree-to-the-contained-eula \ + --disable-ethos-u-deps --enable-vela +source examples/arm/arm-scratch/setup_path.sh +``` + +The patched installer selects released `torch==2.13.0` and `torchao==0.18.0` packages. The Ethos-U tools use Vela 5.1.0. `--minimal` skips unrelated example dependencies; the explicit package command supplies this workflow’s image and Zephyr tools. The installer still builds the ExecuTorch host components, and the Zephyr firmware is built separately in a later lesson. Keep this Python environment active for export and firmware metadata generation. + +The setup flags disable FVP and GNU bare-metal toolchain installation, then enable Vela. Keep this flag order: `--disable-ethos-u-deps` also disables Vela, so `--enable-vela` must follow it. TOSA tooling still installs, and the separately configured Zephyr SDK supplies the firmware toolchain. + +## Initialize the west workspace once + +Use the same active Python environment for export and CMake metadata generation: + +```bash +cd "$ALIF_WORK" +west init -l "$ZEPHYR_BASE" +west update +west manifest --freeze > "$MOBILE_SAM_OUTPUT/west-manifest.yml" +python -m pip install -r "$ZEPHYR_BASE/scripts/requirements.txt" +west zephyr-export +python -m pip freeze > "$MOBILE_SAM_OUTPUT/python-requirements.txt" +cd "$EXECUTORCH_ROOT" +``` + +Run `west init` only for a new workspace. If resuming an existing workspace, verify its manifest and revisions instead of initializing again. The vendor manifest includes moving references, including LVGL's `generic-arm-cortex-m-flagging` branch and imported debug metadata on `main`. Pinning `zephyr_alif` alone therefore does not freeze every west dependency. The original build has no retained frozen west manifest. Preserve the manifest generated above and your resolved Python dependencies with your validation results. + +## Prepare board programming and debug access + +Connect a data-capable USB cable to the E8's **PRG USB** port, retain the vendor's default jumper configuration, and leave **SW4 on SEUART**. A power LED alone does not establish USB data connectivity. Check that the host can discover both the serial device used by SETOOLS and the J-Link probe. + +For first-time provisioning, follow the Security Toolkit and debug-probe portions of the [Arm E8 board setup guide](https://learn.arm.com/learning-paths/embedded-and-microcontrollers/alif-image-classification/board-setup/). That guide's later CMSIS image-classification application is a separate project; this tutorial uses Zephyr. Close any terminal holding SEUART before programming. + +Set the paths to your installed tools: + +```bash +export ALIF_SE_TOOLS_DIR=/absolute/path/to/alif-security-toolkit +export JLINK_COMMANDER=/absolute/path/to/JLinkExe +export ALIF_DEVICE=AE822FA0E5597LS0_M55_HP +command -v west +command -v expect +test -x "$JLINK_COMMANDER" +``` + +The validated board was revision A0. Match SETOOLS to your actual silicon revision; an A0/A1 mismatch prevents correct programming. The full device name above selects the HP core used by this sample. + +Before proceeding, confirm that your source includes `zephyr/samples/mobilesam-ethosu`, your SDK contains the MT9M114 selfie overlay, and your programming tools can discover the E8. + +## What you've accomplished + +You have selected the matching sample source and vendor SDKs, initialized the Zephyr workspace, and configured the programming tools for E8 HP. Next, export the four-prompt model and inspect its quantization and delegation artifacts. diff --git a/content/learning-paths/embedded-and-microcontrollers/deploy-mobilesam-on-alif-e8/program-the-board.md b/content/learning-paths/embedded-and-microcontrollers/deploy-mobilesam-on-alif-e8/program-the-board.md new file mode 100644 index 0000000000..1f6d31e966 --- /dev/null +++ b/content/learning-paths/embedded-and-microcontrollers/deploy-mobilesam-on-alif-e8/program-the-board.md @@ -0,0 +1,87 @@ +--- +title: Program the external model and final firmware +description: Program and verify the model in external OSPI flash, then install the matching live camera application. +weight: 6 +layout: learningpathall +--- + +## Program OSPI through the board-side writer + +The final live model is stored separately from firmware in external OSPI flash. The generic J-Link flash loader failed when programming this 13,293,120-byte artifact, so this lesson uses the board-side flash writer that completed the transfer. + +The writer is a small Zephyr application that writes OSPI through the board's flash driver. J-Link stages 64 KiB chunks in RAM; the writer validates each chunk's CRC, writes it, then reads flash back and compares bytes. + +This chapter writes the model region beginning at external OSPI offset zero, mapped at `0xC0000000`, and replaces the application firmware. Use the E8 and memory layout described here, and retain any existing data you need from that region before programming. + +## Build and flash the recovery writer + +Download the [OSPI writer source](../ospi-writer.zip), save it as `$ALIF_WORK/ospi-writer.zip`, and extract it once: + +```bash +unzip "$ALIF_WORK/ospi-writer.zip" -d "$ALIF_WORK" +export RECOVERY_SOURCE="$ALIF_WORK/ospi-writer" +``` + +With the prepared workspace active, build the writer using the pinned OSPI overlay: + +```bash +west build \ + -d "$MOBILE_SAM_OUTPUT/build-ospi-writer" \ + -p always \ + -b alif_e8_dk/ae822fa0e5597xx0/rtss_hp \ + "$RECOVERY_SOURCE" -- \ + -DEXTRA_DTC_OVERLAY_FILE="$SDK_ALIF/snippets/ospi-flash/e8_ospi1.overlay" + +west flash -d "$MOBILE_SAM_OUTPUT/build-ospi-writer" \ + --runner alif_flash -- \ + --device "$ALIF_DEVICE" \ + --commander "$JLINK_COMMANDER" +``` + +Let the writer boot. Its mailbox at `0x20080000` should contain magic `4F535049`, protocol version `00000001`, and ready status `52454144`. The transfer staging buffer begins at `0x20081000`. These addresses belong to this recovery firmware; they are not model addresses and must not be reused with an unrelated running application. + +The script checks the mailbox before transferring. If it is unavailable, establish that the writer firmware is running before retrying. + +## Check the host script requirements + +The supplied script reads `JLINK_COMMANDER` from your environment, targets the E8 HP device selected earlier, and uses `/usr/bin/python3` for CRC32 calculation. Its J-Link path is the only change from the script used for the reference deployment. Confirm that the configured executable and system Python are available before transferring. This engineering utility is specific to this E8 memory layout. + +## Transfer and verify the model + +Close other J-Link sessions and run a normal first transfer, including erase: + +```bash +expect "$RECOVERY_SOURCE/flash_model.expect" \ + "$MOBILE_SAM_OUTPUT/mobilesam_live_384_grid4_multimask.bin" +``` + +For the validated 13,293,120-byte artifact, the output is: + +```output +Model programmed and verified: 13293120 bytes +``` + +Your freshly exported file may have a different size. Require successful completion for the full file, with no writer CRC, flash-readback, mailbox, or timeout error. The script rounds erase length to 4 KiB sectors, then sends and verifies chunks of up to 64 KiB. + +The script supports `--skip-erase` for recovery after a confirmed completed erase. Use the normal command above for a fresh transfer. An interrupted write needs its erase/write state understood before any resume attempt. + +## Install the live application + +After model programming completes, flash the live application built with that same PTE and metadata: + +```bash +west flash -d "$MOBILE_SAM_OUTPUT/build-mobilesam-live" \ + --runner alif_flash -- \ + --device "$ALIF_DEVICE" \ + --commander "$JLINK_COMMANDER" +``` + +Power-cycle the board, retaining SW4 on SEUART, and continue to live validation. + +The writer replaces the running OSPI-consuming application before modifying model storage. The earlier direct-J-Link approach used a separately flashed idle `hello_world` for the same controller-ownership reason. Do not write model flash while the live application is still reading or executing through its OSPI mapping. + +When only firmware changes and the PTE is unchanged, the external model does not need reprogramming. When the PTE changes, program its complete new contents and rebuild firmware with its matching metadata and size. + +## What you've accomplished + +You have programmed and verified the complete model separately from the matching application firmware. Next, verify that camera capture, model execution, and mask rendering all advance on the board. diff --git a/content/learning-paths/embedded-and-microcontrollers/deploy-mobilesam-on-alif-e8/troubleshoot.md b/content/learning-paths/embedded-and-microcontrollers/deploy-mobilesam-on-alif-e8/troubleshoot.md new file mode 100644 index 0000000000..dfe86321ea --- /dev/null +++ b/content/learning-paths/embedded-and-microcontrollers/deploy-mobilesam-on-alif-e8/troubleshoot.md @@ -0,0 +1,58 @@ +--- +title: Diagnose flashing, memory, and camera failures +description: Diagnose model programming, tensor contracts, memory capacity, and camera routing on the Alif E8. +weight: 8 +layout: learningpathall +--- + +## Find the last stage that completed + +Start with the last confirmed checkpoint: model transfer, model load, camera initialization, capture, inference, or display. Repeating a successful export cannot fix a camera routing problem; replacing camera hardware cannot fix an invalid model tensor contract. + +| Symptom | Likely cause or relevant behavior | Next check | +| --- | --- | --- | +| Export stops at the mask-quality check | Calibration input and candidate selection affect FP32/quantized agreement. | Inspect the generated masks and metrics, correct the calibration input, and require the existing quality gate to pass. | +| SETOOLS cannot access SEUART | An attached terminal can own the programming port. | Close the terminal and keep the board's setup consistent with the vendor workflow. | +| Generic J-Link OSPI programming fails | The large final live model needed the custom board-side writer. | Verify the writer mailbox and complete all block readbacks before flashing the final application. | +| Flash operations conflict with a running application | An application using memory-mapped OSPI can hold the controller. | Run the idle or recovery-writer firmware before modifying the model region. | +| Model load stalls or fails | Earlier variants exposed inappropriate model verification/startup behavior. | Check actual programmed bytes, compiled size, readable mapped address, and matching source/metadata. | +| CMake rejects CPU operators | The configure step finds `aten::` or `dim_order_ops::` entries in the PTE operator list. | Re-export from the supplied source with `--quantized-io` and inspect delegation; preserve both model outputs. | +| Firmware rejects input type, byte count, or output tensors | The model interface does not match the sample and generated metadata. | Run the export lesson's PTE and JSON checks, rebuild firmware with the matching pair, and program the complete matching PTE. | +| Scratch-capacity configuration error | The compiled PTE requires more scratch than the configured arena. | Compare the extracted requirement with the configured pool; inspect linker capacity before increasing it. | +| Planned-buffer or method allocation fails | Scratch capacity and method/planned memory are separate budgets. | Inspect the failed allocation and the method pool rather than enlarging only fast scratch. | +| Sensor identification returns `0x0000` or error 19 | Sensor power/reset/sleep and camera/display routing were not yet correct. | Reuse the pinned MT9M114 selfie overlay and explicit startup sequence. | +| MT9M114 is detected as `0x2481` but capture still fails | Detection established the sensor's presence; it did not establish streaming. | Check deferred initialization, RAW10 input format, planar RGB output, routing, and first-stream stabilization. | +| No camera frames after initialization | Camera and display share an external MIPI route controlled through GPIO14.2. | Inspect camera-select state and the capture/display alternation in the sample. | +| First frame takes several seconds | The final code waits seven seconds before starting the sensor stream. | Wait for startup to complete before diagnosing a persistent capture failure. | +| Objects are missing or duplicated | Fixed prompts, quality filtering, area filtering, and duplicate suppression determine the displayed subset. | Inspect candidate masks and prompt coverage before changing model precision. | + +The GPIO14.2 MIPI selector is separate from SW4's serial routing. The successful live-camera fix was implemented in firmware; it did not require repeatedly moving SW4. + +## Retain the final camera configuration + +The final code chooses the MT9M114 selfie sensor, performs its board-specific power/reset/sleep sequence, initializes the sensor late, configures RAW10 capture and planar RGB output, and waits before streaming. After a frame is dequeued, it routes MIPI to the display. Before requeueing, it routes back to the camera and restarts the stream. + +Use the supplied sample and pinned SDK overlay together. A successful chip-ID probe confirms only the sensor connection; capture still requires the complete initialization and routing sequence. + +## Keep future ports consistent + +Treat a dependency or source refresh as a new configuration to validate. The separate public FVP example has a different model interface and newer Arm backend passes; its successful execution does not validate this board application. + +The supplied patch selects released PyTorch and TorchAO packages by default. Keep its installer and source checkout together when recreating the environment, and record the resolved versions with your validation results. Re-export the model and rebuild firmware together after changing dependencies. + +Zero or unavailable counters in fast FVP mode do not prove CPU fallback. Inspect delegation and completed execution separately. Compiler cycle estimates, timed simulator counters, and physical-device inference duration describe different measurements; retain that distinction when comparing changes. + +## What you've accomplished + +You've exported a quantized MobileSAM program, built its Zephyr application, and programmed the model and firmware on an Alif Ensemble E8. You've also checked the camera, inference, and display stages separately, so you can identify where a later deployment stops making progress. + +## Extend the application + +Choose one change for your next experiment: + +- Change the prompt grid, re-export, and compare mask coverage on representative scenes +- Change input resolution and repeat the quality, scratch-capacity, linker, and target-output checks +- Measure capture, preprocessing, inference, mask selection, and display separately to locate the largest part of the frame time +- Refresh dependencies or vendor revisions in a separate workspace and repeat export, build, model readback, and live execution + +Keep the resolved dependencies, model and firmware hashes, programming log, memory report, and output comparisons with each run. Record the scene, prompt grid, and timing boundary so you can compare results after a change. diff --git a/content/learning-paths/embedded-and-microcontrollers/deploy-mobilesam-on-alif-e8/understand-the-system.md b/content/learning-paths/embedded-and-microcontrollers/deploy-mobilesam-on-alif-e8/understand-the-system.md new file mode 100644 index 0000000000..130dc547a3 --- /dev/null +++ b/content/learning-paths/embedded-and-microcontrollers/deploy-mobilesam-on-alif-e8/understand-the-system.md @@ -0,0 +1,47 @@ +--- +title: Understand the live segmentation pipeline +description: Trace a camera frame through MobileSAM, mask selection, and display on the Alif E8. +weight: 2 +layout: learningpathall +--- + +## Run live segmentation on the E8 + +You'll deploy MobileSAM on an Alif Ensemble E8 Development Kit to capture camera frames, select segmentation masks, and draw them on the board's display. The prompts are fixed during export; the camera image changes at runtime. You'll prepare the supplied source, export the model, build the application, and program the model and firmware separately before validating the live loop. + +Zephyr and ExecuTorch run on the board's Cortex-M55 High-Performance (HP) core, with MobileSAM computation delegated to the Ethos-U85-256 neural processing unit (NPU). An MT9M114 selfie camera supplies images. The 480 × 800 display shows the image, colored masks, inference time, and selected-mask count. + +![Alif E8 display showing a camera scene with three colored segmentation regions and a 1340 ms inference value. Check the displayed masks and reported inference time after deployment.#center](alif-live-demo.jpg "MobileSAM segmentation on the Alif Ensemble E8") + +{{% notice Note %}} +This draft describes a working board deployment. The supplied source uses released PyTorch and TorchAO packages for host-side export. Installation and the four-prompt model export have been checked in a fresh Python environment with these releases. A firmware build and hardware run with the released dependency setup have not yet been repeated. +{{% /notice %}} + +## Follow a frame through the application + +The model uses a 384 × 384 input, four independent positive prompts, three candidate masks per prompt, and int8 model input and outputs. The deployed program occupied 13,293,120 bytes in external Octal SPI (OSPI) flash, mapped at `0xC0000000`. The `.pte` is stored separately from firmware, so exporting a new model changes a separately programmed artifact. + +Follow one frame through these four tasks: + +1. **Capture and prepare the image.** The camera pipeline writes planar RGB into the model input buffer. The CPU normalizes each channel using the exported pixel mean and standard deviation, then applies the exported scale and zero point to produce a `[1, 3, 384, 384]` int8 tensor. Raw camera bytes and quantized model inputs have different meanings, even though both occupy one byte per value. +2. **Run the exported program.** The image encoder produces image embeddings, and the mask decoder combines them with prompt embeddings prepared during export. Quantization uses 8-bit activations and weights (A8W8) globally, with 16-bit activations and 8-bit weights (A16W8) in the image encoder's attention modules to preserve mask quality. The program exposes int8 image input, mask logits, and quality scores; internal attention precision does not change that interface. +3. **Select distinct masks.** Four independent positive prompts sit at `(96, 96)`, `(288, 96)`, `(96, 288)`, and `(288, 288)` in the 384 × 384 image. Each requests three candidates: twelve proposals altogether. The CPU chooses the highest-quality candidate per point, requires quality at least `0.8`, and rejects masks covering less than roughly 0.5% or more than 85% of the mask image. It sorts survivors by quality and removes a proposal when its intersection over union with an already selected mask exceeds `0.75`. At most four masks reach the display. This count describes selected masks, without assigning object categories. +4. **Render and capture again.** The CPU interprets quantized logits, applies the mask threshold, scales the image and overlays for the dashboard, and releases the camera buffer. Camera capture and display share the board's external MIPI route. The application switches that route after capture and restores it before the next frame, so capture and display alternate. + +## Keep export and firmware inputs together + +Keep the `.pte` and its exporter JSON together. CMake generates `model_metadata.h` from the JSON. Image dimensions, preprocessing, quantization parameters, prompt count, and quality threshold form the firmware contract. Prompt embeddings are constants in the `.pte`; editing JSON coordinates cannot move them. Re-export to change prompts or resolution, regenerate firmware metadata, and program the matching model. + +The supplied patch provides the matching live-camera sample and exporter. The main application files are `zephyr/samples/mobilesam-ethosu/src/{main,camera,display}.cpp`, its `CMakeLists.txt`, and `examples/arm/mobilesam_prompt_segmentation_example_ethos_u/model_export/export_mobilesam.py`. Use that source throughout this Learning Path. The separate MobileSAM FVP example uses a 448 × 448 image, floating-point input and output, and a single prompt. + +## Interpret the displayed inference time + +The example dashboard shows approximately **1.34 seconds** around `method.execute()`. Capture, normalization and quantization, mask selection, and display rendering occur outside that interval. Measure the full loop separately to determine camera-to-display latency. + +Advancing frames and changing masks confirm that capture, inference, and display run together. They don't establish segmentation accuracy on new scenes. The validation lesson explains how to check the model contract and distinguish output agreement from scene accuracy. + +## What you've learned and what's next + +You now know how a camera frame reaches the NPU and display, why prompt coordinates are fixed, and which model and metadata files must agree. Before continuing, explain which artifact changes when you move a prompt, why int8 camera input still needs preprocessing, and why four prompts can yield fewer than four displayed masks. + +Next, you'll prepare the matching source and tools and check the board's programming connection. diff --git a/content/learning-paths/embedded-and-microcontrollers/deploy-mobilesam-on-alif-e8/validate-live-segmentation.md b/content/learning-paths/embedded-and-microcontrollers/deploy-mobilesam-on-alif-e8/validate-live-segmentation.md new file mode 100644 index 0000000000..67f4b6c58f --- /dev/null +++ b/content/learning-paths/embedded-and-microcontrollers/deploy-mobilesam-on-alif-e8/validate-live-segmentation.md @@ -0,0 +1,84 @@ +--- +title: Validate the live camera and segmentation loop +description: Confirm camera startup, advancing frames, inference completion, and meaningful mask output on physical E8 hardware. +weight: 7 +layout: learningpathall +--- + +## Establish each stage of execution + +After flashing the final firmware and power-cycling the board, allow the sensor to stabilize. The sample includes a seven-second delay before its first camera stream. A slow first frame during this interval is expected. + +The display should show the live camera scene, colored segmentation overlays, inference duration, and selected-object count. Move an object between quadrants and observe whether new frames and masks appear. A successful build or model-load message alone does not establish live segmentation. + +Use these checkpoints in order: + +1. OSPI is ready and the model opens at the configured address and size. +2. The method loads and exposes an int8 image input with the expected byte count. +3. The MT9M114 sensor initializes and the camera stream produces a frame. +4. `method.execute()` returns successfully. +5. Mask and quality tensors have the expected types and shapes. +6. The display updates, the frame counter advances, and buffers are requeued. + +When an application console is routed and available, the source prints messages similar to: + +```output +Camera ready: MT9M114 to planar RGB 384x384 +Frame 9: 1340 ms, 2 objects +``` + +These are expected source-generated messages. The reference deployment was confirmed through debugger reads of diagnostic memory; these lines illustrate the console format. + +## Inspect progress through the debugger + +The supplied sample exposes a 32-bit `camera_diagnostics` array. Entries 19, 20, and 21 contain the frame number, inference milliseconds, and selected-mask count. The default build does not include GDB type information, so resolve the symbol from the exact ELF you flashed and read its words through J-Link Commander. + +Use the `nm` executable from your Zephyr SDK to locate the symbol, then create a [J-Link command file](https://kb.segger.com/J-Link_Commander#Batch_processing): + +```bash +( +set -e +diagnostic_symbol=$("$ZEPHYR_SDK_INSTALL_DIR/arm-zephyr-eabi/bin/arm-zephyr-eabi-nm" \ + --defined-only "$MOBILE_SAM_OUTPUT/build-mobilesam-live/zephyr/zephyr.elf" | \ + awk '$3 == "camera_diagnostics" {print $1}') +test -n "$diagnostic_symbol" +diagnostic_address=$((0x${diagnostic_symbol} + 19 * 4)) +printf 'halt\nmem32 0x%x 3\ng\nexit\n' "$diagnostic_address" \ + > "$MOBILE_SAM_OUTPUT/read-camera.jlink" +) +``` + +Close other J-Link sessions and take a snapshot. The command file halts for the read and resumes with `g` before disconnecting; it does not reset or program the board: + +```bash +"$JLINK_COMMANDER" -device "$ALIF_DEVICE" -if SWD -speed 4000 \ + -autoconnect 1 -ExitOnError 1 \ + -CommandFile "$MOBILE_SAM_OUTPUT/read-camera.jlink" +``` + +In the three-word `mem32` result, interpret the values in order as hexadecimal frame count, milliseconds, and mask count. For example, `00000004 0000053C 00000001` means frame 4, 1340 ms, and one mask. The address depends on your ELF; never reuse an address from a different build. + +Let several more frames complete, run the snapshot command again, and confirm that the first word increases. The validated board run recorded: + +| Snapshot | Frame number | Inference milliseconds | Selected masks | +| --- | ---: | ---: | ---: | +| First advancing live snapshot | 4 | 1340 | 1 | +| Later advancing live snapshot | 9 | 1340 | 2 | + +The reference run used J-Link memory reads. The symbol lookup and generated command file have been checked against the retained ELF; a fresh board run remains pending. + +The timer starts immediately before `method.execute()` and stops immediately after it. The 1340 ms excludes capture, camera quantization, mask selection, rendering, and frame requeue. Measure the complete loop separately if you need camera-to-display latency or throughput. + +## Separate output agreement from scene accuracy + +For a stored image, compare floating-point and quantized-host masks first, then compare target output with the matching quantized-host result. This separates quantization error from later deployment discrepancies. + +For the live camera application, a changing overlay confirms that the pipeline runs, but it does not prove pixel accuracy. To evaluate accuracy, retain representative frames and compare masks with a reference or labels using the same preprocessing and prompt coordinates. Counted masks are prompt-selected regions, not semantic object classes, and the four-point grid can miss objects between prompts. + +Do not use the `PTE flash verification: passed` startup string as a checksum result. In the final source it is printed without performing a full model checksum. The programming chapter's readback verification is the relevant byte-integrity evidence. + +Before finishing, identify the evidence you have for each checkpoint and record your source revision, model hash, scene, prompt grid, and timing boundary with the result. + +## What you've accomplished + +You've checked the capture, inference, and display stages and identified what the timing and selected-mask count measure. Use the final lesson to diagnose a failed checkpoint or choose a follow-up experiment.