在 Swift 中训练大型语言模型,听起来像是一个充满工程陷阱的决定。Swift 的运行时安全检查、自动引用计数、以及相对保守的编译器优化策略,都让它在数值计算密集型场景下难以匹敌 C 或 C++ 的性能。然而 Matt Gallagher 在 Cocoa with Love 上发布的系列文章第一篇,却给出了一个令人惊讶的答案:通过系统性的工程优化,Swift 不仅能够达到与 C 持平的性能,甚至可以在某些场景下实现反超。这一过程揭示的不仅是 Swift 6.2 新特性的工程价值,更是一套从零构建高性能 LLM 训练流水线的完整方法论。
本文聚焦这一工程路径的核心逻辑:如何在缺乏成熟 ML 框架的前提下,仅凭 Swift 标准库和少量外部工具,构建一个从 Gflop/s 量级迈向 Tflop/s 量级的矩阵乘法引擎。讨论的范围涵盖内存布局优化、SIMD 向量化、多线程并行、AMX 协处理器调用,以及 Metal GPU 加速,最终形成一套可供参考的 Swift 高性能数值计算工程模式。
工程起点:llm.c 作为参照系
选择 Andrej Karpathy 的 llm.c 作为参照实现,是一个务实而精准的工程决策。llm.c 是一套仅约 1000 行纯 C 代码的 GPT-2 兼容实现,剔除了所有第三方依赖和隐藏抽象层,却保留了 LLM 训练的核心计算图:前向传播中的矩阵乘法、反向传播中的梯度计算、以及权重更新。对于想要理解 LLM 训练本质工程问题的开发者而言,llm.c 提供了一个零抽象的基线。
以这个基线为参照,Matt Gallagher 将初始 Swift 实现尽可能忠实地映射到 llm.c 的结构上:同样的四层嵌套循环、同样的内存布局、同样的数据类型。结果却令人沮丧 ——Swift 版本比 C 慢 15 到 20 倍。在 GPT-2 124M 参数模型上,这意味着每秒仅能生成约 1 个 token,20 次完整训练迭代需要近 30 分钟。更准确地说,这个初始实现的性能约为 2.8 Gflop/s,与 1999 年 PowerMac G4 的广告宣传能力处于同一量级 —— 对于现代机器学习 workloads 而言,这是一个完全不可接受的起点。
这里的关键问题并非 Swift 语言本身无法优化,而是 Swift 的默认行为引入了额外的运行时开销。在这个阶段,最大的性能瓶颈来自 Swift 对数组共享所有权的隐式检查。具体而言,每一次对输出数组元素的写入操作,都会触发 _ArrayBuffer.beginCOWMutation() 的唯一性检查 —— 即便数组确实只被当前线程独占,这些检查本身的开销就已经成为绝对瓶颈。这个发现指向了一个核心工程原则:在性能敏感的数值计算代码中,应当尽早识别并消除运行时安全机制引入的非必要开销。
Swift 6.2 新特性:MutableSpan 与 InlineArray 的工程价值
Swift 6.2 为性能优化提供了两个关键工具:MutableSpan 和 InlineArray。前者解决的是数组访问的写时复制开销问题,后者解决的是栈上固定大小数组的需求。这两个特性的组合使用,是 Swift 实现从 Gflop/s 向 Tflop/s 跨越的第一个工程杠杆。
MutableSpan 的工作原理是为数组创建一个可变视图,同时向编译器承诺该视图不会被传递出当前作用域,从而消除了引用计数和唯一性检查的运行时成本。在矩阵乘法的外层循环中使用 var out = out.mutableSpan,将输出数组的访问通过这个视图进行,可以将前向加反向的完整训练迭代性能提升超过 3 倍 —— 而这一改动仅涉及在函数开头添加一行代码。这个收益比例说明了 Swift 运行时检查在实际 workloads 中的开销远比直觉预期的要大。
InlineArray 则解决了另一个长期困扰 Swift 高性能数值计算的问题:栈上固定大小数组的分配。在 llm.c 的优化版本中,作者使用 float result[LOOP_UNROLL] 在栈上预分配一个固定大小的结果缓冲区,用于在循环中暂存多个输出通道的计算结果。在 Swift 6.2 之前,在循环中动态分配 Array<Float> 的成本过高,导致开发者不得不选择手动展开循环 8 次 —— 这是一种可读性极差且难以维护的实现方式。InlineArray 的引入使得 Swift 终于可以在性能关键的内层循环中使用栈分配的固定大小数组,其语义与 C 的栈数组直接对应,从而为后续的 SIMD 向量化优化奠定了结构基础。
这两个特性的组合使用,本质上是在 Swift 的安全承诺与数值计算的性能需求之间寻找一个更优的平衡点。MutableSpan 通过作用域承诺消除了动态检查,而 InlineArray 通过编译期大小确定消除了堆分配开销。对于构建 LLM 训练流水线而言,这意味着 Swift 6.2 已经具备了在关键计算路径上达到 C 级性能的语法和类型系统基础。
SIMD 向量化:Relaxed 与融合乘加
即便消除了 COW 开销和堆分配开销,Swift 的默认编译输出与 C 的 -O3 优化仍然存在显著差距。核心原因在于浮点数运算的默认语义约束:C 代码启用了 -ffast-math 标志,允许编译器使用融合乘加指令(FMA),将 a = a + b * c 的两条浮点运算合并为一条 fmla 指令,在单周期内完成乘法和加法。而 Swift 默认不启用 fast-math,导致编译器只能生成独立的乘法和加法指令,外加大量的数据移动指令来在寄存器之间传递中间结果。
Swift-Numerics 库中的 Relaxed 类型提供了一个可控的放松约束接口。Relaxed.multiplyAdd(a, b, c) 函数将 a + b * c 的语义显式标记为允许使用 FMA 指令,同时明确告知编译器不需要严格遵循 IEEE 754 的精度要求。这一改动的效果是显著的:内层循环从分离的 fmul 和 fadd 指令序列,转变为融合的 fmla.4s 指令 —— 每条 SIMD 指令同时处理 4 个浮点数,16 个时钟周期内即可完成 64 次乘加操作。这个改动带来了近 10 倍的 token 生成速度提升,并将矩阵乘法的吞吐量推升至接近 C 的水平。
值得注意的是,这种 SIMD 向量化并非 Swift 编译器自动完成的优化。编译器能够识别向量化的机会,但在默认语义约束下无法应用 FMA 指令。开发者必须通过 Relaxed 类型显式地表达融合乘加的意图,这是一个需要工程判断的关键决策点:在哪些运算上放松精度约束是可接受的,在哪些运算上必须保持严格精度。在 Matt Gallagher 的实现中,gelu_backward 函数特意保留了原始的分离乘加运算,因为 llm.c 在该函数周围禁用了 fast-math—— 这是一个与数学正确性相关的工程判断,而非性能权衡。
多线程并行:DispatchQueue 的工程取舍
从单核优化转向多核并行,是性能优化的另一个关键台阶。llm.c 使用 OpenMP 注解(如 #pragma omp parallel for)标记可并行化的循环,这种声明式语法在 C 生态中简洁而直观。Swift 缺乏对应的语法特性,必须借助 DispatchQueue.concurrentPerform 实现功能等价的并行化 —— 但这种等价并非无代价的。
在 Swift 中使用 DispatchQueue.concurrentPerform 的关键挑战在于线程安全性与性能要求的冲突。由于 MutableSpan 承诺不离开当前作用域,并行执行的闭包无法直接持有 Span 参数。解决方案是将数组通过 withUnsafeMutableBufferPointer 提取出原始指针,再用 @unchecked Sendable 包装绕过 Swift 6 的并发安全检查。这种模式在功能上是正确的,但在代码美学上远不如 OpenMP 注解优雅:大量的嵌套作用域、显式的分块计算、以及 SendableUnsafeMutableBuffer 的样板代码,使得核心计算逻辑被并发管理的仪式性代码所淹没。
Matt Gallagher 在实现中承认,这是 Swift 在高性能数值计算领域最明显的短板之一。缺乏对并行循环的简洁语法支持,使得 Swift 在这一层面的代码可读性显著落后于 C++ 和 Fortran。未来的 Swift 版本若能引入类似于 OpenMP 的轻量级并行注解,将大幅提升 Swift 在科学计算和 ML 训练领域的工程竞争力。在当前阶段,开发者需要手动管理分块、线程同步和 Sendable 约束,这增加了实现复杂度和出错可能性。
应用这一并行化策略后,核心矩阵乘法函数在 16 核 M3 Max 上获得了 5.4 倍的性能提升。这个数字距离理想的 16 倍线性加速有显著差距,主要原因是内存带宽成为了新的瓶颈而非 CPU 计算能力。对于 LLM 训练中常见的矩阵乘法操作,内存访问模式对最终性能有决定性影响 —— 优化不佳的内存遍历会导致大量缓存未命中,使核心计算单元在等待数据加载时处于空闲状态。
Apple Silicon 的隐藏算力:AMX 协处理器
在 CPU 通用计算单元和 GPU 通用计算单元之外,Apple Silicon 还包含一个鲜少被公开讨论的专用矩阵计算单元:AMX(Apple Matrix Coprocessor)。与 Intel 的 AMX 不同,Apple 的 AMX 从未被官方文档正式命名,公开可访问的唯一途径是通过 Accelerate 框架的 BLAS 实现。社区通过逆向工程揭示了其核心指令集:AMX_MATFP 执行 16×16 矩阵块的融合乘加,AMX_LDX 和 AMX_LDY 分别加载左右操作数矩阵块,AMX_LDZ 将累加器置零,AMX_STZ 将结果写回。
这些指令的存在揭示了 Apple Silicon 设计中的一个核心洞察:对于机器学习中最常见的矩阵乘法操作,16×16 的固定大小块操作是一个经过精心选择的折衷。块太小则指令调度开销占比过高,块太大则内存布局约束过强且缓存效率下降。AMX 在每条指令周期内完成 256 次浮点乘加操作(16×16 的矩阵块),这是通用 SIMD 单元无法直接匹配的算力密度。
直接使用 AMX 指令的工程挑战在于两个层面。首先,Apple 从未正式承诺 AMX 指令集的二进制兼容性,任何直接调用都可能在未来的芯片修订版上失效 ——Matt Gallagher 明确指出,对于实际生产代码应当始终通过 Accelerate 框架访问 AMX。其次,手工编写 AMX 内核需要将矩阵重排为 AMX 喜欢的块布局,这涉及大量的数据打包和分散操作,对内存访问模式优化提出了很高要求。Matt Gallagher 的手工 AMX 实现比优化后的通用 SIMD 实现快约 1.67 倍,但他也承认,如果数据布局优化做得更充分,性能提升应当能达到 2 倍以上。
这一层面的经验揭示了一个更普适的工程原则:专用硬件加速器的潜力释放,取决于算法设计与硬件约束的匹配程度。在 Apple Silicon 上,矩阵乘法的最优实现路径并非直接诉诸 GPU,而是应当在 AMX 单元上精心地平铺数据块 —— 这种布局优化的收益往往超过简单地增加并行度或切换计算设备。
Metal GPU 实现:tiling 策略与 threadgroup 协作
Metal GPU 路径的实现采用了与 CPU 路径不同的并行化模型。在 GPU 上,线程的粒度更细、并发量更大,但线程之间的协作需要通过显式的共享内存同步来实现。Matt Gallagher 的初始 Metal 实现将矩阵乘法的前三个外层循环(B、T、OC)映射为 GPU 网格坐标,而将最内层的 C 维度保留为标量循环 —— 这与 CPU 上的朴素实现结构上高度相似。
然而,初始 Metal 实现并未展现出相对于 AMX 优化的显著优势。原因是类似的:内存访问模式的问题。在 GPU 上,每个工作项独立遍历完整的行向量,导致对全局内存的访问模式高度非连续,带宽利用率低下。解决方案同样是 tiling:将输入矩阵划分为固定大小的方块,先将数据块加载到 threadgroup 共享内存中,再在共享内存上进行计算。
在 Metal 中实现 tiling 涉及三个关键要素。首先,使用 threadgroup float inpTile[MATMUL_TILE][MATMUL_TILE] 和 threadgroup_barrier(mem_flags::mem_threadgroup) 声明共享内存和同步点,确保同一个 threadgroup 中的所有工作项在加载数据前都到达屏障。其次,调整 threadsPerThreadgroup 参数到 MTLSize(width: 16, height: 16) 以充分利用共享内存的缓存局部性。第三,在每个 tiling 迭代中,工作项从共享内存而非全局内存读取数据,从而将内存访问延迟从数百个周期降低到数十个周期。
经过这一系列优化,Matt Gallagher 的 Metal 实现达到了约 1.1 Tflop/s 的吞吐量。这一数字距离 M3 Max GPU 的理论峰值 15 Tflop/s 仍有较大差距,原因在于手工编写的矩阵乘法内核远未达到 Apple 官方 BLAS 实现的优化水平 ——Matt Gallagher 在文中坦诚承认这一点,并预告下一篇文章将对比分析 BLAS、BNNS、CoreML 和 MPSGraph 等框架的性能表现。
工程方法论的提炼
从 2.8 Gflop/s 到 1.1 Tflop/s 的 382 倍性能提升,并非来自某个单一的重大突破,而是来自一系列持续迭代的局部优化。这个过程揭示了一套可复用的工程方法论。
第一层优化针对运行时开销:识别并消除 Swift 默认行为引入的 COW 检查、引用计数和边界检查。MutableSpan 的使用是这一层的关键杠杆,它通过作用域承诺在编译期消除了运行时安全检查的成本。
第二层优化针对指令级并行:显式使用 Relaxed 类型启用 FMA 指令,将乘法和加法融合为单条 SIMD 指令,同时处理多个数据元素。这一层需要开发者主动表达融合运算的意图,而非依赖编译器自动推断。
第三层优化针对线程级并行:在多核 CPU 上使用 DispatchQueue.concurrentPerform 分片处理独立的计算块。这一层面临的主要挑战是 Swift 并发安全模型与高性能数值计算之间的摩擦 —— 当前没有优雅的语法糖来简化这一过程。
第四层优化针对硬件加速器:根据数据的内存布局特征,选择 AMX 协处理器或 GPU 作为计算载体。AMX 适合固定大小的矩阵块操作,GPU 适合大规模并行且数据局部性可优化的场景。
贯穿这四层优化的,是一个反复出现的模式:在关键计算路径上,Swift 的默认安全保证带来了不可忽视的性能代价,而消除这些代价的方式不是放弃安全,而是通过更精确的类型系统和作用域承诺,在更小的范围内解除安全检查,从而在整体代码库仍保持类型安全的前提下,释放出 C 级甚至超越 C 级的性能。
对 Swift ML 生态的启示
这一系列实验的结果,对于 Swift 在机器学习领域的定位具有重要的参考价值。首先,Swift 6.2 的新特性集(MutableSpan、InlineArray、Relaxed)为高性能数值计算提供了基础设施层面的支持,这在两年之前是不可用的。其次,Swift 在矩阵乘法核心计算上可以达到与 C 持平甚至略优的性能,这打破了 Swift 因运行时开销而不适合数值计算的传统认知。第三,在并行化和硬件加速器利用层面,Swift 的工程体验显著落后于 C 和 C++,这是语言设计选择带来的固有差距,需要在未来版本中寻求改进。
对于计划在 Swift 中构建 LLM 训练流水线的团队,本文提供的经验指向几个关键结论:不依赖第三方 ML 框架构建 GPT-2 规模模型的训练流水线是可行的,性能可以达到实用级别;Swift 6.2 的新特性消除了大部分阻碍数值计算性能的历史障碍;在 Apple Silicon 上,AMX 协处理器是矩阵乘法性能的关键来源,应当优先考虑通过 Accelerate 框架利用它;GPU 加速对于更大规模模型的必要性更高,但对于当前测试的模型规模,CPU 加 AMX 的组合已经足够。
值得再次强调的是,Matt Gallagher 在文章结尾明确指出,所有这些手工优化代码都不应被视为生产级选择。Apple 提供的 BLAS、BNNS、CoreML 等框架经过了更长时间的打磨和更深入的硬件调优,在效率和性能上都会超过手工实现。然而,理解手工实现的优化路径,对于深入理解 ML 训练流水线的性能特征、诊断框架层面的性能瓶颈,以及在框架不支持的场景下构建定制化解决方案,都具有不可替代的价值。
资料来源:Cocoa with Love,Training an LLM in Swift, Part 1: Taking matrix multiplication from Gflop/s to Tflop/s,Matt Gallagher,2026 年 4 月。
内容声明:本文无广告投放、无付费植入。
如有事实性问题,欢迎发送勘误至 i@hotdrydog.com。