Jungseob's Note
포스트

Apple Neural Engine의 병목은 데이터 이동이다 - M1 구조와 M3 DMA 실험 읽기

ANE의 MAC·작업 기술자·메모리 경로를 실험 근거로 설명하고, M3 전송 분할의 효과와 M5 관련 해석의 한계를 구분한다.

Apple Neural Engine의 병목은 데이터 이동이다 - M1 구조와 M3 DMA 실험 읽기

TL;DR

  • ANE의 트랜스포머 성능은 곱셈기의 수보다 데이터를 공급하는 방식에 크게 좌우된다. CNN에 맞춘 설계는 가중치를 코어 가까이에 두고 반복해서 쓰는 데 유리하다. 토큰마다 많은 가중치를 읽는 디코딩에서는 같은 메모리 구조가 제약이 된다.
  • M1 ANE의 작업 기술자는 고수준 명령어를 실행하는 프로그램보다 데이터 경로의 레지스터 설정에 가깝다. 컴파일러가 텐서의 배치와 이동을 미리 정한다. 그렇다고 가변 길이 텐서나 트랜스포머를 원천적으로 처리할 수 없는 것은 아니다.
  • M3 실험에서 KernelDMA와 TileDMA의 읽기 대역폭은 각각 37.99GB/s와 59.08GB/s였고 GPU는 77.70GB/s였다. 두 ANE 경로를 함께 썼을 때 실행 시간은 겹치기보다 합산되는 형태였다. 같은 DRAM을 공유해도 실제 공급 속도는 같지 않았다.
  • 연결된 후속 실험에서는 코어당 전송량이 1MiB의 정수배일 때 생기는 성능 급락을 분할 전송으로 피했다. Llama 3.2 1B 처리량은 10.0에서 24.3토큰/초로 개선됐다. 관측된 개선과 내부 프리페치 회로의 원인 가설은 구분해야 한다.
  • M5에서 독립 Neural Engine이 사라졌다는 해석은 공식 사양과 맞지 않는다. Apple은 GPU의 Neural Accelerator와 별도의 16코어 Neural Engine을 함께 명시한다. 원문의 M1 구조 분석, M3 측정, M5에 대한 전망을 서로 섞지 않아야 한다.

Source

Retrospectively Reverse-Engineering Apple’s Neural Engine — Eileen Yoon, 2026년 8월 10일.

보충 자료: 같은 저자의 Getting 50 GB/s Back Out of the ANE. M5 구성은 Apple의 공식 발표와 대조했다. 아래 수치와 회로 해석은 원문 실험의 정리이며 이 노트에서 하드웨어 실험을 재현한 것은 아니다.

Knowledge

CNN에 맞춘 것은 곱셈보다 데이터 이동이다

합성곱과 어텐션에는 모두 내적이 들어간다. 곱하고 누산하는 MAC 회로 자체는 입력이 이미지의 커널인지 쿼리와 키인지 구별하지 않는다. 차이는 어떤 피연산자를 어느 코어에 배치하고 얼마나 오래 보관하며 다음 연산에 어떻게 재사용하는지에 있다. ANE를 CNN 시대의 가속기로 만든 특징도 곱셈기보다 그 주변의 데이터 흐름에서 찾아야 한다.

Eileen Yoon은 ANE를 리버스 엔지니어링한 Linux 드라이버 작업을 약 3년 전에 중단했다. 하드웨어 접근을 열어도 고정된 구조가 지원하는 작업 범위를 크게 넓히기는 어렵다는 판단 때문이었다. 이번 분석의 목표는 ANE를 범용 가속기로 만드는 일보다 연산·스케줄러·메모리·실행 모델에 담긴 설계 가정을 파악하는 데 있다. 작은 가중치를 반복해서 쓰는 CNN과 큰 모델의 가중치를 계속 읽는 자기회귀 디코딩 사이의 차이가 중심이다.

누산기 범위를 출력으로 추론한다

원문이 분석한 M1 ANE는 16개 연산 코어를 가지며 코어당 FP16 MAC 레인은 128개, INT8 레인은 256개다. FP16 기준으로 전체 2,048개 레인이 병렬로 동작한다. 각 레인은 시간에 걸쳐 s ← s + a × b를 반복하고 부분합을 가까운 누산기에 보관한다. 내적과 행렬곱, 합성곱의 구분은 이 레인에 데이터를 배치하고 순서를 정하는 방식에서 생긴다.

누산기의 표현 범위는 단순한 내적 실험으로 조사했다. 한쪽 벡터를 모두 1로 채우고 다른 쪽 값을 v로 고정해 256회 더하면 출력 경계가 드러난다. v가 127.9375일 때는 CPU와 ANE가 모두 32752를 냈지만 v가 128이면 CPU의 32768과 달리 ANE는 양의 무한대를 냈다. 음수 쪽에서는 −128이 −32768로 남고 −128.125에서 음의 무한대가 나타났다.

32768 자체는 FP16으로 표현할 수 있으므로 이 차이를 최종 FP16 출력 형식의 한계만으로 설명할 수는 없다. 저자는 내부 경계가 2의 15제곱 부근이라는 결과를 소수부 16비트의 부호 있는 32비트 고정소수점 Q16.16 누산과 연결한다. 이는 역공학 실험으로 얻은 회로 해석이며 Apple의 공식 산술 규격은 아니다. 입력·출력 자료형이 같다는 이유로 내부 누산까지 CPU와 같다고 가정해서는 안 된다.

tanh는 표와 보간으로 구현된다

MAC 결과 뒤에는 활성화 처리가 붙어 있어 중간 결과를 외부 메모리에 썼다가 다시 읽는 일을 줄인다. tanh 하나를 포함한 Core ML 모델을 컴파일한 뒤 하드웨어 설정을 살피면 연속된 FP16 계수 33개가 나온다. 이 값들은 0부터 4까지 1/8 간격으로 샘플링한 tanh와 일치했다. identity와 ReLU에서는 같은 계수 표가 나타나지 않았고 NonlinearMode도 각각 0, 1, tanh에서는 2로 달랐다.

표가 있다는 사실만으로 출력 사이를 어떻게 채우는지까지 알 수는 없다. 저자는 표에서 인덱스 8의 항목 T8만 1로 두고 나머지는 0으로 만든 뒤 주변 입력을 훑었다. 출력은 입력 절댓값 7/8에서 0, 1에서 1, 9/8에서 다시 0이 되는 삼각형이었다. 이 결과로 mode 2가 인접한 표 항목을 선형 보간하는 33항목 LUT라는 해석을 뒷받침했다. R이 입력 좌표를 확대하며 표의 간격은 2^(-R)로 정해진다.

선형 스케일과 바이어스는 컴파일러가 앞선 합성곱에 흡수할 수도 있다. 원문의 예에서 z = 4x − 2 뒤에 ReLU(z/2 + 1)을 붙였더니 컴파일된 가중치는 4에서 2로, 바이어스는 −2에서 0으로 바뀌었다. 실행할 작업 수를 늘리지 않고 ReLU(2x)로 접은 셈이다. 이 실험은 상수 연산을 합치는 컴파일러 최적화의 증거이며 선형 변환이 반드시 LUT 보간 회로에서 실행된다는 증거로 읽으면 안 된다.

드라이버는 연산 대신 실행 문맥을 제출한다

Linux 드라이버가 ANE에 전달하는 것은 CONV나 MATMUL 같은 고수준 연산 명령이 아니다. 컴파일된 작업 기술자, 즉 TD가 들어 있는 메모리의 주소와 크기를 설정하고 TM_PUSH에 값을 써서 실행을 제출한다. TM_ADDR와 TM_INFO는 실행 상태를 준비하는 레지스터이며 TM_PUSH가 그 상태를 실행으로 넘긴다. 완료되면 하드웨어가 ARM64 코어에 인터럽트를 보낸다.

작업 큐는 qid 0부터 7까지 여덟 개다. 각 큐에는 우선순위와 상태, 두 벌의 주소·크기·NID, 그리고 32항목 BAR 테이블이 있다. 저자는 이 두 슬롯을 한쪽 실행 중 다른 쪽을 준비하는 핑퐁 구조로 해석한다. 기술자 스트림은 무엇을 실행할지 정하고 qid는 그 실행에 사용할 문맥을 고르는 구조다. BAR는 컴파일된 상대 오프셋에 더할 IOVA 기준 주소를 제공한다.

TD의 내용은 데이터 경로의 설정 레지스터에 값을 연속해서 쓰는 패킷이다. 저자가 ControlDMA라고 이름 붙인 패킷의 헤더에는 시작 레지스터 위치와 전송 워드 수에서 1을 뺀 값이 들어간다. 예를 들어 0xf401f800은 오프셋 0x1f800부터 32비트 워드 62개를 복사하는 설정으로 해석된다. 예제의 1×1 합성곱 TD는 628바이트이며 커널 DMA, 텐서 형상, 입력·출력 타일 DMA, L2, MAC과 후처리 설정으로 나뉜다. 이는 범용 명령어 스트림보다 고정된 데이터 경로를 한 차례 작동시키는 설정 묶음에 가깝다.

정적인 실행보다 중요한 메모리 경로

한 작업이 시작되면 설정을 레지스터로 옮기고 가중치를 KMem에, 입력 타일을 L2에 적재한다. MAC 코어가 계산한 결과는 후처리를 거쳐 L2에 놓이고 출력 DMA가 이를 DRAM으로 돌려보낸다. 컴파일러가 이 이동을 미리 계획하면 하드웨어를 작고 전력 효율적으로 만들 수 있다. 대신 컴파일러가 텐서의 배치와 이동을 직접 책임져야 한다.

이런 정적 구성이 트랜스포머를 표현할 수 없다는 뜻은 아니다. 커지는 KV 캐시도 크기에 맞춰 반복하면서 작업을 여러 번 제출하는 방식으로 다룰 수 있다. 원문은 동적 실행의 부족 자체보다 메모리 스트리밍 대역폭을 더 큰 문제로 지목한다. 작업을 표현할 수 있는지와 그 작업을 충분히 빠르게 실행할 수 있는지는 구분해야 한다.

통합 메모리와 로컬 재사용은 다른 층위다

CPU와 GPU, ANE가 같은 DRAM 풀을 쓴다고 해서 ANE의 MAC이 DRAM에서 곧바로 피연산자를 소비하는 것은 아니다. 계산할 데이터는 로컬 SRAM으로 이동해야 한다. 원문이 복원한 구조에는 코어마다 64KiB의 커널 메모리 KMem, MAC 입력을 준비하는 L1 영역, 모든 코어가 공유하는 2MiB L2가 있다. 16개 코어의 KMem을 합하면 1MiB다. 펌웨어의 디버그 루틴이 코어별로 64KiB를 덤프하고 별도로 2MiB L2를 덤프한다는 점이 용량 추론의 근거다.

원문은 M1의 명목 11TOP/s와 시스템 DRAM 68GB/s를 사용해 재사용의 필요성을 설명한다. FP16 피연산자 둘을 매번 새로 읽으면 4바이트를 소비하면서 곱셈과 덧셈 두 연산을 하므로 0.5OP/바이트다. 이 조건에서 11TOP/s를 공급하려면 22TB/s가 필요하다. 같은 명목값의 비율은 약 162OP/바이트이므로 DRAM에서 읽은 데이터를 칩 안에서 충분히 재사용해야 연산부를 채울 수 있다는 계산이다. 이 수치는 데이터형별 실측 성능을 확정하는 값이 아니라 원문의 가정을 사용한 roofline 설명으로 읽어야 한다.

가중치와 활성값의 비대칭

KernelDMASrc는 DRAM의 가중치를 코어별 KMem으로 옮기고 TileDMASrc와 TileDMADst는 DRAM과 L2 사이에서 입력·출력 타일을 옮긴다. 가중치 경로는 적재 전용이고 타일 경로는 입력과 출력 양쪽에 쓰인다. 16개 논리 커널 DMA 레인이 동시에 진행한다는 실험은 있지만 이것만으로 물리적으로 독립된 DMA 엔진이 16개라고 결론 내릴 수는 없다. 요청 생성기나 교차 연결, DRAM 중재 자원을 공유할 가능성이 남는다.

CNN에서는 작은 커널을 한 번 적재한 뒤 여러 입력 위치에 반복 적용하므로 이런 분리가 유리하다. 반면 L2에서 계산한 텐서를 KMem에 바로 넣는 경로가 없으면 그 텐서를 다음 연산의 가중치처럼 쓰기 위해 DRAM을 거쳐야 한다. 큰 가중치나 KV를 계속 공급하는 작업에서는 이 왕복과 스트리밍이 부담이 된다. 저자는 Apple이 L2의 텐서가 동적인 커널 역할을 할 상황을 충분히 예상하지 않았을 가능성을 제기하지만 L2→KMem 경로를 생략한 실제 설계 이유는 확인하지 못했다.

M3에서 측정한 대역폭과 시간의 합산

구조 설명은 주로 M1을 대상으로 하지만 후반의 DRAM 처리량 비교는 M3 실험이다. 캐시보다 큰 버퍼를 의사난수로 채우고 한 번씩 소비하는 작업에서 데이터 크기를 바꾸며 실행 시간을 측정했다. 고정적인 실행 비용보다 크기가 늘 때 시간이 얼마나 추가되는지를 보고 그 기울기의 역수로 지속적인 읽기 대역폭을 구했다. GPU에서는 데이터를 실제로 읽도록 Metal 셰이더가 데이터에 의존하는 체크섬을 계산했다.

그 조건에서 ANE KernelDMA는 37.99GB/s, TileDMA는 59.08GB/s, GPU는 77.70GB/s였다. 두 ANE 경로의 대역폭을 더해 시스템 한도에 가까워질 것이라고 기대할 수 있지만 동시 사용 실험은 달랐다. 결합 실행 시간은 각 경로를 따로 실행한 시간의 합에 가까웠다. 원문의 회귀식은 T_AB = 0.001 + 0.939 T_A + 0.981 T_B이며 두 계수가 모두 1에 가깝다. 이 측정 조건에서는 커널과 타일 전송이 충분히 겹치지 않았다는 뜻이지 모든 세대와 작업에서 가능한 중첩을 전부 배제하는 증거는 아니다.

후속 실험은 1MiB 경계의 급락을 피했다

본문 마지막에 연결된 후속 글은 메모리 병목을 더 좁혀 조사한다. M3 Air에서 출력 차원 N을 4096으로 두었을 때 입력 차원 D가 2048인 작업은 인접한 크기보다 유난히 느렸다. 코어 수를 하나로 줄여도 문제가 남았고 주소를 흩뜨리는 실험도 큰 손실을 회복하지 못했다. D와 N을 반대로 조정하면서 코어당 총 가중치 크기를 같게 유지하자 특정 차원 자체보다 코어당 전송량이 1MiB의 정수배라는 조건이 핵심으로 드러났다.

이 경계에서는 대역폭이 보통의 45~60GB/s에서 17~19GB/s로 떨어졌다. 64바이트 단위로 보면 1MiB는 16,384개, 즉 0x4000개 라인이다. 저자는 주기성과 경계 주변의 회복 곡선을 근거로 14비트 프리페치 링이 한 바퀴 남은 상태와 비어 있는 상태를 구별하지 못할 가능성을 제시한다. 실제 RTL을 확인한 것은 아니므로 랩어라운드와 크레딧 고갈은 원인 가설로 남는다. 256라인이 16KiB라는 단위 관계는 별도로 검산했으며 후속 글의 일부 바이트 표기 불일치는 그대로 옮기지 않았다.

회로 원인을 확정하지 않아도 전송 분할의 효과는 측정할 수 있다. 코어당 1MiB를 한 번에 보내는 대신 512KiB 두 번으로 나누자 한 대조 실험에서 17.25GB/s가 45.52GB/s로 올라갔다. 원래 1MiB 정수배가 아니던 전송을 나눴을 때는 같은 개선이 없었다. LLM 적용에서는 컴파일러가 다시 합치지 않도록 부분 리덕션으로 표현했고 Llama 3.2 1B는 10.0에서 24.3토큰/초, Qwen3-8B는 1.36에서 2.97토큰/초로 개선됐다. 이 결과는 해당 M3 실험과 모델 변환 조건에 한정되며 모든 ANE에서 얻는 일반적인 가속률은 아니다.

ANE의 제약과 폐지 주장은 구분해야 한다

원문 도입은 M5가 ANE 코어를 GPU 안으로 옮겼다는 해석에서 독립 NPU의 쇠퇴를 이야기한다. 그러나 Apple의 M5 공식 발표는 GPU 각 코어의 Neural Accelerator와 개선된 16코어 Neural Engine을 동시에 명시한다. 따라서 GPU 내 신경망 연산의 확대를 ANE의 흡수나 폐지로 단정하면 안 된다. 공식 구성 확인과 저자의 아키텍처 전망을 분리할 필요가 있다.

이 글에서 남길 핵심은 독립 NPU의 종말보다 워크로드와 데이터 경로의 관계다. 같은 MAC 연산이라도 가중치를 오래 재사용하는 작업과 매 토큰마다 많이 읽는 작업은 다른 공급 구조를 요구한다. 드라이버 API를 열거나 최고 연산량만 높여서는 그 차이가 사라지지 않는다. 동시에 후속 실험처럼 전송 형태를 바꿔 회복할 수 있는 성능도 있으므로 구조적 제약과 구현상의 병목을 따로 조사해야 한다.

더 생각해보기

  • 모델의 어느 연산이 연산량보다 DRAM 읽기에 묶이는지 어떻게 구분할 것인가.
  • 통합 메모리라는 설명이 로컬 SRAM 적재 비용을 가리는 경우는 언제인가.
  • 정적 작업 기술자로도 처리할 수 있는 동적 텐서의 범위는 어디까지인가.
  • 하드웨어 역공학에서 실측 결과와 가장 그럴듯한 회로 가설을 어떻게 나누어 기록할 것인가.
  • 컴파일러가 DMA 전송 크기와 병합 여부까지 성능 최적화 대상으로 삼아야 하는가.
  • M1의 구조와 M3의 측정 결과를 새로운 칩 세대에 적용하려면 무엇을 다시 확인해야 하는가.
원문 출처는 본문의 Source에서 확인할 수 있습니다.