深入解析 M4 Apple Neural Engine(第 1 部分):逆向工程 我们如何绕过 CoreML,直接与硬件对话

关于“我们”的说明:
在本系列中,“我们” 指的是 maderix(人类)和 Claude Opus 4.6(由 Anthropic 提供)作为一对搭档共同工作。逆向工程、基准测试和训练代码是协作开发的——人类直觉引导探索,AI 对数据进行推理并撰写分析。我们认为这种人机协作是一种进行系统研究的新颖且自然的方式:一方作为具有直觉的架构师 ,另一方作为编写代码并设计实验的工程师 。
这一切始于一个简单的问题: 能否在 Apple 的神经引擎上训练模型?
Apple 不想让你知道答案。他们不公开 ANE 的指令集架构(ISA)。他们不记录其内部架构。他们甚至不给你直接编程的方式——所有东西都要通过 CoreML,这增加了抽象层、优化通道和开销,使得几乎不可能理解硬件实际在做什么。
所以我们对它进行了逆向工程。
在几天时间里,我们将整个软件栈从 CoreML 向下映射到 IOKit 内核驱动,发现了如何在不借助 CoreML 的情况下编译并在 ANE 上执行程序,攻破了二进制格式,测量了真实的峰值性能(剧透:Apple 的“38 TOPS”数字具有误导性),并最终在一块专为推理设计的芯片上实现了神经网络的训练 。
这是一个三部分系列的第 1 部分。我们在此介绍逆向工程——我们如何剥开层层结构以了解 M4 Neural Engine 的真实面貌以及如何直接与其对话。
什么是神经引擎?
ANE 不是 GPU,也不是 CPU。它是一个图执行引擎 ——一个定功能加速器,接受已编译的神经网络图并将整个图作为一个原子操作来执行。你不会下发单独的乘加指令。你提交描述整个计算图的已编译程序,硬件会端到端地执行它。
Apple 在 A11(2017 年)中引入了神经引擎,最初为双核设计。每一代都在扩展规模:

M4 的 ANE(代号 H16G)就是我们正在研究的对象。16 核,127 个评估请求的队列深度,独立的 DVFS(动态电压/频率调节),以及在空闲时能将其功耗硬性门控降至精确的 0 毫瓦。
先前研究
我们并不是第一个探究 ANE 内部结构的人。
-
hollance/neural-engine — Matthijs Hollemans 对 ANE 行为、性能特征和支持操作的全面社区文档。现存关于 ANE 的最佳单一资源。
-
mdaiter/ane — 早期的逆向工程,提供可运行的 Python 和 Objective-C 示例,记录了 ANECompiler 框架和 IOKit 分发。
-
eiln/ane — 一个为 ANE 逆向工程的 Linux 驱动(Asahi Linux 项目),提供对内核级接口的洞见。
-
apple/ml-ane-transformers — Apple 为 ANE 优化的变换器参考实现,确认了诸如通道优先布局和偏好 1×1 卷积等设计模式。
但据我们所知,之前还无人:(a)在 M4 上无需 CoreML 即实现直接 _ANEClient API 访问,(b)破解内存中 MIL 编译路径,(c)测量绕过 CoreML 开销的真实峰值吞吐量,或(d)在 ANE 上训练模型。
方法论
我们的方法结合了多种技术:
-
类发现 :通过在
AppleNeuralEngine.framework上运行dyld_info -objc——这会导出所有 Objective-C 类和方法 -
方法交换(Method swizzling):拦截 CoreML 对私有 ANE 框架的调用
-
二进制分析 :分析已编译的 E5 包以理解神经程序格式
-
扩展性分析 — 通过改变矩阵尺寸、图深度和通道数以推断硬件拓扑
我们在 AppleNeuralEngine.framework 中发现了 40 多个私有类,包括 _ANEClient 、 _ANEModel 、 _ANERequest 、 _ANEIOSurfaceObject 、 _ANEInMemoryModel 以及更多。
软件栈
下面是完整的 ANE 软件栈,从公共 CoreML API 到硬件:

关键见解:CoreML 并非唯一的入口。位于 AppleNeuralEngine.framework 中的 _ANEClient 类提供了直接访问编译 → 加载 → 评估流水线的能力。CoreML 只是之上的一个便捷层。
_ANEClient:直接访问神经引擎
以下是在不使用 CoreML 的情况下,在 ANE 上编译并运行程序的完整步骤:
// 1. Get shared client connection
id client = [_ANEClient sharedConnection];
// 2. Create model reference
id model = [_ANEModel modelAtURL:compiledURL key:@"mykey"];
// 3. Compile (MIL text → E5 binary, result is cached)
[client compileModel:model options:@{
@"kANEFModelType": @"kANEFModelMIL",
@"kANEFNetPlistFilenameKey": @"model.mil"
} qos:21 error:&err];
// 4. Load program onto ANE hardware
[client loadModel:model options:@{} qos:21 error:&err];
// → programHandle assigned, queueDepth = 127
// 5. Create IOSurface I/O buffers
IOSurfaceRef surface = IOSurfaceCreate(props);
id wrapped = [_ANEIOSurfaceObject objectWithIOSurface:surface];
// 6. Build evaluation request
id req = [_ANERequest requestWithInputs:@[wA, wB]
inputIndices:@[@0, @1]
outputs:@[wOut]
outputIndices:@[@0]
weightsBuffer:nil
perfStats:nil
procedureIndex:@0];
// 7. Execute on ANE
[client evaluateWithModel:model options:@{}
request:req qos:21 error:&err];
// 8. Read results from output IOSurface
IOSurfaceLock(outSurface, kIOSurfaceLockReadOnly, NULL);
float *data = IOSurfaceGetBaseAddress(outSurface);
// ... read results ...
IOSurfaceUnlock(outSurface, kIOSurfaceLockReadOnly, NULL);
I/O 使用 IOSurfaces——与 GPU 纹理使用的共享内存机制相同。这意味着如果共享相同的 IOSurfaceRef,理论上 GPU 与 ANE 之间可以实现零拷贝传输。
主要发现: ANE 支持 127 的队列深度——最多可以同时有 127 个评估请求在处理之中。这个深度远超大多数加速器队列,表明硬件是为高吞吐量的流式推理而设计的。
MIL:输入语言
CoreML 不会以 ONNX 或 protobuf 格式将神经网络发送到 ANE。它使用 MIL —— 机器学习中间语言(Machine Learning Intermediate Language)—— 一种带类型的 SSA(静态单赋值)表示,看起来像这样:
program(1.3)
[buildInfo = dict<string, string>({
{"coremltools-version", "9.0"}
})]
{
func main<ios18>(
tensor<fp16, [1, 1024, 1, 1024]> x,
tensor<fp16, [1, 1024, 1, 1024]> w
) {
bool tx = const()[val = bool(false)];
bool ty = const()[val = bool(false)];
tensor<fp16, [1, 1024, 1, 1024]> out =
matmul(transpose_x = tx, transpose_y = ty,
x = x, y = w);
} -> (out);
}
MIL 意外地可读性很强。每个数值都有精度和形状的类型。操作有名称并采用关键字参数。函数签名用显式维度声明输入张量。
张量布局遵循 ANE 的原生 NCDHW + Interleave 格式: [Batch, Channels, Depth, Height, Width]。对于一个 1024×1024 的矩阵,在 4 维中表示为 [1, 1024, 1, 1024]。
E5 二进制:已编译格式
当 ANECompiler 处理 MIL 程序时,它会生成一个 E5 二进制 — 一个采用 FlatBuffer 结构的文件,包含以下部分:

有趣的是:一个 1024×1024 的矩阵乘法编译后为 2,688 字节 。一个 128×128 的矩阵乘法编译后为 2,680 字节 。几乎相同。E5 二进制并没有编码矩阵乘法算法——它编码的是一个参数化程序 ,其行为在运行时由张量描述符控制。这个“微码”更像是一种配置,而非传统的机器代码。
含义: ANE 硬件很可能只有一小套固定的计算基本操作(卷积、矩阵乘法、按元素运算),这些操作由张量形状描述符参数化。E5 二进制描述了应当如何串联这些基本操作以及如何连接它们,而不是描述具体的计算本身。
内存路径:圣杯
基于文件的编译路径可行,但有一个问题:它需要将 MIL 文本写入磁盘,创建目录结构,并将编译器指向该目录。对于训练——我们需要在每几步就用更新的权重重新编译——这种文件系统往返是不可接受的。
我们发现了 _ANEInMemoryModelDescriptor,它直接在内存中接受 MIL 文本:
id desc = [_ANEInMemoryModelDescriptor
modelWithMILText:milData // NSData*, not NSString*!
weights:weightDict // NSDictionary*, not NSData*!
optionsPlist:nil];
id model = [_ANEInMemoryModel
inMemoryModelWithDescriptor:desc];
[model compileWithQoS:21 options:@{} error:&err];
[model loadWithQoS:21 options:@{} error:&err];
[model evaluateWithQoS:21 options:@{}
request:req error:&err];
使其正常工作我们解决了三个耗费数日调试的难点:
-
NSData,而不是 NSString:参数
milText需要一个包含 UTF-8 字节的NSData*,而不是NSString*。传入字符串会静默失败。 -
NSDictionary,而不是 NSData:参数
weights是一个将权重名称映射到 NSData 二进制块的字典,而不是单个数据缓冲区。 -
临时目录变通 :即使是“内存中”路径,内部也会写入临时目录。如果你没有对默认位置的写入权限,编译会以不明错误失败。我们必须确保有一个可写的临时路径可用。
还有一个有趣的发现:Apple 的内部代码在某个类名中引用了一个 Desctiptor(原文如此)。即便是 Apple 的工程师在私有 API 中也会出现拼写错误。:)
硬件概况:我们已知的信息
通过对 IOKit 的探测、频率/电压缩放分析和功耗测量,我们构建出了关于 M4 ANE 的以下概要:

DVFS 通道
IOKit 的 IOReportLegend 显示 ANE 具有独立的电源管理,支持自适应时钟、抖动以及多种硬件/软件触发机制:
ANE_ADCLK_TRIG — adaptive clock trigger
ANE_ADHWTRG — hardware adaptive trigger
ANE_ADSWTRG — software adaptive trigger
ANE_DITHR_TRIG — dithering trigger
ANE_PPT_TRIG — power/performance tuning
ANE_PPT_SWTRG — software PPT trigger
ANE_PPT_HWTRG — hardware PPT trigger
ANE_EXT_TRIG0-3 — external triggers
这种级别的 DVFS 复杂性表明 ANE 能够根据工作负载特性独立调整其频率和电压,独立于 CPU 和 GPU 的电源域。
支持的操作
从 ANECompiler.framework 导出的内容来看,ANE 本机支持:

值得注意的是,Conv 似乎是 ANE 的主要计算原语。正如我们将在第 2 部分展示的那样,将 matmul 表示为 1×1 卷积可以显著提升吞吐量。
The IOSurface 协议
所有与 ANE 的数据传输都使用 IOSurfaces。协议很简单:
// Create an IOSurface for a 1024x1024 float tensor
NSDictionary *props = @{
@"IOSurfaceWidth": @(1024),
@"IOSurfaceHeight": @(1024),
@"IOSurfaceBytesPerElement": @(4), // float32
@"IOSurfaceBytesPerRow": @(4096), // 1024 * 4
@"IOSurfaceAllocSize": @(4194304),
@"IOSurfacePixelFormat": @(0)
};
IOSurfaceRef surface = IOSurfaceCreate(props);
// Write data
IOSurfaceLock(surface, 0, NULL);
float *ptr = IOSurfaceGetBaseAddress(surface);
memcpy(ptr, data, 4194304);
IOSurfaceUnlock(surface, 0, NULL);
// Wrap for ANE
id wrapped = [_ANEIOSurfaceObject objectWithIOSurface:surface];
由于 IOSurface 与 GPU 纹理共享使用相同机制,这就打开了零拷贝 GPU↔ANE 管道的可能性,两者可以在相同内存上运行。
编译缓存
ANE 编译器将 E5 二进制文件缓存到磁盘以避免重新编译:
~/Library/Caches/<app>/com.apple.e5rt.e5bundlecache/<build>/<hash>/
<hash>.bundle/
└── H16G.bundle/ ← H16G = M4 ANE target
├── H16G.e5 ← FlatBuffer binary (2-3 KB)
└── main/
└── main_ane/
└── model.anehash
首次编译大约需要 20–40 毫秒。命中缓存几乎无需开销。这对推理很重要(编译一次,长期运行),但给训练带来了挑战,因为权重在每一步都在变化。
未被探索的领域
发现的若干类尚未探索,暗示着我们尚未测试的能力:
-
_ANEChainingRequest— 可能允许在一次调度中串联多个已编译模型 -
_ANESharedEvents/_ANESharedSignalEvent/_ANESharedWaitEvent— 类似 Metal 的栅栏/信号原语,用于 GPU↔ANE 同步 -
_ANEPerformanceStats— 可能是硬件性能计数器 -
_ANEVirtualClient— 虚拟化的 ANE 访问,可能用于多进程共享
还有一些我们确实不知道的事情:
-
ANE 核心的具体微架构和指令集 ISA
-
图(graph)中各操作如何分配到核心上
-
ANE 时钟频率(DVFS 使其动态可变)
-
是否可以访问硬件性能计数器
-
确切的 SRAM 拓扑结构(分成多个 bank?统一?每核独立?)
接下来是什么
既然我们已能直接访问 ANE,就可以实际测量它的能力。 第 2 部分 , 我们将对所有内容进行基准测试:矩阵乘法的扩展性、SRAM 性能陡降点、为何卷积比矩阵乘法快 3 倍、为何 Apple 的“38 TOPS”说法具有误导性,以及绕过 CoreML 如何为你带来 2–4 倍的吞吐量提升。
在第 3 部分中,我们将做苹果声称你不能做的事:在神经引擎上训练神经网络。
所有代码可在 github.com/maderix/ANE 的
ane/目录中获取。已在 M4 Mac Mini,macOS 15.x 上测试。