메뉴
HN
Hacker News • 14일 전

애플 뉴럴 엔진(ANE)을 되돌아보며 리버스 엔지니어링하다

IMP
7/10
핵심 요약

개발자가 3년 전 중단했던 M1의 애플 뉴럴 엔진(ANE) 리버스 엔지니어링을 M5에서 ANE가 GPU 코어에 통합되며 독립 NPU의 종말이 가시화된 것을 계기로 다시 시작한 기술 분석 글입니다. ANE는 CNN 시대의 예측 가능한 데이터 재사용 패턴에 최적화된 16개 연산 코어와 2048개 병렬 MAC 레인으로 구성되어 있으며, 이러한 설계가 트랜스포머 워크로드와 맞지 않아 독립 NPU가 쇠퇴하게 되었음을 보여줍니다. 하드웨어 설계 결정이 ML 워크로드의 패러다임 변화를 어떻게 반영하는지 이해하는 데 중요한 글입니다.

번역된 본문

애플 뉴럴 엔진을 되돌아보며 리버스 엔지니어링하다 2026년 8월 10일 (5089 단어)

3년 전, 나는 리버스 엔지니어링한 애플 뉴럴 엔진(ANE) 드라이버 작업을 중단했다. ANE 블록은 그다지 유용하지 않다는 슬픈 자각(깨달음)과 함께, 더 유용한 일을 할 수 있다는 생각이 들었기 때문이었다. 그래서 다른 더 유용한 블록들을 업스트림(커널 메인라인에 반영)하는 작업으로 옮겼다. ANE의 아키텍처는 범용 가속기 플랫폼을 구축하기에는 너무 고유한 설계 방향(오피니언이 강한 구조)이었고, 리눅스 드라이버가 ANE 하드웨어 API 접근을 효과적으로 열어준다 해도 그것이 처리할 수 있는 워크로드의 범위를 넓힐 수는 없었다. 심지어 macOS조차 자체 ANE를 파인더(Finder)에서 업샘플링된 미리보기 이미지를 생성하는 용도로만 정기적으로 사용할 뿐이었다.

https://github.com/eiln/ane/tree/main

M1 다이 샷(칩 사진): https://mastodon.social/@dougall/115149886886125067

M5(2025)의 헤드라인 기능은 "LLM 성능"이었고, 애플은 편리하게도 ANE 코어를 GPU 코어 안에 통합해 버렸다. 나는 이것이 올 줄 알고 있었지만, 공식적으로 독립형 NPU의 종말의 시작처럼 느껴진다.

그래서 ANE의 보이는 죽음을 기리며, 우리는 훨씬 더 쓸모없는 일을 해볼 것이다: 돌아가서 M1의 ANE를 리버스 엔지니어링하고, 시작했던 일을 끝마치는 것이다. 벌써 3년이 지났고(젠장), 나는 처음 이 작업을 했을 때보다 더 많은 것을 알고 있어야 한다. 3년 전의 목표가 ANE에서 연산(ops)을 실행시켜 유용하게 만드는 것이었다면, 이번에는 내부 아키텍처 전체 — 연산, 데이터패스, 스케줄러, 메모리, 실행 모델 — 를 매핑하는 것이 목표다. 왜냐하면 이러한 내부 설계 결정들은 애플이 A11 바이오닉(2017)에서 실리콘에 최초로 반영하기로 한 ML 워크로드에 대한 가정들을 드러내주고, CNN 시대의 NPU에서 오늘날 트랜스포머 워크로드를 실행하는 GPU로의 전환에 대해 무엇을 말해주는지 보여주기 때문이다.

  1. 연산(Compute)

16개의 연산 코어는 아마도 ANE에서 가장 흥미롭지 않은 부분일 것이다. 애플은 원래 조밀한(dense) 이미지 처리 CNN 워크로드를 겨냥했는데, 이는 예측 가능한 재사용 패턴을 가진 조밀한 텐서 축소(reduction) 연산으로 구성된다. M1 ANE 연산 코어는 대규모 병렬 곱셈-누산(MAC, multiply-accumulate) 유닛 어레이이지만, 그것만으로는 어떤 워크로드를 위해 설계되었고 무엇에 강한지 거의 아무것도 말해주지 않는다. 합성곱(convolutional) 레이어는 활성화 윈도우와 학습된 커널 가중치 사이의 내적(dot product)을 계산하고, 어텐션(attention)은 쿼리(query)와 키(key) 벡터 사이의 내적을 계산한다. 내적은 내적이고, MAC이 정확히 그 일을 한다. 2017년 CNN 모델들에 ANE를 특화시킨 것은 MAC 자체가 아니라, MAC을 둘러싼 데이터플로우, 즉 MAC의 입력과 출력이 언제, 어디로 들어오고, 머무르고, 이동하는가이다. 트랜스포머가 깨뜨린 가정 — 특히 자기회귀(autoregressive) 디코딩에서 — 은 예측 가능한 재사용 패턴이었는데, ANE는 이를 활용해 휴대폰에서 실행 가능할 만큼 효율적인 데이터플로우를 설계했었다. M5의 결정은 ANE의 연산 코어가 트랜스포머에 대해 여전히 유용했지만, 다른 데이터플로우 안에서라는 것을 확정해 준다.

그래도, 16개 연산 코어 각각 내부의 데이터패스를 살펴보자:

┌────────────────────── 코어 ─────────────────────┐ │ ┌───────── 256× MAC ─────────┐ ┌────────────┐ │ │ │ MAD ─► add ─► accumulator │─►│ activation │ │ │ │ ▲ │ │ └────────────┘ │ │ │ └──────────┘ │ │ │ └─────────────────────────────┘ │ └─────────────────────────────────────────────────┘

곱셈-누산(Multiply-Accumulate)

ANE에는 16개의 병렬 연산 코어가 있다. 각 연산 코어에는 128개의 FP16(또는 256개의 INT8) 병렬 곱셈-누산(MAC) 레인이 있다. 각 MAC 레인은 다음의 재귀 연산을 수행한다:

s ← s + a × b

두 피연산자 a와 b를 곱한 후, 그 곱을 누적 합(어큐뮬레이터)에 더한다. T 사이클에 걸쳐 MAC 연산을 반복하면 T항의 내적을 계산한다:

s_T = s_0 + Σ(t=0~T-1) a_t · b_t

따라서 MAC 레인은 시간에 걸친 스칼라 축소(reduction)를 수행한다. 16코어 ANE는 2048개의 병렬 MAC 레인을 가진다:

코어당 128 레인 × 16 코어 = 2048개 병렬 MAC 레인

따라서 매 사이클마다 공간적으로 2048개의 병렬 축소가 수행되며, 시간이 유일한 축소 축이다:

S_T[q,p] = S_0[q,p] + Σ(t=0~T-1) a_t[q,p] · b_t[q]

개별 MAC 레인은 행렬이나 텐서의 어떤 차원을 다루고 있는지 알지 못한다.

원문 보기
원문 보기 (영어)
Retrospectively Reverse-Engineering Apple's Neural Engine Aug 10, 2026 (5089 words) I stopped working on the reverse-engineered Apple Neural Engine (ANE) driver three years ago, upon a sad mini realization that the ANE block is just not that useful, and I could be doing more useful things, and moved onto upstreaming other, more useful, blocks. The ANE's architecture was too opinionated to build a general-purpose accelerator platform around it, and a linux driver effectively opening ANE hardware API access could not broaden the class of workloads it could do. Even macOS only regularly uses their own ANE to generate upsampled preview images in Finder. https://github.com/eiln/ane/tree/main M1 die shot: https://mastodon.social/@dougall/115149886886125067 The M5 (2025)'s headline feature was "LLM performance", and they also conveniently folded the ANE cores inside the GPU cores — I knew it was coming, but it officially feels like the beginning of the end for the standalone NPU. So, in honor of the ANE’s apparent demise, we will do something even more useless: go back and reverse-engineer the ANE on the M1, finish what we started. It's been three years (fuck), and I should know more than I did when I first worked on this. If the goal three years ago was to make the ANE useful by running ops on it; this time, it's more about mapping the full internal architecture — compute, datapath, scheduler, memory, and execution model — because those internal design decisions reveal the assumptions about ML workloads that Apple was willing to commit to silicon first in the A11 Bionic (2017), and what that says about the shift from CNN-era NPUs to today's GPUs running transformer workloads. 1. Compute The 16 compute cores are probably the least interesting part of the ANE. Apple originally targeted dense image-processing CNN workloads, which consists of dense tensor reductions with predictable reuse. The M1 ANE compute core is a large parallel array of multiply-accumulate (MAC) units, but that alone says almost nothing about what workloads it was designed for and accels at. A convolutional layer does a dot product between an activation window and learned kernel weights, and attention does a dot product between a query and key vector. A dot product is a dot product, and a MAC does just that. What specialized ANE to the 2017 CNN models is not the MAC, but dataflow surrounding the MACs: when and where MAC inputs and outputs enter, stay, move. The assumption that transformers broke, especially with autoregressive decode, was predictable reuse patterns, which the ANE exploited to architect a dataflow efficient enough to run on phones. The M5 decision confirms that ANE's compute core remained still useful for transformers, but inside a different dataflow. Still, here's the datapath inside each of the 16 compute cores: ┌────────────────────── core ─────────────────────┐ │ ┌───────── 256× MACs ─────────┐ ┌────────────┐ │ │ │ MAD ─► add ─► accumulator │─►│ activation │ │ │ │ ▲ │ │ └────────────┘ │ │ │ └──────────┘ │ │ │ └─────────────────────────────┘ │ └─────────────────────────────────────────────────┘ Multiply-Accumulate ANE has 16 parallel compute cores. Each compute core has 128 FP16 (or 256 INT8) parallel multiply-accumulate (MAC) lanes. Each MAC lane performs the recurrence: \[ s\leftarrow s+a\times b \] Multiply two operands \(a\) and \(b\), and then add the product to the running sum (accumulator). Repeating the MAC operation over T cycles computes a T-term dot product: \[ s_T=s_0 + \sum_{t=0}^{T-1} a_t \, b_t. \] A MAC lane thus performs a scalar reduction over time . A 16-core ANE has 2048 parallel MAC lanes, \[ 128\ \text{lanes/core}\times16\ \text{cores} = 2048\ \text{parallel MAC lanes} \] So each cycle performs 2048 parallel reductions spatially , with time being the only reduction axis: \[ S_T[q,p] = S_0[q,p] + \sum_{t=0}^{T-1} a_t[q,p]\,b_t[q]. \] An individual MAC lane does not know what dimension of the matrix or tensor it is reducing over. It's important to note that a dot product vs matrix multiplication vs convolution arises from how the operands are mapped and scheduled onto the core. The ANE core (with the exception of kernel memory, discussed later) does not encode a 4-channel CNN layer into the hardware. Internally, the MAC datapath consists of a multiplier, adder, and a 32-bit accumulator register. Each cycle, the adder adds the fresh multiplier output with the previous sum, which then becomes the new running sum. operand a ──┐ ┌────────────┐ p[31:0] ┌──────────────┐ s_next[31:0] ┌─────────────┐ ├──►│ MULTIPLIER │────────────►│ 32-BIT ADDER │────────────────►│ ACCUMULATOR │ operand b ──┘ └────────────┘ └──────▲───────┘ └──────┬──────┘ │ │ s[31:0] └────────────────────────────────┘ This feedback path keeps the partial sum in memory local to the MAC lane, so it does not need fetched from an external memory far away, between MAC cycles. Regarding resolution, it does fixed-point reduction with FP16 at readout. The multiplier is 16-bit, accumulated in a 32-bit register as Q16.16, then read out as FP16 via sign-extend and etc. Working in integer (hex) FP16 representation, to probe the accumulator range, build a CoreML ANE program that computes a dot product with a vector of all (1)s, so each multiplier results in a bounded v, but the running sum in the accumulator keeps growing: \[ s=\sum_{i=0}^{255}v=256v. \] (v) CPU hex CPU value ANE hex CoreML value 127.9375 0x77ff 32752 0x77ff 32752 128 0x7800 32768 0x7c00 +∞ −128 0xf800 −32768 0xf800 −32768 −128.125 0xf801 −32800 0xfc00 −∞ Since 32768 is itself a valid FP16 word (0x7800), the ANE's 0x7c00 can't be FP16 output overflow, the clamp happens inside the accumulator, at \(2^{15}\). Thus the accumulator saturates at \(2^{15}\), exactly the range of a signed 32-bit fixed-point value with 16 fractional bits. Nonlinear Activation For a fused layer, the ANE computes: \[ y = f(\sum_k x_k w_k + b) \] Importantly, completed MAC sums feed directly into the post-MAC activation block, avoiding an intermediate memory round-trip. This is possible because the activation is pointwise: once a scalar reduction is complete, its activation depends only on that scalar and can be applied immediately. To determine how the ANE implements tanh() , compile a CoreML model containing a single TANH activation layer and inspect the resulting compiled hardware register file (hwx). The coefficient region contains 33 consecutive FP16 words beginning at 0x4288 : 00004270: 3120 3001 0000 0000 0000 0000 0000 0000 00004280: 0000 0044 0000 003c 0000 f52f d633 bc35 # 0.000000 0.124329 0.244873 0.358398 00004290: 6537 7038 1539 a239 183a 793a c93a 0a3b # 0.462158 0.554688 0.635254 0.704102 0.761719 0.809082 0.848145 0.879883 000042a0: 3e3b 673b 883b a23b b63b c63b d33b dd3b # 0.905273 0.925293 0.941406 0.954102 0.963867 0.971680 0.978027 0.982910 000042b0: e53b eb3b ef3b f33b f63b f83b fa3b fb3b # 0.986816 0.989746 0.991699 0.993652 0.995117 0.996094 0.997070 0.997559 000042c0: fc3b fd3b fe3b fe3b ff3b 0000 0000 0000 # 0.998047 0.998535 0.999023 0.999023 0.999512 000042d0: 003c 0300 6000 0000 0000 0000 0000 0000 Those 33 FP16 words match 33 IEEE LE FP16 quantized samples of \(\tanh(x)\): \[ T_i=\operatorname{round}_{16}\!\left(\tanh(i/8)\right), \qquad i=0,1,\ldots,32. \] Now switch to RELU activation layer: activation program NonlinearMode lookup coefficients identity 0 none ReLU 1 none tanh 2 33 FP16 words Thus, mode 2 selects a custom 33-entry lookup table. 33 points defines 32 intervals. With \(R=3\), the knots are \[ x_i=\frac{i}{8},\qquad i=0,\ldots,32, \] covering \([0,4]\) with spacing \(1/8\). The input maps into the table as \(u=2^R|x|\), so \(R\) sets the knot spacing. The resolution is smoother than its 33 bin; I suspect that adjacent entries are linearly interpolated. To test, build an impulse LUT with a single spike: \[ T_8=1,\qquad T_k=0\ \text{for }k\ne8,\qquad R=3. \] Then sweep the input across the two cells aro