SFT 的 Sequence Packing 如何提高效率?怎样避免跨样本注意力泄漏?
简化版
Sequence Packing 把多个短样本装进较长的训练序列,减少 padding 浪费。若要保持各样本独立,必须同时处理注意力边界、位置编码和 next-token 标签边界;插入 EOS 或屏蔽 loss 都不能单独阻止跨样本读取。
详细版
- 对长度差异大的数据,打包能提高有效 token 占比,但实际加速取决于注意力内核和数据流水线。
- 独立样本应使用分块因果注意力,或让变长注意力内核接收正确的各段边界。
- EOS 是序列结束语义标记,不是硬性的注意力隔离开关。
- Loss Mask 决定哪些目标参与损失,Attention Mask 决定哪些上下文可以被读取。
- 样本首 token 不能成为上一条样本末位置的预测目标;还要保持角色监督与 padding 标签正确。
- 验收时比较打包前后有效位置的 logits、损失和梯度,并保持相同的损失归一化口径。
完整版教学
一、padding 为什么消耗训练预算
常规 batch 为了构成矩形张量,会把短样本补齐到同一长度。 如果内核没有跳过 padding,即使这些位置不计 loss,仍可能消耗计算和中间激活空间。 样本越短、长度差异越大,这部分浪费越明显。 Packing 的动机是让分配出的 token 槽位尽量装真实内容,而不是只提高样本条数。
假设四条样本含结束符后的长度分别为 128、128、256、512,单条容量是 1024。 固定补齐时占 4096 个槽位,真实 token 只有 1024 个,有效比例是 25%。 四条正好能装进一个 1024 长度的包,理想槽位利用率变成 100%。 这只是槽位计算,不表示端到端训练一定加速四倍。
二、拼接后模型会看到什么
普通因果注意力允许当前位置读取前面所有位置。 因此把 A 和 B 拼起来后,B 的第一个 token 默认可以读取 A 的全部内容。 插入 EOS 只是给模型增加一个输入符号,不会自动把注意力分数设为不可见。 若业务目标是独立问答训练,这种条件分布变化就需要显式处理。
拼接输入:[a0 a1 a2] [b0 b1]
普通因果可见性(行是 query,列是 key):
a0 a1 a2 b0 b1
a0 1 0 0 0 0
a1 1 1 0 0 0
a2 1 1 1 0 0
b0 1 1 1 1 0 <- 读取了另一条样本
b1 1 1 1 1 1
例如 A 中有一道题的答案,而 B 是相近问题,模型可能利用训练时意外出现的上下文。 单独推理 B 时没有 A,训练与部署条件便不一致。 有些预训练方案有意采用连续文档拼接,这属于目标选择;不能据此认为所有 SFT 都可忽略边界。
三、如何隔离注意力与位置
要模拟各条样本独立运行,可以限制一个 token 只读取同段且不晚于自身的位置。 逻辑上这是沿对角线排列的多个因果块,各段之间完全不可见。 实际实现不一定构建庞大的二维 mask,也可以传递分段长度给支持该语义的变长内核。 必须核查数据整理器、模型前向和注意力后端是否真正传递并使用了边界信息。
allowed(i,j) = (segment[i] == segment[j]) and (j <= i)
segment_ids = [0, 0, 0, 1, 1]
position_ids = [0, 1, 2, 0, 1]
累计段长示意 = [0, 3, 5]
重置 position IDs 是模拟独立样本的直接方法,但位置归零本身不等于注意力隔离。 某些实现用归零位置推导边界,另一些实现需要显式累计长度;这些是后端约定,不能凭参数名字推断。 纯 RoPE 在特定条件下对同段整体位置平移具有相对位置性质,也不能推广成所有模型的位置设置都无所谓。 变长隔离机制可参考 Transformers 的 Padding-free training 文档。
四、标签右移时还有第二条边界
因果语言模型通常用当前位置的 logits 预测下一个 token。 即使 B 完全读不到 A,若直接把拼接 token 复制成 labels,A 的最后位置仍可能被要求预测 B 的首 token。 这不是注意力泄漏,而是跨样本目标错误,必须在损失层另外屏蔽。 具体屏蔽哪个数组位置,取决于框架是否在内部移动标签。
下面假设框架内部使用 logits[:-1] 对齐 labels[1:],并用 -100 忽略标签。
A、B 各自末尾的 EOS 保持为有效目标,让模型学习正确结束。
每段首位置的 label 被屏蔽,以免上一段末位置预测它;段内的 assistant-only mask 还要继续叠加。
input_ids = [a0, a1, EOS, b0, b1, EOS]
labels = [-100, a1, EOS, -100, b1, EOS]
有效目标:a0 -> a1,a1 -> EOS,b0 -> b1,b1 -> EOS
无效目标:A 的 EOS -> b0
不能把以上数组原封不动搬到已经预先移位的实现中,否则会错屏蔽相邻 token。 应打印实际参与交叉熵的预测位置与目标 token,而不只查看原始 labels。 如果把所有 EOS 都屏蔽,模型还可能失去学习终止的监督信号。
五、怎么验证等价性和真实收益
先选两条很短的样本,关闭 dropout,固定权重,在数值容差内比较独立运行和打包运行的有效 logits。 再检查目标集合是否完全相同,并比较按有效目标 token 求平均的 loss。 如果比较梯度,要把独立样本的 loss 按目标 token 数加权,不能简单平均各样本均值。 否则即使注意力完全正确,也可能因为权重口径不同而看到差异。
| 检查项 | 正确结果 | 常见失败原因 |
|---|---|---|
| 改写 A 后观察 B 的 logits | 独立隔离时基本不变 | 跨段注意力未阻断 |
| 检查 A 末尾预测目标 | 不预测 B 首 token | 标签右移边界遗漏 |
| 比较有效 token 的 loss | 相同归一化下接近 | 角色 mask 或权重变化 |
| 测量有效目标 token/s | 反映真实训练吞吐 | 只统计含 padding 的槽位 |
例如两条样本分别有 10 和 90 个监督 token,token 平均时第二条权重为 90%,样本平均时却是 50%。 因此 packing 后每步有效 token 增多,也需要重新核对梯度累积、学习率调度与训练总量。 打包策略还可能丢尾部或切断长回答,必须统计保留样本数和有效目标数。 动态 padding、按长度分桶也能减少浪费,应作为对照方案一起测量。
六、常见误区与追问
- 误区:EOS 会自动切断 Attention。 EOS 是内容标记,除非实现另外利用它生成隔离边界,否则后续 token 仍可读取前文。
- 误区:把 label 设为 -100 就无法读取该 token。 忽略标签只影响损失,这个 token 仍可能提供 K、V 供其他位置读取。
- 追问:position IDs 归零就足够了吗? 不够,必须确认后端据此分段,或显式提供 attention mask 与段长边界。
- 追问:为什么隔离后 loss 仍然变化? 应检查标签移位、角色监督、位置编码、dropout、数值误差,以及 token 平均与样本平均的区别。
- 误区:槽位利用率四倍就代表速度四倍。 数据准备、通信、内核效率和注意力形状都会限制端到端收益,应测真实有效 token 吞吐。
- 追问:一条多轮对话的各回合要互相隔离吗? 通常不应隔离,因为后续回答需要读取历史;边界应放在独立训练样本之间。
七、加强记忆
把 packing 看成“几份作业装进同一个文件袋”:省掉的是空白纸,但作业之间仍要有隔板。注意力边界决定能看哪份材料,位置编码决定页码,标签边界决定正在批改哪一道答案。验收时先证明独立性与监督目标正确,再比较有效 token 吞吐,才能确认效率提升没有改变训练任务。
记忆钩子:装得紧、看得清、批得对;EOS、Attention Mask、Loss Mask 各管一件事。