从头构建大模型说是🔥☢️

标记化

字节对编码标记器(BPE)

在字符分割中,可以直接使用Unicode转为UTF-8的方式产生字节编码序列

使用 re.escape 防止特殊字符被误解释(通过加入转义的方式避免正则判断出现错误),并使用 re.split 将特殊字符作为分割点分离原始文本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import regex as re

# 多个特殊标记
special_tokens = ["<|endoftext|>", "<|startoftext|>", "<|pad|>"]

# 构建分割模式
# 使用 re.escape 防止特殊字符被误解释
delimiter_pattern = "|".join(re.escape(token) for token in special_tokens)
print("Pattern:", delimiter_pattern)
# 输出: <\|endoftext\|>|<\|startoftext\|>|<\|pad\|>

# 使用 re.split 分割文本
text = "Hello world<|endoftext|>This is another document<|startoftext|>And another<|pad|>Final text"
documents = re.split(delimiter_pattern, text)
print("Split result:", documents)
# 输出: ['Hello world', 'This is another document', 'And another', 'Final text']

一些加速方法,例如 BPE 算法迭代地统计每对字节,并识别出出现频率最高的字节对,将字节对表示为一个新的词库(在 0 ~ 255 外的一个数字,样例中定义为 256 )。该方法能够通过迭代一定的次数减少token的长度

这样做,token中所包含的信息会不会进一步让机器难以总结?🤔🤔

与此同时,可以将大的数据文件切分成4KB左右大小的区块(保证一篇完整的文章不会从中间被切分)以便并行训练模型

pytorch

张量

张量的连续与否取决于该张量在内存中的储存是否连续,当你遇到 view() 报错( view() 要求张量在内存中是连续的,因为它只是重新解释一个连续内存块的含义)或追求极致性能时,就需要考虑并使用 .contiguous() 来修复它。

对于提取张量中的列,行,提取试图,转置等均不会创建原始张量的副本,**但.contiguous().reshape() 操作会返回一个在内存中连续存储的、包含相同数据的张量副本。**在使用时要谨慎

架构与超参数

核心架构

标准化

layernorm 等正则化虽然只使用了极少的 FLOP ,但仍会耗费较多运行时间在内存移动上

相比于 layernorm ,RMS norm 不会减去均值并去除 β 参数,使得内存效率提高

去除偏差项会提高模型训练的稳定度,目前大多数大模型完全省略偏差项,仅在矩阵乘法类型上进行运算

目前基本上正确的做法是 pre-norm ,并且在残差流之外进行 LayerNorms,可以获得更好的梯度传播,更稳定的训练

激活函数

目前最新的模型都使用门控线性单元 SwiGLU 、 GeGLU

GLU 有一个额外的超参数 V ,对于门控单元,要将待乘矩阵缩小一些以确保一切保持参数匹配

架构

串行 or 并行

目前大多数模型依旧采用串行架构

位置嵌入

目前大多数模型采用 Rope embeddings

Rope embeddings 旋转矢量,旋转角度由每个单词的位置和旋转决定

其中确定角度的 θ 有一个时间表(类似于 sin cos 位置嵌入),可覆盖不同的频率范围以获取更高或更低频率的信息

相对定位信息至关重要

Rope 作用原理仍需补充♿♿♿

超参数

前馈层维度

从经验角度上讲,前馈层的维度等于 4 倍模型维度是较优的

若使用 GLU ,前馈层的维度等于 8/3 倍模型维度

前馈层矩阵越宽,能得到更多的并行计算,更好的系统优化收益,与扩大隐藏单元的作用方式不同

头部维度与头部数量

头部维度 == 模型维度 / 头部数量

这只是一般共识,可以作出一些改变

纵横比

模型维度 / 网络层数

通常最佳点为 128 ,早期模型的该比例一般较小

较好的区间为 100 - 200

词库大小(vocabulary size)

呈一个上升趋势

已出世的生产化模型在 100 - 250k token 数量范围

剪枝及一些其他正则化

剪枝(drop out)逐渐不再受青睐(大模型目前不存在过拟合的问题,甚至无法对训练数据进行一次迭代),权重衰减(weight decay)成为新兴

不是为了控制过拟合(权重衰减大小与训练集和验证集的损失比例无关), 而是与优化器的学习率发生作用,存在一些非常复杂的交互,因此其适用于更好的训练效果而不是避免过拟合

稳定性技巧

在训练时范数梯度和损失会遇到很多异常的由梯度爆炸造成的峰值

一般在 softmax 函数上出现该问题

可以使用 z-loss 替代 output softmax 来缓解这一问题

使用 QK norm 替代注意力机制中的 softmax 产生稳定输出,是种非常严厉的干预

也可以无脑加 layer norm (乐🤣)

在推理时仍然使用 layer norm ,layer norm 已经学习到了参数,去除会大幅改变模型参数

Logit soft-capping 没见过的新技术,通过tanh转换一些最大值

注意力头

GQA / MQA

算术强度:总计算操作开销 / 总内存访问开销,越大越好。GPU内存访问开销相对较大,目的是在单次内存访问中做尽可能多的计算(大数量的头,大的批量大小,大的序列长度都能提高这一指标)

在推理时需要 KV cache 技术来增量式更新注意力来进行高校推理,避免整个 QK 矩阵的计算,处理绝对必要的 Key 和 Value 矩阵部分,但这会带来更高的内存访问量🤔

MQA 设置多个查询头,但对于键和值只能设置一个维度或一个头

GQA 增加键和值的头

稀疏注意力机制仅关注部分矩阵,权衡各种表达能力和运行时间,以此获得更大的注意力窗口

滑动窗口注意力机制,在每一层只关注当前位置周围的一小块区域,控制所需的资源总量

Transfomer Block,可以定义一些层使用完全自注意力或者滑动窗口自注意力(使用RoPE)

专家混合系统(MoE)

MoE对比密集架构模型就是好用😋

架构

用选择器层和许多较小的层来替换左侧的大的前馈层,在每次前向传递或每次推理中选择较少数量的副本

专家网络:一组不同的“子模型”(即专家)。每个专家通常是一个前馈神经网络,它们各自擅长处理特定类型或特定模式的数据。

门控网络:一个“路由器”。它的作用是针对每一个输入(比如一个词或一个token),计算应该将输入分配给哪个(或哪些)专家,并决定最终组合这些专家输出时的权重。

专家并行性:可以把每个专家分片到不同的设备上,只需要获取 token 并路由到适当的设备

基础设施很复杂,只有在多节点时才能发挥最大优势,并且路由选择的训练和所涉及的优化问题,其目标要么是启发式的,要么是不稳定的,所以没有将其推广到大模型之外

传统方法是把 MLP 层替换为 MoE 层,如今可以推广到注意力机制作为注意力头,但很少见

设计

有三种路由选择方法

  • token 选择专家 (Top-K 个专家)

  • 专家选择 token (每个专家选择 Top-K 个 token)

  • 全局专家与 token 的映射任务,可以尽可能平衡映射 (需要解决一些复杂的优化问题)

几乎所有 MoE 都选择了 token choice topK 路由

K 这个超参数等于 2 是一个规范选择

Top-K 路由公式,没错!是有一个公式在里面的

共享专家和细粒度专家

训练

RL

强化学习是“正确的解决方案”,但梯度变化和复杂性意味着它尚未得到广泛应用

随机近似

随即探索策略,通过随机添加扰动并对齐退火或执行各种操作,可控制MoE将进行的探索-利用平衡。专家会得到有时意想不到的 token ,这会使专家的专业性降低,但可能更加强大

或者对路由器对数进行乘性扰动,获得不那么脆弱的专家,但效果不如一些基于启发式损失方法

启发式平衡损失

专家与 token 映射的平衡公式

以及 deepseek 提出的每个批次专家的 token 平衡和每个批次设备的 token 平衡

DeepSeek v3 变种,给每个专家加入一个设置学习率的偏差进行平衡(辅助无损平衡)

如果不进行专家平衡损失,实际上多专家会退化为 2 专家,事实上该 MoE 可以做的更好

随机性

MoE 有一个有趣的随机性来源——

令牌丢弃(token dropping) :某个专家没有足够的内存容纳被分配的 token ,多余的 token 会被直接丢弃

稳定性

MoE 很难进行微调,使用 Float32 计算专家路由 (有时使用 z-loss )替换softmax

此处应有 z-loss 的公式

微调

使用稀疏模型会出现很多过拟合的情况

使用大量SFT数据或者将一些层替换为密集层,并只在密集层上微调

升级再造(upcycling)

从原始密集模型中提取 MLP 层,复制多个副本后加入一些扰乱,新增一个从头初始化的路由器,然后就可以当成一个 MoE 开始训练

MoE十分擅长推理,并非每个 MLP 在推理时都处于活动状态

DeepSeek V3 - 非MoE

MLA:多头潜在注意力

将头部投射到较低维的空间中,但与 RoPE 不兼容

MTP:对损失函数进行些许改变,可并行预测多个标记

总结

MoE 充分利用了稀疏性——并非所有输入都需要完整的模型。离散路由很难,但 Top-k 启发式算法似乎有效。目前有大量经验证据表明,MoE 有效且经济高效。

缩放定律

使用小模型将其放大来改进工程技术

随着数据量或者模型大小的改变,我们期待模型表现出某些行为

参数量与误差的关系,数据和模型大小应该如何与性能相关

关键 Bacth size

推理

给定训练过的固定模型,根据提示生成响应

一些评价指标:

  • 首次令牌生成时间 (TTFT):用户在令牌生成前等待的时间(对于交互式应用很重要)

  • 延迟(秒/令牌):用户获取令牌的速度(对于交互式应用很重要)

  • 吞吐量(令牌/秒):对于批处理应用很有用

训练是可以并行的,基于transformer架构的推理还是只能自回归串行工作

KV cache 使用空间换时间,减少推理的令牌生成时间复杂度

延迟与吞吐量之间的权衡:

较小的批次大小可带来更好的延迟,但吞吐量较差

较大的批次大小可带来更好的吞吐量,但延迟较差

推理优化——减小 KV cache

GQA(Group-query-attention)

键和值的数量会减少,查询会增多,跨头部共享

在减少内存使用的同时提高吞吐量

MLA(Multi-head latent attention)

不改变键和值的数量,但会将其投影至低维空间

并且 MLA 在准确性上比 GQA 的表现更好

CLA(Cross-layer attention)

跨层注意力,在各个层之间使用相同的键值投影

局部注意力

只关注最相关的局部区域

有效上下文的规模与层数呈线性关系

KV Cache 大小可以独立于序列长度并保持一定大小,不会因为序列长度增加而增加

但表达能力不强,会损失一定准确度,因此将局部注意力和全局注意力层混合搭建

推理模型架构替换

Transformer 在设计时并没有考虑到繁重的推理工作负载

状态空间模型(State-Space model)

思想来源于信号处理,对长上下文序列进行建模,但时间复杂度大幅下降

在一些联想回忆任务中表现不佳

Mamba 很受欢迎,并成功移植到 Transformer 和 MoE 的模型中

线性注意力,类似于RNN,这种想法被成功扩大,MiniMax(linear attention + full attention)正在训练相当有效的模型

扩散模型(Diffusion model)

不再受自回归约束,并行生成 token ,并持续迭代改良

量化

减少数字的精度( float 转 int 或者转为更低精度的 float)

越少的内存意味着传输的字节数越多,延迟越低,但准确度会有所降低

考虑选择量化具体那一部分的权重

模型剪枝

拆掉昂贵模型的零件,让其变得便宜然后修好它

  1. 定义更快的模型架构

  2. 使用原始模型(架构不同)初始化权重

  3. 修复更快的模型(蒸馏)

推测采样

以上措施都是有损的,但该方法会让你鱼和熊掌兼得

检查比生成更快

使用廉价的草稿模型来生成,甚至说是猜测一些 tokens,并使用目标模型来评估这些 token,这可以并行执行

保证从目标模型获得精确的样本

可用 Medusa 和 EAGLE 更进一步优化

数据

训练阶段:

预训练:使用原始文本(例如,来自网络的文档)进行训练

训练中期:使用更多高质量数据进行训练以增强能力

后训练:使用指令跟踪数据进行微调(或进行强化学习)以实现指令跟踪

在实践中,界限模糊,可能会有更多阶段。

……但基本思路是从[大量低质量数据]到[少量高质量数据]。

预训练

基于书籍和维基百科的数据

来源于网络的数据非常容易受到污染,造成大预言模型的数据中毒

Common crawl 网络爬虫,需要对其中的内容进行处理和筛选

Q&A 格式数据集与用户输入的内容及其响应十分相似

不同的模型采用了不同数据集,并采用并不完全相同的处理方式

如何合法获取数据以及处理得到高质量的数据

过滤与去重

过滤算法

带有 kneser-Ney 平滑的 n-gram 模型,速度很快,但很粗糙

快速文本分类器,词袋嵌入

词袋可扩展至 g-gram,并使用哈希实现(线性分类器)

DSIR(重要性采样)

通用框架

给定目标 T 和原始 R,找到与 T 相似的 R 子集,根据 R 和 T 估计某个模型并推导出评分函数,根据得分将样本保留在 R 中

T 的生成模型 (KenLM):score(x) = p_T(x)

保留 score(x) >= 阈值的样本 x(随机)

判别分类器 (fastText):score(x) = p(T | x)

保留 score(x) >= 阈值的样本 x(随机)

重要性重采样 (DSIR):score(x) = p_T(x) / p_R(x)

以与 score(x) 成比例的概率对样本 x 进行重采样

过滤应用

语言识别,质量过滤,毒性过滤

去重

完全重复,近似重复(相同的文本,但有几个标记不同)

主要使用哈希函数,存在最佳超参数用以限定桶的数量及哈希函数的数量,由插入数据的数量决定

Jaccard similarity

Jaccard (A, B) = |A 交 B| / |A 并 B|,用以判断两种集合的近似程度

minhash 的哈希冲突概率即为 Jaccard similarity ,可根据相似性控制适当的哈希冲突级别

局部敏感哈希,波段 b 和每段的哈希函数数量 r 的超参数,使低于阈值的数据碰撞概率很小,高于阈值的数据碰撞概率很大,r 越大使得曲线右移并更尖锐,b越大使得曲线左移

SFT/RLHF对齐

对LM输出进行更好、更严格的控制

标准方法——模仿(SFT)然后强化(‘RL’ HF)

deepseek R1 基于结果的奖励和 GRPO 很有效果

RL

CLIP 方面的知识