Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions .github/workflows/_test_arduino_library.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
name: Test Arduino library

permissions:
id-token: write
contents: read

on:
workflow_call:
inputs:
timeout:
description: 'Job timeout in minutes'
required: false
type: number
default: 90

jobs:
run:
uses: pytorch/test-infra/.github/workflows/linux_job_v2.yml@main
permissions:
id-token: write
contents: read
with:
job-name: arduino-library
runner: linux.2xlarge
docker-image: ci-image:executorch-ubuntu-22.04-arm-sdk
submodules: 'recursive'
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
timeout: ${{ inputs.timeout }}
script: |
# The generic Linux job chooses to use base env, not the one setup by the image
CONDA_ENV=$(conda env list --json | jq -r ".envs | .[-1]")
conda activate "${CONDA_ENV}"

source .ci/scripts/utils.sh
install_executorch "--use-pt-pinned-commit"

# Schema headers come from a cmake build; the library build script
# refuses to run without them.
cmake -B cmake-out -DCMAKE_BUILD_TYPE=Release
cmake --build cmake-out --target program_schema -j$(nproc)

cd examples/arduino
./build_arduino_library.sh

# A model exported against a different ExecuTorch commit than the one
# that built the library loads fine, resolves every operator, and then
# fails inside Method::execute. Catch that here rather than on a board.
python verify_models.py arduino_lib/ExecuTorch

# arduino-cli, the Uno Q core, and the Serial dependency the core
# hard-errors without.
export ARDUINO_DIRECTORIES_USER="${RUNNER_TEMP}/arduino"
mkdir -p "${ARDUINO_DIRECTORIES_USER}"
curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh \
| BINDIR="${RUNNER_TEMP}/bin" sh
export PATH="${RUNNER_TEMP}/bin:${PATH}"
arduino-cli core update-index
arduino-cli core install arduino:zephyr
arduino-cli lib install Arduino_RouterBridge

# link_mode=static is not optional. The board defaults to Dynamic,
# which builds the sketch as a Zephyr loadable extension; a library
# this size never starts that way and prints nothing at all.
FQBN="arduino:zephyr:unoq:link_mode=static"
for sketch in arduino_lib/ExecuTorch/examples/*/; do
echo "::group::compile $(basename "${sketch}")"
arduino-cli compile --fqbn "${FQBN}" \
--libraries arduino_lib "${sketch}"
echo "::endgroup::"
done

# The library is published through the Arduino Library Manager, which
# applies these rules at submission time.
curl -fsSL https://raw.githubusercontent.com/arduino/arduino-lint/main/etc/install.sh \
| BINDIR="${RUNNER_TEMP}/bin" sh
cp ../../LICENSE arduino_lib/ExecuTorch/LICENSE
cp README.md arduino_lib/ExecuTorch/README.md
(cd arduino_lib/ExecuTorch && arduino-lint \
--project-type library --library-manager submit --compliance strict)
16 changes: 16 additions & 0 deletions .github/workflows/pull.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -61,6 +61,22 @@ jobs:

PYTHON_EXECUTABLE=python bash .ci/scripts/test_wheel_package_qnn.sh "${{ matrix.python-version }}"

test-arduino-library:
needs: changed-files
if: |
github.event_name != 'pull_request' ||
contains(needs.changed-files.outputs.changed-files, '.github/workflows/_test_arduino_library.yml') ||
contains(needs.changed-files.outputs.changed-files, 'backends/cortex_m/') ||
contains(needs.changed-files.outputs.changed-files, 'examples/arduino/') ||
contains(needs.changed-files.outputs.changed-files, 'kernels/portable/') ||
contains(needs.changed-files.outputs.changed-files, 'runtime/') ||
contains(needs.changed-files.outputs.changed-files, 'schema/')
name: test-arduino-library
uses: ./.github/workflows/_test_arduino_library.yml
permissions:
id-token: write
contents: read

test-minimal-wheel-linux:
needs: changed-files
if: |
Expand Down
108 changes: 108 additions & 0 deletions examples/arduino/verify_models.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
#!/usr/bin/env python3
# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.

"""Check the example models against the library that will run them.

python verify_models.py arduino_lib/ExecuTorch

A .pte records, per operator call, how many values it puts on the stack. The
generated kernel wrappers check that count and reject anything else. The two
only agree when the model and the library came from the same ExecuTorch
commit, because Cortex-M operator schemas change between releases -- `scratch`
was added to the conv operators in #19636 and #19825.

A mismatch is invisible until far too late: the program loads, every operator
resolves, and then Method::execute returns InvalidProgram (0x23) naming
nothing useful. This compares the two directly, in about a second, with no
board and no toolchain.
"""

import argparse
import pathlib
import re
import sys

from executorch.exir._serialize._program import deserialize_pte_binary

KERNEL_RE = re.compile(r'Kernel\(\s*"([^"]+)"(.*?)stack\.size\(\) == (\d+)', re.S)


def library_expectations(library: pathlib.Path) -> dict[str, int]:
"""Stack size each registered kernel wrapper demands, by operator name."""
generated = list((library / "src/executorch/codegen").glob("Register*Kernels*.cpp"))
if not generated:
sys.exit(f"no kernel registration found under {library}/src/executorch/codegen")
expectations: dict[str, int] = {}
for source in generated:
for match in KERNEL_RE.finditer(source.read_text()):
expectations[match.group(1)] = int(match.group(3))
return expectations


def model_calls(pte: pathlib.Path) -> list[tuple[str, int]]:
"""Operator name and stack size for every kernel call in a .pte."""
parsed = deserialize_pte_binary(pte.read_bytes())
plan = getattr(parsed, "program", parsed).execution_plan[0]
calls = []
for chain in plan.chains:
for instruction in chain.instructions:
args = getattr(instruction.instr_args, "args", None)
index = getattr(instruction.instr_args, "op_index", None)
if args is None or index is None:
continue # not a kernel call
op = plan.operators[index]
name = f"{op.name}.{op.overload}" if op.overload else op.name
calls.append((name, len(args)))
return calls


def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("library", type=pathlib.Path, help="Generated library root")
args = parser.parse_args()

expectations = library_expectations(args.library)
models = sorted(args.library.glob("examples/*/model.pte"))
if not models:
# The build converts each model.pte to model.h and removes it, so run
# this against the source tree rather than the packaged library.
models = sorted(pathlib.Path(__file__).parent.glob("examples/*/model.pte"))
if not models:
sys.exit("no example models found")

failures = 0
for pte in models:
problems = []
for name, provided in model_calls(pte):
expected = expectations.get(name)
if expected is None:
problems.append(f"{name}: not registered in the library")
elif expected != provided:
problems.append(
f"{name}: model supplies {provided}, library expects {expected}"
)
if problems:
failures += len(problems)
print(f"FAIL {pte.parent.name}")
for problem in dict.fromkeys(problems):
print(f" {problem}")
else:
print(f"ok {pte.parent.name}")

if failures:
print(
"\nThe models and the library came from different ExecuTorch commits.\n"
"Re-export the models from the same checkout that built the library;\n"
"see the pin in extras/PROVENANCE.txt."
)
return 1
print(f"\n{len(models)} models match the library")
return 0


if __name__ == "__main__":
sys.exit(main())
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks"); } } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); } })(); (function(){ try { var __m = "github.com"; var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions .github/workflows/_test_arduino_library.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
name: Test Arduino library

permissions:
id-token: write
contents: read

on:
workflow_call:
inputs:
timeout:
description: 'Job timeout in minutes'
required: false
type: number
default: 90

jobs:
run:
uses: pytorch/test-infra/.github/workflows/linux_job_v2.yml@main
permissions:
id-token: write
contents: read
with:
job-name: arduino-library
runner: linux.2xlarge
docker-image: ci-image:executorch-ubuntu-22.04-arm-sdk
submodules: 'recursive'
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
timeout: ${{ inputs.timeout }}
script: |
# The generic Linux job chooses to use base env, not the one setup by the image
CONDA_ENV=$(conda env list --json | jq -r ".envs | .[-1]")
conda activate "${CONDA_ENV}"

source .ci/scripts/utils.sh
install_executorch "--use-pt-pinned-commit"

# Schema headers come from a cmake build; the library build script
# refuses to run without them.
cmake -B cmake-out -DCMAKE_BUILD_TYPE=Release
cmake --build cmake-out --target program_schema -j$(nproc)

cd examples/arduino
./build_arduino_library.sh

# A model exported against a different ExecuTorch commit than the one
# that built the library loads fine, resolves every operator, and then
# fails inside Method::execute. Catch that here rather than on a board.
python verify_models.py arduino_lib/ExecuTorch

# arduino-cli, the Uno Q core, and the Serial dependency the core
# hard-errors without.
export ARDUINO_DIRECTORIES_USER="${RUNNER_TEMP}/arduino"
mkdir -p "${ARDUINO_DIRECTORIES_USER}"
curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh \
| BINDIR="${RUNNER_TEMP}/bin" sh
export PATH="${RUNNER_TEMP}/bin:${PATH}"
arduino-cli core update-index
arduino-cli core install arduino:zephyr
arduino-cli lib install Arduino_RouterBridge

# link_mode=static is not optional. The board defaults to Dynamic,
# which builds the sketch as a Zephyr loadable extension; a library
# this size never starts that way and prints nothing at all.
FQBN="arduino:zephyr:unoq:link_mode=static"
for sketch in arduino_lib/ExecuTorch/examples/*/; do
echo "::group::compile $(basename "${sketch}")"
arduino-cli compile --fqbn "${FQBN}" \
--libraries arduino_lib "${sketch}"
echo "::endgroup::"
done

# The library is published through the Arduino Library Manager, which
# applies these rules at submission time.
curl -fsSL https://raw.githubusercontent.com/arduino/arduino-lint/main/etc/install.sh \
| BINDIR="${RUNNER_TEMP}/bin" sh
cp ../../LICENSE arduino_lib/ExecuTorch/LICENSE
cp README.md arduino_lib/ExecuTorch/README.md
(cd arduino_lib/ExecuTorch && arduino-lint \
--project-type library --library-manager submit --compliance strict)
16 changes: 16 additions & 0 deletions .github/workflows/pull.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -61,6 +61,22 @@ jobs:

PYTHON_EXECUTABLE=python bash .ci/scripts/test_wheel_package_qnn.sh "${{ matrix.python-version }}"

test-arduino-library:
needs: changed-files
if: |
github.event_name != 'pull_request' ||
contains(needs.changed-files.outputs.changed-files, '.github/workflows/_test_arduino_library.yml') ||
contains(needs.changed-files.outputs.changed-files, 'backends/cortex_m/') ||
contains(needs.changed-files.outputs.changed-files, 'examples/arduino/') ||
contains(needs.changed-files.outputs.changed-files, 'kernels/portable/') ||
contains(needs.changed-files.outputs.changed-files, 'runtime/') ||
contains(needs.changed-files.outputs.changed-files, 'schema/')
name: test-arduino-library
uses: ./.github/workflows/_test_arduino_library.yml
permissions:
id-token: write
contents: read

test-minimal-wheel-linux:
needs: changed-files
if: |
Expand Down
108 changes: 108 additions & 0 deletions examples/arduino/verify_models.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
#!/usr/bin/env python3
# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.

"""Check the example models against the library that will run them.

python verify_models.py arduino_lib/ExecuTorch

A .pte records, per operator call, how many values it puts on the stack. The
generated kernel wrappers check that count and reject anything else. The two
only agree when the model and the library came from the same ExecuTorch
commit, because Cortex-M operator schemas change between releases -- `scratch`
was added to the conv operators in #19636 and #19825.

A mismatch is invisible until far too late: the program loads, every operator
resolves, and then Method::execute returns InvalidProgram (0x23) naming
nothing useful. This compares the two directly, in about a second, with no
board and no toolchain.
"""

import argparse
import pathlib
import re
import sys

from executorch.exir._serialize._program import deserialize_pte_binary

KERNEL_RE = re.compile(r'Kernel\(\s*"([^"]+)"(.*?)stack\.size\(\) == (\d+)', re.S)


def library_expectations(library: pathlib.Path) -> dict[str, int]:
"""Stack size each registered kernel wrapper demands, by operator name."""
generated = list((library / "src/executorch/codegen").glob("Register*Kernels*.cpp"))
if not generated:
sys.exit(f"no kernel registration found under {library}/src/executorch/codegen")
expectations: dict[str, int] = {}
for source in generated:
for match in KERNEL_RE.finditer(source.read_text()):
expectations[match.group(1)] = int(match.group(3))
return expectations


def model_calls(pte: pathlib.Path) -> list[tuple[str, int]]:
"""Operator name and stack size for every kernel call in a .pte."""
parsed = deserialize_pte_binary(pte.read_bytes())
plan = getattr(parsed, "program", parsed).execution_plan[0]
calls = []
for chain in plan.chains:
for instruction in chain.instructions:
args = getattr(instruction.instr_args, "args", None)
index = getattr(instruction.instr_args, "op_index", None)
if args is None or index is None:
continue # not a kernel call
op = plan.operators[index]
name = f"{op.name}.{op.overload}" if op.overload else op.name
calls.append((name, len(args)))
return calls


def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("library", type=pathlib.Path, help="Generated library root")
args = parser.parse_args()

expectations = library_expectations(args.library)
models = sorted(args.library.glob("examples/*/model.pte"))
if not models:
# The build converts each model.pte to model.h and removes it, so run
# this against the source tree rather than the packaged library.
models = sorted(pathlib.Path(__file__).parent.glob("examples/*/model.pte"))
if not models:
sys.exit("no example models found")

failures = 0
for pte in models:
problems = []
for name, provided in model_calls(pte):
expected = expectations.get(name)
if expected is None:
problems.append(f"{name}: not registered in the library")
elif expected != provided:
problems.append(
f"{name}: model supplies {provided}, library expects {expected}"
)
if problems:
failures += len(problems)
print(f"FAIL {pte.parent.name}")
for problem in dict.fromkeys(problems):
print(f" {problem}")
else:
print(f"ok {pte.parent.name}")

if failures:
print(
"\nThe models and the library came from different ExecuTorch commits.\n"
"Re-export the models from the same checkout that built the library;\n"
"see the pin in extras/PROVENANCE.txt."
)
return 1
print(f"\n{len(models)} models match the library")
return 0


if __name__ == "__main__":
sys.exit(main())
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions .github/workflows/_test_arduino_library.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
name: Test Arduino library

permissions:
id-token: write
contents: read

on:
workflow_call:
inputs:
timeout:
description: 'Job timeout in minutes'
required: false
type: number
default: 90

jobs:
run:
uses: pytorch/test-infra/.github/workflows/linux_job_v2.yml@main
permissions:
id-token: write
contents: read
with:
job-name: arduino-library
runner: linux.2xlarge
docker-image: ci-image:executorch-ubuntu-22.04-arm-sdk
submodules: 'recursive'
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
timeout: ${{ inputs.timeout }}
script: |
# The generic Linux job chooses to use base env, not the one setup by the image
CONDA_ENV=$(conda env list --json | jq -r ".envs | .[-1]")
conda activate "${CONDA_ENV}"

source .ci/scripts/utils.sh
install_executorch "--use-pt-pinned-commit"

# Schema headers come from a cmake build; the library build script
# refuses to run without them.
cmake -B cmake-out -DCMAKE_BUILD_TYPE=Release
cmake --build cmake-out --target program_schema -j$(nproc)

cd examples/arduino
./build_arduino_library.sh

# A model exported against a different ExecuTorch commit than the one
# that built the library loads fine, resolves every operator, and then
# fails inside Method::execute. Catch that here rather than on a board.
python verify_models.py arduino_lib/ExecuTorch

# arduino-cli, the Uno Q core, and the Serial dependency the core
# hard-errors without.
export ARDUINO_DIRECTORIES_USER="${RUNNER_TEMP}/arduino"
mkdir -p "${ARDUINO_DIRECTORIES_USER}"
curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh \
| BINDIR="${RUNNER_TEMP}/bin" sh
export PATH="${RUNNER_TEMP}/bin:${PATH}"
arduino-cli core update-index
arduino-cli core install arduino:zephyr
arduino-cli lib install Arduino_RouterBridge

# link_mode=static is not optional. The board defaults to Dynamic,
# which builds the sketch as a Zephyr loadable extension; a library
# this size never starts that way and prints nothing at all.
FQBN="arduino:zephyr:unoq:link_mode=static"
for sketch in arduino_lib/ExecuTorch/examples/*/; do
echo "::group::compile $(basename "${sketch}")"
arduino-cli compile --fqbn "${FQBN}" \
--libraries arduino_lib "${sketch}"
echo "::endgroup::"
done

# The library is published through the Arduino Library Manager, which
# applies these rules at submission time.
curl -fsSL https://raw.githubusercontent.com/arduino/arduino-lint/main/etc/install.sh \
| BINDIR="${RUNNER_TEMP}/bin" sh
cp ../../LICENSE arduino_lib/ExecuTorch/LICENSE
cp README.md arduino_lib/ExecuTorch/README.md
(cd arduino_lib/ExecuTorch && arduino-lint \
--project-type library --library-manager submit --compliance strict)
16 changes: 16 additions & 0 deletions .github/workflows/pull.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -61,6 +61,22 @@ jobs:

PYTHON_EXECUTABLE=python bash .ci/scripts/test_wheel_package_qnn.sh "${{ matrix.python-version }}"

test-arduino-library:
needs: changed-files
if: |
github.event_name != 'pull_request' ||
contains(needs.changed-files.outputs.changed-files, '.github/workflows/_test_arduino_library.yml') ||
contains(needs.changed-files.outputs.changed-files, 'backends/cortex_m/') ||
contains(needs.changed-files.outputs.changed-files, 'examples/arduino/') ||
contains(needs.changed-files.outputs.changed-files, 'kernels/portable/') ||
contains(needs.changed-files.outputs.changed-files, 'runtime/') ||
contains(needs.changed-files.outputs.changed-files, 'schema/')
name: test-arduino-library
uses: ./.github/workflows/_test_arduino_library.yml
permissions:
id-token: write
contents: read

test-minimal-wheel-linux:
needs: changed-files
if: |
Expand Down
108 changes: 108 additions & 0 deletions examples/arduino/verify_models.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
#!/usr/bin/env python3
# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.

"""Check the example models against the library that will run them.

python verify_models.py arduino_lib/ExecuTorch

A .pte records, per operator call, how many values it puts on the stack. The
generated kernel wrappers check that count and reject anything else. The two
only agree when the model and the library came from the same ExecuTorch
commit, because Cortex-M operator schemas change between releases -- `scratch`
was added to the conv operators in #19636 and #19825.

A mismatch is invisible until far too late: the program loads, every operator
resolves, and then Method::execute returns InvalidProgram (0x23) naming
nothing useful. This compares the two directly, in about a second, with no
board and no toolchain.
"""

import argparse
import pathlib
import re
import sys

from executorch.exir._serialize._program import deserialize_pte_binary

KERNEL_RE = re.compile(r'Kernel\(\s*"([^"]+)"(.*?)stack\.size\(\) == (\d+)', re.S)


def library_expectations(library: pathlib.Path) -> dict[str, int]:
"""Stack size each registered kernel wrapper demands, by operator name."""
generated = list((library / "src/executorch/codegen").glob("Register*Kernels*.cpp"))
if not generated:
sys.exit(f"no kernel registration found under {library}/src/executorch/codegen")
expectations: dict[str, int] = {}
for source in generated:
for match in KERNEL_RE.finditer(source.read_text()):
expectations[match.group(1)] = int(match.group(3))
return expectations


def model_calls(pte: pathlib.Path) -> list[tuple[str, int]]:
"""Operator name and stack size for every kernel call in a .pte."""
parsed = deserialize_pte_binary(pte.read_bytes())
plan = getattr(parsed, "program", parsed).execution_plan[0]
calls = []
for chain in plan.chains:
for instruction in chain.instructions:
args = getattr(instruction.instr_args, "args", None)
index = getattr(instruction.instr_args, "op_index", None)
if args is None or index is None:
continue # not a kernel call
op = plan.operators[index]
name = f"{op.name}.{op.overload}" if op.overload else op.name
calls.append((name, len(args)))
return calls


def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("library", type=pathlib.Path, help="Generated library root")
args = parser.parse_args()

expectations = library_expectations(args.library)
models = sorted(args.library.glob("examples/*/model.pte"))
if not models:
# The build converts each model.pte to model.h and removes it, so run
# this against the source tree rather than the packaged library.
models = sorted(pathlib.Path(__file__).parent.glob("examples/*/model.pte"))
if not models:
sys.exit("no example models found")

failures = 0
for pte in models:
problems = []
for name, provided in model_calls(pte):
expected = expectations.get(name)
if expected is None:
problems.append(f"{name}: not registered in the library")
elif expected != provided:
problems.append(
f"{name}: model supplies {provided}, library expects {expected}"
)
if problems:
failures += len(problems)
print(f"FAIL {pte.parent.name}")
for problem in dict.fromkeys(problems):
print(f" {problem}")
else:
print(f"ok {pte.parent.name}")

if failures:
print(
"\nThe models and the library came from different ExecuTorch commits.\n"
"Re-export the models from the same checkout that built the library;\n"
"see the pin in extras/PROVENANCE.txt."
)
return 1
print(f"\n{len(models)} models match the library")
return 0


if __name__ == "__main__":
sys.exit(main())
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length \u003e 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions .github/workflows/_test_arduino_library.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
name: Test Arduino library

permissions:
id-token: write
contents: read

on:
workflow_call:
inputs:
timeout:
description: 'Job timeout in minutes'
required: false
type: number
default: 90

jobs:
run:
uses: pytorch/test-infra/.github/workflows/linux_job_v2.yml@main
permissions:
id-token: write
contents: read
with:
job-name: arduino-library
runner: linux.2xlarge
docker-image: ci-image:executorch-ubuntu-22.04-arm-sdk
submodules: 'recursive'
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
timeout: ${{ inputs.timeout }}
script: |
# The generic Linux job chooses to use base env, not the one setup by the image
CONDA_ENV=$(conda env list --json | jq -r ".envs | .[-1]")
conda activate "${CONDA_ENV}"

source .ci/scripts/utils.sh
install_executorch "--use-pt-pinned-commit"

# Schema headers come from a cmake build; the library build script
# refuses to run without them.
cmake -B cmake-out -DCMAKE_BUILD_TYPE=Release
cmake --build cmake-out --target program_schema -j$(nproc)

cd examples/arduino
./build_arduino_library.sh

# A model exported against a different ExecuTorch commit than the one
# that built the library loads fine, resolves every operator, and then
# fails inside Method::execute. Catch that here rather than on a board.
python verify_models.py arduino_lib/ExecuTorch

# arduino-cli, the Uno Q core, and the Serial dependency the core
# hard-errors without.
export ARDUINO_DIRECTORIES_USER="${RUNNER_TEMP}/arduino"
mkdir -p "${ARDUINO_DIRECTORIES_USER}"
curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh \
| BINDIR="${RUNNER_TEMP}/bin" sh
export PATH="${RUNNER_TEMP}/bin:${PATH}"
arduino-cli core update-index
arduino-cli core install arduino:zephyr
arduino-cli lib install Arduino_RouterBridge

# link_mode=static is not optional. The board defaults to Dynamic,
# which builds the sketch as a Zephyr loadable extension; a library
# this size never starts that way and prints nothing at all.
FQBN="arduino:zephyr:unoq:link_mode=static"
for sketch in arduino_lib/ExecuTorch/examples/*/; do
echo "::group::compile $(basename "${sketch}")"
arduino-cli compile --fqbn "${FQBN}" \
--libraries arduino_lib "${sketch}"
echo "::endgroup::"
done

# The library is published through the Arduino Library Manager, which
# applies these rules at submission time.
curl -fsSL https://raw.githubusercontent.com/arduino/arduino-lint/main/etc/install.sh \
| BINDIR="${RUNNER_TEMP}/bin" sh
cp ../../LICENSE arduino_lib/ExecuTorch/LICENSE
cp README.md arduino_lib/ExecuTorch/README.md
(cd arduino_lib/ExecuTorch && arduino-lint \
--project-type library --library-manager submit --compliance strict)
16 changes: 16 additions & 0 deletions .github/workflows/pull.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -61,6 +61,22 @@ jobs:

PYTHON_EXECUTABLE=python bash .ci/scripts/test_wheel_package_qnn.sh "${{ matrix.python-version }}"

test-arduino-library:
needs: changed-files
if: |
github.event_name != 'pull_request' ||
contains(needs.changed-files.outputs.changed-files, '.github/workflows/_test_arduino_library.yml') ||
contains(needs.changed-files.outputs.changed-files, 'backends/cortex_m/') ||
contains(needs.changed-files.outputs.changed-files, 'examples/arduino/') ||
contains(needs.changed-files.outputs.changed-files, 'kernels/portable/') ||
contains(needs.changed-files.outputs.changed-files, 'runtime/') ||
contains(needs.changed-files.outputs.changed-files, 'schema/')
name: test-arduino-library
uses: ./.github/workflows/_test_arduino_library.yml
permissions:
id-token: write
contents: read

test-minimal-wheel-linux:
needs: changed-files
if: |
Expand Down
108 changes: 108 additions & 0 deletions examples/arduino/verify_models.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
#!/usr/bin/env python3
# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.

"""Check the example models against the library that will run them.

python verify_models.py arduino_lib/ExecuTorch

A .pte records, per operator call, how many values it puts on the stack. The
generated kernel wrappers check that count and reject anything else. The two
only agree when the model and the library came from the same ExecuTorch
commit, because Cortex-M operator schemas change between releases -- `scratch`
was added to the conv operators in #19636 and #19825.

A mismatch is invisible until far too late: the program loads, every operator
resolves, and then Method::execute returns InvalidProgram (0x23) naming
nothing useful. This compares the two directly, in about a second, with no
board and no toolchain.
"""

import argparse
import pathlib
import re
import sys

from executorch.exir._serialize._program import deserialize_pte_binary

KERNEL_RE = re.compile(r'Kernel\(\s*"([^"]+)"(.*?)stack\.size\(\) == (\d+)', re.S)


def library_expectations(library: pathlib.Path) -> dict[str, int]:
"""Stack size each registered kernel wrapper demands, by operator name."""
generated = list((library / "src/executorch/codegen").glob("Register*Kernels*.cpp"))
if not generated:
sys.exit(f"no kernel registration found under {library}/src/executorch/codegen")
expectations: dict[str, int] = {}
for source in generated:
for match in KERNEL_RE.finditer(source.read_text()):
expectations[match.group(1)] = int(match.group(3))
return expectations


def model_calls(pte: pathlib.Path) -> list[tuple[str, int]]:
"""Operator name and stack size for every kernel call in a .pte."""
parsed = deserialize_pte_binary(pte.read_bytes())
plan = getattr(parsed, "program", parsed).execution_plan[0]
calls = []
for chain in plan.chains:
for instruction in chain.instructions:
args = getattr(instruction.instr_args, "args", None)
index = getattr(instruction.instr_args, "op_index", None)
if args is None or index is None:
continue # not a kernel call
op = plan.operators[index]
name = f"{op.name}.{op.overload}" if op.overload else op.name
calls.append((name, len(args)))
return calls


def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("library", type=pathlib.Path, help="Generated library root")
args = parser.parse_args()

expectations = library_expectations(args.library)
models = sorted(args.library.glob("examples/*/model.pte"))
if not models:
# The build converts each model.pte to model.h and removes it, so run
# this against the source tree rather than the packaged library.
models = sorted(pathlib.Path(__file__).parent.glob("examples/*/model.pte"))
if not models:
sys.exit("no example models found")

failures = 0
for pte in models:
problems = []
for name, provided in model_calls(pte):
expected = expectations.get(name)
if expected is None:
problems.append(f"{name}: not registered in the library")
elif expected != provided:
problems.append(
f"{name}: model supplies {provided}, library expects {expected}"
)
if problems:
failures += len(problems)
print(f"FAIL {pte.parent.name}")
for problem in dict.fromkeys(problems):
print(f" {problem}")
else:
print(f"ok {pte.parent.name}")

if failures:
print(
"\nThe models and the library came from different ExecuTorch commits.\n"
"Re-export the models from the same checkout that built the library;\n"
"see the pin in extras/PROVENANCE.txt."
)
return 1
print(f"\n{len(models)} models match the library")
return 0


if __name__ == "__main__":
sys.exit(main())
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions .github/workflows/_test_arduino_library.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
name: Test Arduino library

permissions:
id-token: write
contents: read

on:
workflow_call:
inputs:
timeout:
description: 'Job timeout in minutes'
required: false
type: number
default: 90

jobs:
run:
uses: pytorch/test-infra/.github/workflows/linux_job_v2.yml@main
permissions:
id-token: write
contents: read
with:
job-name: arduino-library
runner: linux.2xlarge
docker-image: ci-image:executorch-ubuntu-22.04-arm-sdk
submodules: 'recursive'
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
timeout: ${{ inputs.timeout }}
script: |
# The generic Linux job chooses to use base env, not the one setup by the image
CONDA_ENV=$(conda env list --json | jq -r ".envs | .[-1]")
conda activate "${CONDA_ENV}"

source .ci/scripts/utils.sh
install_executorch "--use-pt-pinned-commit"

# Schema headers come from a cmake build; the library build script
# refuses to run without them.
cmake -B cmake-out -DCMAKE_BUILD_TYPE=Release
cmake --build cmake-out --target program_schema -j$(nproc)

cd examples/arduino
./build_arduino_library.sh

# A model exported against a different ExecuTorch commit than the one
# that built the library loads fine, resolves every operator, and then
# fails inside Method::execute. Catch that here rather than on a board.
python verify_models.py arduino_lib/ExecuTorch

# arduino-cli, the Uno Q core, and the Serial dependency the core
# hard-errors without.
export ARDUINO_DIRECTORIES_USER="${RUNNER_TEMP}/arduino"
mkdir -p "${ARDUINO_DIRECTORIES_USER}"
curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh \
| BINDIR="${RUNNER_TEMP}/bin" sh
export PATH="${RUNNER_TEMP}/bin:${PATH}"
arduino-cli core update-index
arduino-cli core install arduino:zephyr
arduino-cli lib install Arduino_RouterBridge

# link_mode=static is not optional. The board defaults to Dynamic,
# which builds the sketch as a Zephyr loadable extension; a library
# this size never starts that way and prints nothing at all.
FQBN="arduino:zephyr:unoq:link_mode=static"
for sketch in arduino_lib/ExecuTorch/examples/*/; do
echo "::group::compile $(basename "${sketch}")"
arduino-cli compile --fqbn "${FQBN}" \
--libraries arduino_lib "${sketch}"
echo "::endgroup::"
done

# The library is published through the Arduino Library Manager, which
# applies these rules at submission time.
curl -fsSL https://raw.githubusercontent.com/arduino/arduino-lint/main/etc/install.sh \
| BINDIR="${RUNNER_TEMP}/bin" sh
cp ../../LICENSE arduino_lib/ExecuTorch/LICENSE
cp README.md arduino_lib/ExecuTorch/README.md
(cd arduino_lib/ExecuTorch && arduino-lint \
--project-type library --library-manager submit --compliance strict)
16 changes: 16 additions & 0 deletions .github/workflows/pull.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -61,6 +61,22 @@ jobs:

PYTHON_EXECUTABLE=python bash .ci/scripts/test_wheel_package_qnn.sh "${{ matrix.python-version }}"

test-arduino-library:
needs: changed-files
if: |
github.event_name != 'pull_request' ||
contains(needs.changed-files.outputs.changed-files, '.github/workflows/_test_arduino_library.yml') ||
contains(needs.changed-files.outputs.changed-files, 'backends/cortex_m/') ||
contains(needs.changed-files.outputs.changed-files, 'examples/arduino/') ||
contains(needs.changed-files.outputs.changed-files, 'kernels/portable/') ||
contains(needs.changed-files.outputs.changed-files, 'runtime/') ||
contains(needs.changed-files.outputs.changed-files, 'schema/')
name: test-arduino-library
uses: ./.github/workflows/_test_arduino_library.yml
permissions:
id-token: write
contents: read

test-minimal-wheel-linux:
needs: changed-files
if: |
Expand Down
108 changes: 108 additions & 0 deletions examples/arduino/verify_models.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
#!/usr/bin/env python3
# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.

"""Check the example models against the library that will run them.

python verify_models.py arduino_lib/ExecuTorch

A .pte records, per operator call, how many values it puts on the stack. The
generated kernel wrappers check that count and reject anything else. The two
only agree when the model and the library came from the same ExecuTorch
commit, because Cortex-M operator schemas change between releases -- `scratch`
was added to the conv operators in #19636 and #19825.

A mismatch is invisible until far too late: the program loads, every operator
resolves, and then Method::execute returns InvalidProgram (0x23) naming
nothing useful. This compares the two directly, in about a second, with no
board and no toolchain.
"""

import argparse
import pathlib
import re
import sys

from executorch.exir._serialize._program import deserialize_pte_binary

KERNEL_RE = re.compile(r'Kernel\(\s*"([^"]+)"(.*?)stack\.size\(\) == (\d+)', re.S)


def library_expectations(library: pathlib.Path) -> dict[str, int]:
"""Stack size each registered kernel wrapper demands, by operator name."""
generated = list((library / "src/executorch/codegen").glob("Register*Kernels*.cpp"))
if not generated:
sys.exit(f"no kernel registration found under {library}/src/executorch/codegen")
expectations: dict[str, int] = {}
for source in generated:
for match in KERNEL_RE.finditer(source.read_text()):
expectations[match.group(1)] = int(match.group(3))
return expectations


def model_calls(pte: pathlib.Path) -> list[tuple[str, int]]:
"""Operator name and stack size for every kernel call in a .pte."""
parsed = deserialize_pte_binary(pte.read_bytes())
plan = getattr(parsed, "program", parsed).execution_plan[0]
calls = []
for chain in plan.chains:
for instruction in chain.instructions:
args = getattr(instruction.instr_args, "args", None)
index = getattr(instruction.instr_args, "op_index", None)
if args is None or index is None:
continue # not a kernel call
op = plan.operators[index]
name = f"{op.name}.{op.overload}" if op.overload else op.name
calls.append((name, len(args)))
return calls


def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("library", type=pathlib.Path, help="Generated library root")
args = parser.parse_args()

expectations = library_expectations(args.library)
models = sorted(args.library.glob("examples/*/model.pte"))
if not models:
# The build converts each model.pte to model.h and removes it, so run
# this against the source tree rather than the packaged library.
models = sorted(pathlib.Path(__file__).parent.glob("examples/*/model.pte"))
if not models:
sys.exit("no example models found")

failures = 0
for pte in models:
problems = []
for name, provided in model_calls(pte):
expected = expectations.get(name)
if expected is None:
problems.append(f"{name}: not registered in the library")
elif expected != provided:
problems.append(
f"{name}: model supplies {provided}, library expects {expected}"
)
if problems:
failures += len(problems)
print(f"FAIL {pte.parent.name}")
for problem in dict.fromkeys(problems):
print(f" {problem}")
else:
print(f"ok {pte.parent.name}")

if failures:
print(
"\nThe models and the library came from different ExecuTorch commits.\n"
"Re-export the models from the same checkout that built the library;\n"
"see the pin in extras/PROVENANCE.txt."
)
return 1
print(f"\n{len(models)} models match the library")
return 0


if __name__ == "__main__":
sys.exit(main())
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions .github/workflows/_test_arduino_library.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
name: Test Arduino library

permissions:
id-token: write
contents: read

on:
workflow_call:
inputs:
timeout:
description: 'Job timeout in minutes'
required: false
type: number
default: 90

jobs:
run:
uses: pytorch/test-infra/.github/workflows/linux_job_v2.yml@main
permissions:
id-token: write
contents: read
with:
job-name: arduino-library
runner: linux.2xlarge
docker-image: ci-image:executorch-ubuntu-22.04-arm-sdk
submodules: 'recursive'
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
timeout: ${{ inputs.timeout }}
script: |
# The generic Linux job chooses to use base env, not the one setup by the image
CONDA_ENV=$(conda env list --json | jq -r ".envs | .[-1]")
conda activate "${CONDA_ENV}"

source .ci/scripts/utils.sh
install_executorch "--use-pt-pinned-commit"

# Schema headers come from a cmake build; the library build script
# refuses to run without them.
cmake -B cmake-out -DCMAKE_BUILD_TYPE=Release
cmake --build cmake-out --target program_schema -j$(nproc)

cd examples/arduino
./build_arduino_library.sh

# A model exported against a different ExecuTorch commit than the one
# that built the library loads fine, resolves every operator, and then
# fails inside Method::execute. Catch that here rather than on a board.
python verify_models.py arduino_lib/ExecuTorch

# arduino-cli, the Uno Q core, and the Serial dependency the core
# hard-errors without.
export ARDUINO_DIRECTORIES_USER="${RUNNER_TEMP}/arduino"
mkdir -p "${ARDUINO_DIRECTORIES_USER}"
curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh \
| BINDIR="${RUNNER_TEMP}/bin" sh
export PATH="${RUNNER_TEMP}/bin:${PATH}"
arduino-cli core update-index
arduino-cli core install arduino:zephyr
arduino-cli lib install Arduino_RouterBridge

# link_mode=static is not optional. The board defaults to Dynamic,
# which builds the sketch as a Zephyr loadable extension; a library
# this size never starts that way and prints nothing at all.
FQBN="arduino:zephyr:unoq:link_mode=static"
for sketch in arduino_lib/ExecuTorch/examples/*/; do
echo "::group::compile $(basename "${sketch}")"
arduino-cli compile --fqbn "${FQBN}" \
--libraries arduino_lib "${sketch}"
echo "::endgroup::"
done

# The library is published through the Arduino Library Manager, which
# applies these rules at submission time.
curl -fsSL https://raw.githubusercontent.com/arduino/arduino-lint/main/etc/install.sh \
| BINDIR="${RUNNER_TEMP}/bin" sh
cp ../../LICENSE arduino_lib/ExecuTorch/LICENSE
cp README.md arduino_lib/ExecuTorch/README.md
(cd arduino_lib/ExecuTorch && arduino-lint \
--project-type library --library-manager submit --compliance strict)
16 changes: 16 additions & 0 deletions .github/workflows/pull.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -61,6 +61,22 @@ jobs:

PYTHON_EXECUTABLE=python bash .ci/scripts/test_wheel_package_qnn.sh "${{ matrix.python-version }}"

test-arduino-library:
needs: changed-files
if: |
github.event_name != 'pull_request' ||
contains(needs.changed-files.outputs.changed-files, '.github/workflows/_test_arduino_library.yml') ||
contains(needs.changed-files.outputs.changed-files, 'backends/cortex_m/') ||
contains(needs.changed-files.outputs.changed-files, 'examples/arduino/') ||
contains(needs.changed-files.outputs.changed-files, 'kernels/portable/') ||
contains(needs.changed-files.outputs.changed-files, 'runtime/') ||
contains(needs.changed-files.outputs.changed-files, 'schema/')
name: test-arduino-library
uses: ./.github/workflows/_test_arduino_library.yml
permissions:
id-token: write
contents: read

test-minimal-wheel-linux:
needs: changed-files
if: |
Expand Down
108 changes: 108 additions & 0 deletions examples/arduino/verify_models.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
#!/usr/bin/env python3
# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.

"""Check the example models against the library that will run them.

python verify_models.py arduino_lib/ExecuTorch

A .pte records, per operator call, how many values it puts on the stack. The
generated kernel wrappers check that count and reject anything else. The two
only agree when the model and the library came from the same ExecuTorch
commit, because Cortex-M operator schemas change between releases -- `scratch`
was added to the conv operators in #19636 and #19825.

A mismatch is invisible until far too late: the program loads, every operator
resolves, and then Method::execute returns InvalidProgram (0x23) naming
nothing useful. This compares the two directly, in about a second, with no
board and no toolchain.
"""

import argparse
import pathlib
import re
import sys

from executorch.exir._serialize._program import deserialize_pte_binary

KERNEL_RE = re.compile(r'Kernel\(\s*"([^"]+)"(.*?)stack\.size\(\) == (\d+)', re.S)


def library_expectations(library: pathlib.Path) -> dict[str, int]:
"""Stack size each registered kernel wrapper demands, by operator name."""
generated = list((library / "src/executorch/codegen").glob("Register*Kernels*.cpp"))
if not generated:
sys.exit(f"no kernel registration found under {library}/src/executorch/codegen")
expectations: dict[str, int] = {}
for source in generated:
for match in KERNEL_RE.finditer(source.read_text()):
expectations[match.group(1)] = int(match.group(3))
return expectations


def model_calls(pte: pathlib.Path) -> list[tuple[str, int]]:
"""Operator name and stack size for every kernel call in a .pte."""
parsed = deserialize_pte_binary(pte.read_bytes())
plan = getattr(parsed, "program", parsed).execution_plan[0]
calls = []
for chain in plan.chains:
for instruction in chain.instructions:
args = getattr(instruction.instr_args, "args", None)
index = getattr(instruction.instr_args, "op_index", None)
if args is None or index is None:
continue # not a kernel call
op = plan.operators[index]
name = f"{op.name}.{op.overload}" if op.overload else op.name
calls.append((name, len(args)))
return calls


def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("library", type=pathlib.Path, help="Generated library root")
args = parser.parse_args()

expectations = library_expectations(args.library)
models = sorted(args.library.glob("examples/*/model.pte"))
if not models:
# The build converts each model.pte to model.h and removes it, so run
# this against the source tree rather than the packaged library.
models = sorted(pathlib.Path(__file__).parent.glob("examples/*/model.pte"))
if not models:
sys.exit("no example models found")

failures = 0
for pte in models:
problems = []
for name, provided in model_calls(pte):
expected = expectations.get(name)
if expected is None:
problems.append(f"{name}: not registered in the library")
elif expected != provided:
problems.append(
f"{name}: model supplies {provided}, library expects {expected}"
)
if problems:
failures += len(problems)
print(f"FAIL {pte.parent.name}")
for problem in dict.fromkeys(problems):
print(f" {problem}")
else:
print(f"ok {pte.parent.name}")

if failures:
print(
"\nThe models and the library came from different ExecuTorch commits.\n"
"Re-export the models from the same checkout that built the library;\n"
"see the pin in extras/PROVENANCE.txt."
)
return 1
print(f"\n{len(models)} models match the library")
return 0


if __name__ == "__main__":
sys.exit(main())
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions .github/workflows/_test_arduino_library.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
name: Test Arduino library

permissions:
id-token: write
contents: read

on:
workflow_call:
inputs:
timeout:
description: 'Job timeout in minutes'
required: false
type: number
default: 90

jobs:
run:
uses: pytorch/test-infra/.github/workflows/linux_job_v2.yml@main
permissions:
id-token: write
contents: read
with:
job-name: arduino-library
runner: linux.2xlarge
docker-image: ci-image:executorch-ubuntu-22.04-arm-sdk
submodules: 'recursive'
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
timeout: ${{ inputs.timeout }}
script: |
# The generic Linux job chooses to use base env, not the one setup by the image
CONDA_ENV=$(conda env list --json | jq -r ".envs | .[-1]")
conda activate "${CONDA_ENV}"

source .ci/scripts/utils.sh
install_executorch "--use-pt-pinned-commit"

# Schema headers come from a cmake build; the library build script
# refuses to run without them.
cmake -B cmake-out -DCMAKE_BUILD_TYPE=Release
cmake --build cmake-out --target program_schema -j$(nproc)

cd examples/arduino
./build_arduino_library.sh

# A model exported against a different ExecuTorch commit than the one
# that built the library loads fine, resolves every operator, and then
# fails inside Method::execute. Catch that here rather than on a board.
python verify_models.py arduino_lib/ExecuTorch

# arduino-cli, the Uno Q core, and the Serial dependency the core
# hard-errors without.
export ARDUINO_DIRECTORIES_USER="${RUNNER_TEMP}/arduino"
mkdir -p "${ARDUINO_DIRECTORIES_USER}"
curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh \
| BINDIR="${RUNNER_TEMP}/bin" sh
export PATH="${RUNNER_TEMP}/bin:${PATH}"
arduino-cli core update-index
arduino-cli core install arduino:zephyr
arduino-cli lib install Arduino_RouterBridge

# link_mode=static is not optional. The board defaults to Dynamic,
# which builds the sketch as a Zephyr loadable extension; a library
# this size never starts that way and prints nothing at all.
FQBN="arduino:zephyr:unoq:link_mode=static"
for sketch in arduino_lib/ExecuTorch/examples/*/; do
echo "::group::compile $(basename "${sketch}")"
arduino-cli compile --fqbn "${FQBN}" \
--libraries arduino_lib "${sketch}"
echo "::endgroup::"
done

# The library is published through the Arduino Library Manager, which
# applies these rules at submission time.
curl -fsSL https://raw.githubusercontent.com/arduino/arduino-lint/main/etc/install.sh \
| BINDIR="${RUNNER_TEMP}/bin" sh
cp ../../LICENSE arduino_lib/ExecuTorch/LICENSE
cp README.md arduino_lib/ExecuTorch/README.md
(cd arduino_lib/ExecuTorch && arduino-lint \
--project-type library --library-manager submit --compliance strict)
16 changes: 16 additions & 0 deletions .github/workflows/pull.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -61,6 +61,22 @@ jobs:

PYTHON_EXECUTABLE=python bash .ci/scripts/test_wheel_package_qnn.sh "${{ matrix.python-version }}"

test-arduino-library:
needs: changed-files
if: |
github.event_name != 'pull_request' ||
contains(needs.changed-files.outputs.changed-files, '.github/workflows/_test_arduino_library.yml') ||
contains(needs.changed-files.outputs.changed-files, 'backends/cortex_m/') ||
contains(needs.changed-files.outputs.changed-files, 'examples/arduino/') ||
contains(needs.changed-files.outputs.changed-files, 'kernels/portable/') ||
contains(needs.changed-files.outputs.changed-files, 'runtime/') ||
contains(needs.changed-files.outputs.changed-files, 'schema/')
name: test-arduino-library
uses: ./.github/workflows/_test_arduino_library.yml
permissions:
id-token: write
contents: read

test-minimal-wheel-linux:
needs: changed-files
if: |
Expand Down
108 changes: 108 additions & 0 deletions examples/arduino/verify_models.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
#!/usr/bin/env python3
# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.

"""Check the example models against the library that will run them.

python verify_models.py arduino_lib/ExecuTorch

A .pte records, per operator call, how many values it puts on the stack. The
generated kernel wrappers check that count and reject anything else. The two
only agree when the model and the library came from the same ExecuTorch
commit, because Cortex-M operator schemas change between releases -- `scratch`
was added to the conv operators in #19636 and #19825.

A mismatch is invisible until far too late: the program loads, every operator
resolves, and then Method::execute returns InvalidProgram (0x23) naming
nothing useful. This compares the two directly, in about a second, with no
board and no toolchain.
"""

import argparse
import pathlib
import re
import sys

from executorch.exir._serialize._program import deserialize_pte_binary

KERNEL_RE = re.compile(r'Kernel\(\s*"([^"]+)"(.*?)stack\.size\(\) == (\d+)', re.S)


def library_expectations(library: pathlib.Path) -> dict[str, int]:
"""Stack size each registered kernel wrapper demands, by operator name."""
generated = list((library / "src/executorch/codegen").glob("Register*Kernels*.cpp"))
if not generated:
sys.exit(f"no kernel registration found under {library}/src/executorch/codegen")
expectations: dict[str, int] = {}
for source in generated:
for match in KERNEL_RE.finditer(source.read_text()):
expectations[match.group(1)] = int(match.group(3))
return expectations


def model_calls(pte: pathlib.Path) -> list[tuple[str, int]]:
"""Operator name and stack size for every kernel call in a .pte."""
parsed = deserialize_pte_binary(pte.read_bytes())
plan = getattr(parsed, "program", parsed).execution_plan[0]
calls = []
for chain in plan.chains:
for instruction in chain.instructions:
args = getattr(instruction.instr_args, "args", None)
index = getattr(instruction.instr_args, "op_index", None)
if args is None or index is None:
continue # not a kernel call
op = plan.operators[index]
name = f"{op.name}.{op.overload}" if op.overload else op.name
calls.append((name, len(args)))
return calls


def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("library", type=pathlib.Path, help="Generated library root")
args = parser.parse_args()

expectations = library_expectations(args.library)
models = sorted(args.library.glob("examples/*/model.pte"))
if not models:
# The build converts each model.pte to model.h and removes it, so run
# this against the source tree rather than the packaged library.
models = sorted(pathlib.Path(__file__).parent.glob("examples/*/model.pte"))
if not models:
sys.exit("no example models found")

failures = 0
for pte in models:
problems = []
for name, provided in model_calls(pte):
expected = expectations.get(name)
if expected is None:
problems.append(f"{name}: not registered in the library")
elif expected != provided:
problems.append(
f"{name}: model supplies {provided}, library expects {expected}"
)
if problems:
failures += len(problems)
print(f"FAIL {pte.parent.name}")
for problem in dict.fromkeys(problems):
print(f" {problem}")
else:
print(f"ok {pte.parent.name}")

if failures:
print(
"\nThe models and the library came from different ExecuTorch commits.\n"
"Re-export the models from the same checkout that built the library;\n"
"see the pin in extras/PROVENANCE.txt."
)
return 1
print(f"\n{len(models)} models match the library")
return 0


if __name__ == "__main__":
sys.exit(main())
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions .github/workflows/_test_arduino_library.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
name: Test Arduino library

permissions:
id-token: write
contents: read

on:
workflow_call:
inputs:
timeout:
description: 'Job timeout in minutes'
required: false
type: number
default: 90

jobs:
run:
uses: pytorch/test-infra/.github/workflows/linux_job_v2.yml@main
permissions:
id-token: write
contents: read
with:
job-name: arduino-library
runner: linux.2xlarge
docker-image: ci-image:executorch-ubuntu-22.04-arm-sdk
submodules: 'recursive'
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
timeout: ${{ inputs.timeout }}
script: |
# The generic Linux job chooses to use base env, not the one setup by the image
CONDA_ENV=$(conda env list --json | jq -r ".envs | .[-1]")
conda activate "${CONDA_ENV}"

source .ci/scripts/utils.sh
install_executorch "--use-pt-pinned-commit"

# Schema headers come from a cmake build; the library build script
# refuses to run without them.
cmake -B cmake-out -DCMAKE_BUILD_TYPE=Release
cmake --build cmake-out --target program_schema -j$(nproc)

cd examples/arduino
./build_arduino_library.sh

# A model exported against a different ExecuTorch commit than the one
# that built the library loads fine, resolves every operator, and then
# fails inside Method::execute. Catch that here rather than on a board.
python verify_models.py arduino_lib/ExecuTorch

# arduino-cli, the Uno Q core, and the Serial dependency the core
# hard-errors without.
export ARDUINO_DIRECTORIES_USER="${RUNNER_TEMP}/arduino"
mkdir -p "${ARDUINO_DIRECTORIES_USER}"
curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh \
| BINDIR="${RUNNER_TEMP}/bin" sh
export PATH="${RUNNER_TEMP}/bin:${PATH}"
arduino-cli core update-index
arduino-cli core install arduino:zephyr
arduino-cli lib install Arduino_RouterBridge

# link_mode=static is not optional. The board defaults to Dynamic,
# which builds the sketch as a Zephyr loadable extension; a library
# this size never starts that way and prints nothing at all.
FQBN="arduino:zephyr:unoq:link_mode=static"
for sketch in arduino_lib/ExecuTorch/examples/*/; do
echo "::group::compile $(basename "${sketch}")"
arduino-cli compile --fqbn "${FQBN}" \
--libraries arduino_lib "${sketch}"
echo "::endgroup::"
done

# The library is published through the Arduino Library Manager, which
# applies these rules at submission time.
curl -fsSL https://raw.githubusercontent.com/arduino/arduino-lint/main/etc/install.sh \
| BINDIR="${RUNNER_TEMP}/bin" sh
cp ../../LICENSE arduino_lib/ExecuTorch/LICENSE
cp README.md arduino_lib/ExecuTorch/README.md
(cd arduino_lib/ExecuTorch && arduino-lint \
--project-type library --library-manager submit --compliance strict)
16 changes: 16 additions & 0 deletions .github/workflows/pull.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -61,6 +61,22 @@ jobs:

PYTHON_EXECUTABLE=python bash .ci/scripts/test_wheel_package_qnn.sh "${{ matrix.python-version }}"

test-arduino-library:
needs: changed-files
if: |
github.event_name != 'pull_request' ||
contains(needs.changed-files.outputs.changed-files, '.github/workflows/_test_arduino_library.yml') ||
contains(needs.changed-files.outputs.changed-files, 'backends/cortex_m/') ||
contains(needs.changed-files.outputs.changed-files, 'examples/arduino/') ||
contains(needs.changed-files.outputs.changed-files, 'kernels/portable/') ||
contains(needs.changed-files.outputs.changed-files, 'runtime/') ||
contains(needs.changed-files.outputs.changed-files, 'schema/')
name: test-arduino-library
uses: ./.github/workflows/_test_arduino_library.yml
permissions:
id-token: write
contents: read

test-minimal-wheel-linux:
needs: changed-files
if: |
Expand Down
108 changes: 108 additions & 0 deletions examples/arduino/verify_models.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
#!/usr/bin/env python3
# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.

"""Check the example models against the library that will run them.

python verify_models.py arduino_lib/ExecuTorch

A .pte records, per operator call, how many values it puts on the stack. The
generated kernel wrappers check that count and reject anything else. The two
only agree when the model and the library came from the same ExecuTorch
commit, because Cortex-M operator schemas change between releases -- `scratch`
was added to the conv operators in #19636 and #19825.

A mismatch is invisible until far too late: the program loads, every operator
resolves, and then Method::execute returns InvalidProgram (0x23) naming
nothing useful. This compares the two directly, in about a second, with no
board and no toolchain.
"""

import argparse
import pathlib
import re
import sys

from executorch.exir._serialize._program import deserialize_pte_binary

KERNEL_RE = re.compile(r'Kernel\(\s*"([^"]+)"(.*?)stack\.size\(\) == (\d+)', re.S)


def library_expectations(library: pathlib.Path) -> dict[str, int]:
"""Stack size each registered kernel wrapper demands, by operator name."""
generated = list((library / "src/executorch/codegen").glob("Register*Kernels*.cpp"))
if not generated:
sys.exit(f"no kernel registration found under {library}/src/executorch/codegen")
expectations: dict[str, int] = {}
for source in generated:
for match in KERNEL_RE.finditer(source.read_text()):
expectations[match.group(1)] = int(match.group(3))
return expectations


def model_calls(pte: pathlib.Path) -> list[tuple[str, int]]:
"""Operator name and stack size for every kernel call in a .pte."""
parsed = deserialize_pte_binary(pte.read_bytes())
plan = getattr(parsed, "program", parsed).execution_plan[0]
calls = []
for chain in plan.chains:
for instruction in chain.instructions:
args = getattr(instruction.instr_args, "args", None)
index = getattr(instruction.instr_args, "op_index", None)
if args is None or index is None:
continue # not a kernel call
op = plan.operators[index]
name = f"{op.name}.{op.overload}" if op.overload else op.name
calls.append((name, len(args)))
return calls


def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("library", type=pathlib.Path, help="Generated library root")
args = parser.parse_args()

expectations = library_expectations(args.library)
models = sorted(args.library.glob("examples/*/model.pte"))
if not models:
# The build converts each model.pte to model.h and removes it, so run
# this against the source tree rather than the packaged library.
models = sorted(pathlib.Path(__file__).parent.glob("examples/*/model.pte"))
if not models:
sys.exit("no example models found")

failures = 0
for pte in models:
problems = []
for name, provided in model_calls(pte):
expected = expectations.get(name)
if expected is None:
problems.append(f"{name}: not registered in the library")
elif expected != provided:
problems.append(
f"{name}: model supplies {provided}, library expects {expected}"
)
if problems:
failures += len(problems)
print(f"FAIL {pte.parent.name}")
for problem in dict.fromkeys(problems):
print(f" {problem}")
else:
print(f"ok {pte.parent.name}")

if failures:
print(
"\nThe models and the library came from different ExecuTorch commits.\n"
"Re-export the models from the same checkout that built the library;\n"
"see the pin in extras/PROVENANCE.txt."
)
return 1
print(f"\n{len(models)} models match the library")
return 0


if __name__ == "__main__":
sys.exit(main())
Loading