happy-llm 阅读笔记

dawn_r1sing Lv3

NLP

基础任务

  • 中文分词
  • 字词切分:unhappiness = un + happi + ness
  • 词性标注
  • 文本分类
  • 实体识别:识别 人名、地名等
  • 关系抽离:识别实体间的关系,如判断因果关系、xx关系
  • 文本摘要:概括,抽取式、生成式
  • 机器翻译
  • 自动问答

文本表示

如何将自然语言数字化

“静态参数 → 动态参数”

向量空间模型

每个维度代表一个特征项,每个维度是词表中的一个词 → 数据稀疏性、维数灾难问题(5/16384)

词向量之间是绝对

n-gram语言模型

基于统计(条件概率,考虑前n-1个词来估计第n个词

问题:预测能力差;n较大时出现数据稀疏性问题,大部分词的概率是0;模型参数急剧增加;忽略词之间的范围关系;

word2vec

学习词之间的上下文关系生成词的密集向量表示、捕捉词之间的语义关系,使得语义相近的词在向量空间中的距离较近

  • CBOW:上下文 → 中心词

  • Skip-Gram:中心词 → 上下文

    词表中的每个词一开始都会被随机分配一个向量,然后训练会不断调整这些向量,使得中心词和它周围出现的词的向量更加接近,和不常同时出现的词的向量更远。

  • 词向量之间是相对

ELMo

同一个词在不同句子中有不同的向量表示

  • 训练:
    • 双向LSTM:根据前文(前文隐藏状态)预测下一个词,根据后文(后文隐藏状态)预测上一个词
    • 计算出隐藏状态公式的固定参数
    • model学会了如何计算上下文隐藏向量,进而学会理解上下文
  • 下游任务应用:利用model的上下文理解能力,计算输入中每个词的上下文向量,作为新的特征输入

但这样的计算量过大,每次作业都要重新计算每个词的向量

因此后面又出现了注意力机制(缓存kv,更新q)

神经网络
  • 全连接神经网络:n*n*n
  • 卷积神经网络:a*b*c*b
  • 循环神经网络:RNN,历史信息作为输入,存在环
    • 依次输入,依次计算(先输入历史信息,才能计算新的向量),限制并行能力
    • 难捕捉长相关

注意力机制

情景
  • 输入序列1(,2)

  • 输出上下文向量

原理

黑盒model,通过结果反推原理的抽象理解

本质是获得两个序列中token之间的相关度,根据这个相关度分配注意力

Q 来自于 Decoder 的输入,K 与 V 来自于 Encoder 的输出

  • Query:序列1-查询向量

  • Key:序列2-输入中所有 token 向量形成的矩阵,代表token之间关系

  • Value:序列2-输入中所有 token 向量形成的矩阵,代表token本身的含义

  1. Q*K 得到查询(序列1)和序列2中每一个token的关系(关联度、相似度)
  2. softmax(Q*K/…) 获得概率分布进行归一化(activate function),得到权重(还需要进行放缩)
  3. softmax(Q*K/…) * V得到注意力分配

tmp5D21

自注意力

计算本身序列中每个元素对其他元素的注意力分布,获得上下文向量

encoder

核心

  • 将一个词向量映射为三个向量

  • Q=xWQ

    K=xWK

    V=xWV

  • 其他一样

训练手段

掩码自注意力

普通self-sttention是可以看到历史信息和未来信息的,通过mask使得model只能获取历史信息

tmpC54D

多头注意力

本质上就是设定 n组W矩阵,与输入进行内积,计算多个注意力向量,然后拼接起来

简化计算 → 先拼接,再内积

  • 依旧是seq_len*hidden_len的矩阵W,但它是由n个seq_len*d拼接而成(因此要求 n|hidden_len,其实就是增加一个维度的事
  • 后续内积操作不变
  • Concat(head1,head2,…,headn)⋅WO:Wo即为最后的线性变换

img

代码流程示意:重点在于把head和seq维度交换一下,这样就相当于是对head_i进行计算,实现了多头注意力机制

img

基本模块:encoder/decoder

前反馈神经FNN

一种MLP多层感知机,模拟连续函数,提取特征(线性扩展维度。非线性将高维空间切分地更细致,最后恢复原维度)

全连接,处理attention得到的上下文向量

  • 线性层1
  • relu
  • 线性层2
  • dropout过拟合
层归一化

由于每一层输出的分布是不同的,所以将其进行归一化转换成标准正态分布

  • 批归一化

  • 层归一化

    计算每个样本在所有层上的均值和方差,使得每个样本的分布稳定

残差连接
  • 下一层的输入 = 上一层的输入 and 上一层的输出
  • 防止model的退化
1
2
h = x + self.attention.forward(self.attention_norm(x))
out = h + self.FNN.forward(self.FNN_norm(h))
encoder
  • encoder = n个encoder layer + 层归一化

    encoder layer = 层归一化 + self-attention + 残差连接 + 层归一化 + FNN + 残差连接

  • positional encoding 加入位置信息

decoder
  • decoder layer = 掩码自注意力层 + 多头注意力层 + FNN (每一个还有对应的归一层)

架构:transformer

embedding层
  • 将index转换成向量,≈ 存储固定大小字典的向量查找表

  • 本质是一个查表的过程,但首先需要先将这个表训练出来(表的形状为vocab_size*dim)

  • 流程:

    分词器:将自然语言切分,获得index (bsz,seq_len,1)

    embedding层:对应index转换成长度为dim的向量

    输出(bsz,seq_len,embedding_dim)

位置编码
  • 对于简单的注意力机制,各个位置token的地位是一样的,为保留序列中的相对位置信息,采用位置编码机制
  • 采取正余弦函数编码(绝对位置编码

tmp6982

  • 可以在短长度embedding的基础上计算更长长度的embedding
  • 对于固定间距k,PE(pos+k)可由PE(pos)计算得到
流程

input→ embedding层 → 位置编码 → encoder layers → decoder layers → 线性层 → softmax → output

结构

encoder-only(BERT

  • 实现 监督学习、泛化能力弱 → 预训练+微调(泛化能力强)
  • 局限:
    • MLM 和下游任务微调不一致
    • max_query_len
结构

embedding → encoder → linear+activation+linear → softmax → 输出类别

encoder:attention、残差连接、前反馈层

其中attention中包含positional embedding

tmpB3DF

Non-Autoregressive 预测并行,但不能生成整个sequence,只是填空

340M parameters 参数量巨大

干细胞

bert使用transformer实现的encoder + MLM(能看到左右?两侧的token)

预训练
  • 大规模数据 → 无监督
Masked language model(MLM)

预测下文内容,是直接拟合从左到右的语义关系,忽略了双向的语义关系

使用mask实现拟合双向语义

  • 完形填空:将输入中的token随机遮住(用特殊符号mask取代

  • 改进:15%进行遮蔽(最终要计算交叉熵),其中

    原因:实际训练过程中,后续的微调也需要model关注非mask内容,单纯的完形填空使得model只将注意力放在mask上,并且只进行预测任务(model走捷径时会忽视上下文的学习

    80% mask → 拟合双向语义

    10% 替换为随机token → 保持对上下文的学习(不要偷懒,每个token都要理解他的上下文)

    10% 不变 → 让model不仅仅预测【mask】部分

    数据处理的代码思路:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
encoding = self.tokenizer(
text,
max_length=self.max_length,
padding='max_length',
truncation=True,
return_tensors='pt'
)

input_ids = encoding['input_ids'].squeeze()
attention_mask = encoding['attention_mask'].squeeze()

labels = input_ids.clone()

probability_matrix = torch.full(labels.shape, self.mlm_probability) # 生成形状为labels的矩阵,值均为mlm_probability

special_tokens_mask = [
self.tokenizer.get_special_tokens_mask(val, already_has_special_tokens=True) # 获得val中special token的位置向量(01向量)
for val in labels.tolist()
]
special_tokens_mask = torch.tensor(special_tokens_mask, dtype=torch.bool) # 将 01 变为bool向量

probability_matrix.masked_fill_(special_tokens_mask, value=0.0) # 将special token对应位置赋值为0.0

# 根据probability_matrix中的概率进行伯努利采样,生成mask索引(其中为0.0的special token一定是0 → flase)
masked_indices = torch.bernoulli(probability_matrix).bool()

# 将非mask位置的标签设为-100(在计算loss时忽略)
labels[~masked_indices] = -100

# 80%的概率用[MASK]替换
indices_replaced = torch.bernoulli(torch.full(labels.shape, 0.8)).bool() & masked_indices
input_ids[indices_replaced] = self.tokenizer.convert_tokens_to_ids(self.tokenizer.mask_token)

# 10%的概率用随机词替换
indices_random = torch.bernoulli(torch.full(labels.shape, 0.5)).bool() & masked_indices & ~indices_replaced
random_words = torch.randint(len(self.tokenizer), labels.shape, dtype=torch.long)
input_ids[indices_random] = random_words[indices_random] # 好奇怪的赋值,代价稍微有点大

# 剩下的10%保持原词不变

下一句子预测NSP

拟合句子之间的关系

  • 判断两个句子是否是连续的上下文关系
fine-tune

较低成本适配下游任务

[CLS] 代表整个句子的语义表征,在一些下游任务中很有用

RoBERTa

BERT基础上

  • 去掉NSP

  • 动态mask策略:将mask操作放在训练过程中进行,使得每一个epoch训练数据的mask都不一样

    更高效、易实现

  • 更大规模的预训练数据和步长(epoch↑

  • 更大的 bpe 词表

ALBERT

减小model参数,保持model能力

  • embedding参数进行分解:将大查找表变成一个小查找表+线性变化

    V*H = V*h + h*H 在embedding层后面加一个线性层,将维度恢复为hidden_dim

  • encoder跨层参数共享

    各个encoder层参数高度一致,因此只保留一层encoder参数,也就可以扩大隐藏层维度(更宽的model)

    参数规模大大减小,但训练和推理速度很低

  • SOP预训练任务

    正例:两个正序句子 反例:两个逆序句子

    要求model能够拟合句子关系、学习顺序关系

encoder-decoder(T5

将所有NLP任务统一为 文本到文本的转换,增强了model的通用性

任务描述前缀

将一个序列转换成另一个序列

  • tokenizer。。

  • transformer

    • encoder(双向) → 获取上下文向量

      • self-attention(no mask)
      • Add & Norm
      • FFN
      • Add & Norm
    • decoder

      • self-attention:

        • 输入为x(decoder历史输出)

        • mask,让model从左到右进行预测输出(单向)

      • Add & Norm

      • cross-attention:

        • 输入为 decoder self-attention输出,encoder输出
        • q通过decoder self-attention的输出计算,kv通过encoder的输出进行计算
        • no mask,让model关注encoder的全部输出进行输出(双向)
      • Add & Norm

      • FFN

      • Add & Norm

预训练
  • 任务 MLM:

    给定文本序列,随机选取token进行遮蔽,用占位符[token]代替,被遮蔽的token序列为目标序列

    主要是针对encoder的训练

decoder-only

文本生成 → 使用掩码注意力机制

基本结构
  • tokenizer

  • embedding - dropout

  • decoder

    • norm
    • 掩码自注意力:核心不同点
      • 计算xq xk xv,调整形状(分出维度head来)
      • xq, xk 旋转位置编码
      • (kv repeat)
      • 注意进行维度顺序调整
      • q@k,+ mask
      • softmax
      • attn_dropout
      • @v
      • 线性wo
      • resid_dropout
    • 残差连接
    • norm
    • MLP:拟合连续函数,进行特征提取
      • ConV1D (相比线性矩阵,重点关注局部特征
      • 激活函数
      • *ConV1D
      • ConV1D
      • dropout
    • 残差连接
  • 归一化norm

  • 区分 train eval

    • train:
      • linear,获取..vocab_size向量
      • 计算loss:cross_entropy
    • eval:
      • 只需要输出最后一个位置的vocab_size向量,用它来预测下一token
      • loss=0
    1
    2
    3
    4
    5
    6
    7
    if targets is not None:
    logits = self.output(h)
    self.last_loss = F.cross_entropy(logits.view(-1, logits.size(-1)), targets.view(-1), ignore_index=0, reduction='none')
    else:
    # 推理时的小优化:只对最后一个位置的输出进行前向传播
    logits = self.output(h[:, [-1], :])
    self.last_loss = None
GPT
预训练
  • CLM - 因果语言模型

    虽说是单向的,但由于任务难度大,model很难偷懒,会深入理解上下文,更符合“生成”的要求

    对于一个输入序列长256,期待输出序列长256的任务,模型会不断根据前 256 个 token、输入+预测出来的token……进行256次计算,最后生成⼀个序列长为512的文本

  • GPT1

  • GPT2:

    增加了参数规模(训练数据、模型体量)

    pre-norm

    以 零样本学习 为目标:不进行微调,直接向预训练模型描述问题,即可输出

  • GPT3:

    增加了参数规模(训练数据、模型体量)

    稀疏注意力机制:无需每两个token之间都进行注意力计算

    few-shot:给模型提供少量示例(一般是直接写在prompt中,3-5个),也称上下文学习

LLaMA

目前LLM的普适架构

结构与GPT基本类似

发展:支持更长文本输入,分组查询注意力机制

GLM

中文LLM,独特model架构

不同之处:

  1. 使用post-norm:先完成残差计算,再进行norm(参数正则化效果更好

    GPT、LLaMA使用pre-norm,先进行norm,再进行残差计算,这样可以避免梯度爆炸或梯度消失

    解释:计算归一化参数时,需注意反向传播的梯度(pre-norm:

    tmpB0D1

  2. 单个线性层,而非MLP(无非线性)

  3. 激活函数GeLUs:更平滑

预训练
  • GLM 通用语言模型:自编码+自回归

    输入序列随机遮蔽一连串token,要求model输出

  • model既要理解上下文 预测遮蔽部分(MLM),还要在遮蔽内部逐个预测token(CLM)

  • 但在超大规模的预训练中,CLM优势远超于MLM,因此后续LLM重点还是放在CLM上

LLM

逐渐接替PLM预训练语言模型(预训练后续还要单独微调)

GPT-3开始

预训练+对齐(SFT、RLHF)

能力
  • 涌现能力:相同模型架构与预训练任务的前提下,某些能力在小型模型中不明显,在大型模型中表现突出

  • 上下文学习能力:无需进行微调,在prompt中添加几个示例(调整prompt)即可

  • 指令遵循

  • 逐步推理能力:CoT思维链推理策略

特点
  • 多语言支持:训练数据就是多语言的
  • 长文本处理:采用旋转位置编码,具有长度外推作用
  • 拓展多模态:引入Adapter层(将其他模态的向量映射到LLM可接受的范围,对齐作用)和图像编码器。。。
  • 幻觉:杜撰 → 在prompt中进行限制,通过RAG指导生成

预训练

  • LLaMA架构

  • CLM任务,预测句子的下一token,loss的计算是所有token

  • 庞大的参数量和语料库

  • 分布式训练框架:Deepspeed……

    • 数据并行

      每张GPU上运行一个模型示例,将1个batch的训练数据分配给不同GPU,计算出每张GPU的梯度

      将这些梯度聚合,更新所有GPU上的模型参数,重复。

      这和顺序训练并不完全相同,但这样模拟了更大的batch_size,达到了更好的效果

    • 模型并行

      当一张GPU无法存放完整的模型参数时,将不同层放到不同GPU上

    • ……

  • 训练数据:数据配比、处理与清洗(框架)

    数据质量往往比体量更重要

    • 处理流程:

      • 文档准备

      • 语料过滤:

        • 基于model:训练一个分类器
        • 计算语料的质量指标
      • 语料去重:大量重复文本会显著影响模型的泛化能力

        hash算法计算相似性、基于子串

Deepspeed
ZeRO零冗余优化器

优化数据并行时每张卡的显存占用,从而支持更大规模的模型

  1. 显存占用包括:

    • 模型状态:模型参数、梯度、Adam状态参数

    • 剩余状态

  2. 优化策略:

    • 对Adam状态参数进行分片,每张GPU中存储1/n

    • 对梯度进行分片

    • 对模型参数进行分片

  3. 问题:

​ 随着分片的增加,GPU的通讯开销也在增加,GPU利用率下降

SFT- 有监督微调

预训练得到的model能够流程的接出下文,但不知道问题的含义,无法适配下游任务

因此还是需要进行微调

本质仍是CLM任务:向下预测序列,但loss的计算只关注assistent的content部分,这也就使得model能对指令进行理解

与微调的不同之处:
  • 传统预训练模型的微调需要针对具体的下游任务,不同任务需要分别进行对应的微调
  • SFT 通过指令微调,使模型获得 泛化的通用指令遵循能力
数据

{指令,input,output}

{system,user,assistent}

  • 多且多样
  • 配比
  • 高质量:人工标注数据/model生成
  • 多轮对话的形式
多轮对话能力
  • 构造多轮对话样本:直接要求model预测每一轮对话的assistent的content

RLHF - 人类反馈强化学习

RM - 奖励模型
  • 拟合人类偏好

    本质是文本分类模型,在传统LLM框架后面接一个分类层

  • 将提示输入至model中,得到模型输出进行打分

    但打分得到的标量奖励会放大差异,一般是对同一问题的不同回复进行排名

  • 具体流程:

    • 将prompt输入至model,得到两种输出,人类对输出进行比较,谁更优?
    • 获得一个**(提示, 获胜的响应/Chosen Response, 失败的响应/Rejected Response)** 的三元组,这就RM训练所需的、最标准的原始数据格式
    • 将chosen_example、rejected_example(一对好坏)分别输入reward model中得到标量奖励,模型通过最大化二者的奖励差异来计算loss,反向传播训练reward model
    • 重复上述操作,获得多个三元组,进行多次训练
PPO训练 - 近端策略优化算法

组成:

  • LLM:

    • actor(参数更新):智能体
    • ref(不进行参数更新):保证actor不偏航,防止model失去之前经过pretrain、sft获得的能力
  • RM

    • critic(参数更新):为每个位置的token预测后续输出的评分,评估生成当前token的长期价值
    • reward(不进行参数更新):只评价当前的输入,即时奖励

流程:(逐token

  • 将prompt分别输入至 actor model、ref model,计算二者输出token的KL散度(要求actor不要偏离原始model太远,确保模型输出合理连贯的文本而不是乱码)

    tmp1AFA

  • reward、critic分别对actor的输出token进行打分(reward输出对当前token的评分,critic输出预测从当前token到最后的累加奖励)

  • 计算奖励,更新参数(actor、critic):

tmp7C59

image-20251001185247669

代码实现

3065155938B5BA3538B73634F08A4938 C25FFC4E9120FCD03923F9C8AA883634

RMSNorm

均方根norm

1
2
# 这里*是逐元素乘法
return x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.esp)

tmp5FC1

attention

由于Q与KV的运算需求,采用kv_head时需保证KV矩阵能够正常repeat扩展成Q的形状,因此要满足head_dim%kv_head_dim == 0

C8C2C821D8EBBDBF5D66B31215426352

位置编码-旋转嵌入

img

对词向量进行旋转,使得两个位置向量的内积只取决于相对位置

  • 位置向量表示绝对位置信息
  • 位置向量的内积表示相对位置信息

tmp6480

通过上式我们发现,对词向量进行旋转(*e_imθ)即可实现 内积→相对位置

  1. 先确定旋转角度(频率)

    其中的t展示的是seq位置

img

然后计算三角函数,获得旋转嵌入的实部和虚部

1
2
3
4
5
6
7
8
def precomoute_freqs_cis(dim: int, end: int , theta: float = 10000.0):
# 从0开始,步长为2(一个实部,一个虚部,同一个负数的旋转是相同的),总共dim/2长
freqs = 1.0 / (theta ** (torch.arange(0, dim, 2)[:(dim//2)].float() / dim))
t = torch.arange(end, device=freqs.device)
freqs = torch.outer(t, freqs).float() # 外积
freqs_cos = torch.cos(freqs) # 实部
freqs_sin = torch.sin(freqs) # 虚部
return freqs_cos, freqs_sin
  1. 将得到的实部和虚部矩阵的形状调整好(没有的维度设置为1)
1
2
3
4
5
6
7
def reshape_for_broadcast(freqs_cis: torch.Tensor, x: torch.Tensor):
ndim = x.ndim
assert 0 <= 1 < ndim
assert freqs_cis.shape == (x.shape[1], x.shape[-1])
shape = [d if i == 1 or i == ndim-1 else 1 for i, d in enumerate(x.shape)] # 将除了seq_len、dim的其他维度设置为1,便于广播

return freqs_cis.view(shape)
  1. 旋转

实部和虚部:约定俗成将输入的最后一维进行拆分,一半作为实部,一半作为虚部

实质是一种向量旋转:x向量 和 freqs_cis向量

  • xq.shape[:-1] 表示 xq 除了最后一维的所有维度。
  • (-1, 2) 表示将最后一维重新划分为两部分,其中 -1 表示自动推断大小,2 表示最后一维的大小固定为 2。
  • .unbind(-1):
    • unbind(dim) 是 PyTorch 的一个操作,用于沿指定维度将张量分解为多个张量。
    • -1 表示沿最后一维分解。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def apply_rot_emb(
xq: torch.Tensor,
xk: torch.Tensor,
freqs_cos: torch.Tensor,
freqs_sin: torch.Tensor
) -> Tuple[torch.Tensor, torch.Tensor]:
xq_r, xq_i = xq.float().reshape(xq.shape[:-1] + (-1, 2)).unbind(-1) # 最后一维拆两半
xk_r, xk_i = xk.float().reshape(xk.shape[:-1] + (-1, 2)).unbind(-1)

freqs_cos = reshape_for_broadcast(freqs_cos, xq_r)
freqs_sin = reshape_for_broadcast(freqs_sin, xq_r)

xq_out_r = xq_r * freqs_cos - xq_i * freqs_sin
xq_out_i = xq_r * freqs_sin + xq_i * freqs_cos
xk_out_r = xk_r * freqs_cos - xk_i * freqs_sin
xk_out_i = xk_r * freqs_sin + xk_i * freqs_cos

xq_out = torch.stack((xq_out_r, xq_out_i), dim=-1).flatten(3) # 将第3维和第4维展平
xk_out = torch.stack((xk_out_r, xk_out_i), dim=-1).flatten(3)

return xq_out.type_as(xq), xk_out.type_as(xk)
repeat_kv

使用分组查询注意力机制,实现多个Q使用相同的k v(也就是将kv简化,减少存储)

在Q与V进行乘积计算注意力权重过程中,两个矩阵需要对齐,因此需要repeat k v

注意,head和kv_head的head_dim是相同的,它们最后一维是相同的。

1
2
3
4
5
6
7
# n_rep = n_heads / n_kv_heads, 一个kv头重复了n_rep遍(对应了n_rep个q)
def repeat_kv(x: torch.Tensor, n_rep: int):
batch_size, seq_len, n_kv_heads, head_dim = x.shape
if n_rep == 1:
return x
x [:, :, :, None, :].expand(batch_size, seq_len, n_kv_heads, n_rep, head_dim).reshape(batch_size, seq_len, n_kv_heads * n_rep, head_dim)
return x
attention

注意这里的mask:

此时得到的score是一个seq*seq的矩阵,第i行第j列是指 第i个token对第j个token的注意力,因此mask的形状这样的:第1行只保留第1列,因此第1个token只能看到自己,第2行保留1、2列,因此第2个token只能看到第1、2个token,以此类推。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
class Attention(nn.Module):
def __init__(self, args: ModelConfig):
super().__init__()
self.n_kv_heads = args.n_heads if args.n_kv_heads is None else args.n_kv_heads
assert self.n_heads % args.n_kv_heads == 0

model_parallel_size = 1
self.n_local_heads = args.n_heads // model_parallel_size
self.n_local_kv_heads = args.n_kv_heads // model_parallel_size
self.n_rep = self.n_local_heads // self.n_local_kv_heads

self.head_dim = args.dim // args.n_heads

self.wq = nn.Linear(args.dim, args.n_heads*self.head_dim, bias=False)
self.wk = nn.Linear(args.dim, args.n_heads*self.head_dim, bias=False)
self.wv = nn.Linear(args.dim, args.n_heads*self.head_dim, bias=False)

self.attn_dropout = nn.Dropout(args.dropout)
self.resid_dropout = nn.Dropout(args.dropout)
self.dropout = args.dropout

self.wo = nn.Linear(args.n_heads*self.head_dim, args.dim, bias=False)

self.flash = hasattr(torch.nn.funtional, 'scaled_dot_product_attention')
if not self.flash :
print("Slow attention")
# mask是什么时候使用的?是在xq@xk之后使用的,此时最后两维都至多为seq,所以这里mask的维度会是...seq*seq
mask = torch.full((1, 1, args.max_seq_len, args.max_seq_len), float("-inf"))
mask = torch.triu(mask, diagonal=1)
self.register_buffer("mask", mask)


def forward(self, x: torch.Tensor, freqs_cos: torch.Tensor, freqs_sin: torch.Tensor):
bsz, seq, _ = x.shape
xq, xk, xv = self.wq(x), self.wk(x), self.wv(x)

xq = xq.view(bsz, seq, self.n_local_heads, self.head_dim)
xk = xk.view(bsz, seq, self.n_local_heads, self.head_dim)
xv = xv.view(bsz, seq, self.n_local_heads, self.head_dim)

xq, xk = apply_rot_emb(xq, xk, freqs_cos, freqs_sin)

xk = repeat_kv(xk, self.n_rep)
xv = repeat_kv(xv, self.n_rep)

xq = torch.transpose(1, 2)
xk = torch.transpose(1, 2)
xv = torch.transpose(1, 2)

if self.flash:
torch.nn.functional.scaled_dot_product_attention(xq, xk, xv, attn_mask=None, dropout_p=self.dropout if self.training else 0.0, is_causal=True)
else:
scores = torch.matmul(xq, xk.transpose(2, 3)) / math.sqrt(self.head_dim)
assert hasattr(self, 'mask')
scores += self.mask[:, :, :seq, :seq]
scores = torch.nn.functional.softmax(scores.float(), dim=-1).type_as(xq)
scores = self.attn_dropout(scores)
output = torch.matmul(scores, xv)

output = output.transpose(1, 2).reshape(bsz, seq, self.head_dim * self.n_local_heads)
output = self.wo(output)
output = self.resid_dropout(output)
return output
MLP

拟合连续函数,注意维度变化(就是过三层线性层+dropout)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MLP(nn.Module):
def __init__(self, dim: int, hidden_dim: int, multiple_of: int, dropout: float):
super().__init__()
if hidden_dim is None:
hidden_dim = 4 * dim
hidden_dim = int(2*hidden_dim/3)
hidden_dim = multiple_of * ((hidden_dim + multiple_of - 1) // multiple_of)
self.w1 = nn.Linear(dim, hidden_dim, bias=False)
self.w2 = nn.Linear(hidden_dim, dim, bias=False)
self.w3 = nn.Linear(dim, hidden_dim, bias=False)

self.dropout = nn.Dropout(dropout)

def forward(self, x):
return self.dropout(self.w2(F.silu(self.w1(x)) * self.w3(x)))

transformer

DecoderLayer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class DecoderLayer(nn.Module):
def __init__(self, layer_id: int, args: ModelConfig):
super(). __init__()
self.n_heads = args.n_heads
self.dim = args.dim
self.head_dim = args.dim // args.n_heads

self.attention_norm = RMSNorm(args.dim, args.eps)
self.ffn_norm = RMSNorm(args.dim, args.eps)
self.attention = Attention(args)
self.feed_forward = MLP(args.dim, args.hidden_dim, args.multiple_of, args.dropout)
self.layer_id = layer_id

def forward(self, x, freqs_cos, freqs_sin):
# 这是错误的!残差连接norm之前的x
# x = self.attention_norm(x)
# h = x + self.attention.forward(x, freqs_cos, freqs_sin)
h = x + self.attention.forward(self.attention_norm(x), freqs_cos, freqs_sin)
out = h + self.feed_forward.forward(self.ffn_norm(h))

return out
generate

循环生成每一个token

  • 取出最大上下文token
  • 前向传播(forward)获取最后时间的logits
  • 根据logits计算索引,temperature缩放logits
    • 贪心:找最大概率对应的token id
    • 前k:将概率小于第k大的赋值为-inf
    • softmax+multinomial得到最终id
  • 合并至输入(x)中重复上述过程,继续预测下一个token id
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
class Transformer(nn.Module):
config_class = ModelConfig
last_loss: Optional[torch.Tensor]
def __init__(self, args: ModelConfig):
super().__init__()
self.args = args
self.vocab_size = args.vocab_size
self.n_layers = args.n_layers

self.tok_embeddings = nn.Embedding(args.vocab_size, args.dim)
self.dropout = nn.Dropout(args.dropout)
self.layers = nn.ModuleList()
for layer_id in range(args.n_layers):
self.layers.append(DecoderLayer(layer_id, args))
self.norm = RMSNorm(args.dim, args.norm_eps)
self.output = nn.Linear(args.dim, args.vocab_size, bias=False)

self.tok_embeddings.weight = self.output.weight # 权重共享,减少参数,输入输出一致表达

freqs_cos, freqs_sin = precompute_freqs_cis(args.dim//args.n_heads, args.max_seq_len)
self.register_buffer("freqs_cos", freqs_cos, persistent=False) # 注册缓冲区,内容属于model一部分,但不会被更新
self.register_buffer("freqs_sin", freqs_sin, persistent=False)

self.apply(self._init_weights)
for pn, p in self.named_parameters():
if pn.endswith('w3.weight') or pn.endswith('wo.weight'):
nn.init.normal_(p, mean=0.0, std=0.02/math.sqrt(2*self.n_layers))

self.last_loss = None
self.OUT = CausalLMOutputWithPast()
self._no_split_modules = [name for name, _ in self.named_modules()]

def _init_weights(self, module):
if isinstance(module, nn.Linear):
nn.init.normal_(module.weight, mean=0.0, std=0.02)
if module.bias is not None:
nn.init.zeros_(module.bias)
elif isinstance(module, nn.Embedding):
nn.init.normal_(module.weight, mean=0.0, std=0.02)

def forward(self, tokens: torch.Tensor, targets: Optional[torch.Tensor] = None, **keyargs) -> torch.Tensor:
if 'input_ids' in keyargs:
tokens = keyargs['input_ids']
if 'attention_mask' in keyargs:
targets = keyargs['attention_mask']

bsz, seq = tokens.shape
h = self.tok_embeddings(tokens)
h = self.dropout(h)

freqs_cos = self.freqs_cos[:seq]
freqs_sin = self.freqs_sin[:seq]

for layer in self.layers:
h = layer(h, freqs_cos, freqs_sin)
h = self.norm(h)

if targets is not None:
logits = self.output(h)
# logits:bsz, seq, vocab 将前两维展开
# targets:bsz, seq 将两维度展开
# 忽略padding的0
self.last_loss = F.cross_entropy(logits.view(-1, logits.size(-1)), targets.view(-1), ignore_index=0, reduction='none')
else:
logits = self.output(h[:, [-1], :]) # 只对最后一个seq进行前向传播
self.last_loss = None

self.OUT.__setitem__('logits', logits) # (batch_size, 1, vocab_size)
self.OUT.__setitem__('last_loss', self.last_loss)
return self.OUT


@torch.inference_mode()
def generate(self, idx, stop_id=None, max_new_tokens=256, temperature=1.0, top_k=None):
index = idx.shape[1]
for _ in range(max_new_tokens):
# 从倒数第max_seq_len个元素开始,到最后一个元素(后面max个)
# idx: (batch_size, seq_len)
idx_cond = idx if idx.shape[1] <= self.args.max_seq_len else idx[:, -self.args.max_seq_len:]

logits = self(idx_cond).logits
logits = logits[:, -1, :] # (batch_size, vocab_size)只去最后一个seq(也就是最后一个token),并去除该维度

if temperature == 0.0:
_, idx_next = torch.topk(logits, k=1, dim=-1)
else:
logits = logits / temperature # 放大差异
if top_k is not None:
v, _ = torch.topk(logits, min(top_k, logits.shape[-1]))
# [:, -1]:取每个样本的倒数第 1 个元素。
# [:, [-1]]:取每个样本的倒数第 1 个元素,但保持原来的二维形状。
# 这里是为了广播,匹配logits
logits[logits < v[:,[-1]]] = -float('Inf')
probs = F.softmax(logits, -1)
idx_next = torch.multinomial(probs, num_samples=1)

if idx_next == stop_id:
break

idx = torch.cat((idx, idx_next), dim=-1)

return idx[:, index:] # 返回生成的token

tokenizer

将文本分割成较小单位

  1. word-base:根据空格和标点进行分割
  2. character-base:根据字符分割,token序列太长、丢失词级别语义
  3. subword:比单词小,但比字符大
    • BPE:在词汇表中 迭代地 合并 最频繁出现的 相邻字符对,作为一个字词
    • wordpiece:合并能最大化训练数据总概率的词对
    • unigram:将分词任务视为一个在给定词汇表下的概率问题
数据预处理

根据原始结构 获取 现有结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import json
from tqdm import tqdm

def split_text(text, chunk_size=512):
return [text[i:i+chunk_size] for i in range(0, len(text), chunk_size)]

inputfile = "data/mobvoi.jsonl"

with open("data/seq_monkey_datawhale.jsonl", 'a', encoding='utf-8') as pretrain:
with open(inputfile, 'r', encoding='utf-8') as f:
data = f.readlines()
for line in tqdm(data, desc=f"processing lines in {inputfile}", leave=False):
line = json.loads(line)
text = line['text']
chunks = split_text(text)
for chunk in chunks:
pretrain.write(json.dumps({'text': chunk}, ensure_ascii=False) + '\n')

def convert_message(data):
message = [
{"role": "system", "content": "你是一个AI助手"}
]

for item in data:
if item['from'] == 'human':
message.append({'role': 'user', 'content': item['value']})
elif item['from'] == 'assistant':
message.append({'role': 'assistant', 'content': item['value']})
return message

with open("data/BelleGroup_sft.jsonl", 'a', encoding='utf-8') as sft:
with open("data/BelleGroup.jsonl", 'r', encoding='utf-8') as f:
lines = f.readlines()
for line in tqdm(lines, desc="processing lines in data/BelleGroup.jsonl", unit="lines"):
data = json.loads(line)
m = convert_message(data['conversations'])
sft.write(json.dumps(m, ensure_ascii=False) + '\n')
训练与测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
import random
import json
import os

from transformers import AutoTokenizer, PreTrainedTokenizerFast
from tokenizers import (
decoders,
models,
pre_tokenizers,
trainers,
Tokenizer,
)

from tokenizers.normalizers import NFKC
from typing import Generator
from datasets import load_dataset

# 获取生成器
def read_texts_from_jsonl(file_path: str) -> Generator[str, None, None]:
with open(file_path, 'r', encoding='utf-8') as f:
for line_num, line in enumerate(f, 1): # 指定从1开始
try:
data = json.loads(line)
if 'text' not in data:
raise KeyError(f"Missing 'text' field in line {line_num}")
yield data['text']
except json.JSONDecodeError:
print(f"Error decoding JSON in line {line_num}")
continue
except KeyError as e:
print(e)
continue

def create_tokenizer_config(save_dir: str) -> None:
config={
"add_bos_token": False,
"add_eos_token": False,
"add_prefix_space": False,
"bos_token": "|im_start|",
"eos_token": "|im_end|",
"pad_token": "|im_end|",
"unk_token": "<unk>",
"model_max_length": 1000000000000000019884624838656,
"clean_up_tokenization_spaces": False,
"tokenizer_class": "PreTrainedTokenizerFast",
"chat_template": (
"{% for message in messages %}"
"{% if message['role'] == 'system' %}"
"<|im_start|>system\n{{ message['content'] }}<|im_end|>\n"
"{% elif message['role'] == 'user' %}"
"<|im_start|>user\n{{message['content']}}<|im_end|>\n"
"{% elif message['role'] == 'assistant' %}"
"<|im_start|>assistant\n{{message['content']}}<|im_end|>\n"
"{% endif %}"
"{% endfor %}"
"{% if add_generation_prompt %}"
"{{'<|im_start|>assistant\n'}}"
"{% endif %}"
)
}
with open(os.path.join(save_dir, 'tokenizer_config.json'), 'w', encoding='utf-8') as f:
json.dump(config, f, ensure_ascii=False, indent=4)

special_tokens_map = {
"bos_token": "<|im_start|>",
"eos_token": "<|im_end|>",
"unk_token": "<|unk|>",
"pad_token": "<|im_end|>",
"additional_special_tokens": ["<s>", "</s>"]
}
with open(os.path.join(save_dir, "special_tokens_map"), 'w', encoding='utf-8'):
json.dumps(special_tokens_map, ensure_ascii=False, indent=4)

def train_tokenizer(data_path: str, save_dir: str, vocab_size: int = 8192) -> None:
os.makedirs(save_dir, exist_ok=True)

tokenizer = Tokenizer(models.BPE(unk_token='<unk>'))
tokenizer.normalizer = NFKC() # 正则化器
tokenizer.decoder = decoders.ByteLevel() # 解码器

special_tokens = [
"<unk>",
"<s>",
"</s>",
"<im_start>",
"<im_end>"
]

trainer = trainers.BpeTrainer(
vocab_size=vocab_size,
min_frequency=2,
show_progress=True,
special_tokens=special_tokens,
initial_alphabet=pre_tokenizers.ByteLevel.alphabet() # 初始化字母表
)

print(f"Training tokenizer with data from {data_path}")
texts = read_texts_from_jsonl(data_path) # 获得一个生成器
tokenizer.train_from_iterator(texts, trainer, length=os.path.getsize(data_path))

try:
assert tokenizer.token_to_id("<unk>") == 0
assert tokenizer.token_to_id("<s>") == 1
assert tokenizer.token_to_id("</s>") == 2
assert tokenizer.token_to_id("<im_start>") == 3
assert tokenizer.token_to_id("<im_end>") == 4
except AssertionError as e:
print("Special tokens mapping error: ", e)
raise
tokenizer.save(os.path.join(save_dir, 'tokenizer.json'))

create_tokenizer_config(save_dir)
print(f"Tokenizer saved to {save_dir}")

def eval_tokenizer(tokenizer_path: str) -> None:
try:
tokenizer = AutoTokenizer.from_pretrained(tokenizer_path)
except Exception as e:
print("load tokneizer failed.", e)
return

print("\n=== Tokenizer基本信息 ===")
print(f"vocab_size: {len(tokenizer)}" )
print(f"special tokens: {tokenizer.all_special_tokens}")
print(f"special tokens id: {tokenizer.all_special_ids}")

message = [
{"role": "system", "content": "你是一个AI助手。"},
{"role": "user", "content": "how are you?"},
{"role": "assistant", "content": "I'm fine, thank you. And you?"},
{"role": "user", "content": "I'm good."},
{"role": "assistant", "content": "That's great to hear."},
]

print("\n=== 聊天模板测试 ===") # 测试jinjia模板设置的正确性
prompt = tokenizer.apply_chat_template(
message,
tokenize=False,
add_generation_prompt=True
)

print("generate prompt:\n ", prompt, sep="")

# 测试编码解码
print("\n=== 编码解码测试 ===")
encoded = tokenizer(prompt, truncation=True, max_length=256)
decoded = tokenizer.decode(encoded['input_ids'], skip_special_tokens=False)
print("Decoded text matches original:", decoded == prompt)

# 测试特殊token处理
print("\n=== 特殊token处理 ===")
test_text = "<|im_start|>user\nHello<|im_end|>"
encoded = tokenizer(test_text).input_ids
decoded = tokenizer.decode(encoded)
print(f"original: {test_text}")
print(f"decoded: {decoded}")
print("special tokens preserved: ", decoded == test_text)



train_tokenizer(
data_path='data/seq_monkey_datawhale.jsonl',
save_dir='data/',
vocab_size=6144
)
print("\nend\n")
eval_tokenizer('data/')

LLM预训练

预训练数据集
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class PretrainDataset(Dataset):
def __init__(self, data_path: str, tokenizer, max_len=512):
super().__init__()
self.data_path = data_path
self.tokenizer = tokenizer
self.max_len = max_len
self.padding = 0

with open(data_path, 'r', encoding='utf-8') as f:
self.data = f.readlines()

def __len__(self):
return len(self.data)

def __getitem__(self, index):
sample = json.loads(self.data[index])
text = f"{self.tokenizer.bos_token}{sample['text']}"
input_ids = self.tokenizer(text).data['input_ids'][:self.max_len]
text_len = self.len(text)
padding_len = self.max_len - text_len
input_ids = input_ids + [self.padding]*padding_len

X = np.array(input_ids[:-1]).astype(np.int64) # 除去最后一个元素
Y = np.array(input_ids[1:]).astype(np.int64) # 除去第一个元素

mask = [1] * text_len + [0]*padding_len
mask = np.array(mask[1:]).astype(np.int64) # 对齐Y,计算该部分的loss

return torch.from_numpy(X), torch.from_numpy(Y), torch.from_numpy(mask)
SFTDataset

多轮对话数据集。输入上一轮对话内容,输出当前轮对话内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
class SFTDataset(Dataset):
def __init__(self, data_path, tokenizer, max_len=512):
super().__init__()
self.data_path = data_path
self.tokenizer = tokenizer
self.max_len = max_len
self.padding = 0

with open(data_path, 'r', encoding='utf-8') as f:
self.data = f.readlines()

def __len__(self):
return len(self.data)

# 对assistent的conten进行mask
def generate_loss_mask(self, input_ids):
n = len(input_ids)
mask = [0] * n
a_sequence = [3, 1074, 537, 500, 203] # <|im_start|>assistant\n
a_length = len(a_sequence)
i = 0

while i+a_length <= n:
match = True
for idx in range(a_length):
if a_sequence[idx] != input_ids[i + idx]:
match = False
break
if match == True:
j = None
for idx in range(i+a_length, n):
if input_ids[idx] == 4:
j = idx
break

if j is not None:
start = i + a_length
end = j
if start < end:
for pos in range(start, end+1):
if pos < len(mask):
mask[pos] = 1
i += a_length
else:
i += 1
return mask

def __getitem__(self, index):
sample = json.loads(self.data[index])
text = self.tokenizer.apply_chat_template(sample, tokenize=False, add_generation_prompt=False)
index_ids = self.tokenizer(text).data['index_ids'][:self.max_len]
n = len(index_ids)
n_padding = self.max_len - n
index_ids = index_ids + [self.padding]*n_padding

mask = self.generate_loss_mask(index_ids)

X = np.array(index_ids[:-1]).astype(np.int64)
Y = np.array(index_ids[1:]).astype(np.int64)
mask = np.array(mask[1:]).astype(np.int64)

return torch.from_numpy(X), torch.from_numpy(Y), torch.from_numpy(mask)
预训练
  1. parse args

  2. init:包括model创建,tokenizer加载,多GPU设置,to device等

  3. dataset、dataloader、scaler、optimizer

    scaler:GradScaler用于动态调整loss的缩放因子

    由于混合精度,使用float16 时可能存不下太小的loss,导致梯度变成0,训练停滞

    因此先使用scaler将loss放大,梯度放大至float16能够存下,然后计算高梯度,最后使用scaler将梯度恢复

    6285497E39755975179759E06869F4C3

  4. train_epoch

    • 学习率计算

      三个阶段:预热、余弦退火、保持最小

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      def lr(it, all):
      warmup_iters = args.warmup_iters
      lr_decay_iters = all
      min_lr = args.learning_rate / 10

      if it < warmup_iters:
      return args.learning_rate * it / warmup_iters

      if it > warmup_iters:
      return min_lr

      decay_ratio = (it - warmup_iters) / (lr_decay_iters - warmup_iters)
      assert 0 <= decay_ratio <=1
      coeff = 0.5*(1.0 + math.cos(math.pi * decay_ratio))
      return min_lr + (args.learning_rate - min_lr) * coeff
    • 获取loss,scaler→optimizer(每accumulation_steps更新一次)→梯度清零 (ctx范围内的计算操作会按照底层指定的精度进行)

    • 日志、状态保存

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      def train_epoch(epoch):
      start_time = time.time()

      for step, (X, Y, loss_mask) in enumerate(train_loader):
      X = X.to(args.device)
      Y = Y.to(args.device)
      loss_mask = loss_mask.to(args.device)

      lr = get_lr(epoch * iter_per_epoch + step, args.epochs * iter_per_epoch)
      for param_group in optimizer.param_groups:
      param_group['lr'] = lr

      with ctx:
      out = model(X, Y)
      loss = out.last_loss / args.accumulation_steps
      loss_mask = loss_mask.view(-1)
      loss = torch.sum(loss_mask * loss) / loss_mask.sum()

      scaler.scale(loss).backward() # 将loss放大,计算放大后的梯度

      if (step+1) % args.accumulation_steps == 0: # 每accumulation_steps更新一次model,模拟大batch
      scaler.unscale_(optimizer) # 恢复放大的梯度
      torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip) # 梯度裁剪,防止梯度爆炸

      scaler.step(optimizer) # 优化器步进,参数更新
      scaler.update() # 更新缩放因子

      optimizer.zero_grad(set_to_none=True) # 清空梯度

      if step % args.log_interval == 0:
      spend_time = time.time() - start_time
      Logger(
      f'Epoch:[{epoch+1}]/[{args.epochs}]({step}/{args.iter_per_epoch}) loss: {loss.item()*args.accumulation_steps:.3f} lr: {optimizer.param_groups[-1]['lr']} epoch_time: {spend_time / (step+1)*iter_per_epoch // 60 - spend_time // 60} min;'
      )
      if args.use_swanlab:
      swanlab.log({
      "loss" : loss.item() * args.accumulation_steps,
      "lr" : optimizer.param_group[-1]['lr']
      })

      # 每save_interval步保存一次model
      if (step+1) % args.save_interval == 0:
      model.eval()
      ckp = f'{args.save_dir}/pretrain_{lm_config.dim}_{lm_config.n_layers}_{lm_config.vocab_size}.path'

      state_dict = model.module.state_dict() if isinstance(model, torch.nn.DataParallel) else model.state_dict()
      torch.save(state_dict, ckp)
      model.train()

      # 每20000步保存一个带步数的检查点文件名
      if (step+1) % 20000 == 0:
      model.eval()
      ckp = f"{args.save_dir}/pretrain_{lm_config.dim}_{lm_config.n_layers}_{lm_config.batch_size}_step{step+1}.pth"
      state_dict = model.module.state_dict() if isinstance(model, torch.nn.DataParallel) else model.state_dict()
      torch.save(state_dict, ckp)
      model.train()
SFT训练

与预训练的不同之处在于,dataset使用SFTDataset、初始化时加载预训练model(权重)

  • 训练时:只计算assistent content的loss(mask矩阵),对照XY相同位置的每一token计算loss,所以train时直接传入X、Y即可,也不需要将assistent content去掉

  • 等后面model进行推理时,会直接根据现有token对下一token进行预测,然后拼接、再推理预测

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    def train_epoch(epoch):
    """训练一个epoch"""
    start_time = time.time()
    for step, (X, Y, loss_mask) in enumerate(train_loader):
    X = X.to(args.device)
    Y = Y.to(args.device)
    loss_mask = loss_mask.to(args.device)

    # 获取学习率并更新优化器
    lr = get_lr(epoch * iter_per_epoch + step, args.epochs * iter_per_epoch)
    for param_group in optimizer.param_groups:
    param_group['lr'] = lr

    # 前向传播
    with ctx:
    out = model(X, Y)
    loss = out.last_loss / args.accumulation_steps
    loss_mask = loss_mask.view(-1)
    loss = torch.sum(loss * loss_mask) / loss_mask.sum()

    # 反向传播
    scaler.scale(loss).backward()

    # 更新权重
    if (step + 1) % args.accumulation_steps == 0:
    scaler.unscale_(optimizer)
    torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip)

    scaler.step(optimizer)
    scaler.update()

    optimizer.zero_grad(set_to_none=True)

    # 打印日志
    if step % args.log_interval == 0:
    spend_time = time.time() - start_time
    Logger(
    'Epoch:[{}/{}]({}/{}) loss:{:.3f} lr:{:.7f} epoch_Time:{}min:'.format(
    epoch + 1,
    args.epochs,
    step,
    iter_per_epoch,
    loss.item() * args.accumulation_steps,
    optimizer.param_groups[-1]['lr'],
    spend_time / (step + 1) * iter_per_epoch // 60 - spend_time // 60))
    if args.use_swanlab:
    swanlab.log({
    "loss": loss.item() * args.accumulation_steps,
    "lr": optimizer.param_groups[-1]['lr']
    })

    # 保存模型
    if (step + 1) % args.save_interval == 0:
    model.eval()
    ckp = f'{args.save_dir}/sft_dim{lm_config.dim}_layers{lm_config.n_layers}_vocab_size{lm_config.vocab_size}.pth'

    # 处理多卡保存
    state_dict = model.module.state_dict() if isinstance(model, torch.nn.DataParallel) else model.state_dict()
    torch.save(state_dict, ckp)
    model.train()

    # 定期保存模型
    if (step + 1) % 20000 == 0:
    model.eval()
    ckp = f'{args.save_dir}/sft_dim{lm_config.dim}_layers{lm_config.n_layers}_vocab_size{lm_config.vocab_size}_step{step+1}.pth'

    state_dict = model.module.state_dict() if isinstance(model, torch.nn.DataParallel) else model.state_dict()
    torch.save(state_dict, ckp)
    model.train()

model的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
from contextlib import nullcontext
import torch
from transformers import AutoTokenizer

from LLM import ModelConfig, Transformer

class TextGenerator():
def __init__(
self,
tokenizer_dir='./tokenizer_k/',
check_point='./pretrain_1024_18_6144.pth',
seed=42,
device=None,
dtype="bfloat16",
):
self.tokenizer_dir = tokenizer_dir
self.checkpoint=check_point
self.seed = seed
self.device = device or ('cuda:0' if torch.cuda.is_available() else 'cpu')
self.dtype = dtype
self.device_type = 'cuda' if 'cuda' in self.device else 'cpu'

torch.manual_seed(seed)
torch.cuda.manual_seed(seed)
torch.backends.cuda.matmul.allow_tf32 = True
torch.backends.cudnn.allow_tf32 = True

# 将dtype转换成对象
ptdtype = {'float32': torch.float32, 'float16': torch.float16, 'bfloat16':torch.bfloat16}[self.dtype]

self.ctx = nullcontext() if self.device_type == 'cpu' else torch.amp.autocast(device_type=self.device_type, dtype=ptdtype)

self.model = Transformer(ModelConfig(dim=1024, n_layers=18))
checkpoint_dict = torch.load(self.checkpoint, map_location=self.device)
unwant_prefix = '_orig_mod.'
for k, v in list(checkpoint_dict.items()):
if k.startswith(unwant_prefix):
checkpoint_dict[k[len(unwant_prefix):]] = checkpoint_dict.pop(k)
self.model.load_state_dict(checkpoint_dict,strict=False)

num_params = sum(p.numel() for p in self.model.parameters() if p.requires_grad)
print(f"Model has {num_params/1e6:.3f}M parameters.")

self.model.eval()
self.model.to(self.device)
self.tokenizer = AutoTokenizer.from_pretrained(self.tokenizer_dir)

def pretrain_sample(
self,
start='hello',
num_samples=3,
max_new_tokens=256,
temperature=0.7,
top_k=300
):
if start.startswith('File:'):
with open(start[5:], 'r', encoding='utf-8') as f:
start = f.read()
start_ids = self.tokenizer(start).data['input_ids']
x = (torch.tensor(start_ids, dtype=torch.long, device=self.device)[None, ...])
generated_texts=[]
with torch.no_grad():
with self.ctx:
for k in range(num_samples):
y = self.model.generate(x, max_new_tokens=max_new_tokens, temperature=temperature, top_k=top_k)
generated_texts.append(self.tokenizer.decode(y[0].tolist()))
return generated_texts


if __name__ == '__main__':
generator = TextGenerator()

pretrain_prompts=[
'<|im_start|>北京大学是',
'<|im_start|>清华大学的计算机科学与技术学院'
]
for i in range(len(pretrain_prompts)):
generated_texts=generator.pretrain_sample(start=pretrain_prompts[i], num_samples=1, max_new_tokens=120, temperature=0.75)
print(f"\n{pretrain_prompts[i]} {generated_texts[0]}\n")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained('./', trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained('./',dtype=torch.float32, trust_remote_code=True).eval()
message = [
{'role': 'system', 'content': '你是一个AI助手,你的名字叫小明。'}
]

while True:
content = input("user:")
message.append({'role': 'user', 'content': content})

index_ids = tokenizer.apply_chat_template(message, tokenize=False, add_generation_prompts=True)
index_ids = tokenizer(index_ids).data['input_ids']
x = (torch.tensor(index_ids, dtype=torch.long)[None, ...]).to(model.device)
with torch.no_grad():
y = model.generate(x, stop_id=tokenizer.eos_token_id, max_new_tokens=512, temperature=0.6)
response = tokenizer.decode(y[0].tolist())
print(f"assistent: ==={response}===\n")

message.append({'role': 'assistent', 'content': response})
  • Title: happy-llm 阅读笔记
  • Author: dawn_r1sing
  • Created at : 2025-10-24 19:39:15
  • Updated at : 2025-10-24 19:40:07
  • Link: https://dawnrisingdong.github.io/2025/10/24/happy-llm-阅读笔记/
  • License: This work is licensed under CC BY-NC-SA 4.0.