极客时间《Linux 性能优化实战》案例 git,以及相应的博客课程
Java 并发编程实战–王宝令
Java 性能调优实战
B站:AI System & AI Infra(zomi酱)
B站李沐老师论文精读
论文阅读
Gpipe:流水线并行,将模型多层均分放在不同的卡上,然后每一批次的数据训练时,再切分成多个mini-batch(一般要为卡的4倍大小),这样就可以提升多卡并行执行的效率(否则,整个过程就一个batch,0卡执行时,其他卡等待,0卡执行完,1卡执行,而其他所有卡都空闲,…)。计算过程中,不保留中间变量(激活函数得到的数值),反向传播更新梯度时,再重新计算一次forward(占整个真实计算的1/3)。这是算力换内存的思想!!!以此获取更多的显存,从而增大mini-batch大小。
Megatron-LM:张量并行(只适用于Transformer模型上)
大模型相关岗位技术要求
大模型网络:Bert、Transformer、GPT、SwitchTransformer
大模型训练框架:DeepSpeed Megatron、TensorRT-LLM
大模型微调方法:常见的几种微调方法
大模型推理计算库:Flash Attention、Paged Attention、FastTransformer、Byte-Transformer
大模型推理平台:TensorRT
大模型分布式训练技术:大模型分布式训练技术,PS(Parameter Server,参数服务器)
MoE技术
GPU知识相关
GPU基础知识
GPU的存储结构类似CPU一样,也可以分层3层,DRAM、HBM、SRAM(内存由高到底,访问速度由低到高)
如上图所示,在A100 GPU有40-80GB的高带宽内存(HBM,它是由多个DRAM堆叠出来的),带宽为1.5-2.0 TB/s,而每108个流处理器(SM)有192KB的SRAM,带宽估计在19TB/s左右。
一方面,从上面的数字可以看出SRAM的访问速率是HBM的10倍左右,然而其能承载的数据量却远远小于HBM。
在GPU的编程架构中,最小的执行单元为warp(即为固定大小的线程束,一般为32个线程为一个warp,它们在同一个时钟周期内并行执行相同的指令),warp是SM中的调度单元,不同的warp在计算时会从SRAM中读取计算所需的数据(即共享存储寄存器)。
它对应的是编程架构中的block,一个SM可以包含多个Block,每个Block可以包含多个线程束Warp,但是Block不能跨SM。
当计算所依赖的数据量较大需要跨SM时访问时,则会从更下级的HBM存储器中拷贝数据进行计算。
它对应的是编程架构中的Grid,其能控制访问HBM显存。
其次,在CUDA编程框架中提供了从HBM拷贝到SRAM的计算原语,我们可以指定部分数据拷贝在SRAM中进行计算。
flashAttention 就是基于GPU内存结构对Transformer进行优化的。
大模型推理并行技术
大模型的并行切分方式对通信和时延的要求:
切分方式 | 通信操作 | 通信量级(单卡) | 时延要求 | 集群部署方式要求 |
---|---|---|---|---|
Tensor并行 | AllReduce | 百GB | 秒级 | 节点内 |
流水线并行 | Send/Recv | MB | 秒级 | 节点间 |
数据并行 | AllReduce | GB | 十秒级 | 节点内/间 |
优化器并行 | Allgather/reduce scatter | 百GB | 秒级 | 节点内 |
专家并行 | All2All | 百GB | 秒级 | 高网络吞吐 |
数据并行和流水线并行对节点间的带宽要求较低;但专家并行(MOE的并行技术EP),需要节点间较高的带宽才可以支持。
大模型LLM推理相关技术
- 投机推理
- KV cache
- Continuous batching
- Paged Attention (PA)
- Flash Attention (FA)
FA相关讲解
大模型技术栈
1、模型设计:将问题转化为对应的大模型,模型框架的选择与设计;
2、训练作业:GPU,NPU,学习率,batch size,初始化,优化器等;
3、学习策略:监督学习,无监督学习,强化学习等;
4、分布式框架;
5、SFT;
6、分布式优化、内存优化、算子加速、集群组网;
7、高性能存储、高可靠集群;
硬件通信方式
机器内通信:共享内存、PCIe(GPU与CPU之间)、NVLink(GPU与GPU之间);
机器间通信:TCP/IP网络、RDMA网络(AI框架常用的)
RDMA:远程内存直连,有以下3个重要特性
- CPU offload:无需CPU干预,远程主机CPU缓存cache不会被访问的内存内存所填充;
- kernel bypass:专有verbs interface,应用程序可以直接在用户态执行数据传输;
- zero copy:每个应用程序都能直接访问集群中的设备的虚拟内存;
软件通信协调
- MPI:定义了多个原语的消息传递接口,这一接口主要被用于多进程间的通信。MPI系统通信方式是建立在点对点通信之上。而集合通讯是建立在端到端通信的基础上,在一组进程内的通讯原语。
- NCCL/HCCL:NVIDIA和华为的
- Gloo
通信实现方式
点对点通信Send/Recv:TCP/IP,RDMA
集合式通信All Reduce:TCP/IP,NCCL
- 一对多:Scatter / Broadcast
Broadcast:某个节点想把自身的数据发送到其他节点,那么就可以使用广播机制。分布式机器学习中常用于网络参数的初始化
Scatter :将主节点的数据进行划分并散布至其他指定的节点 - 多对一:Gather / Reduce
Reduce:一系列简单运算操作的统称,细分可以包括sum、min、max、prod、lor等类型的规约操作;
Gather :将多个Sender上的数据收集到单节点上,可以理解为反向的Scatter - 多对多:All Reduce / All Gather
All Reduce:在所有的节点上都应用同样的reduce操作
All Gather:收集所有数据到所有节点上。发送多个数据到多个节点很有用,即在多对多通信模式的场景。
大模型性能调优工具:profiler
Tensor切分、变形等方法
- Tensor的链接操作
cat
torch.cat(tensors, dim = 0, out = None)
stack
torch.stack(inputs, dim=0)
- Tensor的切分操作
torch.chunk(input, chunks, dim=0)
torch.split(tensor, split_size_or_sections, dim=0)
torch.unbind(input, dim=0)
Pytorch矩阵转秩
在PyTorch中有两个函数,分别是permute()和transpose()可以用来实现矩阵的转秩,或者说交换不同维度的数据。比如在调整卷积层的尺寸、修改channel的顺序、变换全连接层的大小的时候,我们就要用到它们。
>>> x = torch.rand(2,3,5)
>>> x.shape
torch.Size([2, 3, 5])
>>> x = x.permute(2,1,0)
>>> x.shape
torch.Size([5, 3, 2])
>>> x.shape
torch.Size([2, 3, 4])
>>> x = x.transpose(1,0)
>>> x.shape
torch.Size([3, 2, 4])
需要注意的是,经过了transpose或者permute处理之后的数据,变得不再连续了
还是接着刚才的例子说,我们使用torch.rand(2,3,4)得到的tensor,在内存中是连续的,但是经过transpose或者permute之后呢,比如transpose(1,0),内存虽然没有变化,但是我们得到的数据“看上去”是第0和第1维的数据发生了交换,现在的第0维是原来的第1维,所以Tensor都会变得不再连续。
Pytorch形状变换
在PyTorch中有两种常用的改变形状的函数,分别是view和reshape。
>>> x = torch.randn(4, 4)
>>> x.shape
torch.Size([4, 4])
>>> x = x.view(2,8)
>>> x.shape
torch.Size([2, 8])
>>> x = x.permute(1,0)
>>> x.shape
torch.Size([8, 2])
>>> x.view(4, 4)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
RuntimeError: view size is not compatible with input tensor's size and stride (at least one dimension spans across two contiguous subspaces). Use .reshape(...) instead.
结合代码可以看到,利用permute,我们将第0和第1维度的数据进行了变换,得到了[8, 2]形状的Tensor,在这个新Tensor上进行view操作,忽然就报错了,为什么呢?其实就是因为view不能处理内存不连续Tensor的结构。
那这时候要怎么办呢?我们可以使用另一个函数,reshape:
>>> x = x.reshape(4, 4)
>>> x.shape
torch.Size([4, 4])
这样问题就迎刃而解了。其实reshape相当于进行了两步操作,先把Tensor在内存中捋顺了,然后再进行view操作。