메뉴
HN
Hacker News • 16일 전

애플 M3 뉴럴 엔진에서 50 GB/s 되찾기

IMP
8/10
핵심 요약

애플 M3 뉴럴 엔진(ANE)의 RTL 성능 에라타로 인해 가중치 크기가 1 MiB의 정수 배일 때 DRAM 처리량이 공칭 45-60 GB/s에서 17-19 GB/s로 급감한다. 이는 저수준 하드웨어 분석을 통해 발견되었으며, 커널 DMA 엔진의 문제 경로를 우회함으로써 Llama 3.2 1B의 토큰 처리량이 10.0에서 24.3 tokens/s로, Qwen3-8B가 1.36에서 2.97 tokens/s로 각각 2배 이상 향상되었다.

번역된 본문

ANE에서 50 GB/s 되찾기 (2026년 8월 10일, 3,162 단어)

소개

애플 M3 뉴럴 엔진(ANE)의 RTL 성능 에라타(erratum)는 총 가중치 크기가 1 MiB의 정수 배일 때마다 DRAM 가중치 스트리밍 처리량을 공칭 45-60 GB/s에서 17-19 GB/s로 제한합니다. 현재 이 문제는 ANEMLL의 15개 모델 중 7개에 영향을 미칩니다. 커널 DMA 엔진의 투기적 프리페치 링(speculative prefetch ring)에서 문제가 되는 경로를 우회함으로써 Llama 3.2 1B의 토큰 처리량은 10.0에서 24.3 tokens/s로(DRAM 사용량은 24.7에서 60.0 GB/s로), Qwen3-8B는 1.36에서 2.97 tokens/s로(DRAM 사용량은 22.4에서 48.7 GB/s로) 향상되었습니다.

발견

저는 단일 토큰 디코딩을 위해 뉴럴 엔진의 DRAM 가중치 스트리밍 처리량(GB/s)을 프로파일링하고 있었습니다:

X[1,D] × W[D,N] = Y[1,N]

N=4096일 때, D=1536이 Llama 3.2의 기본값인 D=2048보다 거의 3배 빠르게 실행되는 것을 발견했습니다.

STATIC(순수 KernelDMA) 복제본당 중앙값 µs, N=4096:

D: 576 / 768 / 1024 / 1280 / 1536 / 2048 rep a: 150.4 / 196.8 / 238.1 / 293.8 / 310.9 / 997.6 rep b: 157.8 / 190.2 / 250.1 / 288.3 / 326.8 / 995.0 rep c: 148.7 / 189.6 / 249.4 / 275.2 / 316.5 / 995.4

D=2048 근처의 값을 스윕(sweep)해 보니:

이상하군요? D=2048에서 처리량은 16.93 GB/s였습니다. D=2016에서는 44.5 GB/s였습니다. 즉, 44.505062 − 16.930761 = 27.574301 GB/s (61.96% 감소) 차이가 납니다. 44.5 GB/s에서 16.93 GB/s로, 27.57 GB/s의 급락이 발생한 것입니다.

참고로 이 스윕 데이터는 M3 Air에서 수집되었으며, 단일 실행 내에서 동일한 열/부하 조건에서 40회 반복되었습니다. 또한 ANE 레지스터 파일의 DMA 크기와 주소만 유일하게 변경되는 변수임을 확인했습니다:

D=2044 / D=2048 / D=2052일 때, TD+0x004의 예상 사이클은 모두 동일하고, TD+0x078의 core 1 base는 0x000ff800 / 0x00100000 / 0x00100800, core 2 base는 0x001ff000 / 0x00200000 / 0x00201000, ... core 15 base는 0x00ef8800 / 0x00f00000 / 0x00f07800, TD+0x0b4–0x0f0의 core sizes(×16)는 core base와 동일, TD+0x134의 Common.Cin은 0x000007fc / 0x00000800 / 0x00000804, TD+0x1f0의 L2 소스 스트라이드는 0x00007fc0 / 0x00008000 / 0x00008040, TD+0x1f4의 알 수 없는 스트라이드 미러도 동일한 값, TD+0x214의 L2 결과 base는 0x0008fc0 / 0x0009000 / 0x0009050입니다.

그래서 D의 전체 범위를 스윕해 보았습니다:

KernelDMA DRAM D-크기 스윕(대화형): 2,747개의 원시 관측치 · 67개 D 값 × 41회 무작위 라운드 · Apple M3, 모든 시간 측정 관측치의 중앙값 추세

이건 좋은 발상이었습니다. D=2048에서 공명(resonance)이 보였기 때문입니다. 처리량(GB/s)을 텐서 차원(D)에 대해 FFT를 수행할 줄은 몰랐았는데, 여기 그 결과가 있습니다:

분명히 메모리 컨트롤러의 처리량은 텐서 차원 공간에서 파장 2048의 지배적 고조파(dominant harmonic)를 가집니다. 그리고 안타깝게도 그것은 딥(하락 구간)입니다 :(

D=2048의 모든 배수는 마찬가지로 17-19 GB/s의 고정된 대역폭 하한에 제한됩니다. D=2048의 배수에서 처리량은 공칭 45-60 GB/s에서 17-19 GB/s로 급격히 떨어지며, 단 ~256 라인 떨어진 곳에서 공칭값으로 회복합니다.

이것은 RTL 정확성 버그가 아닙니다. 커널 DMA는 여전히 전송을 올바르게 완료하기 때문입니다. 하지만 2048 근처의 요청들은 크레딧이 부족한 별도의 이슈(issue) 체제로 강제되어, 불행히도 매우 흔한 전송 크기에서 28-43 GB/s(최악의 경우 60→17)만큼 처리량을 질식시키고 있습니다.

가설 1 - DRAM 공간 상관관계

16개 코어가 2의 거듭제곱 스트라이드에서 동일한 DRAM 뱅크에 앨리어싱(aliasing)되고 있는 걸까요?

DRAM은 병렬 데이터 인터페이스입니다. DRAM 대역폭은 DQ(데이터) 핀 수 × 핀당 데이터 전송률입니다:

DRAM BW = N × R = 128 bit × 6.4 GT/s = 102.4 GB/s

M3의 LPDDR-6400이 102.4 GB/s라는 것은 광고된 100 GB/s와 일치합니다. 지속 가능한 DRAM 대역폭은 엄밀히 그 DQ 활용률이며, 102.4 GB/s DRAM 상한에 미달하는 매 1 GB/s는 DQ가 대기하는 추가 사이클을 의미합니다.

원문 보기
원문 보기 (영어)
Getting 50 GB/s Back Out of the ANE Aug 10, 2026 (3162 words) Introduction An RTL performance erratum in the Apple M3 Neural Engine throttles DRAM weight streaming throughput down to 17–19 GB/s from the nominal 45–60 GB/s, whenever the total weight size is an integer multiple of 1 MiB, which currently affects 7 of ANEMLL ’s 15 models. Avoiding the problematic path in the kernel DMA engine's speculative prefetch ring increased Llama 3.2 1B token throughput from 10.0 to 24.3 tokens/s (DRAM usage from 24.7 to 60.0 GB/s), and Qwen3-8B from 1.36 to 2.97 tokens/s (DRAM usage from 22.4 to 48.7 GB/s). llama-split4-benchmark.html ' style="display:block;width:1e3px;height:764px;border:0;transform-origin:top left"> Discovery I was profiling the neural engine's DRAM weight streaming throughput (GB/s) for single token decode: \[ X[1,D] \times W[D,N] = Y[1,N]. \] At \(N=4096\), I noticed that \(D=1536\) ran nearly 3× faster than \(D=2048\), the default used in Llama 3.2. STATIC (pure KernelDMA) median µs per replica, N=4096: D 576 768 1024 1280 1536 2048 rep a 150.4 196.8 238.1 293.8 310.9 997.6 rep b 157.8 190.2 250.1 288.3 326.8 995.0 rep c 148.7 189.6 249.4 275.2 316.5 995.4 Sweeping the D around the neighborhood of D = 2048: Huh? At D=2048, throughput was 16.93 GB/s. At D=2016, throughput was 44.5 GB/s, meaning 44.505062 − 16.930761 = 27.574301 GB/s (61.96% lower). A 27.57 GB/s drop , from 44.5 down to 16.93 GB/s. Note that the sweep data was collected on an M3 Air, repeated across 40 runs, under the same thermal/load conditions in a single run. I also ensured that the ANE register file's DMA size and address were the only variables being changed: D=2044 D=2048 D=2052 TD+0x004 estimated cycles 0x 000001e a 0x 000001e b 0x 000001e c TD+0x078 core 1 base 0x 00 0ff8 00 0x 00 1000 00 0x 00 1008 00 TD+0x07c core 2 base 0x 00 1ff 000 0x 00 200 000 0x 00 201 000 ... TD+0x0b0 core 15 base 0x 00 ef88 00 0x 00 f000 00 0x 00 f078 00 TD+0x0b4–0x0f0 core sizes ×16 0x 00 0ff8 00 0x 00 1000 00 0x 00 1008 00 TD+0x134 Common.Cin 0x 00000 7fc 0x 00000 800 0x 00000 804 TD+0x1f0 L2 source stride 0x 0000 7fc 0 0x 0000 800 0 0x 0000 804 0 TD+0x1f4 unknown stride mirror 0x 0000 7fc 0 0x 0000 800 0 0x 0000 804 0 TD+0x214 L2 result base 0x 0000 8fc 0 0x 0000 900 0 0x 0000 905 0 So then I sweep across the whole aperture of D: KernelDMA DRAM D-size sweep (interactive) KernelDMA DRAM D-size sweep 2,747 raw observations &middot; 67 D values &times; 41 randomized rounds &middot; Apple M3 all timed observations median trend ' style="display:block;width:1e3px;height:720px;border:0;transform-origin:top left"> That was a good idea, because I'm seeing a resonance at D = 2048. Never thought I’d do an FFT of throughput (GB/s) against tensor dimension (D), but here it is: Apparently the memory controller's throughput has a dominant harmonic with wavelength 2048 in tensor-dimension space. And sadly, it's a dip :(. All multiples of D = 2048 are similarly capped at a fixed bandwidth floor of 17-19 GB/s: At multiples of D = 2048, throughput sharply drops from the nominal 45–60 GB/s down to 17–19 GB/s, and recovers to nominal just ~256 lines away. This is not an RTL correctness bug, as kernel DMA still completes the transfer correctly. But the requests around 2048 are being forced into a separate, credit-starved issue regime, choking throughput by an unreasonable 28–43 GB/s (worst case 60→17), at transfer sizes that are, unfortunately, very common. Hypothesis 1 - DRAM spatial correlation Are the 16 cores aliasing onto the same DRAM bank at power-of-two strides? DRAM is a parallel data interface: DRAM bandwidth is the number of DQ (data) pins times the data rate per pin, \[ \text{DRAM BW} = N \times R = 128\ \text{bit} \times 6.4\ \text{GT/s} = 102.4\ \text{GB/s} \] M3's LPDDR-6400's 102.4 GB/s checks out with the advertised 100 GB/s. Sustained DRAM bandwidth is strictly that DQ utilization, and every GB/s short of the 102.4 GB/s DRAM ceiling means every extra cycle that the DQ line sat idle. DRAM TLDR: DRAM memory controller uses parallel accesses to stream bits through the high speed DQ pins; a large DRAM array is divided into banks and bandwidth (roughly) depends on spreading parallel requests across banks. Parallelism buys throughput if the resources are independent. If parallel requesters go for the same resource, their requests will serialize back-to-back and effectively be throttled at the single rate. A throttled floor at ~17–19 GB/s (while their immediate neighbors run at 45–60 GB/s), could be explained by collapse happening at pow-2 boundaries. It's also good to start low level: additional AXI requests can't do anything if they're requesting the same physical bank. Core Contention The neural engine has several avenues of parallelism, the first class being core-level parallelism. ANE has 16 cores in parallel. Cores divide work by partitioning a buffer evenly across \(N\) cores, and mutually agreeing to work on a different slice. We know the cores are assigned to fetch a different slice of the weight buffer, but ANE still has 16 cores all requesting their slice from DRAM in parallel, on the same cycle. If each core fetches their own slice from DRAM, then streaming latency should take the same amount of time whether one core or all 16 cores are enabled, because their requests should be serviced in parallel. However, if there is reduced bandwidth due to any core contention, then reducing the number of cores could ironically increase throughput, for the throttled D=2048 case. Sweeping the number of active cores for D=2048 and D=2016: Latency is constant from 1 to 16 active cores for both D=2016 and D=2048, meaning the throttling is present even at the core=1. The problem exists at the per-core level, the problem is replicated across cores. Address Contention Even after ruling out core-level contention, I still suspected some DRAM contention due to the power-of-two period. A power-of-two stride like 2048 adds \(2^k\) at each rotation, meaning the lower bits \([0..k-1]\) are constant. DRAM hashes the physical address so that strided access patterns get spatially decorrelated across different banks, so a hash collapsing the lower bits, or aliasing the upper \(2^k\) bit, could explain the pow2 periodicity. To test if DRAM spatial correlation is the issue, we scramble the address that the weights are fetched from. The address was randomly scrambled and spread across the whole ~64 MiB IOVA arena (59.90 MiB span), so it was scrambled both in-page and out-of-page. To rule out thermal drift on the fanless M3 Air, baseline and scrambled samples were interleaved run-to-run, so any thermal ramp hits both conditions equally. KernelDMA D=2048 dense vs scatter KernelDMA D=2048 throughput: every run, in order 287 interleaved rounds &middot; dense &amp; scatter measured back-to-back each round &middot; 16&nbsp;MiB per transfer &middot; hover any point. ' style="display:block;width:1e3px;height:620px;border:0;transform-origin:top left"> Median throughput of the baseline was 31.37 GB/s, and median throughput of the randomly scrambled addresses was 32.29 GB/s. The random scramble had a marginally higher sustained throughput of 1 GB/s average, suggesting that we may have attacked some spatial correlation through scrambling in this run, but (1) this is not proven across all cases (2) scrambling cannot recover the ~+200% throughput drop needed to explain the collapse. Hypothesis 2 - RTL integer wraparound Recall that the collapse repeated at every integer multiple of D = 2048: Q: What repeats at exact power-of-two integer boundaries? A: Integer overflows in fixed-width digital logic. module line_counter ( input wire clk, input wire reset, input wire advance, output reg [ 13 : 0 ] line_count ); always @( posedge clk) begin if (reset) line_count <= 14'h0000 ; else if (advance) line_count <= line_count + 1 'b1 ; // wrap at 0x3fff + 1 -> 0x0000 end endmodule Kernel Dimension \[ X[1,D] \times W[D,N] = Y[1,N]. \] Where \(D\)