赞
踩
“10b”、“13b”、“70b” 等术语通常指的是大型神经网络模型的参数数量。其中的 “b” 代表 “billion”,也就是十亿。表示模型中的参数量,每个参数用来存储模型的权重和偏差等信息。例如:
“10b
” 意味着模型有大约 100 亿
个参数。
“13b” 意味着模型有大约 130 亿个参数。
“70b” 意味着模型有大约 700 亿个参数。
例如:Meta 开发并公开发布的 Llama 2 系列大型语言模型 (LLM),这是一组经过预训练和微调的生成文本模型,参数规模从
70 亿(7b)
到700 亿(70b)
不等。经过微调的 LLMs(称为 Llama-2-Chat)针对对话场景进行了优化。
meta-llama/Llama-2-7b-hf
meta-llama/Llama-2-13b-hf
meta-llama/Llama-2-70b-hf
输入 仅输入文本
输出 仅生成文本
模型架构 Llama 2 是一种使用优化的 Transformer 架构的自回归语言模型。调整后的版本使用监督微调(SFT)和带有人类反馈的强化学习(RLHF)来适应人类对有用性和安全性的偏好。
模型参数的精度通常指的是参数的数据类型,它决定了模型在内存中存储和计算参数时所使用的位数。以下是一些常见的模型参数精度及其含义,以及它们在内存中所占用的字节数:
含义:单精度浮点数用于表示实数,具有较高的精度,适用于大多数深度学习应用。
字节数:4 字节(32 位)
含义:半精度浮点数用于表示实数,但相对于单精度浮点数,它的位数较少,因此精度稍低。然而,它可以在某些情况下显著减少内存占用并加速计算。
字节数:2 字节(16 位)
含义:双精度浮点数提供更高的精度,适用于需要更高数值精度的应用,但会占用更多的内存。
字节数:8 字节(64 位)
含义:整数用于表示离散的数值,可以是有符号或无符号的。在某些情况下,例如分类问题中的标签,可以使用整数数据类型来表示类别。
字节数:通常为 4 字节(32 位)或 8 字节(64 位)
注意: 模型参数精度的选择往往是一种权衡。使用更高精度
的数据类型可以提供更高的数值精度,但会占用更多的内存
并可能导致计算速度变慢
。相反,使用较低精度
的数据类型可以节省内存
并加速计算
,但可能会导致数值精度损失
。在实际应用中,选择模型参数的精度需要根据具体任务、硬件设备和性能要求进行权衡考虑。
实际上,通常情况下并没有标准的整数数据类型为 int4 或 int8,因为这些整数数据类型不太常见,且在大多数计算机体系结构中没有直接支持。在计算机中,整数通常以字节为单位进行存储,所以 int4 表示一个 4 位的整数,int8 表示一个 8 位的整数。
然而,近年来在深度学习领域中,出于模型压缩和加速的考虑,研究人员开始尝试使用较低位数的整数来表示模型参数。例如,一些研究工作中使用的`int4`、`int8`等整数表示法是通过`量化(quantization)技术`来实现的。
在量化技术中,int4 和 int8 分别表示 4 位和 8 位整数。这些整数用于表示模型参数,从而减少模型在存储和计算时所需的内存和计算资源。量化是一种模型压缩技术,通过将浮点数参数映射到较低位数的整数,从而在一定程度上降低了模型的计算和存储成本。以下是对这两种精度的解释以及它们在内存中占用的字节数:
含义:int4 使用 4 位二进制来表示整数。在量化过程中,浮点数参数将被映射到一个有限的范围内的整数,然后使用 4 位来存储这些整数。
字节数:由于一个字节是 8 位,具体占用位数而非字节数,通常使用位操作存储。
含义:int8 使用 8 位二进制来表示整数。在量化过程中,浮点数参数将被映射到一个有限的范围内的整数,然后使用 8 位来存储这些整数。
字节数:1 字节(8 位)
注意: 在量化过程中,模型参数的值被量化为最接近的可表示整数,这可能会导致一些信息损失。因此,在使用量化技术时,需要平衡压缩效果和模型性能之间的权衡,并根据具体任务的需求来选择合适的量化精度。
模型推理(inference)是指在已经训练好的模型上对新的数据进行预测或分类。推理阶段通常比训练阶段要求更低的显存,因为不涉及梯度计算和参数更新等大量计算
轻量化模型设计:在推理阶段,可以考虑使用轻量级模型结构,如移动设备上的 MobileNet、EfficientNet 等,以减少内存占用和计算量。
模型剪枝和量化:通过模型剪枝(移除冗余参数)和量化(降低参数精度)可以显著减少模型的内存占用,同时保持合理的性能。
模型压缩和量化:使用模型压缩技术(如 Knowledge Distillation)可以将复杂模型的知识传递给一个更小的模型,从而在内存占用和性能之间找到平衡点。
Batch Size 设置:在推理阶段,较小的批次大小可以减少内存使用。但要注意,过小的批次大小可能会影响推理性能。
内存复用和延迟加载:在推理过程中,可以考虑使用内存复用技术,即重复使用某些中间计算结果,以减少重复计算和内存开销。另外,使用延迟加载可以在需要时才加载数据,减少内存占用。
总之,在模型训练和推理阶段都有许多策略可以优化内存使用。选择适合您应用场景的策略,可以提高性能、减少资源消耗,并确保您的计算资源得到了最大程度的利用。
以下是计算模型推理时所需显存的一些关键因素:
模型结构: 模型的结构包括层数、每层的神经元数量、卷积核大小等。较深的模型通常需要更多的显存,因为每一层都会产生中间计算结果。
输入数据: 推理时所需的显存与输入数据的尺寸有关。更大尺寸的输入数据会占用更多的显存。
批处理大小 BatchSize: 批处理大小是指一次推理中处理的样本数量。较大的批处理大小可能会增加显存使用,因为需要同时存储多个样本的计算结果。
数据类型 DType: 使用的数据类型(如单精度浮点数、半精度浮点数)也会影响显存需求。较低精度的数据类型通常会减少显存需求。
中间计算: 在模型的推理过程中,可能会产生一些中间计算结果,这些中间结果也会占用一定的显存。
要估算模型推理时所需的显存,可以按照以下步骤:
模型加载: 计算模型中所有参数的大小,包括权重和偏差。
确定输入数据尺寸: 根据模型结构和输入数据大小,计算推理过程中每个中间计算结果的大小。
选择批次大小: 考虑批处理大小和数据类型对显存的影响。
计算显存大小: 将模型参数大小、中间计算结果大小和额外内存需求相加,以得出总显存需求或者使用合适的库或工具计算出推理过程中所需的显存。
通常情况下,现代深度学习框架(如 TensorFlow、PyTorch 等)提供了用于推理的工具和函数,可以帮助您估算和管理模型推理时的显存需求。
因为全精度模型参数是float32
类型, 占用 4 个字节,粗略计算:1b(10亿)个模型参数,约占用4G显存
(实际大小:10^9 * 4 / 1024^3 ~= 3.725 GB),那么 LLaMA 的参数量为 7b,那么加载模型参数需要的显存为:3.725 * 7 ~= 26.075 GB
如果您的显存不足 32GB,那么可以设置半精度的 FP16/BF16 来加载,每个参数只占 2 个字节,所需显存就直接减半,只需要约 13GB。虽然模型效果会因精度损失而略微降低,但一般在可接受范围。
如果您的显存不足 16GB,那么可以采用 int8 量化后,显存再减半,只需要约 6.5GB,但是模型效果会更差一些。
如果您的显存不足 8GB,那么只能采用 int4 量化,显存再减半,只需要约 3.26GB。
完美运行、成功上车
。
注意: 上述是加载模型到显存所需大小,在模型的推理过程中,可能会产生一些中间计算结果,这些中间结果也会占用一定的显存,所以显存大小不能刚好是参数量的大小,不然就会 OOM。
自我实践 1 使用 RTX A6000 显卡 (50GB 显存) 进行 70B 的 int4 量化模型部署,可正常运行。
模型训练(train)是指在给定训练数据集的基础上,通过优化算法调整模型的参数,使其能够更好地适应训练数据,并在未见过的数据上表现出良好的泛化能力。训练阶段通常比推理阶段要求更多的显存,因为涉及梯度计算和参数更新等大量计算。
模型训练涉及到多个关键概念,下面是显存和内存之间的关系以及在训练过程中的作用:
显卡 GPU:GPU 的内存硬件存储介质与 CPU 的类似,主要的区别是设计的结构有所差异。GPU 内存硬件的分类,按照是否在芯片上面可以分为片上 (on chip) 内存和片下 (off chip) 内存,片上内存主要用于缓存 (cache) 以及少量特殊存储单元(如 texture),特点:速度快,存储空间小;片下内存主要用于全局存储 (global memory) 即常说的显存,特点:速度相对慢,存储空间大。GPU 的内部存储分为片上存储和片下存储,指的硬件所在位置,为了满足 GPU 的应用场景,对存储功能进行了细分,包括:局部内存 (local memory)、全局内存 (global memory)、常量内存 (constant memory)、图像 / 纹理 (texture memory)、共享内存 (shared memory)、寄存器 (register)、L1/L2 缓存、常量内存 / 纹理缓存 (constant/texture cache)。
显存(GPU 显存):显存是位于图形处理单元(GPU)上的内存,用于存储模型的参数、权重、中间计算结果以及训练数据的一部分。由于 GPU 在并行计算方面的强大性能,显存通常用于高效地执行模型的前向传播、反向传播和优化算法。显存的大小限制了可以训练的模型大小以及每次批量处理的样本数量。
内存(系统内存):内存是计算机系统中的主要存储区域,用于存储程序代码、数据和运行时状态。在 AI 模型训练过程中,内存主要用于存储模型的源代码、训练数据的加载、预处理以及一些中间计算结果。与显存相比,内存通常具有更大的容量,但速度相对较慢
。
在 AI 模型训练过程中,显存和内存之间的交互是关键的:
数据加载与预处理:训练数据通常存储在内存或者分布式存储中,然后通过批量加载到显存中进行训练。在加载数据时,可能需要进行预处理(如归一化、数据增强等),这些预处理步骤可能会涉及内存和显存之间的数据传输。
前向传播和反向传播:在训练期间,模型的前向传播(计算输出)和反向传播(计算梯度)都涉及显存中的模型参数和权重。这些计算会在 GPU 上高效执行,利用了其并行计算能力。
梯度计算与参数更新:在反向传播过程中,计算得到的梯度用于更新模型的参数和权重。这一过程可能涉及到从显存到内存的数据传输,因为参数更新可能需要在内存中进行。
批量处理和优化算法:大多数训练过程中会使用批量处理(mini-batch)的方式,每个批次的数据都会在显存中加载和处理。优化算法(如梯度下降)的执行通常涉及显存中的参数和梯度计算。
显存和内存在 AI 模型训练中扮演着关键角色,它们之间的高效协同工作有助于加速训练过程并降低资源消耗。同时,合理的显存管理和数据处理策略可以提高训练效率和性能。
在模型训练阶段和推理阶段,优化内存使用都是非常重要的,因为内存是有限资源,合理管理内存可以提高性能和效率。以下是在这两个阶段分别优化内存的一些方法:
模型训练阶段优化内存:
批量处理(Mini-Batch):使用批量处理技术可以有效减少每次迭代中的内存使用。在每个迭代中,只需要加载和处理一个批次的数据,而不是全部数据,这可以显著减少内存需求。
数据预处理和增强:在加载数据之前,对数据进行预处理(如归一化、裁剪等)和数据增强(如随机翻转、旋转等)可以减少需要存储的中间结果,从而降低内存使用。
梯度累积(Gradient Accumulation):在某些情况下,可以将多个小批次的梯度累积起来,然后一次性进行参数更新。这样可以减少每次梯度计算产生的内存消耗。
混合精度训练:使用混合精度训练(例如,使用半精度浮点数)可以减少模型参数和梯度的内存占用,同时保持训练稳定性。这需要硬件和深度学习框架的支持。
模型并行和数据并行:对于大型模型,可以将模型分成多个部分,分别在不同的 GPU 上训练(模型并行),或者将不同批次的数据分布在不同 GPU 上进行处理(数据并行)。这可以减少单个 GPU 上的内存需求。
以下是计算模型推理时所需显存的一些关键因素:
模型权重
模型权重是模型参数中的一部分,通常是指神经网络中连接权重(weights)。这些权重决定了输入特征与网络层之间的连接强度,以及在前向传播过程中特征的传递方式。所以模型
梯度
在训练过程中,计算梯度用于更新模型参数。梯度与模型参数的维度相同。
采用 AdamW 优化器:每个参数占用 8 个字节,需要维护两个状态。意味着优化器所使用的显存量是模型权重的 2 倍;
采用 经过 bitsandbytes 优化的 AdamW 优化器:每个参数占用 2 个字节,相当于权重的一半;
采用 SGD 优化器:占用显存和模型权重一样。
输入数据和标签
训练模型需要将输入数据和相应的标签加载到显存中。这些数据的大小取决于每个批次的样本数量以及每个样本的维度。
中间计算
在前向传播和反向传播过程中,可能需要存储一些中间计算结果,例如激活函数的输出、损失值等。
临时缓冲区
在计算过程中,可能需要一些临时缓冲区来存储临时数据,例如中间梯度计算结果等。减少中间变量也可以节省显存,这就体现出函数式编程语言的优势了。
硬件和依赖库的开销
显卡或其他硬件设备以及使用的深度学习框架在进行计算时也会占用一些显存。
最重要的两个指标:
显存占用
GPU 利用率
显存占用和 GPU 利用率是两个不一样的东西,显卡是由 GPU 计算单元和显存等组成的,显存和 GPU 的关系有点类似于内存和 CPU 的关系。显存可以看成是空间,类似于内存。
显存用于存放模型、数据;
显存越大,所能运行的网络也就越大;
GPU 计算单元类似于 CPU 中的核,用来进行数值计算。衡量 GPU 计算量的单位通常是 FLOPS,即每秒浮点运算次数(Floating Point Operations Per Second):每秒能执行的 flop 数量。FLOPS 值越大,计算能力越强大、速度越快。
神经网络模型占用显存的包括:
模型自身的参数
模型的输出
只有有参数的层,才会占用显存。这部份的显存占用和输入无关,模型加载完成之后就会占用。
有参数的层:
卷积
全连接
BatchNorm
Embedding 层
… …
无参数的层:
多数的激活层 (Sigmoid/ReLU)
池化层
Dropout
… …
模型的参数数目:
Linear(M->N) 全连接参数数目:M×N
Conv2d(Cin, Cout, K) 卷积参数数目:Cin × Cout × K × K
BatchNorm(N) BatchNorm 参数数目:2N
Embedding(N,W) Embedding 参数数目:N × W
参数占用显存 = 参数数目 × n
n = 4 : float32
n = 2 : float16
n = 8 : double64
Float32 是在深度学习中最常用的数值类型,称为单精度浮点数,每一个单精度浮点数占用 4 Byte 显存。
在 PyTorch 中,执行 model = MyModel().cuda() 之后就会输出占用显存大小,占用的显存大小基本与上述分析的显存差不多(会稍大一些,因为其它开销)。
模型中与输入无关的显存占用包括:
参数 W
梯度 dW(一般与参数一样)
优化器的动量(普通 SGD 没有动量,momentum-SGD 动量与梯度一样,Adam 优化器动量的数量是梯度的两倍)
输入输出的显存主要看输出的 feature map 的形状:
模型输出的显存占用:
需要计算每一层的 feature map 的形状(多维数组的形状)
需要保存输出对应的梯度用以反向传播(链式法则)
显存占用与 batch size 成正比
模型输出不需要存储相应的动量信息。
深度学习中神经网络的显存占用,我们可以得到如下公式:
显存占用 = 模型显存占用 + batch_size × 每个样本的显存占用
可以看出显存不是和 batch-size 简单的成正比,尤其是模型自身比较复杂的情况下:比如全连接很大,Embedding 层很大。
另外需要注意:
输入(数据,图片)一般不需要计算梯度
神经网络的每一层输入输出都需要保存下来,用来反向传播,但是在某些特殊的情况下,我们可以不要保存输入。比如 ReLU,在 PyTorch 中,使用 nn.ReLU(inplace = True) 能将激活函数 ReLU 的输出直接覆盖保存于模型的输入之中,节省不少显存。
数据类型:Int8
模型参数: 7B * 1 bytes = 7GB
梯度:同上 7GB
优化器参数: AdamW 2 倍模型参数 7GB * 2 = 14GB
LLaMA 的架构 (hidden_size= 4096, intermediate_size=11008, num_hidden_lavers= 32, context.length = 2048),所以每个样本大小:(4096 + 11008) * 2048 * 32 * 1byte = 990MB
A100 (80GB RAM) 大概可以在 int8 精度下 BatchSize 设置为 50
综上总现存大小:7GB + 7GB + 14GB + 990M * 50 ~= 77GB
Llama-2-7b-hf 模型 Int8 推理由上个章节可得出现存大小6.5GB
, 由此可见,全量训练模型训练需要的显存是至少推理的十几倍
。
备注:模型训练所需 GPU 显存是本地笔记本所不能完成的,但是我们一般正常使用模型的预测推理服务还是没多大问题的
显存的总占用可以通过将上述各部分的大小相加来计算。在实际应用中,需要根据模型结构、数据批次大小、优化算法等因素来估计和管理显存的使用,以防止内存不足导致训练过程中断。使用一些工具和库(如 TensorFlow、PyTorch 等)可以帮助您监控和管理显存的使用情况。实际影响显存占用的因素还有很多,所以只能粗略估计个数量级。
监听显卡,每 1 秒刷新一次:watch -n -1 -d nvidia-smi
在深度学习中,一般占用显存最多的是卷积等层的输出,模型参数占用的显存相对较少,而且不太好优化。
节省显存方法:
降低 batch-size
下采样 (NCHW -> (1/4)*NCHW)
减少全连接层(一般只留最后一层分类用的全连接层)
时间更宝贵,尽可能使模型变快(减少 flops)
显存占用不是和 batch size 简单成正比,模型自身的参数及其延伸出来的数据也要占据显存
batch size 越大,速度未必越快。在你充分利用计算资源的时候,加大 batch size 在速度上的提升很有限
尤其是 batch-size,假定 GPU 处理单元已经充分利用的情况下:
增大 batch size 能增大速度,但是很有限(主要是并行计算的优化)
增大 batch size 能减缓梯度震荡,需要更少的迭代优化次数,收敛的更快,但是每次迭代耗时更长。
增大 batch size 使得一个 epoch 所能进行的优化次数变少,收敛可能变慢,从而需要更多时间才能收敛(比如 batch_size 变成全部样本数目)。
在模型整个训练过程中中,占用显存大概分以下几类:
模型中的参数 (卷积层或其他有参数的层)
模型在计算时产生的中间参数 (即输入图像在计算时每一层产生的输入和输出)
backward 的时候产生的额外的中间参数
优化器在优化时产生的额外的模型参数 但其实,我们占用的显存空间为什么比我们理论计算的还要大,原因大概是因为深度学习框架一些额外的开销。
除了算法层的优化,最基本的优化方式如下:
减少输入数据单条的大小
减少 batch_size,减少每次的输入数据批量大小
多使用下采样,池化层
一些神经网络层可以进行小优化,利用 relu 层中设置 inplace
购买显存更大的显卡
深度学习框架侧
等等
模型推理服务部署 GPU 配置推荐如下:
了解了模型训练、模型推理时显存计算情况后,优化的思路方向也就有了,比如:使用当前比较主流的一些分布式计算框架 DeepSpeed、Megatron 等,都在降低显存方面做了很多优化工作,比如:量化、模型切分、混合精度计算、Memory Offload 等,
了解显存管理机制主要是为了减少显存碎片化带来的影响。也就解决了我们经常遇到的一些问题:为什么报错信息里提示显存够,但还是遇到了 OOM?
malloc 分配失败的情况下的错误信息:
CUDA out of memory. Tried to allocate 1.24 GiB (GPU 0; 15.78 GiB total capacity; 10.34 GiB already allocated; 456.50 MiB free; 14.21 GiB reserved in total by PyTorch)
Tried to allocate:指本次 malloc 时预计分配的 alloc_size;
total capacity:由 cudaMemGetInfo 返回的 device 显存总量;
already allocated:由统计数据记录,当前为止请求分配的 size 的总和;
free:由 cudaMemGetInfo 返回的 device 显存剩余量;
reserved:BlockPool 中所有 Block 的大小,与已经分配的 Block 大小的总和。即:reserved = already allocated + sum size of 2 BlockPools
注意: reserved + free 并不等同于 total capacity,因为 reserved 只记录了通过 PyTorch 分配的显存,如果用户手动调用 cudaMalloc 或通过其他手段分配到了显存,是没法在这个报错信息中追踪到的(又因为一般 PyTorch 分配的显存占大部分,分配失败的报错信息一般也是由 PyTorch 反馈的)。
在这个例子里,device 只剩 456.5MB,不够 1.24GB,而 PyTorch 自己保留了 14.21GB(储存在 Block 里),其中分配了 10.3GB,剩 3.9GB。那为什么不能从这 3.9GB 剩余当中分配 1.2GB 呢?原因肯定是显存碎片化了。
实现自动碎片整理的关键特性:split
通过环境变量会指定一个阈值 max_split_size_mb,实际上从变量名可以看出,指定的是最大的可以被 “split” 的 Block 的大小。
被 split 的操作很简单,当前的 Block 会被拆分成两个 Block,第一个大小正好为请求分配的 size,第二个则大小为 remaining,被挂到当前 Block 的 next 指针上(这一过程见源码 L570~L584)。这两个 Block 的地址自然而然成为连续的了。随着程序运行,较大的 Block(只要仍小于阈值 max_split_size_mb)会不断被分成小的 Block。值得注意的是,由于新 Block 的产生途径只有一条,即通过步骤三中的 alloc_block 函数经由 cudaMalloc 申请,无法保证新 Block 与其他 Block 地址连续,因此所有被维护在双向链表内的有连续地址空间的 Block 都是由一个最初申请来的 Block 拆分而来的。
一段连续空间内部(由双向链表组织的 Blocks)如图所示:
当 Block 被释放时,会检查其 prev、next 指针是否为空,及若非空是否正在被使用。若没有在被使用,则会使用 try_merge_blocks (L1000) 合并相邻的 Block。由于每次释放 Block 都会检查,因此不会出现两个相邻的空闲块,于是只须检查相邻的块是否空闲即可。这一检查过程见 free_block 函数(L952)。又因为只有在释放某个 Block 时才有可能使多个空闲块地址连续,所以只需要在释放 Block 时整理碎片即可。
关于阈值 max_split_size_mb ,直觉来说应该是大于某个阈值的 Block 比较大,适合拆分成稍小的几个 Block,但这里却设置为小于这一阈值的 Block 才进行拆分。个人理解是,PyTorch 认为,从统计上来说大部分内存申请都是小于某个阈值的,这些大小的 Block 按照常规处理,进行拆分与碎片管理;但对大于阈值的 Block 而言,PyTorch 认为这些大的 Block 申请时开销大(时间,失败风险),可以留待分配给下次较大的请求,于是不适合拆分。默认情况下阈值变量 max_split_size_mb 为 INT_MAX,即全部 Block 都可以拆分。可能有的人有开始有了疑问:如果全部 Block 都拆分了,碎片整理后还是 OOM 怎么办?
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。