Post

大模型知识点

大模型知识点

通信原语

一、大模型的基础知识

  • 最初的输入(token 序列)是 input_ids,形状通常为 [batch_size, seq_len],经过Embedding层后,通常变为 [batch_size, seq_len, hidden_dim]
    • 每个元素是一个词或子词的 ID(介于 [0, vocab_size)
  • Q 的形状为 [batch_size, seq_len, hidden_dim]
  • 大词表模型最后有个线性层(W^out 大小 [hidden_dim, vocab_size]),把每个时间步的 hidden state 投影到词表维度,最后flatten成[batch_size∗seql​en,vocab_size]。分类网络最后一层一般会选用softmax 和 cross entropy(s-p) 来计算损失

1.1 add、norm以及dropout

Transformer中Add&Norm层的理解

Add操作:

残差连接就是把网络的输入和输出相加,即网络的输出为F(x)+x,在网络结构比较深的时候,网络梯度反向传播更新参数时,容易造成梯度消失的问题,但是如果每层的输出都加上一个x的时候,就变成了F(x)+x,对x求导结果为1,有效解决了梯度消失问题。

Norm操作:

输入的词向量的形状是(x,y,z),x对应batch,y对应句子长度,z对应 词向量维度,Norm分有BN,LN,IN,具体可以看上方博客

Dropout:
  • Dropout在前向传播时,Dropout 会以某个概率 p 对输入随机置零,减少过拟合
  • 缩放(Scaling)将未被归零的部分除以 (1−p)
  • 在测试/推理阶段,神经网络通常关掉 Dropout(或改用推断模式的缩放),以保证所有神经元全部激活。

dropout

1.2 梯度导致的数据维度变化

overall

matric4

  • y是标量,x是向量 matric1

  • y是向量,x是标量 matric2

  • 内积求导(注意:向量对向量求导结果是一个矩阵) neiji matric3

  • 样例(加粗代表向量)

$y=x^TA$,求导为$A^T$

正向传播和反向传播数据依赖:因为需要存储正向的所有中间结果

direction

1.3 回归和分类的区别

回归是一个单连续值的输出,和真实值的区别作为loss

分类是多个输出,代表预测为第i类的置信度


下面几个内容为了学了deepseek的思想

1.4 RoPE

位置编码和相对位置有关,绝对位置不是那么重要 。RoPE位置编码的要点:

  1. 先做多Head的投影,再加上旋转位置编码
  2. 用的仍然是正弦和余弦函数操作,当两个向量计算内积时,直接转换成了包含相对位置信息。
  3. 只对Q,K做旋转位置编码,V不变
  4. d为embedding的维度,m为绝对位置

RoPE

1.5 MLA(Multi-head Latent Attention)

[1.1有MHA的计算形式],对于每个token,KVCache的缓存占用为2$n_h * d_h * l(n_h为头的数量,d_h为每个头的维度),l为layer的数量$

MLA

实际上不存在两个上采样矩阵,可以直接从$c_t^{KV}$开始计算,WUK可以融入WQ,WUV可以融入WO,例如在算attention的时候,$q^Tk=h_q(W_1W_2)h_k$,训练的时候会直接得到括号里的内容

这种计算方式,对RoPE旋转位置编码是有影响的。也就是不能直接在压缩后的向量上应用RoPE(只对QK进行变换),那么可不可以在解压后的向量上应用RoPE呢?可以,但是影响效率,因为前面已经说过不显示计算解压后的向量,而是直接应用压缩后的向量。如何解决呢?再造一个向量,单独应用RoPE

RoPE_in_MLA MLA_dimm

1.6 MoE

DeepSeekMoE

moe_archi

下面是计算路由的函数,sigmoid

example router MoE less_loss

route的行为是对输入进行一个linear,把linear的结果进行分组(取决于要激活的专家个数),在组内挑选得分最高的几个,求和代表为该组的得分,最后选出对应的组的index,最后再从组index中找出对应专家的index和专家权重

1.7 NSA(Native Sparse Attention)

推训一致,就是不需要和之前的所有词计算相关性,而只需要和其中某些最重要的词计算相关性。解决的是稀疏attention的问题

想象一下我们在阅读一个很长的文章的时候是如何做的?

  1. 先从头到尾大概浏览一下,大概知道哪些段落是我感兴趣的
  2. 逐字的精度我之前“标记”过的重点段落
  3. 对于我特别感兴趣的字句,再分别向前后扩展连续的语句,看看有没有更重要的内容。
    通俗的解释,NSA就是利用了上面的三步的思想,解决了Sparse Attention的问题。

NSA_archi 讲解13:24 NSA2

需要注意的是,压缩和sliding可以直接应用到flash attention中,但是中间的选择模块,每一个q会对应到不同的kv,需要用GQA,将选择到同一个KV的Q给group起来

1.7 Flash attentionsee_here

kvcache

flash_attention

d为QKV_gen中的权重维度,Br和Bc的取值根据SRAM的大小,存储qkvo4份,计算如下

flash_attention1

在GPU中,QKV存在HBM中,在计算softmax的时候,会分块加载到SRAM中。比如Q不断更新,K暂时不变,Q全部轮询完了之后,加载下一块K,继续更新Q直到S全部计算完毕

在这样的过程中,【 load:QKV,S ,P,V】 【store:S,P,O】 ,下面是没有经过flash attention优化的计算方式,需要三次读+写

flash_attention3

flash_attention2

flash_attention4

但是flash attention可以让我们一次读+写,不需要再将PV存储到HBM

,load QKV,在softmax阶段,存储一个全局的m和l,再修正S中的最大local值带来的误差,写回HBM的就是

flash_attention5

外循环是KV层面,内循环是Q层面,循环完一个Q就刷完了O,循环KV的结果是不断刷新O(原基础上叠加每一行)

在SRAM上计算的变量如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
K, V: Bc*d 
Q, O: Br*d ≤ d*d 
S, P: Br*Bc ≤ d*Bc 
l, m: Br ≤ d 
注意,整个M的容量约是4*Bc*d  
内循环是对O进行从上往下刷新,而外循环KV是重复刷新整个O,因此内循环load Q之后的计算,都会更新l,m,O

 step1: 导入KV (+2*Bc*d) 
 step2: 导入Q, O, l, m (+2*d^2+2d)   
 step3: 计算S (+Bc*d) (共3*Bc*d+2*d(d+1)) 
 step4: 释放QK (-2*Bc*d) (Bc*d+2*d(d+1))  
 step5: 用S计算m~ (+d),用S和m~计算P~ (+Bc*d)  
 step6: 释放S(-Bc*d)  
 step7: 利用P~计算l~ (+d)  
 step8: line11的m和l更新 (+2*d)  
 step9: 利用l^new, m^new, l, m, O, P~, V原地更新O并储存O (-d^2)  
 step10: 储存l_new, m^new (-2*d)  
  step11: 释放其他变量

1.8 ring attention

ringattention

1.9 前向传播和反向传播

反向传播

只有在分类任务里面,softmax和Loss是紧挨着的时候才会有这种巧妙的情况,像现在的transformer是用不上的,因为后面还有很多FFN

1.10 Boundry

compute time:受throughput限制,参数指标FLOPS = Floating point operations /s 每秒可以执行多少次浮点运算,例如4090可以达到82.58 TFLOPS

memory access latency:受bandwidth限制,但4090只能达到1.01TB/s ,加载数据的速度远远低于实际执行计算的速度

Computational Intensity: TFLOPS/B ,每次访问内存时要执行的浮点运算次数,描述了一个Roofline Model

roof_model

1.11 分布式下的并行

动画理解DP,DDP,DeepSpeed ZeRO

  1. DP:数据拆分,模型复制,梯度聚合,权重更新后广播(单进程,多线程,只能利用一个CPU,单个进程读取mini-batch,切分n份给GPU)
  2. DDP:数据由不同的CPU以不同进程读取,分bucket实现梯度反向传播的边计算边传输,最后利用GPU之间的ring-allreduce实现同步(多线程读取多个mini-batch,在本地GPU进行计算)
  3. 模型太大,显存放不下–>DeepSpeed ZeRO-1(Zero Redundancy Optimizer):每个GPU存储全部的FP16的网络参数和梯度,但是优化器的所有参数(占比很大)按网络层数拆分到每个GPU。这样在边反向传播的时候可以边同步属于自己的梯度,更新各自的网络参数,最后广播给其他GPU实现全局的权重更新。 【DeepSpeed ZeRO-1将FP16梯度也拆分了,DeepSpeed ZeRO-3 进一步将FP16的梯度以及网络参数也拆分】—— DeepSpeed ZeRO-3由于缺少部分FP16模型参数,在前向和后向传播的时候,需要拥有该参数的GPU进行广播,可以overlap广播和计算的时间。

举个例子,存在GPU012的时候,前向传播的模型参数需要靠拥有该参数的GPU0广播给1,2。而反向传播时,GPU0,1计算完本地的FP16梯度,被GPU2 all-gather到本地,实现梯度聚合
ZeRO1和2的进程总传入/传出为2X(X为模型参数量),而ZeRO3为3X,但是降低了更多的显存占用

在分布式场景下,性能的考虑因素:计算b个样本梯度的时间$t_1$,传输m个参数或者梯度的传输时间$t_2$,每个batch的耗时为max(t1,t2),增大batchsize(通过增大b和GPU的数量n)会使得收敛变慢(可以调小学习率),但是还是要更多的epoch,导致训练时间增大(总结就是,我用了更多的计算单元,我训练单次时间减少了,但是为了收敛到更好,我需要更多的训练次数,我的训练时间还是没有降低)


张量并行(层内并行,包含所有层,但是每一层只含一部分的参数)

列并行,会切分权重但不切分输入,好处是可以让硬件并行。虽然计算attention不需要allreduce,但是后面的Dropout还是需要allreduce

行并行,输入和权重都会切分

tensor_para tensor_para1

需要两次通讯,第一次是broadcast输入X,第二次是allreduce的g,同步dropout之后的结果

流水线并行(层间并行,切割MLP的层数):只适合于ring

Gpipe 论文论文mini_batch把一个大的batch拆分成多个小的batch,只存储输入X,反向传播需要的中间激活值可以通过X重新进行计算(计算换空间),这样反向传播不需要储存每一层的中间激活值。

需要注意的是,一次额外的前向计算开销占整个系统计算开销的1/3

mini_batch_pp

PipeDream使用非交错式1F1B,没有减少bubble但是节省了峰值显存。

二、Switch Transformers

route_moe

“专家权重”或“可信度”通常由一个 路由器 (Router)门控网络 (Gating Network) 来计算:

每个 token先经过前面的自注意力层与 Add+Norm 等操作后,得到一段向量h。路由器常常只是一个 单层线性变换(有时会加上激活函数),把 h 投影到“专家数”维度上。获得向量z,z 的每个分量对应一个专家(Expert)在“未归一化”下的得分,将 z 做 softmax 得到概率分布$p_i$,对应第i个专家的可信度。

split

在多核(或多GPU/TPU)环境中对大型模型进行并行化的split方案:

第一行模型权重在各核心之间的切分方式第二行输入数据在各核心之间的切分方式同一个颜色 / 同一个大矩形往往表示的是同一个权重矩阵的某一块;

  • Data Parallelism:每个设备(核心)都持有完整的模型参数,但只处理一部分的数据(即 batch 切分)
  • Model and Data Parallelism:不同的核心各自保存不同的一部分参数。大家合起来才构成完整模型。不同颜色一般是用来区分同一个大批次(batch)里被切分开的不同子块。所有颜色合起来才是同一次 forward/backward 的整个 batch ,换句话说,整张图中的所有颜色块加起来才构成一次完整的 batch;在实际训练中,我们往往会在每个训练 step 中取一个大的 batch,然后按照硬件并行策略把它拆分给各核心,各核心并行处理后再在梯度或激活等环节进行必要的通信与汇总。
  • Expert and Data Parallelism:每个小颜色方块可以看作一个**专家(Expert)对应的参数 ** ,但“非专家”部分(如自注意力层、嵌入层等)通常会完整复制,

三、deepseek中的pytorch代码

基本的一些函数

1
2
3
4
5
6
7
8
9
10
11
12
#1. torch.topk 函数用于返回输入张量中指定维度上的前 k 个最大元素及其对应的索引
values, indices = torch.topk(x, k=2, dim=1)
#2. scatter_ :根据indices索引,讲第一个参数的值分散到目标张量的指定位置
torch.zeros([3,3]).scatter_(1, indices, True)
#3. unsqueeze操作,它的主要作用是在指定的维度上插入一个大小为1的新维度,从而改变张量的形状
y = torch.unsqueeze(x, dim=0)
#4. gather可以根据给定的索引从输入张量中收集元素,从而构建一个新的张量。(与scatter互为反操作)
output = torch.gather(input_tensor, dim=1, index=index_tensor)
#5. bincount用于统计非负整数张量中每个值出现的次数。
input_tensor = torch.tensor([1, 1, 2, 2, 10])
output = torch.bincount(input_tensor)
output: tensor([0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 1]) #0-10

ner_datasets[“train”].features 可以查看数据集的特征

This post is licensed under CC BY 4.0 by the author.