Skip to content
allbillyPublic

About

Run ops on Apple ANE in NPU register with pure python on M1 Asahi Linux. No Espresso, No CoreML, no metal, no .mlmodels file, no .hwx file, no ANEcompiler, no private Apple API, no anecc, nothing.

Resources

Stars

21 stars

Watchers

2 watching

Forks

Repository files navigation

Apple ANE running on Asahi

Ask DeepWiki

This repo run ops on Apple ANE in NPU register with pure python and numpy on M1 Asahi Linux. No Espresso, No CoreML, no metal, no .mlmodels file, no .hwx file, no ANEcompiler, no private Apple API, no anecc, nothing. Even numpy is optional.

The Orion GPT-2 text-generation port reads cached Hugging Face weights, regenerates ANE tile-packed coefficients into the external cache, and includes tokenization, KV caching, a CPU backend, and an M1 ANE replay backend with first-run numerical checks. Start with gpt2/first-run.sh. On base M1 Asahi, all 24 decode kernels and full generation parity passed. After reducing output copies and defaulting OpenBLAS to one thread, four 32-token runs measured 58.18 generated tokens/s combined; warmed decode measured 70.18 steps/s for the two-token prompt. See the Asahi measurements for timing scope.

Mirai Qwen3.5-0.8B-M now has a packed W4 CPU runtime, an independent Uzu BF16 reference check, and experimental Linux M1 ANE projections. Start with qwen35/first-run.sh setup, then verify or generate. Separate prefill and decode measurements now cover short, 128-token and 512-token prompts on CPU and ANE. CPU SDOT matched every saved token choice. The corrected ANE policy scales and compensates FP16 projections using spare batch rows, with one hardware submission per projection. Three matched runs now passed all 576 decode choices with maximum full-logit NRMSE 0.381%, down from 17.30%. Paired ANE decode fell from 17.38 to 11.57 tok/s. The CPU path is the default. Active desktop load and paging limit speed comparisons. No new macOS dump was used.

For a separate macOS Core ML comparison, see the Core ML Qwen3.5 notes. They record graph-lowering and placement lessons from an upstream ANEMLL port; this repository's runtime remains the direct-register Asahi implementation described here.

Whisper now runs trained tiny.en on this Asahi M1 using the existing matrix stream for 24 encoder projections, with other operations and decoding on CPU. No new macOS dump was used. All 80 captured raw logit argmaxes match CPU and the independent HF model across three clips, with maximum full-logit NRMSE 0.333% versus HF. Host and CPU kernel optimizations reduced ANE encode time from 721.41 to 345.80 ms. The Asahi warm table measures 468.18 ms to transcribe the 11-second JFK clip using ANE projections, versus 417.49 ms on CPU. The macOS table measures five routes; its full-encoder ANE + CPU decoder took 68.37 ms. The graph and CPU backends differ, and the Linux hybrid is currently slower.

PR ANEForge fast encoder speed reference on M1 (2026-10-08). These runs use multilingual tiny/base/small, the full 1,500-position audio context and four threads. Encoder time includes input staging, the full ANE encoder and CPU cross-attention K/V; compilation and decoding are excluded.

Model macOS: ANE + Accelerate BLAS measured macOS: ANE + OpenBLAS NEON measured Asahi: ANE + OpenBLAS measured
tiny 17.17 ms 28.65 ms 32.13 ms
base 36.69 ms 63.30 ms 72.70 ms
small 142.51 ms 334.71 ms 282.32 ms

All three ran on Asahi with the unchanged Mac timing harness: two persistent contexts, two excluded warmups and five measurements each. The ten measured calls ranged 31.98–32.48 / 72.35–75.57 / 281.02–288.75 ms for tiny/base/small. Native blocking ANE dispatch medians were 14.24 / 30.22 / 106.86 ms, and readback medians were 1.33 / 1.81 / 2.71 ms. Each model's zero-mel encoder output matched its Mac hash byte for byte on all 23 calls of a separate three-warmup/twenty-measurement replay, including small's two coefficient banks. Compared with saved Mac OpenBLAS medians, tiny/base took 12.1% / 14.8% longer and small took 15.7% less time. The libraries differ (Fedora OpenBLAS 0.3.29 versus Mac 0.3.34), clocks were not controlled, and small's Mac samples ranged 297–406 ms; these results do not isolate the source of the difference. These are encoder timing comparisons with exact Mac zero-input output agreement. Separate local speech checks now measure maximum encoder relative L2 error against an independent FP32 reference of 3.63% / 1.87% / 2.66% for tiny/base/small. The corresponding strict numerical checks fail, while all 81/84/90 raw token argmaxes match. These are local numerical results, separate from the exact Mac zero-mel replay comparison and the encoder timing boundary. See the tiny, base and small accuracy receipts. The latest tiny run also profiles the loaded driver at its original 1-us poll setting: the last native call in each context spent about 14.21 ms waiting for completion and 0.009 ms draining IRQ events. These are driver wall times, with no ANE clock or hardware-duration counter available. See Asahi receipts for tiny profiled rerun, base and small, Mac measured runs, benchmark details and checkpoint repacking/native replay. Large weights and HWX dumps remain ignored; Asahi rebuilds them from external safetensors or lossless F16/F32 GGUF using the compact packing recipes.

Accuracy-qualified tiny.en paired control. Native Asahi whisper.cpp now reuses the macOS C++ packing and FP32 combination arithmetic with a Linux DRM transport. All 80 full-vocabulary decoder vectors pass NRMSE < 0.005 versus CPU and independent HF, with matching histories/raw argmaxes. Worst paired/HF NRMSE is 0.408%; repeated mel, encoder outputs and complete decoder traces are byte identical on all three clips. Each encode uses 24 submissions / 32 hardware tasks and four existing graph workers.

Tiny.en, 11-second JFK clip Encoder + cross-K/V Decoder total Whole transcription
macOS FP32 CPU + Apple BLAS — saved 252.24 ms 85.94 ms 369.01 ms
macOS paired ANE + Apple BLAS — saved 366.88 ms 87.14 ms 483.76 ms
Asahi FP32 CPU + OpenBLAS — measured 216.57 ms 72.95 ms 307.35 ms
Asahi paired ANE + OpenBLAS — measured 624.32 ms 78.79 ms 723.42 ms

Each backend has two persistent contexts, two excluded warmups and ten measurements per clip per context, four workers and a full 30-second encoder context. The paired path remains slower than CPU and macOS. Linux readback takes 379.37 ms for 332.66 MB of padded FP16 products, versus 13.34 ms in blocking ioctl. The buffers use the driver's write-combined mapping; packing and combining take 10.43 / 12.50 ms. CPU BLAS libraries and frontends differ across hosts, and clocks were not fixed. This validates native accuracy on local matching inputs; exact Mac speech arrays and E5RT executable identity remain unverified. The original fused fast graph still fails its separate strict accuracy gate. Native Asahi receipt, saved Mac receipt and shared preparation/run commands.

GPT-2 training on Asahi replays the captured forward and backward kernels through the same driver. A full 124M parameter run completed ten Adam updates on this machine; its fixed-batch loss fell from 4.42788 to 0.000207. The transformer runs on ANE, with the vocabulary head, loss and optimizer on CPU.

macOS follow-up review (2026-10-06)

The unrestricted rerun receipt records the latest hardware results after the execution sandbox was removed. The initial follow-up receipt retains the earlier restricted attempts. Capture and replay commands retain the measurement boundaries and remaining native Asahi checks.

The subsequent review verified macOS MUL execution through private MIL compilation: both 64-value cases pass exactly. ANEForge's private E5RT path also works. Our custom HWX dumps are assigned to guarded Asahi replay; standalone macOS loading is a retained historical rejection control. The runtime scope records that boundary. Replay validation now rejects empty fixtures and changed gates before device access; benchmark observations bracket process start and exit. Earlier timing classifications retain their sampled scope.

The macOS CPU results cover 18 Qwen3.5 runs and the GPT-2 package, CPU parity and warmed decode checks. All 97 retained artifact hashes and the evidence archive hash were rechecked. The current GPT-2 inference/training and Qwen projection shapes already have validated templates; their existing replay paths do not need fresh HWX dumps.

Follow-up Result Remaining work
Qwen BF16 Uzu layer captures for arithmetic prefixes 3–23 Completed on macOS: native BF16 matches independent Uzu at all 23 tokens, 552 layer vectors and 5,711,360 logits. Both Mac implementations differ from the retained Asahi arithmetic logits. Compare independent all-token captures on Asahi using the retained Cargo lock; keep the original exact fixtures.
Quiet, repeated CPU measurements Three phases completed: 54/54 numerical runs passed. Thirteen passed the sampled host-activity gates; 41 timings were affected. The unrestricted phase passed 18/18 with complete host queries, including five timings passing the sampled gates. All attempts are retained; these same-boot phases do not establish an isolated OS comparison.
Longer-context and free-generation numerical checks Completed CPU checks: 1K/2K contexts and four shorter prompts, 384/384 decode choices; four independent greedy streams, 256/256 choices. All full-logit errors passed the unchanged 0.5% gate. Floating traces and an accurate-ANE invocation are retained for native Asahi replay.
Static HWX compile-options investigation Seven variants exported identical one-task MUL structures. Historical custom-HWX macOS loading controls failed; the MUL computes exactly through private E5RT and daemon-compiled MIL. Custom exported HWX execution belongs on Asahi. Standalone macOS loading is removed from pending work; option recognition and native Asahi replay remain separate questions.
Whisper trained encoder and more audio The original fast 1,779-task graph is freshly recaptured as a 149 KiB package, with native input strides and byte-exact external-weight repacking. Warm 11-second transcription takes 15.83 ms encode / 66.81 ms whole. All three encoder outputs and 80 raw token choices match; the 1,783-task wrapper remains a slower baseline. The original fast graph also fails the strict full-logit gate on Mac. New original-package Linux replay and matching stage profiles remain pending; see the review. Constructed clips do not establish diverse-corpus accuracy or WER.
64-token training All 27 templates exported with actual ANE output fixtures. Independent loss/gradient gates and three updates passed, with 580 actual ANE dispatches per update; loss fell from 3.271003 to 0.428595. Replay guards pass. Native Asahi fixture replay and model-level training validation.
Fused Qwen recurrence All 16 real-input cases pass on ANE after compensated scaling and explicit exponential SiLU. Maximum state/output NRMSE is 0.294%/0.317%, below the unchanged 0.5% gate. Four programs export with actual ANE fixtures and pass replay guards. Native Asahi replay remains pending; this layer-0 probe does not enable a fused full-model decoder.

The Qwen follow-up details describe the numerical gates and required reference captures. The separate mlx-ane-sd/task.md capture remains assigned to the other session and is excluded from this review. GPT-2 CoreML/Orion/MLX comparisons already have retained macOS reports; repeating them adds value when testing a changed implementation or making a more controlled performance comparison. The newly added Whisper measurements already cover five macOS routes on one short clip. The new follow-up adds Core ML inference and three constructed durations. Its unrestricted ANE, Metal and Core ML results are recorded separately from the historical restricted attempts and the earlier warm-context measurements.

Thanks for the prior work from geohotz eiln freedomtan mdaiter , some scripts in experimental/* are from freedomtan/coreml_to_ane_hwx

ANE detailed hardware and patent analysis by Maynard Handley

M5 H17(Not yet on asahi) register programming, conv, conv_int8, layernorm, softmax by MidasMulli and opcode and H17 HWX formatBEEFFACE

TODO

For normal user

For base M1 (T8103), the runtime overlay and loadable KMD can supply the missing ANE device tree nodes without rebuilding the kernel or changing boot files. Build with make -C kmod, install with sudo make -C kmod install, then load with sudo modprobe ane. Revalidated on a base M1 MacBook Air after upgrading to Fedora Asahi Remix 44 and booting 7.1.13+: all ten operation checks, runtime autosuspend, driver unload/reload, and kernel fault checks passed with the runtime overlay. GPT-2 ANE inference also passed all 49 reference kernels and a fresh 32-token generation; see the inference results. See the linked instructions for kernel requirements, permissions and hardware validation. This replaces the incomplete old libane/ane-overlay.dts recipe; do not apply that old overlay to the boot device tree.

The earlier Asahi Linux Fedora 6.14.8 overlay setup worked once, but its boot installation steps were not reproducible. Use the documented runtime path above for the new overlay work.

✅ Tested on Asahi Linux fedora 6.19.11+ built from my fork of fairy-dust branch of asahi linux

1. Install Asahi Linux, build with dts and install kmd for ANE

The kernel build below describes the earlier setup and the optional Type-C display work. Base M1 users can use kmod for ANE without this kernel rebuild.

On MacOS, run

curl https://alx.sh | sh

After Asahi Linux is installed, build kernel with ANE (and typec dp) support

git clone https://github.com/AsahiLinux/linux.git --branch fairydust --single-branch
cd linux

# for M1
curl -L https://github.com/eiln/linux/commit/bf6651bb55212f2cfab573bd0d49bf5c601b4703 | git apply

# for M1 Pro (not tested) 
curl -L https://github.com/eiln/linux/commit/297491ef3126f057d708d24bdcb658356d9ce25d | git apply

# Below steps are from https://grzegorz-smajdor.com/blog/2026-monitor-asahi-fedora/

sudo dnf install -y gcc gcc-c++ make bc bison flex elfutils-libelf-devel ncurses-devel \
  python3 zlib-devel libuuid-devel dwarves xz zstd clang llvm lld git \
  rust rust-src bindgen-cli
task_running_kernel=$(uname -r)
if [ -r "/boot/config-$task_running_kernel" ]; then
  cp "/boot/config-$task_running_kernel" .config
elif [ -r "/lib/modules/$task_running_kernel/build/.config" ]; then
  # Custom kernels may keep their config only in the build tree. If this is
  # that same tree, its .config is already in place.
  if [ ! "/lib/modules/$task_running_kernel/build/.config" -ef .config ]; then
    cp "/lib/modules/$task_running_kernel/build/.config" .config
  fi
else
  echo "Cannot find the running kernel config; select an Asahi kernel config first." >&2
  exit 1
fi

# The Asahi GPU driver requires Rust, even though the ANE KMD is written in C.
# Stop and resolve any toolchain errors reported by this check before building.
make LLVM=1 rustavailable || exit 1
scripts/config --enable RUST --module DRM_ASAHI

# Press Enter for default answers
make LLVM=1 oldconfig

# Edit the .config to ensure Alt Mode support is built as modules:
vim .config
CONFIG_TYPEC_DP_ALTMODE=m
CONFIG_TYPEC_NVIDIA_ALTMODE=m
CONFIG_TYPEC_TBT_ALTMODE=m
CONFIG_EFI_SBAT_FILE=""
CONFIG_QRTR_MHI=n

# Resolve dependencies again, then verify GPU support was retained. Kconfig
# can drop DRM_ASAHI when Rust or another required option is unavailable.
make LLVM=1 olddefconfig
if ! grep -q '^CONFIG_RUST=y$' .config || ! grep -q '^CONFIG_DRM_ASAHI=m$' .config; then
  echo "Rust/Asahi GPU support is missing; fix the toolchain/configuration before building." >&2
  exit 1
fi

make LLVM=1 -j$(nproc)
make LLVM=1 dtbs -j$(nproc)

sudo make LLVM=1 modules_install
sudo make LLVM=1 dtbs_install

task_kernel_release=$(make LLVM=1 -s kernelrelease)
sudo mkdir -p "/usr/lib/modules/$task_kernel_release/dtb"
sudo cp -r arch/arm64/boot/dts/* "/usr/lib/modules/$task_kernel_release/dtb/"
sudo make LLVM=1 install

# Edit /etc/default/grub to show boot menu
vim /etc/default/grub 
GRUB_TIMEOUT_STYLE=menu
GRUB_TIMEOUT=5

sudo grub2-mkconfig -o /boot/grub2/grub.cfg
sudo reboot

# Choose the newly installed kernel, mine is Asahi Linux fedora 6.19.11+

# Verify after boot
lsmod | grep typec
sudo modprobe typec_displayport
sudo modprobe typec_nvidia
sudo modprobe typec_thunderbolt
ls /sys/bus/typec/devices/
ls /sys/class/drm/
sudo modprobe asahi
ls -l /dev/dri/renderD*
echo -e "typec_displayport\ntypec_nvidia\ntypec_thunderbolt" | sudo tee /etc/modules-load.d/fairydust.conf

# Note monitor suport only on one blessed typec port on M1

Add GPU support to an existing GCC kernel build

An existing ~/linux build can reuse its object files only when the source, compiler, linker, flags and relevant configuration remain unchanged. The current 7.1.13+ tree recorded GCC 15.2.1 and GNU binutils in its build configuration. Set the paths below to that exact compiler and linker. Kbuild rebuilds all C objects when the compiler version differs, so do not use generic gcc or ld after a Fedora upgrade without checking them. If the matching toolchain is unavailable, use the fresh LLVM build above. Keep the existing .config, .o and .cmd files for this incremental path.

Rust support can work with GCC for compatible configurations. The current ARM64 configuration has no LTO, KASAN or randstruct enabled and has the BTF language-exclusion support required by Rust. Check toolchain availability and the resolved configuration before building:

sudo dnf install -y rust rust-src bindgen-cli
cd ~/linux
task_cc=/path/to/matching-gcc
task_ld=/path/to/matching-ld
if [ ! -x "$task_cc" ] || [ ! -x "$task_ld" ]; then
  echo "Set task_cc and task_ld to the matching executable paths before continuing." >&2
  exit 1
fi
task_kernel_cc=$(sed -n 's/^CONFIG_CC_VERSION_TEXT="\(.*\)"$/\1/p' .config)
task_build_cc=$(LC_ALL=C "$task_cc" --version | head -n 1)
task_kernel_ld=$(sed -n 's/^CONFIG_LD_VERSION=//p' .config)
task_build_ld=$(scripts/ld-version.sh "$task_ld" | awk '{print $2}')
if [ "$task_build_cc" != "$task_kernel_cc" ] ||
   [ "$task_build_ld" != "$task_kernel_ld" ]; then
  echo "Use the exact compiler and linker versions recorded in .config, or use the fresh LLVM build." >&2
  exit 1
fi
cp -p .config .config.before-rust-gpu
make CC="$task_cc" LD="$task_ld" rustavailable || exit 1
scripts/config --enable RUST --module DRM_ASAHI
make CC="$task_cc" LD="$task_ld" olddefconfig
if ! grep -q '^CONFIG_RUST=y$' .config || ! grep -q '^CONFIG_DRM_ASAHI=m$' .config; then
  echo "Rust/Asahi GPU support was not retained; resolve the configuration before building." >&2
  exit 1
fi
make CC="$task_cc" LD="$task_ld" -j"$(nproc)" vmlinuz.efi modules dtbs

Kbuild rebuilds the Rust support, GPU driver, selected dependencies and any existing objects affected by the configuration changes, then relinks the kernel. The total depends on those dependencies; it is not restricted to drivers/gpu/drm/asahi/. Existing unchanged DTBs can be reused.

This configuration installs vmlinuz.efi. Building only Image modules leaves that EFI boot image unchanged, so refresh it before installation. Install the fairydust DTBs in the location used by Fedora's kernel hooks:

cd ~/linux
make CC="$task_cc" LD="$task_ld" -j"$(nproc)" vmlinuz.efi dtbs
task_kernel_release=$(make CC="$task_cc" LD="$task_ld" -s kernelrelease)
sudo make CC="$task_cc" LD="$task_ld" modules_install
sudo make CC="$task_cc" LD="$task_ld" dtbs_install \
  INSTALL_DTBS_PATH="/usr/lib/modules/$task_kernel_release/dtb"
sudo ln -sfn "/usr/lib/modules/$task_kernel_release/dtb" "/boot/dtb-$task_kernel_release"
sudo install -m 644 .config "/boot/config-$task_kernel_release"
sudo install -m 644 System.map "/boot/System.map-$task_kernel_release"
sudo make CC="$task_cc" LD="$task_ld" install
sudo grub2-mkconfig -o /boot/grub2/grub.cfg

Fedora's install hooks select /boot/dtb-$task_kernel_release, update m1n1, generate the initramfs and register the boot entry. Check that installation finishes successfully before rebooting. This build retains release 7.1.13+, so installation replaces the existing entry for that release.

Reboot into the updated kernel, then run sudo modprobe asahi and check ls -l /dev/dri/renderD*. The running Rust-disabled kernel cannot load the new Rust driver by itself. Rebuild and reinstall the ANE runtime overlay and KMD against the updated kernel before loading ANE. Do not run make clean or make mrproper for this incremental path.

Kernel Rust toolchain requirements describe GCC configuration limits; a complete LLVM build remains the best supported alternative if the GCC build encounters a toolchain limitation.

Install KMD of ANE,

git clone https://github.com/eiln/ane && cd ane
sudo make
cd ane && sh run.sh install

if make failed, git clone my fork and retry https://github.com/allbilly/libane

Note: After dnf update / kernel update, the boot.bin is replaced and external monitor and ANE device tree was no longer supported. run theses install command again

sudo make LLVM=1 modules_install
sudo make LLVM=1 dtbs_install
sudo make LLVM=1 install
sudo reboot

2. Run examples

Supported ops: ADD, MUL, MIN, MAX, SUMSQ, CONV, CONCAT, GEMM, RELU, SIGMOID

#   op_mode=0 → a+b       add       (default)
#   op_mode=1 → a*b       mul
#   op_mode=2 → max(a,b)  max
#   op_mode=3 → min(a,b)  min
#   op_mode=4 → (a+b)^2   sq

pip install numpy
python examples/elementwise.py add
python examples/elementwise.py mul
python examples/elementwise.py max
python examples/elementwise.py min
python examples/elementwise.py sq

python examples/conv.py
python examples/concat.py
python examples/gemm.py
python examples/relu.py
python examples/relu_l2.py # using l2 cache
python examples/sigmoid.py # Look up table

For developer

If you would like to run new ops or model not inside examples/* , follow these steps. PR adding new ops to examples are more than welcome.

1. Generate mlmodel and hwx

Using MacOS 12.4 VM

UTM install macos https://ipsw.me/macOS/12.4/

  • version gucessed from last commit time in eiln/anecc, it worked. Other MacOS version < 14 might works too.
  • hwx gernerated by different MacOS are doucmented macos_hwx.md
python gen_mlmodel.py test.mlmodel
git clone https://github.com/freedomtan/coreml_to_ane_hwx && cd coreml_to_ane_hwx && make && mv ./coreml2hwx ../ && cd ../
./coreml2hwx ./test.mlmodel
cp /tmp/hwx_output/test/model.hwx ./mul.hwx

Working hwx example in hwx/*

The following historical control loads an Apple system H13 HWX on macOS through the private _ANEClient path. It does not establish loading of our custom exports. Use ANEForge's private MIL compile/dispatch path for macOS execution and the guarded Asahi loader for our exported HWX. See the runtime scope and findings.

cd coreml_to_ane_hwx/ane
make run_hwx_with_ane_client
./run_hwx_with_ane_client \
  /System/Library/PrivateFrameworks/VideoProcessing.framework/Versions/A/Resources/cnn_frame_enhancer_320p.H13.espresso.hwx

No access to MacOS (Github action)

Check the gihub action config ane-generation.yml, trigger manually and go to actions/runs/runid/ -> Artifacts -> download and unzip

  • ❌ Not working now, sadly macos14 is the oldest macos version on GH action, which is not yet supported by anecc, you need MacOS 12.4 VM
  • If you have no access to Macos 12, you can only use pre-generated .hwx here

2. (Optional) Parse hwx

python parse.py hwx/sum.hwx          # H13 (M1/M4), default
python parse.py mul_h16_macos26.hwx 7 # H16 (A17 Pro/M4), explicit subtype

Subtype: default is 4 (H13). For H16-format HWX generated on macOS >= 26 for newer ANE, pass 7. The parser auto-detects H13 vs H16 from the subtype value — it doesn't auto-detect from the binary.

3. Convert and run ane

compile python binding from eiln and then copy libane_python.so to /usr/lib/

uv venv --python=3.11 && source .venv/bin/activate
uv pip install https://github.com/eiln/anecc.git#subdirectory=anecc https://github.com/eiln/ane.git#subdirectory=bindings/python/python
anecc hwx/sum.hwx -o hwx/sum.ane
python run.py ./hwx/sum.ane 

4. hwx2py

Run ANE ops without anecc and .ane file

  • op.hwx -> hwx2py -> op_from_hwx.py
  • it extract the cmd buf from hwx file as hex blob and replay directly. Python files in examples/* are cleaned and commented version originally generated from hwx2py.py
  • cleaned with how_to_parse_hex_blob.md
python experimental/hwx2.py hwx/sum.hwx -o sum_from_hwx.py
python sum_from_hex.py

5. How to run CONV

Via anecc (compiled .ane model)

asahi@fedora:~/allbilly_ane$ anecc hwx/tinygrad/conv.hwx -o hwx/conv.ane
anecc::info: found input 1/1: (1, 3, 1, 1)
anecc::info: found output 1/1: (1, 3, 1, 1)
anecc::info: compiled anec to: hwx/conv.ane

asahi@fedora:~/allbilly_ane$ python run.py ./hwx/conv.ane
(1, 3, 1, 1) float16
[[18.]]

(run.py fills all channels with 3.0; depthwise conv with kernel weight 2.0 gives 3×2×3=18)

Via direct register programming

Both conv_from_hwx.py and conv_from_relu.py produce the same correct result:

asahi@fedora:~/allbilly_ane$ python examples/conv_from_hwx.py 
submit returned: 0
Total output values: 8192, non-zero count: 3
Non-zero indices: [ 0 32 64]
Non-zero values: [12.  6. 12.]
output[:64] = [12.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.
  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  6.  0.  0.  0.
  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.
  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.]

asahi@fedora:~/allbilly_ane$ python examples/conv_from_relu.py 
submit returned: 0
Total output values: 8192, non-zero count: 3
Non-zero indices: [ 0 32 64]
Non-zero values: [12.  6. 12.]
output[:64] = [12.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.
  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  6.  0.  0.  0.
  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.
  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.]

Both are correct. Non-zero at indices [0, 32, 64] = channels 0/1/2 with values [12, 12, 12]:

  • Input: channel 0 = 1.0, channel 1 = 2.0, channel 2 = 3.0
  • Kernel: 1×1 pointwise (ConvCfg kw=1, kh=1), all 9 weights = 2.0
  • Output: each channel = 1×2 + 2×2 + 3×2 = 12 ✓

Bug fix: The original kernel_hex had 12 extra leading zero bytes, shifting all 9 weight positions by 6 fp16. This caused the KDMA to read zeros instead of the actual 2.0 weights. The fix uses the compiled .ane file which has the kernel data in the proper format.

Which one is the reference? conv_from_hwx.py is the original HWX-parsed version (loads firmware from hwx/tinygrad/conv.hwx). conv_from_relu.py was derived by adapting the relu structure with conv register values. Both use the same firmware + kernel data and produce identical results.

Common pitfalls:

  • The kernel hex must be byte-exact to the reference (wrong kernel weight positions caused channels 1 and 2 to show ~0 instead of 6.0 and 12.0)
  • Both files require the conv.hwx firmware file at hwx/tinygrad/conv.hwx
  • The input data layout uses STRIDE=32 between channels, matching the firmware's expected layout

6. How to run RELU

Via anecc (compiled .ane model)

asahi@fedora:~/allbilly_ane$ anecc hwx/tinygrad/relu.hwx -o hwx/relu.ane
anecc::info: found input 1/1: (1, 1, 1, 77)
anecc::info: found output 1/1: (1, 1, 1, 77)
anecc::info: compiled anec to: hwx/relu.ane

asahi@fedora:~/allbilly_ane$ python run.py ./hwx/relu.ane
(1, 1, 1, 77) float16
[[3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3.
  3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3.
  3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3. 3.
  3. 3. 3. 3. 3.]]

(run.py fills all 77 elements with 3.0; relu(3.0) = 3.0, all pass through)

Via direct register programming

Relu uses TileDMA source + conv pipeline (Family 2). Three examples demonstrate different approaches:

1. Raw firmware (examples/relu.py): Uses the relu.hwx firmware as-is with TileDMA source registers. Works standalone with no L2 priming.

asahi@fedora:~/allbilly_ane$ python examples/relu.py
output = [0. 5. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
expected relu = [0. 5. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]

(Input: element 0=-3.0, element 1=5.0; relu: [-3 → 0, 5 → 5, ...])

2. From add (experimental/relu_from_add.py): Derived from the add.py firmware structure, uses an alternative L2-style register layout where L2Cfg=0x6c013800 acts as a stream header redirecting to TileDMA Src bank. Works standalone.

asahi@fedora:~/allbilly_ane$ python experimental/relu_from_add.py
output =  [3. 5. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
expected relu =  [3. 5. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]

(Input: element 0=3.0, element 1=5.0; both positive → pass through)

3. L2-source standalone (examples/relu_l2.py): Uses L2 cache controller for input data sourcing instead of TileDMA. The SrcStream header at host 0x168 is re-routed from ANE 0x13800 (TileDMA Src) to ANE 0x04800 (L2). No firmware modules loaded. PE/NE disabled — the conv pipeline naturally clamps negative inputs to zero (ReLU behavior is in hardware, not software). The np.maximum in the Python script only computes the expected output for comparison.

asahi@fedora:~/allbilly_ane$ python examples/relu_l2.py
output = [0. 5. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
expected relu = [0. 5. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]

Key architectural difference vs TileDMA source (relu.py): relu_l2.py re-routes the SrcStream at host offset 0x168 to point to ANE 0x04800 (L2 cache controller) instead of 0x13800 (TileDMA Src engine). The L2's SourceCfg/SourceBase/SourceRowStride registers (placed at the re-routed TileDMA Src region 0x16C-0x1AC) handle input data sourcing. No firmware modules are loaded (all-zero FW DMA context at 0x2C). PE and NE are fully disabled — the conv pipeline naturally clamps negative inputs to zero without NE activation.

TileDMA vs L2: which is better?

Aspect TileDMA (relu.py) L2 (relu_l2.py)
Firmware loaded Yes (DMA_ACTIVE entries) None (all-zero KDMA)
Pipeline stages TileDMA→Conv→PE→NE→L2→Dst L2→Conv→Dst
Startup time Longer (firmware init) Instant
NE ReLU Hardware via MACCfg bit 16 Conv pipeline clamps negatives
Code complexity 320 lines, structured registers 113 lines, hex blob
Flexibility Multi-input, advanced strides Single sequential source

L2 is better for simple pass-through ops (relu, identity) — no firmware loading, shorter pipeline, fewer registers. The conv pipeline naturally clamps negatives to zero without NE.

TileDMA is better for complex ops (conv, gemm, concat) needing multi-input, kernel weights, or advanced addressing patterns.

There is no elementwise_l2.py — elementwise add needs PE/NE active for actual computation (summing two inputs), which defeats the purpose of the L2 approach. The L2 vs TileDMA comparison is fully demonstrated by relu.py vs relu_l2.py.

7. How to run GEMM (Matrix Multiply)

Cin=512, Cout=512, uses TileDMA source + KDMA kernel weights. The __TEXT.__const section in the original .hwx has all-zero weights (generated with np.zeros for pipeline testing). With injected non-zero weights, GEMM produces correct output:

asahi@fedora:~/allbilly_ane$ python examples/gemm.py
submit returned: 0
output[:64] = [189.5   0.    0.    0.    0.    0.    0.    0.    0.    0.    0.    0.
   0.    0.    0.    0.    0.    0.    0.    0.    0.    0.    0.    0.
   0.    0.    0.    0.    0.    0.    0.    0.  189.5   0.    0.    0.
   0.    0.    0.    0.    0.    0.    0.    0.    0.    0.    0.    0.
   0.    0.    0.    0.    0.    0.    0.    0.    0.    0.    0.    0.
   0.    0.    0.    0. ]

examples/gemm.py injects 0.5 weights into __TEXT.__const at runtime. With 256 active input positions (stride 32 in 8192-element buffer), output per channel ≈ 256 × 0.5 weighted by NE processing.

The zero-weight bug: The original gemm.hwx from tinygrad has __TEXT.__const full of zeros (524288 bytes, 0 non-zero fp16 out of 262144). Generate a new one on macOS with real weights:

# In gen_mlmodel.py:
K = 512
weights = np.random.randn(K, K).astype(np.float32)  # non-zero!
builder.add_inner_product(name='ip_layer', W=weights, ...)

Or inject weights directly (as gemm.py does) — no macOS needed for this approach.

asahi@fedora:~/allbilly_ane$ anecc hwx/tinygrad/gemm.hwx -o hwx/gemm.ane
anecc::info: found input 1/1: (1, 512, 1, 1)
anecc::info: found output 1/1: (1, 512, 1, 1)
anecc::info: compiled anec to: hwx/gemm.ane

asahi@fedora:~/allbilly_ane$ python run.py ./hwx/gemm.ane
(1, 512, 1, 1) float16
[[0.]]

asahi@fedora:~/allbilly_ane$ python examples/hwx2py.py hwx/tinygrad/gemm.hwx -o examples/gemm_from_hwx.py --use-anecc
Wrote examples/gemm_from_hwx.py

asahi@fedora:~/allbilly_ane$ python examples/gemm_from_hwx.py
SUBMIT ret=0
output[0] = 0.0

Analysis: Both methods return 0 — the KDMA weights are present in the .ane file (krn_size=524288) but the actual weight values are all zero (0 non-zero bytes out of 524288). The tinygrad model file has zero-initialized weights. Standalone script:

asahi@fedora:~/allbilly_ane$ python examples/gemm.py
submit returned: 0
output[:64] = [0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
NOTE: GEMM kernel weights are all zero in tinygrad model

Input: 512 elements at STRIDE=32 (SrcRowStride=0x40=64 bytes). Without real weights, any input gives 0.

Root cause: The gemm.hwx Mach-O __TEXT.__const section (524288 bytes = 512×512 weight matrix) is all zeros. The tinygrad model file was generated for ANE pipeline testing without actual trained weights.

How to regenerate with real weights

Follow the same process as Section 1 "Generate hwx", but modify gen_mlmodel.py:

For GEMM (inner_product):

# In gen_mlmodel.py, uncomment the inner_product line:
K = 512
weights = np.zeros((K, K)) + 3.0   # non-zero weights!
# or: weights = np.random.randn(K, K).astype(np.float32)
bias = np.ones(K)
builder.add_inner_product(name='ip_layer', W=weights, b=bias,
    input_channels=K, output_channels=K, has_bias=True,
    input_name='image', output_name='probs')

For CONCAT:

input_features = [('image', datatypes.Array(16)), ('image2', datatypes.Array(16))]
output_features = [('probs', datatypes.Array(32))]
builder = NeuralNetworkBuilder(input_features+input_features2, output_features)
builder.add_elementwise(name='concat', input_names=['image', 'image2'],
    output_name='probs', mode='CONCAT')

Then on macOS:

python gen_mlmodel.py           # generates test.mlmodel
./coreml2hwx ./test.mlmodel      # ANE compiles and dumps .hwx
cp /tmp/hwx_output/test/model.hwx ./gemm.hwx  # or concat.hwx

Can GitHub Actions work? Yes, for weighted ops. GH Actions only has macOS 14 runners, but:

  • Weighted ops (GEMM, Conv, Sigmoid): CoeffDMAConfig=0x81 in our gemm.hwx proves macOS 14 correctly generates real KDMA. The existing zero weights come from np.zeros in gen_mlmodel.py, not from macOS version. Changing to non-zero weights on GH Actions will produce correct .hwx files.
  • Elementwise ops (Add, Mul, Relu): macOS 14 adds spurious CoeffDMAConfig=0x80 entries (all 16 channels) that macOS 12 doesn't. The hwx2py --no-clean flag preserves them, or use --use-anecc which strips them automatically. The KDMA data in __TEXT.__const is zero anyway for elementwise ops (no weights needed), so the spurious entries don't affect correctness — they just add noise.

macOS 12 Monterey (real machine or VM) is only strictly needed for clean elementwise hwx files without any spurious KDMA entries.

8. How to run CONCAT

Cin=16, Cout=16, TileDMA source, 2 inputs (16 + 16384 → 16400 output). Loads kernel data from compiled .ane file — anecc handles the KDMA kernel region setup properly.

Concat is the only multi-tile example — all others (add, relu, conv, gemm, sigmoid) use td_count=1. Concat chains 2 task descriptors via W7=0x300 (next_ptr) and requires td_count=2 with tsk_size=0x574 (total size of both tiles) in the submit call. Using td_count=1 causes tile 2 to be silently skipped.

asahi@fedora:~/allbilly_ane$ python examples/concat.py
submit returned: 0
Total output channels: 16400
First 4 (tile1, src2→2.0): [2. 2. 2. 2.]
Channels 16380-16383 (tile1): [2. 2. 2. 2.]
Channels 16384-16387 (tile2, src1→3.0): [3. 3. 3. 3.]
Last 4 (tile2): [3. 3. 3. 3.]
All 2.0 (tile1): True
All 3.0 (tile2): True

2 inputs (src1=3.0 for 16 channels, src2=2.0 for 16384 channels). Tile 1 processes src2 (rbase0=6), tile 2 processes src1 (rbase0=5). Both write to different offsets in the output buffer via DstBaseAddr (tile1=0, tile2=0x100000). Buffer sizes must be large enough for stride-based access (1MB+ for 16384 channels × stride 64).

The spurious CoeffDMAConfig=0x80 pattern (macOS 14+) is handled by anecc's kernel region setup.

9. How to run SIGMOID

Cin=1, Cout=1, TileDMA source, KDMA with valid coefficient data. This is the only weight-model that produces correct output through hwx2py.

asahi@fedora:~/allbilly_ane$ anecc hwx/tinygrad/sigmoid.hwx -o hwx/sigmoid.ane
anecc::info: found input 1/1: (1, 1, 1, 77)
anecc::info: found output 1/1: (1, 1, 1, 77)
anecc::info: compiled anec to: hwx/sigmoid.ane

asahi@fedora:~/allbilly_ane$ python run.py ./hwx/sigmoid.ane
(1, 1, 1, 77) float16
[[0.9526367 ... 0.9526367]]

asahi@fedora:~/allbilly_ane$ python examples/hwx2py.py hwx/tinygrad/sigmoid.hwx -o examples/sigmoid_from_hwx.py --use-anecc
Wrote examples/sigmoid_from_hwx.py

asahi@fedora:~/allbilly_ane$ python examples/sigmoid_from_hwx.py
SUBMIT ret=0
output[0] = 0.95263671875

Analysis: Output 0.95263671875 matches expected sigmoid(3.0) = 1/(1+e⁻³) ≈ 0.952574 within fp16 precision. The sigmoid model has C=1, STRIDE=96, valid KDMA coefficients (0x81, not spurious 0x80 pattern), and the 77-element output all correctly shows sigmoid(3.0).

For reverse engineer

No particualr orders, poke around these steps if u like

Check out the code in experimental/*, some example to reverse firmware with GchidraMCPd

Extract ANE firmware

ipsw img4 extract —im4p —output out2 h13_ane_fw_styx_j5x.im4p

Some random firmware notes

  • Apple Neural Engine Internal
  • CANEController::CmdProcessor() is the main function that parses ~70 commands.
  • Find CSneTMDrv parseTD =CSneTMDrvH13_ParseOutTd
  • aneCmdSend
  • /System/Library/PrivateFrameworks/ANECompiler.framework/ANECompiler ZinIrRegBitPrintOutDebug -> broken sym link
  • In ANECompiler : ZinIrRegBitPrintOutDebug(unsigned int, ZinIrCodegenTd_v5 *, int, std::ostream &)

Dump BO

handle[0]=1 → BO for tile/buffer index 0 (where the command/weights live)
handle[4]=2 → BO for output tile (dst 0)
handle[5]=3 → BO for input 0
handle[6]=4 → BO for input 1

python3 /home/asahi/ane-ex/dump.py /tmp/sum_cmd.bin \
  --decode-cmd --cmd-sbs-compact-grouped

python3 /home/asahi/ane-ex/dump.py /tmp/sum_weights.bin --dtype fp16 --count 64


python3 /home/asahi/ane-ex/dump.py /tmp/ane_bo_04_post.bin --dtype fp16 --tile 1,64,1,1,64,64 --count 8
[3. 3. 3. 3. 3. 3. 3. 3.]

python3 /home/asahi/ane-ex/dump.py /tmp/ane_bo_05.bin --dtype fp16 --tile 1,64,1,1,64,64 --count 8
[1. 1. 1. 1. 1. 1. 1. 1.]

python3 /home/asahi/ane-ex/dump.py /tmp/ane_bo_06.bin --dtype fp16 --tile 1,64,1,1,64,64 --count 8
[2. 2. 2. 2. 2. 2. 2. 2.]

Dump IOCTL

sudo bpftrace -e '
tracepoint:syscalls:sys_enter_ioctl /args->cmd == 0xc0186441/ { printf("ANE BO_INIT\n"); }
tracepoint:syscalls:sys_enter_ioctl /args->cmd == 0xc0086442/ { printf("ANE BO_FREE\n"); }
tracepoint:syscalls:sys_enter_ioctl /args->cmd == 0xc0986443/ { printf("ANE SUBMIT\n"); }'

Attaching 3 probes...
ANE BO_INIT
ANE BO_INIT
ANE BO_INIT
ANE BO_INIT
ANE BO_INIT
ANE SUBMIT
ANE BO_FREE
ANE BO_FREE
ANE BO_FREE
ANE BO_FREE
ANE BO_FREE

Dump IOCTL submit

You can use bpftrace to dump ioctl submit content. Most ops use td_count=1 (single tile). The only exception is concat.py which chains 2 tiles and uses td_count=2.

sudo bpftrace -e '
struct drm_ane_submit {
    unsigned long long tsk_size;  //  __u64 / uint64
    unsigned int td_count;        //  __u32 / uint32
    unsigned int td_size;
    unsigned int handles[1];      // use 1 or other number as bpftrace does not support variable ANE_TILE_COUNT 
    unsigned int btsp_handle;
    unsigned int pad;
};

tracepoint:syscalls:sys_enter_ioctl 
/args->cmd == 0xc0986443/ 
{
    $s = (struct drm_ane_submit *)args->arg;
    printf("ANE SUBMIT: tsk_size=%llu, td_count=%u, td_size=%u, btsp_handle=%u\n", 
           $s->tsk_size, $s->td_count, $s->td_size, $s->btsp_handle);
}'

ANE SUBMIT (single-tile, e.g. add/relu): tsk_size=628, td_count=1, td_size=628, btsp_handle=0
ANE SUBMIT (multi-tile, e.g. concat):   tsk_size=1396, td_count=2, td_size=628, btsp_handle=5

but i modified the kernel driver directly to print the dump.

ANE NN {
  fd=3
  data=0xaaab63898000
  anec={size=17024 td_size=628 td_count=1 tsk_size=628 krn_size=16384 src_count=2 dst_count=1}
  btsp_chan={map=0xfffece008000 size=16384 handle=5 offset=4295049216}
  chans=[
    00: {map=0xfffece7d0000 size=32768 handle=1 offset=4294967296},
    01: {map=(nil) size=0 handle=0 offset=0},
    02: {map=(nil) size=0 handle=0 offset=0},
    03: {map=(nil) size=0 handle=0 offset=0},
    04: {map=0xfffece7cc000 size=16384 handle=2 offset=4295000064},
    05: {map=0xfffece1dc000 size=16384 handle=3 offset=4295016448},
    06: {map=0xfffece00c000 size=16384 handle=4 offset=4295032832},
    07: {map=(nil) size=0 handle=0 offset=0},
    08: {map=(nil) size=0 handle=0 offset=0},
    09: {map=(nil) size=0 handle=0 offset=0},
    10: {map=(nil) size=0 handle=0 offset=0},
    11: {map=(nil) size=0 handle=0 offset=0},
    12: {map=(nil) size=0 handle=0 offset=0},
    13: {map=(nil) size=0 handle=0 offset=0},
    14: {map=(nil) size=0 handle=0 offset=0},
    15: {map=(nil) size=0 handle=0 offset=0},
    16: {map=(nil) size=0 handle=0 offset=0},
    17: {map=(nil) size=0 handle=0 offset=0},
    18: {map=(nil) size=0 handle=0 offset=0},
    19: {map=(nil) size=0 handle=0 offset=0},
    20: {map=(nil) size=0 handle=0 offset=0},
    21: {map=(nil) size=0 handle=0 offset=0},
    22: {map=(nil) size=0 handle=0 offset=0},
    23: {map=(nil) size=0 handle=0 offset=0},
    24: {map=(nil) size=0 handle=0 offset=0},
    25: {map=(nil) size=0 handle=0 offset=0},
    26: {map=(nil) size=0 handle=0 offset=0},
    27: {map=(nil) size=0 handle=0 offset=0},
    28: {map=(nil) size=0 handle=0 offset=0},
    29: {map=(nil) size=0 handle=0 offset=0},
    30: {map=(nil) size=0 handle=0 offset=0},
    31: {map=(nil) size=0 handle=0 offset=0}
  ]
}
ANE SUBMIT {
  tsk_size=628
  td_count=1
  td_size=628
  btsp_handle=5
  pad=0
  handles=[1, 0, 0, 0, 2, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
}
CMD_BUF (handle[0]) (size=628):
  0000: 00 00 00 02 00 00 00 00 22 04 00 00 00 00 00 00
  0010: 6a f8 ff 00 00 00 00 00 00 98 00 30 00 00 00 00
  0020: 66 49 02 00 00 00 00 00 00 f8 01 f4 00 00 00 00
  0030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0050: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0070: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0090: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  00a0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  00b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  00c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  00d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  00e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  00f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0100: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0110: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0120: 00 00 00 00 00 00 00 3c 01 00 01 00 01 00 00 00
  0130: 2a 00 00 00 40 00 00 00 40 00 00 00 01 00 01 00
  0140: 01 00 00 00 21 a0 00 50 41 20 00 00 01 00 01 00
  0150: 01 00 00 00 04 00 00 00 00 00 00 00 33 00 00 00
  0160: 00 00 00 00 00 00 00 00 00 38 01 6c 81 38 03 00
  0170: 80 38 03 00 00 00 00 00 40 00 00 00 40 00 00 00
  0180: 00 10 00 00 00 00 00 00 00 00 00 00 40 00 00 00
  0190: 40 00 00 00 00 10 00 00 00 00 00 00 00 00 00 00
  01a0: 00 00 00 00 31 20 00 01 30 20 00 00 00 00 00 00
  01b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  01c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  01d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 48 00 44
  01e0: 00 00 00 00 72 01 50 01 00 00 00 00 10 00 00 00
  01f0: 20 04 00 00 00 04 00 00 00 04 00 00 40 04 00 00
  0200: 10 00 00 00 20 04 00 00 00 04 00 00 00 04 00 00
  0210: 7a 01 50 00 60 08 00 00 00 00 00 00 00 00 00 00
  0220: 00 00 00 00 00 00 00 00 00 88 00 0c 00 00 08 00
  0230: 00 00 00 3c 00 00 00 3c 00 00 80 3f 00 c8 00 10
  0240: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0250: 00 00 00 00 00 78 01 18 c1 00 00 04 00 00 00 00
  0260: 40 00 00 00 40 00 00 00 00 10 00 00 00 00 00 00
  0270: 31 20 00 01
(1, 64, 1, 1) float16
[[[[5.]]
    ...
  [[5.]]]] 

Improvements

  1. Add PASS/FAIL validation with tolerance instead of just printing: match = np.allclose(output, expected, atol=0.1) print(f"{'PASS' if match else 'FAIL'}")

  2. Add a batch test mode (--all flag or mode="all") that runs all ops and reports results, like the RKNPU version's loop.

  3. Add SUB support — the ANE PE can do a - b = a + (-b), achievable by negating the L2 source for the second operand.

  4. Use np.allclose for comparison instead of printing arrays side-by-side — makes pass/fail obvious at a glance.

Reference

About

Run ops on Apple ANE in NPU register with pure python on M1 Asahi Linux. No Espresso, No CoreML, no metal, no .mlmodels file, no .hwx file, no ANEcompiler, no private Apple API, no anecc, nothing.

Resources

Stars

21 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages