Skip to content

Add bare-metal AArch64 platform support (RTOS / no OS) - #379

Open
npitre wants to merge 3 commits into
pytorch:mainfrom
npitre:zephyr-aarch64
Open

Add bare-metal AArch64 platform support (RTOS / no OS)#379
npitre wants to merge 3 commits into
pytorch:mainfrom
npitre:zephyr-aarch64

Conversation

@npitre

@npitre npitre commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

This PR adds cpuinfo support for AArch64 targets running the Zephyr
RTOS. It is motivated by ongoing work to bring XNNPACK and ExecuTorch
to Zephyr on Arm Cortex-A class cores (specifically the Arm
Corstone-1000 Edge-AI platform with Cortex-A320 and Ethos-U85 NPU).

Summary:

  • Platform init backend (src/arm/zephyr/init.c): reads ARM64 ID
    registers (ID_AA64ISAR0/1_EL1, ID_AA64PFR0/1_EL1,
    ID_AA64ZFR0_EL1, ID_AA64SMFR0_EL1, MIDR_EL1) directly to
    detect ISA features. Features detected include AES, SHA1/2, CRC32,
    atomics, dotprod, FP16, BF16, I8MM, SVE/SVE2, and SME/SME2 (with
    sub-feature flags). Uarch is decoded via the existing
    cpuinfo_arm_decode_vendor_uarch().

  • Topology: single package/cluster with arch_num_cpus() cores
    (reflecting Zephyr's CONFIG_MP_MAX_NUM_CPUS). Cache geometry uses
    conservative Cortex-A defaults (64KB L1, 512KB L2, 64B lines) —
    CCSIDR_EL1-based detection is left as a TODO.

  • CMake: recognise CMAKE_SYSTEM_NAME=Generic with ZEPHYR_BASE
    defined as a supported platform, and select the Zephyr ARM init
    source for AArch64 targets.

  • Logging: use printf on Zephyr (like the Hexagon path), since
    Zephyr's POSIX layer does not provide the same STDERR_FILENO/
    STDOUT_FILENO semantics as Linux.

Thread-safe initialisation uses pthread_once via Zephyr's POSIX
compatibility layer.

Note: the ID register reads require EL1 or higher. Zephyr runs
application code at EL1 by default, so this is fine for normal use.
If CONFIG_USERSPACE is enabled, cpuinfo_initialize() must be
called from a privileged context (e.g. main thread or SYS_INIT)
before any user-mode threads that depend on cpuinfo.

Tested on the Arm Corstone-1000-A320 FVP (Cortex-A320, ARMv9.2-A)
running Zephyr with XNNPACK inference. cpuinfo correctly detects
NEON, SVE2 (with VL=16 bytes), dotprod, FP16, and BF16; XNNPACK
selects KleidiAI NEON kernels accordingly and executes
ExecuTorch-exported models with expected output.

Commits:

  • Add Zephyr RTOS support for AArch64 targets
  • CMake: add Zephyr RTOS platform support
  • log: add Zephyr RTOS support

@fbarchard

Copy link
Copy Markdown
Contributor

Thanks for the interesting PR. There are 2 parts to this:

  1. Add Cortex-A320 to MIDR decode table
    In this part, the order looks odd... i guess you sorted by MIDR.. but ok and I'm thinking it should be a PR on its own.
    Does it really have all those armv9 features?!

  2. Add Zephyr RTOS
    within that you use MRS to detect the isa.
    Would this mrs code be testable/usable on any other OS? or is hwcap available so common code could be shared/testable?
    Is it possible to make the 'platform' a more generic RTOS, so it would work on things like FreeRTOS too?

in XNNPack we may want to add uarch cortex_a320 to select appropriate microkernels. especially if KleidiAI is disabled.. perhaps treat it like a520
plan b for xnnpack would be change hardware-config to do the detects without cpuinfo, like hexagon does

@npitre

npitre commented Apr 28, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review!

Per your suggestion I've split this into two PRs:

Addressing your points:

A320 features: yes, A320 is ARMv9.2-A and implements the mandatory feature set (NEON, SVE2, dotprod, FP16, BF16, I8MM). See the Cortex-A320 TRM. We don't claim any of those statically though — the Zephyr backend reads ID_AA64ISAR0/1, PFR0/1, ZFR0, SMFR0 at runtime.

MRS detection on other OSes: the MRS approach should work on any RTOS that runs the application at EL1 or higher. On Linux, applications run at EL0 where the ID registers are not directly accessible — Linux exposes them via the HWCAP_CPUID mechanism, where the kernel traps MRS at EL0 and emulates the read with the system-wide value. So the same general approach is usable, just via the kernel hook on Linux rather than direct MRS.

RTOS generalisation (FreeRTOS, etc.): the code is almost OS-agnostic — only arch_num_cpus() is Zephyr-specific. Generalising the directory and dispatch to cover other RTOSes would be straightforward if needed.

XNNPACK uarch dispatch for A320: there's a follow-up XNNPACK patch ready that maps cpuinfo_uarch_cortex_a320xnn_uarch_cortex_a510 (since xnn_uarch_cortex_a520 doesn't exist either, A510 is the closest existing target). It'll be submitted as a small follow-up once #384 lands. We can revisit the kernel choice once we have real silicon for benchmarking.

Plan B (XNNPACK self-detection without cpuinfo): the intent is to have A320 fully probed via cpuinfo once these pieces are in place. Duplicating detection in XNNPACK would mean two sources of truth.

@npitre
npitre force-pushed the zephyr-aarch64 branch from 8982f73 to 83952ee Compare May 5, 2026 18:25
@npitre

npitre commented May 15, 2026

Copy link
Copy Markdown
Contributor Author

Gentle ping — anything else needed here, or is this ready to land?

@fbarchard

Copy link
Copy Markdown
Contributor

AI review has 4 concerns

The pull request "Add Zephyr RTOS platform support for AArch64" generally makes sense as it addresses the need to run cpuinfo (and consequently libraries like XNNPACK and ExecuTorch) on
AArch64 targets running Zephyr RTOS.

However, there are a few technical issues and areas for improvement in the proposed changes:

1. Potential Breakage of Hexagon Logging

In src/log.c , the PR groups Hexagon and Zephyr together and changes the logging mechanism for Hexagon:

-#elif defined(__hexagon__)
-    qurt_printf("%s", out_buffer);
+#elif defined(__hexagon__) || defined(__ZEPHYR__)
+    out_buffer[prefix_length + format_length] = '\n';
+    out_buffer[prefix_length + format_length + 1] = '\0';
+    printf("%s", out_buffer);

• Issue: This replaces Hexagon's use of qurt_printf (specific to QuRT RTOS) with standard printf . In Hexagon DSP environments, standard printf may not be redirected correctly or might not
be available, which is why qurt_printf was originally used.
• Recommendation: Keep Hexagon using qurt_printf and separate the Zephyr implementation to use printf :
#elif defined(hexagon)
qurt_printf("%s", out_buffer);
#elif defined(ZEPHYR)
out_buffer[prefix_length + format_length] = '\n';
out_buffer[prefix_length + format_length + 1] = '\0';
printf("%s", out_buffer);
#else

2. Compilation Failure on 32-bit ARM (ARM32) Zephyr Targets

The CMake changes append src/arm/zephyr/init.c if ZEPHYR_BASE is defined, but this is within a block that matches both 32-bit and 64-bit ARM architectures:

ELSEIF(CPUINFO_TARGET_PROCESSOR MATCHES "^(armv[5-8].*|aarch64|arm64.*)$" ...)
  ...
  ELSEIF(DEFINED ZEPHYR_BASE)
    LIST(APPEND CPUINFO_SRCS src/arm/zephyr/init.c)

• Issue: src/arm/zephyr/init.c contains AArch64-specific assembly (e.g., reading id_aa64isar0_el1 via mrs ) and calls cpuinfo_arm_decode_vendor_uarch with 3 arguments (which expects 4
arguments on ARM32 due to the conditional has_vfpv4 parameter). If someone attempts to build cpuinfo for an ARM32 Zephyr target, compilation will fail.
• Recommendation: Either restrict the file addition in CMake to AArch64/ARM64 only:
ELSEIF(DEFINED ZEPHYR_BASE AND CPUINFO_TARGET_PROCESSOR MATCHES "^(aarch64|arm64.*)$")
LIST(APPEND CPUINFO_SRCS src/arm/zephyr/init.c)
Or guard the entire content of src/arm/zephyr/init.c with #if CPUINFO_ARCH_ARM64 .

3. CMAKE_SYSTEM_NAME Matching for Zephyr

The PR adds Generic to the allowed CMAKE_SYSTEM_NAME list in CMakeLists.txt to prevent warnings and initialization failures:

-ELSEIF(NOT CMAKE_SYSTEM_NAME MATCHES "^(Windows|WindowsStore|CYGWIN|MSYS|Darwin|Linux|Android|FreeBSD|Emscripten)$")
+ELSEIF(NOT CMAKE_SYSTEM_NAME MATCHES "^(Windows|WindowsStore|CYGWIN|MSYS|Darwin|Linux|Android|FreeBSD|Emscripten|Generic)$")

• Issue: Zephyr's standard build system typically sets CMAKE_SYSTEM_NAME to Zephyr . If cpuinfo is integrated as a Zephyr module, it will likely see CMAKE_SYSTEM_NAME as Zephyr , which
would still trigger the warning and set CPUINFO_SUPPORTED_PLATFORM to FALSE .
• Recommendation: Add Zephyr to the matched patterns as well:
ELSEIF(NOT CMAKE_SYSTEM_NAME MATCHES "^(Windows|WindowsStore|CYGWIN|MSYS|Darwin|Linux|Android|FreeBSD|Emscripten|Generic|Zephyr)$")

4. POSIX Dependency

In src/init.c , the PR uses pthread_once for Zephyr initialization:

#elif defined(__ZEPHYR__)
    pthread_once(&init_guard, &cpuinfo_zephyr_arm_init);

• Note: This assumes that Zephyr has POSIX thread support enabled (e.g., CONFIG_PTHREAD_IPC ). While common for complex applications (like ExecuTorch), it restricts usage on minimal Zephyr
configurations that might disable POSIX APIs to save space. A fallback using simple boolean guards (similar to the Emscripten non-pthread path) could be considered if strict POSIX adherence is
not required by other parts of cpuinfo on Zephyr.

Summary

The core logic of reading ARM64 ID registers directly is correct and necessary for Zephyr. If the author addresses the Hexagon regression and ensures ARM32 builds do not break, the PR is solid
and makes sense to merge.

@fbarchard fbarchard left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

should there be a bazel update with this?
the mrs approach apparently supports a white list for the cpuid range of values on modern linux, but when new fields are added, the white list needs to grow.

as a one off test I put a structured exception handler around mrs and actual instructions and the problem i ran into is you need a very new (aka future) version of linux kernel.
maybe some day...

we also have the basic detect in xnnpack, but to properly tune for a320 we'd want to at least confirm that behavior is very similar to a520 or something else.
a510 and a515 have microcoded instructions that are fixed in a520 if we tuned a kernel for a320/a520 they are similar?

on xnnpack side if you run a benchmark like f32_gemm_bench and find a kernel that is faster than what is selected in gemm_config.c send a PR to switch it. The build for android is
bazel build --config=android_arm64 -c opt bench/f32_gemm_bench
and the benchmark takes 3 parameters. 128 128 128 is a generic convolution size

npitre added 3 commits July 29, 2026 01:26
Detect ISA features on AArch64 targets that run without an OS, that is under
an RTOS or on bare metal, by reading the ID registers directly.

This is not an alternative spelling of the Linux path, the two cannot share
detection logic: there is no kernel to publish HWCAP here, and conversely the
ID registers are inaccessible from Linux userspace. Reading them requires EL1,
which is where an RTOS application runs.

Nothing in the new file is tied to a particular RTOS. The single piece of
external information needed is the core count, which comes from get_num_cpus():
Zephyr provides arch_num_cpus(), anything else falls back to a single core.
Supporting another RTOS means adding a case there.

cpuinfo_initialize() guards this path with a plain flag rather than
pthread_once(), following the Emscripten-without-pthreads precedent, so that
cpuinfo still builds where POSIX threads are unavailable or deliberately
disabled to save space.

Topology is reported as one package with one cluster. Cache geometry uses
conservative Cortex-A defaults; reading CCSIDR_EL1 for the real values is left
as a follow-up.
Select the bare-metal init when CMAKE_SYSTEM_NAME is "Generic", CMake's name
for a target without an operating system, which is what both Zephyr and
FreeRTOS set. "Generic" therefore also joins the list of recognised platforms
so that cpuinfo_initialize() is not stubbed out for them.

The source is added only for AArch64 targets: the ID register layout it reads
does not apply to AArch32, so an ARM32 build must not pick it up.

CPUINFO_BAREMETAL is defined for the library targets, selecting both that init
path and its non-POSIX init guard.
Bare-metal targets have no file descriptors to write() to, so route logging
through printf() there, the way the Hexagon path already does.

Keep Hexagon on qurt_printf(): its RTOS does not necessarily redirect the
standard printf(), which is why that path exists in the first place.
@npitre npitre changed the title Add Zephyr RTOS platform support for AArch64 Add bare-metal AArch64 platform support (RTOS / no OS) Jul 29, 2026
npitre added a commit to npitre/executorch that referenced this pull request Jul 29, 2026
XNNPACK and cpuinfo carry Zephyr/Cortex-A320 changes that are not yet
merged upstream:

- XNNPACK: Cortex-A320 kernel selection (google/XNNPACK#10173, approved)
- cpuinfo: Zephyr RTOS platform support for AArch64 (pytorch/cpuinfo#379)

Drop this commit once both land and the upstream pins pick them up. The
generic XNNPACK Zephyr platform support (google/XNNPACK#10060) and the
Cortex-A320 MIDR decode (pytorch/cpuinfo#384) are already merged upstream
and no longer carried here.
@npitre

npitre commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, this was useful feedback. I've restructured the PR rather than patched around the comments.

Auditing our own code, essentially nothing in it was Zephyr-specific: of ~150 lines, the only Zephyr dependency was a single call to get the core count. Everything else is plain AArch64 ID-register reads. So the platform is now "bare metal", meaning an RTOS or no OS at all, rather than Zephyr:

  • src/arm/zephyr/init.c -> src/arm/baremetal/init.c, and cpuinfo_zephyr_arm_init -> cpuinfo_arm_baremetal_init to match the existing cpuinfo_arm_{linux,mach,windows}_init naming.
  • The one piece of OS-specific information is isolated in a get_num_cpus() accessor: Zephyr provides arch_num_cpus(), anything else falls back to a single core. Supporting FreeRTOS means adding a case there, nothing more.
  • Selection is keyed on CMAKE_SYSTEM_NAME STREQUAL "Generic", CMake's name for a target without an OS, which is what both Zephyr and FreeRTOS set. That is what makes it work beyond Zephyr.

On whether the MRS code could be shared with, or tested on, another OS via hwcap: I don't think the two paths can share detection logic, and it is worth being explicit about why. The distinction is not Zephyr versus Linux, it is EL1 versus userspace. hwcap only exists if a kernel publishes it, and there is no kernel here; conversely the ID registers are inaccessible from Linux userspace, which is precisely why the Linux path uses hwcap. Each path is the only option available in its own environment. The upside is that the new file is testable anywhere code runs at EL1.

On the four points from the automated review:

  1. Hexagon logging: correct, that was a real regression and I've fixed it properly. Hexagon keeps qurt_printf(); the bare-metal path is a separate branch, with a comment recording why Hexagon has its own.
  2. ARM32: also correct. The source is now added only for AArch64 targets, since the ID register layout it reads does not apply to AArch32.
  3. Matching CMAKE_SYSTEM_NAME "Zephyr": this one I'd push back on. Zephyr sets it to Generic, unconditionally, in cmake/modules/FindTargetTools.cmake. Adding a Zephyr pattern would be dead code, and Generic is also the more useful thing to match, per above.
  4. pthread_once: fixed, and thank you for pointing at the Emscripten precedent. That path now uses a plain flag rather than pthread_once(), so cpuinfo builds where POSIX threads are unavailable or deliberately disabled. Initialization happens before secondary cores are released, so the flag is sufficient.

On the XNNPACK uarch selection: that is google/XNNPACK#10173, which is approved. You suggested treating A320 like an A520, and it is worth separating two things: the uarch mapping only picks GEMM microkernel variants and tile sizes, while SVE2 and the other ISA features come from arch_flags via cpuinfo's ISA detection, so the mapping cannot gate them. Given that, I mapped A320 to xnn_uarch_cortex_a510, which is the only ARMv9 little-core entry that exists and sits in the same tuning group as a710/a715/x1-x4. xnn_uarch_cortex_a520 does not exist, and cpuinfo_uarch_cortex_a520 is currently unmapped there, so an A520 resolves to xnn_uarch_unknown and baseline tuning; A510 gets A320 modern tuning instead. A320 silicon is not available to profile yet, so a supported near relative seemed better than a distinct entry with no measurements behind it.

Verified on a Corstone-1000-A320 target (FVP, since silicon is not out yet): MIDR reads back 0x410fd8f0, part 0xd8f, which decodes to cpuinfo_uarch_cortex_a320 (0x300553) through the table added in #384, and the ISA detection reports neon, fp16, dot, i8mm, bf16, sve and sve2 all present. XNNPACK then selects its NEON microkernels off the back of that, and inference produces correct results.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants