大多数开发者接触现代AI,都是从外部开始的。装一个包,加载模型,调用 generate(),几秒后屏幕上出现文本。这个过程当然有用,但它几乎隐藏了让模型得以运转的全部工程决策。

分词器到底在产出什么?进入嵌入层的是什么形状?查询、键、值是如何形成的?因果掩码在哪里被应用?KV缓存里存了什么?如果训练中断,想要真正恢复而不是仅仅重新加载权重,究竟要把什么写进磁盘?当整个系统用原生C++20应用实现,而不是Python编排层时,又会发生什么变化?

打开网易新闻 查看精彩图片

这些问题最终成为《纯C++变换器:从第一性原理设计、分词、训练、优化并部署一个仅解码器语言模型》这本书的基础。书背后的项目始终遵循一个原则:变换器应当被当作一个具有明确数学、数据、运行时、状态和验证契约的工程系统来对待。

变换器比神经网络本身更大

仅解码器语言模型通常被介绍为一个预测下一个token的函数。从数学上说,这没错。但从操作层面看,它并不完整。一个可运行的系统必须包含更多:文本经过分词器变成token ID,再经过数据集与上下文窗口、嵌入层、解码器块、词表logits,最后到达损失或采样,以及检查点、生成或对话。

每一个箭头都代表一份契约。如果分词器产出的ID超出了嵌入表预期的词表范围,系统就是无效的。如果训练使用一个分词器,推理却加载另一个,两个文件可能各自有效,但整体包是错的。如果架构声明有12个注意力头,而隐藏维度无法被正确整除,实现就应该在训练开始前拒绝这个配置。

这是我在构建参考实现时学到的最重要的一课:当无效状态被尽早拒绝时,原生AI软件会变得更容易调试。例如:

if ( hidden_size % attention_heads != 0 ) {    throw std::invalid_argument( "hidden_size must be divisible by attention_heads" );}

这个检查看起来微不足道,但它保护了后续每一次reshape操作。

为什么选择C++

一个显而易见的问题是:Python已经拥有极其出色的AI生态,为什么还要用C++来构建变换器学习项目?答案不只是性能。一次Python调用和一次C++调用,最终可能执行的是同一个优化过的CUDA或CPU内核。C++并不会神奇地让矩阵乘法变得更快。

C++带来的是另一种东西:对系统边界的可见性和控制力。使用原生C++时,一些事情会变得难以忽视。