衔言渡意是一个中英法三语互译的小模型。训练到第二版(15.5M 参数,192 维,6+6 层),BLEU 到 15.6 后走平,val loss 不再下降——容量到顶了。

需要扩容,但往哪个方向扩?

加层和加宽不是同一件事。层数管的是串行处理深度——嵌套从句需要逐层解开,层不够就解不动内层。维度管的是并行信息负载——同一层里能同时追踪多少实体,维度不够就记不住。两种瓶颈的表现不同,扩错方向等于浪费参数。

这篇记录我怎么区分这两种瓶颈、怎么构造诊断测试、以及最终怎么决定扩容方向。

两种瓶颈

层数管串行,维度管并行

Transformer 的每一层在做同样的事:self-attention 让 token 之间交换信息,FFN 做非线性变换,残差连接把结果叠加到上一层的输出上。层与层之间是串行的——第二层的输入是第一层的输出,第三层依赖第二层的结果。

这意味着层数决定了模型能处理多深的串行依赖。翻译里最典型的串行结构是嵌套从句:“大会请秘书长提交关于会员国落实委员会建议的措施的报告”——要先理解“委员会建议”,才能理解“落实建议的措施”,才能理解“关于措施的报告”,才能理解“请秘书长提交”。每一层嵌套都依赖内层的结果,层不够就解不动。

维度管的是另一件事:同一层里能同时追踪多少信息。“中国、法国、联合王国、俄罗斯联邦、美国和日本提出了经济制裁、军事合作、人道主义援助、外交谈判、技术转让和环境保护的提案”——这里没有嵌套,结构很简单(主语 + 谓语 + 宾语),但需要同时记住六个国家和六个议题。维度不够就开始丢东西。

失败模式不同

两种瓶颈在输出里的表现完全不同。

串行不足时,模型犯的是结构性错误。它知道句子里有哪些元素,但组装关系是错的——从句归属搞反,嵌套层级丢失,主句蒸发只剩内层从句。模型不是没看见那些词,是没能把结构关系一层层解开。

并行不足时,模型犯的是遗漏和混淆。结构没问题,主谓宾关系都对,但实体记不全——六个国家只输出四个,张冠李戴,或者同一个实体重复生成。

这个区分是诊断的基础:看输出错在哪里,就能反推是哪个轴到了瓶颈。

诊断方法

识别任务里的两个轴

面对一个序列任务,问两个问题:

“什么必须先算完才能算下一步?”——这是串行轴。翻译里是嵌套从句,代码生成里是嵌套控制流(外层循环依赖内层条件的结果),摘要里是因果链条(结论依赖推理步骤),分类里是条件嵌套(先判断 A 类,A 类里再分 A1 和 A2)。

“什么必须同时记住?”——这是并行轴。翻译里是并列实体,代码生成里是同时追踪的变量,摘要里是需要覆盖的独立主题,分类里是同时权衡的特征。

这两个轴在所有序列任务里都存在,只是具体映射不同。

构造诊断用例

两个轴各分三级难度。关键是控制变量。

测串行时,保持并行负载低——最多两个并列项,只递增嵌套深度。S1 无嵌套,S2 一层嵌套,S3 两层嵌套。

测并行时,保持结构简单——主谓宾,不带嵌套,只递增并列实体数。P1 两项,P2 四项,P3 六项。

混在一起就分不清哪个轴出了问题。

每级不能只放一句,偶然因素太大。衔言渡意用了每级 3 句,2 轴 × 3 级 × 3 句 = 18 句,六个翻译方向随机覆盖。

读失败模式

串行崩溃的特征是结构性错误——从句关系乱,主句蒸发,嵌套层级丢失。

并行崩溃的特征是遗漏和混淆——实体丢失,张冠李戴,重复生成。

如果两边都崩:把难度从高往低降,看哪边先恢复正常。先恢复的是余量更多的轴,后恢复的才是更紧的瓶颈。

实证:衔言渡意的诊断

15.5M 模型的诊断结果

15.5M 模型(192 维,6+6 层),BLEU 15.6 走平后,跑诊断测试集。

并行轴(目标语言均为中文):

P1,2 项并列,全对:

China and France submitted proposals on economic sanctions. → 中国和法国提出了经济制裁的建议。

P2,4 项并列,全对:

La Chine, la France, le Royaume-Uni et la Fédération de Russie ont soumis des propositions concernant les sanctions économiques et la coopération militaire. → 中国、法国、联合王国和俄罗斯联邦提出了经济制裁和军事合作的建议。

P3,6 项并列,丢实体:

China, France, the United Kingdom, the Russian Federation, the United States and Japan submitted proposals on economic sanctions, military cooperation, humanitarian assistance, diplomatic negotiations, technology transfer and environmental protection. → 中国、法国、美利坚合众国和日本提出了经济制裁、军事合作、外交谈判、技术转让和环境保护的建议。

丢了“联合王国”和“俄罗斯联邦”,丢了“人道主义援助”。六个国家只记住四个,六个议题只记住五个。192 维装不下这么多并行信息。

串行轴更严重

S1,无嵌套,过:

Le Conseil a adopté la résolution à l'unanimité. → 理事会未经表决通过了决议。

结构对,“à l'unanimité”(全票通过)译成“未经表决”是词汇映射错误,不是结构问题。

S2,一层嵌套,丢外层从句:

The Committee recommends that Member States adopt measures in accordance with resolution 1325. → 会员国根据第1325号决议采取措施。

“The Committee recommends that”整个主句蒸发了。模型只译出了内层从句,好像委员会推荐这件事从来没有发生过。

S3,三层嵌套,全崩:

L'Assemblée générale a prié le Secrétaire général de lui présenter un rapport sur les mesures prises par les États Membres pour donner suite aux recommandations formulées par le Comité dans le rapport soumis à sa soixantième session. → 秘书长要求各会员国报告 suite委员会第六十届会议的建议。

结构散架,混入未翻译的法语词“suite”。三层嵌套对 6 层 encoder 来说太深了。

结论:双瓶颈,串行更紧急——S2 一层嵌套就崩,P3 六项才开始丢。

扩容决策

三条线索指向不同方向:

串行诊断指向加层。S2 丢了“The Committee recommends that”,encoder 解不动句级依赖,decoder 映射不出嵌套结构。6+6 不够,扩到 8+8。

并行诊断指向加宽。P3 在六项并列时丢实体,192 维的并行容量到了上限。

S1 的词汇映射错误(“à l'unanimité” → “未经表决”)指向信息密度不足。结构没问题但词义搞错,说明每个 token 分到的参数预算不够精确。token_per_param 从 5 降到 2。

三条线汇合:d_model_calculator 基于 8+8 层和 token_per_param=2 输出 384 维,6 头。最终架构:384d × 6h × 8+8L,51.7M 参数。

验证

拿 15.5M 全崩的句子重新跑。

S3,三层嵌套(15.5M 输出“秘书长要求各会员国报告 suite 委员会...”的那条),51.7M 法→中:

大会请秘书长向各会员国提出关于为执行委员会第六十届会议提交的报告提出的建议而采取的步骤的报告。

三层嵌套结构完整保留——大会请秘书长、会员国采取措施、落实委员会建议、第六十届会议报告,层层关系都在。

P3,六项并列(15.5M 丢了“联合王国”“俄罗斯联邦”“人道主义援助”的那条),51.7M 英→中:

中国、法国、英国、俄罗斯联邦、美国和日本提出了经济制裁、军事合作、人道主义援助、外交谈判、技术转让和环境保护的提案。

六国六议题全部保留,零遗漏。

8+8 层解决了嵌套结构的串行依赖,384 维解决了多实体的并行负载。

旋钮,可能不止两个

8+8 层 + 384 维基本解决了诊断发现的两类瓶颈,但测试时遇到一个有意思的句子:

输入:L'Assemblée générale a prié le Secrétaire général de lui présenter un rapport sur les mesures prises par les États Membres pour donner suite aux recommandations formulées par le Comité dans le rapport soumis à sa soixantième session.

输出:大会请秘书长向各会员国提出关于为执行委员会第六十届会议提交的报告提出的建议而采取的步骤的报告。

结构完整——大会、秘书长、会员国、委员会、报告、建议,四层嵌套关系都读得出来。实体也全在,没有遗漏。错的地方很细:原文是“秘书长向大会提交报告”,输出成了“秘书长向各会员国提出报告”。语义角色绑定搞错了。

这不是串行不足(结构完整),也不是并行不足(实体都在)。attention 已经把相关 token 都拉到一起了,问题出在拿到这堆信息之后——“prié...de...présenter”这个结构到底是“A 请 B 向 C 提交”还是别的论元分配,需要在每个 token 位置做一次非线性计算。这正是 FFN 的职责。

顺着这个想,FFN 的扩展比可能是维度和层数之外的第三个旋钮。和前两者比,它的代价更小:d_model 是平方级扩张(QKV 投影、残差流、embedding 全跟着动),加层是完整 attention+FFN 叠加,而 FFN 倍率从 4 到 6 只动每层的中间维度,参数增长线性而且局部。如果语义角色类的精度问题是个独立的容量轴,那它可能对应一个更轻的调节手段。

但这只是一个样本,不足以下结论。而且更可能的解释是语料——180 万句只是 UN 平行语料的几十分之一,这种三层嵌套带多论元的拗口长句本来就稀疏,模型见得不够。架构和数据同时不足时,把原因单独归给其中一方都不对。

顺着 FFN 这条线再想下去,其他的结构细节也能对上功能角色。

Transformer 的 attention 默认是多头的,多头的标准解释是“不同的头关注不同的视角”——一个头管语法依赖、一个头管指代、一个头管远距离关系。但这个解释和我的框架冲突:视角切换、逐步提炼,这些是层数管的事。如果头也管这个,功能就和层数重叠了。

换个角度看:d_model 管的是一层的总并行带宽——同一层里能同时装多少信息。头数是把这个带宽拆成几个独立通道,每个通道追踪一个相互独立的关键点。头维度是每个通道的容量——追踪一个点需要多少表达空间。三个参数在不同层次上,互不重叠。

至此五个旋钮各自对应清晰的功能:

  • 层数——串行深度,嵌套解开几层
  • d_model——总并行带宽,一层同时装多少信息
  • FFN 倍率——每个位置的非线性算力,论元分配与语义角色绑定
  • 头数——并行通道数,同时追踪几个独立关键点
  • 头维度——单通道容量,追踪一个点需要多少表达空间

实现上有一个小问题需要处理:标准的 nn.MultiheadAttention 模块把 W_Q/W_K/W_V 都设成方阵 [d_model, d_model],reshape 成 [n_heads, d_head] 后自然得到 d_model = n_heads × d_head。这是模块实现的约定,不是数学必然——把投影矩阵改成长方形 [d_model, n_heads × d_head],输出投影 W_O 改成 [n_heads × d_head, d_model] 把扩张的空间扭回来,头维度就能独立于 d_model 调节。

好消息是这不影响 kernel 优化。FlashAttention 的工作对象是投影后的 Q/K/V 张量,只看 head_dim 的具体值,根本不关心投影矩阵是什么形状。只要 head_dim 落在支持的离散档位里(8、16、32、64、128、256 这些 2 的幂次),加速就全保留;反之会 fallback 到通用 kernel,失去 FlashAttention 的好处。

所以头维度作为旋钮的可选值是离散的——实际可调档位就 {64, 128, 256} 这三个,再小没有追踪精度的意义,再大会超出 SRAM 容量。但三个档位对“精度不够就加倍”这种调节已经够用。

五个旋钮有一个调节顺序,也是调整优先级:

d_model → 层数 → FFN 倍率 → 头数 → 头维度

前四个是从数据特征和任务结构直接推出来的基础旋钮。头维度放在最后,有两个理由:一是大多数场景下 64 维足够追踪一个关键点,这个旋钮多数时候不用动;二是它是触发式的——只在前四个都调过还解决不了的“单点追踪精度”问题上才启用。启用代价也不大:只要 attention 是自己写的,把方阵投影换成长方形投影是几行代码的事,kernel 优化不丢。

这个框架在理论上闭合了,但同样是推演,下个项目才能验证。FFN 倍率、头数、头维度三个旋钮,目前都还没在真实训练里动过。

局限性

串行和并行不总是能干净分离。嵌套三层从句、每层又带并列项——两轴耦合了,诊断用例的“控制变量”原则就不好守。实际任务里这种耦合很常见,诊断结果会模糊。

诊断用例需要手工构造。你得理解任务的结构特征,知道什么算“串行复杂度递增”、什么算“并行负载递增”,然后自己写测试句。这不是能自动化的事。

这是一个可修正的起点——给你一个有依据的扩容方向,不是精确到层数的答案。实际需要多少层、多少维度,还是要训了才知道。但至少不是盲扩。


上一篇从数据反推层数和维度,给出训练的起点。这篇从训练反馈出发,诊断瓶颈方向,决定怎么扩。两篇合在一起,覆盖了从起点到迭代的路径。

起点定好了,训练撞墙后怎么判断往哪个方向扩——加层还是加宽——是另一个问题。

这个问题现在回答完了。