Skip to content

Arm backend: Size Test for Cortex-M #9569

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Apr 3, 2025
Merged
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
171 changes: 171 additions & 0 deletions .github/scripts/run_nm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perhaps we could use bloaty?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah I did think about it. This is just nm TBH so not sure if we need it to be more complicated than that. Wrote a python wrapper to print sorted table, and use cross-tools. But bloaty is nice though a bit "bloated" with dependency etc.

# 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.

import re
import subprocess
import sys
from dataclasses import dataclass
from typing import Dict, List, Optional, Union


@dataclass
class Symbol:
name: str
addr: int
size: int
symbol_type: str


class Parser:
def __init__(self, elf: str, toolchain_prefix: str = "", filter=None):
self.elf = elf
self.toolchain_prefix = toolchain_prefix
self.symbols: Dict[str, Symbol] = self._get_nm_output()
self.filter = filter

@staticmethod
def run_nm(
elf_file_path: str, args: Optional[List[str]] = None, nm: str = "nm"
) -> str:
"""
Run the nm command on the specified ELF file.
"""
args = [] if args is None else args
cmd = [nm] + args + [elf_file_path]
try:
result = subprocess.run(cmd, check=True, capture_output=True, text=True)
return result.stdout
except FileNotFoundError:
print(f"Error: 'nm' command not found. Please ensure it's installed.")
sys.exit(1)
except subprocess.CalledProcessError as e:
print(f"Error running nm on {elf_file_path}: {e}")
print(f"stderr: {e.stderr}")
sys.exit(1)

def _get_nm_output(self) -> Dict[str, Symbol]:
args = [
"--print-size",
"--size-sort",
"--reverse-sort",
"--demangle",
"--format=bsd",
]
output = Parser.run_nm(
self.elf,
args,
nm=self.toolchain_prefix + "nm" if self.toolchain_prefix else "nm",
)
lines = output.splitlines()
symbols = []
symbol_pattern = re.compile(
r"(?P<addr>[0-9a-fA-F]+)\s+(?P<size>[0-9a-fA-F]+)\s+(?P<type>\w)\s+(?P<name>.+)"
)

def parse_line(line: str) -> Optional[Symbol]:

match = symbol_pattern.match(line)
if match:
addr = int(match.group("addr"), 16)
size = int(match.group("size"), 16)
type_ = match.group("type").strip().strip("\n")
name = match.group("name").strip().strip("\n")
return Symbol(name=name, addr=addr, size=size, symbol_type=type_)
return None

for line in lines:
symbol = parse_line(line)
if symbol:
symbols.append(symbol)

assert len(symbols) > 0, "No symbols found in nm output"
if len(symbols) != len(lines):
print(
"** Warning: Not all lines were parsed, check the output of nm. Parsed {len(symbols)} lines, given {len(lines)}"
)
if any(symbol.size == 0 for symbol in symbols):
print("** Warning: Some symbols have zero size, check the output of nm.")

# TODO: Populate the section and module fields from the linker map if available (-Wl,-Map=linker.map)
return {symbol.name: symbol for symbol in symbols}

def print(self):
print(f"Elf: {self.elf}")

def print_table(filter=None, filter_name=None):
print("\nAddress\t\tSize\tType\tName")
# Apply filter and sort symbols
symbols_to_print = {
name: sym
for name, sym in self.symbols.items()
if not filter or filter(sym)
}
sorted_symbols = sorted(
symbols_to_print.items(), key=lambda x: x[1].size, reverse=True
)

# Print symbols and calculate total size
size_total = 0
for name, sym in sorted_symbols:
print(f"{hex(sym.addr)}\t\t{sym.size}\t{sym.symbol_type}\t{sym.name}")
size_total += sym.size

# Print summary
symbol_percent = len(symbols_to_print) / len(self.symbols) * 100
print("-----")
print(f"> Total bytes: {size_total}")
print(
f"Counted: {len(symbols_to_print)}/{len(self.symbols)}, {symbol_percent:0.2f}% (filter: '{filter_name}')"
)
print("=====\n")

# Print tables with different filters
def is_executorch_symbol(s):
return "executorch" in s.name or s.name.startswith("et")

FILTER_NAME_TO_FILTER_AND_LABEL = {
"all": (None, "All"),
"executorch": (is_executorch_symbol, "ExecuTorch"),
"executorch_text": (
lambda s: is_executorch_symbol(s) and s.symbol_type.lower() == "t",
"ExecuTorch .text",
),
}

filter_func, label = FILTER_NAME_TO_FILTER_AND_LABEL.get(
self.filter, FILTER_NAME_TO_FILTER_AND_LABEL["all"]
)
print_table(filter_func, label)


if __name__ == "__main__":
import argparse

parser = argparse.ArgumentParser(
description="Process ELF file and linker map file."
)
parser.add_argument(
"-e", "--elf-file-path", required=True, help="Path to the ELF file"
)
parser.add_argument(
"-f",
"--filter",
required=False,
default="all",
help="Filter symbols by pre-defined filters",
choices=["all", "executorch", "executorch_text"],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be FILTER_NAME_TO_FILTER_AND_LABEL.keys() (wrapped in list() if we must)

)
parser.add_argument(
"-p",
"--toolchain-prefix",
required=False,
default="",
help="Optional toolchain prefix for nm",
)

args = parser.parse_args()
p = Parser(args.elf_file_path, args.toolchain_prefix, filter=args.filter)
p.print()
54 changes: 54 additions & 0 deletions .github/workflows/trunk.yml
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,60 @@ jobs:
# Run arm unit tests using the simulator
backends/arm/test/test_arm_baremetal.sh test_pytest_ethosu_fvp

test-arm-cortex-m-size-test:
name: test-arm-cortex-m-size-test
uses: pytorch/test-infra/.github/workflows/linux_job_v2.yml@main
permissions:
id-token: write
contents: read
with:
runner: linux.2xlarge
docker-image: executorch-ubuntu-22.04-arm-sdk
submodules: 'true'
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
timeout: 90
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"
.ci/scripts/setup-arm-baremetal-tools.sh
source examples/arm/ethos-u-scratch/setup_path.sh

# User baremetal toolchain
arm-none-eabi-c++ --version
toolchain_cmake=examples/arm/ethos-u-setup/arm-none-eabi-gcc.cmake
toolchain_cmake=$(realpath ${toolchain_cmake})

# Build and test size test
bash test/build_size_test.sh "-DCMAKE_TOOLCHAIN_FILE=${toolchain_cmake} -DEXECUTORCH_BUILD_ARM_BAREMETAL=ON"
elf="cmake-out/test/size_test"

# Dump basic info
ls -al ${elf}
arm-none-eabi-size ${elf}

# Dump symbols
python .github/scripts/run_nm.py -e ${elf}
python .github/scripts/run_nm.py -e ${elf} -f "executorch" -p "arm-none-eabi-"
python .github/scripts/run_nm.py -e ${elf} -f "executorch_text" -p "arm-none-eabi-"

# Add basic guard - TODO: refine this!
arm-none-eabi-strip ${elf}
output=$(ls -la ${elf})
arr=($output)
size=${arr[4]}
threshold="102400" # 100KiB
echo "size: $size, threshold: $threshold"
if [[ "$size" -le "$threshold" ]]; then
echo "Success $size <= $threshold"
else
echo "Fail $size > $threshold"
exit 1
fi

test-coreml-delegate:
name: test-coreml-delegate
uses: pytorch/test-infra/.github/workflows/macos_job.yml@main
Expand Down
2 changes: 2 additions & 0 deletions examples/arm/ethos-u-setup/arm-none-eabi-gcc.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -97,5 +97,7 @@ add_compile_options(
# -Wall -Wextra -Wcast-align -Wdouble-promotion -Wformat
# -Wmissing-field-initializers -Wnull-dereference -Wredundant-decls -Wshadow
# -Wswitch -Wswitch-default -Wunused -Wno-redundant-decls
-Wno-error=deprecated-declarations
-Wno-error=shift-count-overflow
-Wno-psabi
)
2 changes: 1 addition & 1 deletion test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ set(EXECUTORCH_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/..)
include(${EXECUTORCH_ROOT}/tools/cmake/Utils.cmake)

# Find prebuilt executorch library
find_package(executorch CONFIG REQUIRED)
find_package(executorch CONFIG REQUIRED HINTS ${CMAKE_INSTALL_PREFIX})

# Let files say "include <executorch/path/to/header.h>".
set(_common_include_directories ${EXECUTORCH_ROOT}/..)
Expand Down
17 changes: 13 additions & 4 deletions test/build_size_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,35 @@ set -e
# shellcheck source=/dev/null
source "$(dirname "${BASH_SOURCE[0]}")/../.ci/scripts/utils.sh"

EXTRA_BUILD_ARGS="${@:-}"
# TODO(#8357): Remove -Wno-int-in-bool-context
COMMON_CXXFLAGS="-fno-exceptions -fno-rtti -Wall -Werror -Wno-int-in-bool-context"
# TODO: Replace -ET_HAVE_PREAD=0 with a CMake option.
# FileDataLoader used in the size_test breaks baremetal builds with pread when missing.
COMMON_CXXFLAGS="-fno-exceptions -fno-rtti -Wall -Werror -Wno-int-in-bool-context -DET_HAVE_PREAD=0"

cmake_install_executorch_lib() {
echo "Installing libexecutorch.a"
clean_executorch_install_folders
update_tokenizers_git_submodule
local EXTRA_BUILD_ARGS="${@}"

CXXFLAGS="$COMMON_CXXFLAGS" retry cmake -DBUCK2="$BUCK2" \
-DCMAKE_CXX_STANDARD_REQUIRED=ON \
-DCMAKE_INSTALL_PREFIX=cmake-out \
-DCMAKE_BUILD_TYPE=Release \
-DEXECUTORCH_BUILD_EXECUTOR_RUNNER=OFF \
-DOPTIMIZE_SIZE=ON \
-DPYTHON_EXECUTABLE="$PYTHON_EXECUTABLE" \
${EXTRA_BUILD_ARGS} \
-Bcmake-out .
cmake --build cmake-out -j9 --target install --config Release
}

test_cmake_size_test() {
CXXFLAGS="$COMMON_CXXFLAGS" retry cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=cmake-out -Bcmake-out/test test
CXXFLAGS="$COMMON_CXXFLAGS" retry cmake -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX=cmake-out \
${EXTRA_BUILD_ARGS} \
-Bcmake-out/test test

echo "Build size test"
cmake --build cmake-out/test -j9 --config Release
Expand All @@ -46,5 +55,5 @@ if [[ -z $PYTHON_EXECUTABLE ]]; then
PYTHON_EXECUTABLE=python3
fi

cmake_install_executorch_lib
test_cmake_size_test
cmake_install_executorch_lib ${EXTRA_BUILD_ARGS}
test_cmake_size_test ${EXTRA_BUILD_ARGS}
Loading