pytorch在有限的资源下部署大语言模型(以ChatGLM-6B为例)

Part1知识准备

在PyTorch中加载预训练的模型时,通常的工作流程是这样的:

my_model = ModelClass(...)
state_dict =
torch.load(checkpoint_file)

用简单的话来说,这些步骤是:

  1. 用随机初始化的权重创建模型。

  2. 从磁盘上加载模型权重(在一个通常被称为状态字典的字典中)。

  3. 在模型中加载这些权重。

虽然这对常规大小的模型来说非常有效,但当我们处理一个巨大的模型时,这个工作流程有一些明显的局限性:在第1步,我们在RAM中加载一个完整版本的模型,并花一些时间随机初始化权重(这将在第3步被丢弃)。在第2步,我们在RAM中加载另一个完整版本的模型,并使用预训练的权重。如果你正在加载一个具有60亿个参数的模型,这意味着你需要为每个模型的副本提供24GB的RAM,所以总共需要48GB(其中一半用于在FP16中加载模型)。

1使用accelerate

上下文管理器

引入accelerate处理大模型的第一个工具是上下文管理器init_empty_weights(),它可以帮助你在不使用任何RAM的情况下初始化一个模型,这样,步骤1就可以可以在任何尺寸的模型上进行。以下是它的工作原理:

from accelerate import init_empty_weights

with init_empty_weights():
    my_model = ModelClass(...)

例如:

with init_empty_weights():
    model = nn.Sequential(*[nn.Linear(10000, 10000) for _ in range(1000)])

初始化一个空的模型,参数略多于100B。这有赖于PyTorch 1.9中引入的元设备(meta device)。在上下文管理器下的初始化过程中,每次创建一个参数时,它都会移动到该设备上。

分布式检查点

你的模型有可能大到即使是一个副本也无法装入RAM。这并不意味着它不能被加载:如果你有一个或几个GPU,这将有更多的内存可用于存储你的模型。在这种情况下,如果你的检查点被分割成几个较小的文件,我们称之为检查点碎片,效果会更好。

accelerate将处理分片检查点,只要你遵循以下格式:你的检查点应该在一个文件夹中,有几个文件包含部分状态字典,应该有一个JSON格式的索引,包含一个字典将参数名称映射到包含其权重的文件。例如,我们可以有一个包含以下内容的文件夹:

first_state_dict.bin
index.json
second_state_dict.bin

与index.json是以下文件:

{
  "linear1.weight": "first_state_dict.bin",
  "linear1.bias": "first_state_dict.bin",
  "linear2.weight": "second_state_dict.bin",
  "linear2.bias": "second_state_dict.bin"
}

first_state_dict.bin包含 "linear1.weight "和 "linear1.bias "的权重。second_state_dict.bin是 "linear2.weight "和 "linear2.bias "的权重。

加载权重

第二个工具是引入了一个函数load_checkpoint_and_dispatch(),它将允许你在你的空模型中加载一个检查点。这支持完整的检查点(一个单个文件包含整个状态描述)以及分片检查点。它还会在你可用的设备(GPU、CPURAM)上自动分配这些权重,所以如果你正在加载一个分片检查点,最大的RAM使用量将是最大分片的大小。

from accelerate import init_empty_weights
from transformers import AutoConfig, AutoModelForCausalLM

checkpoint = "EleutherAI/gpt-j-6B"
config = AutoConfig.from_pretrained(checkpoint)

with init_empty_weights():
    model = AutoModelForCausalLM.from_config(config)

请注意,在transformer中用from_config加载模型并不绑定权重,这在加载不包含绑定权重的重复键的检查点时可能导致问题。所以你应该在加载检查点之前绑定权重。

model.tie_weights()

然后加载我们刚刚下载的检查点:

model = load_checkpoint_and_dispatch(
    model, "sharded-gpt-j-6B", device_map="auto", no_split_module_classes=["GPTJBlock"]
)

通过传递device_map="auto",根据可用的资源,我们告诉模型的每一层放置在哪里。

  • 首先,我们使用GPU上的最大可用空间。

  • 如果我们仍然需要空间,我们将剩余的权重存储在CPU上。

  • 如果没有足够的RAM,我们将剩余的权重作为内存映射的张量存储在硬盘上。

no_split_module_classes=["GPTJBlock"] 表示属于GPTJBlock的模块不应该在不同的设备上被分割。你应该在这里设置所有包括某种residual(残差连接)的块。

你可以通过hf_device_map来查看accelearte挑选的设备图。

model.hf_device_map
{'transformer.wte': 0,
 'transformer.drop': 0,
 'transformer.h.0': 0,
 'transformer.h.1': 0,
 'transformer.h.2': 0,
 'transformer.h.3': 0,
 'transformer.h.4': 0,
 'transformer.h.5': 0,
 'transformer.h.6': 0,
 'transformer.h.7': 0,
 'transformer.h.8': 0,
 'transformer.h.9': 0,
 'transformer.h.10': 0,
 'transformer.h.11': 0,
 'transformer.h.12': 0,
 'transformer.h.13': 0,
 'transformer.h.14': 0,
 'transformer.h.15': 0,
 'transformer.h.16': 0,
 'transformer.h.17': 0,
 'transformer.h.18': 0,
 'transformer.h.19': 0,
 'transformer.h.20': 0,
 'transformer.h.21': 0,
 'transformer.h.22': 0,
 'transformer.h.23': 0,
 'transformer.h.24': 1,
 'transformer.h.25': 1,
 'transformer.h.26': 1,
 'transformer.h.27': 1,
 'transformer.ln_f': 1,
 'lm_head': 1}

如果你喜欢明确地决定每层的位置,你也可以自己设计你的设备图。在这种情况下,上面的命令变成了:

model = load_checkpoint_and_dispatch(model, "sharded-gpt-j-6B", device_map=my_device_map)

运行模型

现在我们已经做到了这一点,我们的模型位于几个设备之间,也许还有硬盘。但它仍然可以作为一个普通的PyTorch模型使用:

from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained(checkpoint)
inputs = tokenizer("Hello, my name is", return_tensors="pt")
inputs = inputs.to(0)
output = model.generate(inputs["input_ids"])
tokenizer.decode(output[0].tolist())

在幕后,accelerate为模型添加了钩子,因此:

  • 在每一层,输入被放在正确的设备上(因此,即使你的模型分散在几个GPU上,它也能工作)。

  • 对于卸载在CPU上的权重,就在向前传递之前,它们被放在GPU上,并在之后被清理掉。

  • 对于卸载在硬盘上的权重,它们被加载在RAM中,然后在向前传递之前被放在GPU上,并在之后被清理掉。 这样,即使你的模型不适合在某个GPU或CPU RAM上运行,你也可以运行推理!

设计一个设备图

你可以通过以下选项"auto", "balanced", "balanced_low_0", "sequential"让acclerate处理设备图的计算,或自己创建一个。如果你想更多地控制每个层应该去哪里,你可以在一个元设备上的模型上推导出模型的所有尺寸(从而计算出一个设备图)。

当你没有足够的GPU内存来容纳整个模型时,所有的选项都会产生相同的结果(也就是把所有能装的东西都装到GPU上,然后把重量卸到CPU上,如果没有足够的内存,甚至卸到磁盘上)。

当你有比模型大小更多的GPU内存可用时,这里是每个选项之间的区别:

  • "auto"和"balanced"在所有可用的GPU上平均分配模型,使你有可能使用大于1的批次大小。

  • "balanced_low_0 "将模型均匀地分割到所有的GPU上,除了第一个GPU之外,并且只将不适合其他GPU的部分放在GPU 0上。当你需要使用GPU 0对输出进行一些处理时,这个选项是非常好的,比如使用transformers的生成函数时。

  • "顺序 "将在GPU 0上安装它可以安装的东西,然后在GPU 1上移动,以此类推(所以如果不需要,就不会使用最后的GPU)。

首先注意,你可以通过使用max_memory参数(在fer_auto_device_map()和所有使用该参 的函数中可用)限制每个GPU上使用的内存。当设置max_memory时,你应该传递一个包含GPU标识符(例如0、1等)和 "cpu "键的字典,用于你希望用于CPU卸载的最大RAM。这些值可以是一个整数(以字节为单位),也可以是一个代表数字及其单位的字符串,例如 "10GiB "或 "10GB"。

这里有一个例子,我们不希望在两个GPU上各使用超过10GiB,而在模型权重上不超过30GiB的CPU内存:

from accelerate import infer_auto_device_map

device_map = infer_auto_device_map(my_model, max_memory={0: "10GiB", 1: "10GiB", "cpu": "30GiB"})

当PyTorch发生首次分配时,它会加载CUDA内核,根据GPU的情况,它需要大约1-2GB的内存。因此,你的可用内存总是少于GPU的实际大小。要查看实际使用了多 少内存,请执行torch.ones(1).cuda()并查看内存使用情况。因此,当你用max_memory创建内存映射时,确保相应地调整可用的内存,以避免出先OOM。

此外,如果你对你的输出做一些额外的操作而不把它们放回CPU(例如在transformer的生成方法里面),如果你把你的输入放在一个GPU上,这个GPU将比其他的消耗更多的内存(加速器总是把输出放回输入的设备)。因此,如果你想优化最大的批处理量,并且你有很多GPU,给第一个GPU较少的内存。例如在8x80 A100设置上使用BLOOM-176B,接近理想的映射是:

max_memory = {0: "30GIB", 1: "46GIB", 2: "46GIB", 3: "46GIB", 4: "46GIB", 5: "46GIB", 6: "46GIB", 7: "46GIB"}

你可以看到,我们给其余7个GPU的内存比GPU 0多了50%。

如果你选择自己完全设计设备映射,它应该是一个字典,键是你的模型的模块名称,值是一个有效的设备标识符(例如GPU是一个整数)或CPU卸载的 "cpu",磁盘卸载的 "disc"。键需要覆盖整个模型,然后你可以按照你的意愿定义你的设备映射:例如,如果你的模型有两个块(比方说block1和block2),它们各自包含三个线性层(比方 说线性1、线性2和线性3),一个有效的设备映射可以是:

device_map = {"block1": 0, "block2": 1}

另一个有效的可能是:

device_map = {"block1": 0, "block2.linear1": 0, "block2.linear2": 1, "block2.linear3": 1}

另一方面,这个是无效的,因为它没有涵盖模型的每个参数:

device_map = {"block1":0, "block2.linear1":1, "block2.linear2":1}

为了达到最高的效率,请确保你的设备映射以连续的方式将参数放在GPU上(例如 ,不要将第一个权重放在GPU 0上,然后将权重放在GPU 1上,最后一个权重再放 回GPU 0),以避免在GPU之间进行多次数据传输。

限制和进一步发展

我们知道目前API的局限性:

  • 虽然理论上这只可以在一个CPU上工作,并有潜在的磁盘卸载,但你至少需要一个GPU来运行这个API。这将在进一步的开发中得到解决。

  • infer_auto_device_map() (或load_checkpoint_and_dispatch()中的 device_map="auto")试图在你执行它的时候最大化它所看到的GPU和CPU RAM。虽然PyTorch在有效地管理GPU RAM方面非常出色(当不需要时就会归还),但对于Python和CPU RAM来说,这并不完全正确。因此,自动计算的设备图可能对CPU来说过于紧张。如果你因内存不足而出现崩溃,请将一些模块移到磁盘设备上。

  • infer_auto_device_map()(或者load_checkpoint_and_dispatch()中的device_map="auto")是按顺序属性设备的(以避免来回移动东西),所以如果你的第一层比你的GPU的大小大,最后会把所有东西都放在CPU/磁盘上。

  • load_checkpoint_and_dispatch()和load_checkpoint_in_model()目前没有对你的状态描述与你的模型相比的正确性进行任何检查(这将在未来的版本中被修复),所以如果你试图加载一个键不匹配或丢失的检查点,你可能会得到一些奇怪的错误。

  • 当你的模型被分割到几个GPU上时,所使用的模型并行性是天真的,没有经过优化,这意味着在某个时间只有一个GPU在工作,而另一个则处于闲置状态。

  • 当权重被卸载在CPU/硬盘上时,没有预取(还没有,我们会在未来的版本中努力做到这一点),这意味着权重在需要时被放到GPU上,而不是之前。

  • 如果你运行的硬件没有磁盘和CPU之间的快速通信(如NVM),硬盘卸载可能会非常慢.

Part2部署ChatGLM-6B

基础环境:

torch==2.0.0+cu118
transformers==4.28.1
accelerate==0.18.0
Tesla T4 15.3G
内存:11.8G

下载相关文件:

git clone https://github.com/THUDM/ChatGLM-6B
cd ChatGLM-6B

git clone --depth=1 https://huggingface.co/THUDM/chatglm-6b THUDM/chatglm-6b
git clone --depth=1 https://huggingface.co/THUDM/chatglm-6b-int4 THUDM/chatglm-6b-int4

pip install -r requirements.txt
pip install gradio
pip install accelerate

正常情况下,我们使用Chat-GLM需要的显存大于13G,内存没有评估过,但上述的肯定是不够的,16G应该可以。

2第一种方案

直接使用量化以后的模型:

from accelerate import infer_auto_device_map, init_empty_weights, load_checkpoint_and_dispatch
from transformers import AutoConfig, AutoModel, AutoModelForCausalLM, AutoTokenizer
import gradio as gr
import torch
import time

tokenizer = AutoTokenizer.from_pretrained("./THUDM/chatglm-6b-int4", trust_remote_code=True)
model = AutoModel.from_pretrained("./THUDM/chatglm-6b-int4", trust_remote_code=True).half().cuda()

model = model.eval()

def predict(input, history=None):
    print(f'predict started: {time.time()}');
    if history is None:
        history = []
    response, history = model.chat(tokenizer, input, history)
    return response, history

while True:
  text = input(">>用户:")
  response, history = model.chat(tokenizer, input, history)
  print(">>CHatGLM:", response)

GPU使用4.9G,内存使用5.5G。

3第二种方案

使用acclerate,只有一块GPU。

%cd /content/ChatGLM-6B

from accelerate import infer_auto_device_map, init_empty_weights, load_checkpoint_and_dispatch
from transformers import AutoConfig, AutoModel, AutoModelForCausalLM, AutoTokenizer
import gradio as gr
import torch
import time


tokenizer = AutoTokenizer.from_pretrained("./THUDM/chatglm-6b", trust_remote_code=True)
config = AutoConfig.from_pretrained("./THUDM/chatglm-6b", trust_remote_code=True)
with init_empty_weights():
  model = AutoModel.from_config(config, trust_remote_code=True)

for name, _ in model.named_parameters():
  print(name)
# device_map = infer_auto_device_map(model, no_split_module_classes=["GLMBlock"])
# print(device_map)
device_map = {'transformer.word_embeddings': 0, 'transformer.layers.0': 0, 'transformer.layers.1': 0, 'transformer.layers.2': 0, 'transformer.layers.3': 0, 'transformer.layers.4': 0, 'transformer.layers.5': 0, 'transformer.layers.6': 0, 'transformer.layers.7': 0, 'transformer.layers.8': 0, 'transformer.layers.9': 0, 'transformer.layers.10': 0, 'transformer.layers.11': 0, 'transformer.layers.12': 0, 'transformer.layers.13': 0, 'transformer.layers.14': 0, 'transformer.layers.15': 0, 'transformer.layers.16': 0, 'transformer.layers.17': 0, 'transformer.layers.18': 0, 'transformer.layers.19': 0, 'transformer.layers.20': 0, 'transformer.layers.21': 'cpu', 'transformer.layers.22': 'cpu', 'transformer.layers.23': 'cpu', 'transformer.layers.24': 'cpu', 'transformer.layers.25': 'cpu', 'transformer.layers.26': 'cpu', 'transformer.layers.27': 'cpu', 'transformer.final_layernorm': 'cpu', 'lm_head': 'cpu'}
model = load_checkpoint_and_dispatch(model, "./THUDM/chatglm-6b", device_map=device_map, offload_folder="offload", offload_state_dict=True, no_split_module_classes=["GLMBlock"]).half()

def predict(input, history=None):
    print(f'predict started: {time.time()}');
    if history is None:
        history = []
    response, history = model.chat(tokenizer, input, history)
    return response, history

while True:
  history = None
  text = input(">>用户:")
  response, history = model.chat(tokenizer, text, history)
  print(">>CHatGLM:", response)

GPU使用9.7G,内存使用5.9G。第一轮输入你好后GPU使用11.2G。

4第三种方案

使用accelerate,多块GPU。

环境:windwos下。GPU:4*4090 24G。内存:128G。python>=3.8,torch==2.0+117,transformers==4.28.1,acclerate==0.18.0。

import os
os.environ["cuda_visible_devices"] = "0,1"

from accelerate import infer_auto_device_map, init_empty_weights, load_checkpoint_and_dispatch
from transformers import AutoConfig, AutoModel, AutoModelForCausalLM, AutoTokenizer
# import gradio as gr
# import torch
import time


tokenizer = AutoTokenizer.from_pretrained(".\\chatglm-6b\\", trust_remote_code=True)
config = AutoConfig.from_pretrained(".\\chatglm-6b\\", trust_remote_code=True)
with init_empty_weights():
  model = AutoModel.from_config(config, trust_remote_code=True)

for name, _ in model.named_parameters():
  print(name)
# device_map = infer_auto_device_map(model, no_split_module_classes=["GLMBlock"])
# print(device_map)
# device_map = {'transformer.word_embeddings': 0, 'transformer.layers.0': 0, 'transformer.layers.1': 0, 'transformer.layers.2': 0, 'transformer.layers.3': 0, 'transformer.layers.4': 0, 'transformer.layers.5': 0, 'transformer.layers.6': 0, 'transformer.layers.7': 0, 'transformer.layers.8': 0, 'transformer.layers.9': 0, 'transformer.layers.10': 0, 'transformer.layers.11': 0, 'transformer.layers.12': 0, 'transformer.layers.13': 0, 'transformer.layers.14': 0, 'transformer.layers.15': 0, 'transformer.layers.16': 0, 'transformer.layers.17': 0, 'transformer.layers.18': 0, 'transformer.layers.19': 0, 'transformer.layers.20': 0, 'transformer.layers.21': 'cpu', 'transformer.layers.22': 'cpu', 'transformer.layers.23': 'cpu', 'transformer.layers.24': 'cpu', 'transformer.layers.25': 'cpu', 'transformer.layers.26': 'cpu', 'transformer.layers.27': 'cpu', 'transformer.final_layernorm': 'cpu', 'lm_head': 'cpu'}
model = load_checkpoint_and_dispatch(model, ".\\chatglm-6b\\", device_map="balanced", offload_folder="offload", offload_state_dict=True, no_split_module_classes=["GLMBlock"]).half()

def predict(input, history=None):
    print(f'predict started: {time.time()}')
    if history is None:
        history = []
    response, history = model.chat(tokenizer, input, history)
    return response, history

while True:
  history = None
  text = input(">>用户:")
  response, history = model.chat(tokenizer, text, history)
  print(">>CHatGLM:", response)

注意,这里我们设置设备映射为balanced,并只使用前两块GPU。显卡占用情况:

会发现平均分配了显存,当然GPU 0分配得更多些、至此,关于如何进行大模型推理就全部完成了。 

Part3参考

https://huggingface.co/docs/accelerate/usage_guides/big_modeling
https://github.com/THUDM/ChatGLM-6B/issues/69
https://github.com/THUDM/ChatGLM-6B/issues/200

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
ChatGLM-6B 模型部署可以分为以下几个步骤: 1. 下载模型:您可以从 EleutherAI 的 GitHub 仓库下载 ChatGLM-6B 模型。 2. 安装依赖:为了运行 ChatGLM-6B,您需要安装 PyTorch、Transformers 和 Flask 等必要的依赖。 3. 编写代码:您需要编写代码来加载模型并将其包装在 Flask 应用程序中。一个简单的示例代码如下: ```python from transformers import AutoTokenizer, AutoModelForCausalLM import torch from flask import Flask, request, jsonify app = Flask(__name__) # 加载模型和tokenizer tokenizer = AutoTokenizer.from_pretrained("EleutherAI/gpt-neo-2.7B") model = AutoModelForCausalLM.from_pretrained("EleutherAI/gpt-neo-2.7B") # 设置模型为eval模式并移动到GPU上(如果有的话) device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model.eval() model.to(device) # 定义API路由 @app.route('/chat', methods=['POST']) def chat(): # 解析请求的数据 data = request.json message = data['message'] # 对输入进行编码 inputs = tokenizer.encode(message, return_tensors='pt').to(device) # 生成回复 outputs = model.generate(inputs, max_length=50, do_sample=True) # 对输出进行解码 reply = tokenizer.decode(outputs[0], skip_special_tokens=True) # 返回回复 return jsonify({'reply': reply}) if __name__ == '__main__': app.run(debug=True, port=5000) ``` 4. 运行应用程序:运行 Flask 应用程序,并通过 API 调用与 ChatGLM-6B 进行交互。 以上是 ChatGLM-6B 模型部署的简单示例,您可以根据实际情况进行修改和扩展。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

wsnbb_2023

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值