借着经常变动的热点,聊聊大模型的部署方案。
本篇文章会一直更新,同时也会进行增删改查。
目前已经出来llama2。
LLM的服务的性能要求
对于我们的服务来说:
- 响应速度要快,每秒的tokens越多越好
- 稳定性,出现core了不会影响推理或者可以及时恢复
- 支持各种形式,http、sse、grpc等形式
其实最重要的一点还是要快,其次必须要量化,因为大模型的权重往往很大,量化可以有效减小权重(省显卡),是非常必要的。要做成服务的话,需要稍稍花一点功夫和魔改一下,幸运的是网上已经有不错的demo实现供我们尝试,这篇文章主要是总结下以LLAMA为例的LLM部署方案,希望能抛砖引玉。
对于用户端:
- 模型生成文字速度提升,用户排队时间缩短
以LLAMA为例
LLM很大,比如GPT3-175B的大模型,700GB的参数以及运行过程中600GB的激活值,1.3TB总共,正常来说,得32个A100-40GB才能放的下。
但实际应用中,消费级显卡要比专业显卡便宜的多(比如3090相比A10,同样都是24G显存),所以用消费级显卡部署LLM也很有钱途。一张卡放不下那就放两张,如果没有nvlink,那么PCIE的也凑合用。
回到LLAMA模型,有7B、13B、30B、65B的版本,当然是越大的版本效果最好,相应的也需要更多的显存(其实放到内存或者SSD也是可以的,但是速度相比权重全放到显存里头,肯定要慢)。
LLAMA的实现很多,简单列几个我看过的,都可以参考:
- https://github.com/juncongmoo/pyllama 原始llama的实现方式
- https://github.com/qwopqwop200/GPTQ-for-LLaMa 支持量化,INT4、INT8量化的llama
- https://github.com/tpoisonooo/llama.onnx.git 以ONNX的方式运行llama
量化和精度
对于消费级显卡,直接FP32肯定放不下,一般最基本的是FP16(llama的7B,FP16需要14G的显存,大多数的消费级显卡已经说拜拜了),而INT8和INT4量化则就很有用了,举几个例子:
- 对于3080显卡,10G显存,那么13B的INT4就很有性价比,精度比7B-FP16要高很多
- 对于3090显卡,24G显存,那么30B的INT4可以在单个3090显卡部署,精度更高
可以看下图,列举了目前多种开源预训练模型在各种数据集上的分数和量化精度的关系:
我自己也测试了几个模型,我使用的是A6000显卡,48G的显存,基于GPTQ-for-LLAMA测试了各种不同规格模型的PPL精度和需要的显存大小。
执行以下命令CUDA_VISIBLE_DEVICES=0 python llama.py ${MODEL_DIR} c4 --wbits 4 --groupsize 128 --load llama7b-4bit-128g.pt --benchmark 2048 --check
测试不同量化精度不同规格模型的指标:
# 7B-FP16
Median: 0.03220057487487793
PPL: 5.227280139923096
max memory(MiB): 13948.7333984375
# 7B-INT8
Median: 0.13523507118225098
PPL: 5.235021114349365
max memory(MiB): 7875.92529296875
# 7B-INT4
Median: 0.038548946380615234
PPL: 5.268043041229248
max memory(MiB): 4850.73095703125
# 13B-FP16
Median: 0.039263248443603516
PPL: 4.999974727630615
max memory(MiB): 26634.0205078125
# 13B-INT8
Median: 0.18153250217437744
PPL: 5.039003849029541
max memory(MiB): 14491.73095703125
# 13B-INT4
Median: 0.06513667106628418
PPL: 5.046994209289551
max memory(MiB): 8677.134765625
# 30B-FP16
OOM
# 30B-INT8
Median: 0.2696110010147095
PPL: 4.5508503913879395
max memory(MiB): 34745.9384765625
# 30B-INT4
Median: 0.1333252191543579
PPL: 4.526902675628662
max memory(MiB): 20070.197265625
30B的FP16和65B的爆显存了,还没搞自己的多卡环境,之后会补充结果到这里。
可以先看看其他大佬的测试结果,大概有个预期,65B模型的话,INT4在两张3090上是可以放得下的:
速度测试
我这边也测试了llama在huggingface中Python库的实现速度、以及基于GPTQ的量化、多卡的速度,其中stream模式参考了text-generation-webui中的Stream实现。
其中FP16就是直接调用GPTQ-for-LLaMa/llama_inference.py
中未量化的FP16推理,调用方式为model.generate
,model来自:
model = LlamaForCausalLM.from_pretrained(model, torch_dtype='auto',
# load_in_8bit=True, device_map='auto'
)
而量化版本的模型:
-
INT8模型调用方式同FP16,参数
load_in_8bit
设置为True,直接利用hugglingface的transformer库INT8的实现 -
INT4的模型使用GPTQ的方式进行量化,具体代码通过triton语言(区别于我之前提到的triton inference server)进行加速
下面是测试结果:
模型 | 平台 | 精度 | 显存 | 速度 | 备注 |
---|---|---|---|---|---|
llama-7B | A4000 | FP16 | 13.6g | Output generated in 1.74 seconds (28.74 tokens/s, 50 tokens) Output generated in 9.63 seconds (25.97 tokens/s, 250 tokens) |
GPTQ-for-LLaMa/llama_inference.py 测试包含模型前后处理 利用率99% |
A4000 | 4-bit | 5g | Output generated in 2.89 seconds (17.51 tokens/s, 50 tokens) | ||
A4000 | 4-bit | 5g | Output generated in 2.93 seconds (17.11 tokens/s, 50 tokens) | stream模式 一次输出1个token |
|
A4000 | 4-bit | 3.4+2.3g | Output generated in 2.91 seconds (17.31 tokens/s, 50 tokens) | 多卡测试双A4000 两张卡利用率 20-30左右 |
|
A4000 | INT8 | 8.3g | Output generated in 10.20 seconds (5.8 tokens/s, 50 tokens) int8 实现用的huggingface的实现 |
利用率25% |
我这边拿我的A4000测了下,测试从tokenizer编码后开始,到tokenizer解码后结束。
大概的结论:
-
FP16速度最快,因为INT4和INT8的量化没有优化好(理论上INT8和INT4比FP16要快不少),而INT4的triton优化明显比huggingface中INT8的实现要好,建议使用INT4量化
-
stream模式和普通模型的速度似乎差不多
A6000的懒得测试了,补充一个网上搜到的指标,A6000相比A4000相当于类似于3090和3070的性能差距吧…
LLM和普通小模型在部署方面的区别
在我以往的任务中,都是处理CV相关的模型,检测、分类、识别、关键点啥的,最大模型可能也只有2、3G的样子。平时的检测模型,大点的也就300多M,在FP16或者INT8量化后则更小了,所以一般来说没有显存焦虑(当然有特殊情况,有时候可能会在一张卡上部署很多个模型,比如自动驾驶场景或者其他工业场景,这时候也是需要合理分配模型的显存占用)。
但部署LLM这种大模型就不一样了,随便一个6、7B的模型,动不动就20多G的权重;65B、175B的模型更是有几百G,模型变得异常大,因为这个原因,原来的部署方式都要发生一些变化。
模型方面的区别
-
首先模型很大,大模型导出ONNX有一些问题,ONNX保存大模型的权重存在一些限制
-
LLM模型中一般包含很多if-else分支,比如是否采用kv-cache,对转换一些个别结构的模型(比如tensorrt)不是很友好
-
我们之前都是单GPU运行,多GPU的话,很多常用的runtime都不支持,onnxruntime或者tensorrt(tensorrt有内测多GPU的支持)默认都不支持多GPU
-
对于大模型来说,量化是必要的,因为FP16或者FP32的模型需要的显存太大,都是钱啊。量化起来也不容易,QAT代价太大,PTQ校准的时候也需要很大的内存和显存,会用INT8和INT4量化
-
网上对于这类模型的加速kernel不是很多,可以参考的较少,很多需要自己手写
服务方式的区别
对于小模型来说,推理速度一般不会太慢,基本都在500ms以内,稍微等待下就得到结果了,也即是普通的http请求,一次请求得到一次结果。
而LLM因为是一个一个token产出来的,如果等全部token都出来再返回,那么用户等待时间就挺长的,体验不好,所以一般都是使用stream模式,就是出一点返回一点,类似于打字机的赶脚。
开源部署方案探讨
借着开源的方案,大家一起抛砖引玉。
vllm
更多参数可以在 vllm/engine/arg_utils.py
找到,其中比较重要的有:
max-num-seqs
:默认 256,当max-num-seqs
比较小时,较迟接收到的 request 会进入waiting_list
,直到前面有request 结束后再被添加进生成队列。当max-num-seqs
太大时,会出现一部分 request 在生成了 3-4 个 tokens 之后,被加入到 waiting_list(有些用户出现生成到一半卡住的情况)。过大或过小的max-num-seqs
都会影响用户体验。max-num-batched-tokens
:很重要的配置,比如你配置了max-num-batched-tokens=1000
那么你大概能在一个 batch 里面处理 10 条平均长度约为 100 tokens 的 inputs。max-num-batched-tokens
应尽可能大,来充分发挥 continuous batching 的优势。不过似乎(对于 TGI 是这样,vllm 不太确定),在提供 HF 模型时,该max-num-batched-tokens
能够被自动推导出来。
lightllm
fastertransformer_backend 方案
对于生产环境的部署,可以使用triton inference server,然后基于tritonserver有fastertransformer-backend。fastertransformer-backend是一个支持多个LLM模型的backend,手工实现了很多高性能的针对transformer的算子,每个模型都是手写cuda搭建起来的,性能相比使用库要高一档。不过代价就是比较难支持新的模型框架,需要修改大量的源码。
NVIDIA Triton introduces Multi-GPU Multi-node inference. It uses model parallelism techniques below to split a large model across multiple GPUs and nodes:
-
Pipeline (Inter-Layer) Parallelism that splits contiguous sets of layers across multiple GPUs. This maximizes GPU utilization in single-node.
-
Tensor (Intra-Layer) Parallelism that splits individual layers across multiple GPUs. This minimizes latency in single-node
所幸开源社区的大佬很多,近期的非官方PR也支持了LLAMA。我自己尝试跑了下速度要比第一个huggingface的实现要快20%,这个精度基于FP16,支持多卡,目前暂时未支持INT8和INT4。
利用加速库分而治之的方案
我们知道LLAMA的7B模型包含32个结构相同的decoder:
# transformers/src/transformers/models/llama/modeling_llama.py
self.layers = nn.ModuleList([LlamaDecoderLayer(config) for _ in range(config.num_hidden_layers)])
因此我们也可以将这32个子模型分别使用我们常用的加速库部署起来,比如7B的大模型,拆分成子模型每个模型300多M,使用TensorRT可以轻松转换,也有大佬已经在做了:
7B的llama模型转成ONNX大概是这些:
- decoder-merge-0.onnx
- embed.onnx
- head.onnx
- norm.onnx
串起来的话,可以把这些模型放到不同的显卡上也是可以的,比如两张显卡,第一张卡放15个子模型,第二张卡放剩余17个子模型,组成pipeline parallelism也是可以的。
有几点需要注意:
- 加速库可以使用不限于TensorRT,比如TVM、AItemplate啥的
- 需要一个后端将所有子模型串起来,最好C++实现
- 对于kv-cache,怎么存放需要考虑下
可以使用triton-inference-server去组pipeline,不同子模型的instance可以放到不同的gpu上。
服务化常用指标
- 用户 cceyda 希望能看到当前在队列中等待的请求数量,以便更好地进行调度。他可以看到
nv_inference_queue_duration_us
的持续时间,但不知道有多少请求正在等待。 - cceyda 提到了
nv_inference_queue_summary_us_count
,但他认为这只是计算统计数据时使用的请求计数,并不表示有多少请求正在队列中等待。 - cceyda 还想在 grafana 中添加每秒的平均吞吐量指标,并询问了一个具体的方法。
- 贡献者 rmccorm4 回复了 cceyda 的问题,解释了当前的指标如何工作,并提供了一些关于如何使用这些指标的建议。rmccorm4 还提到,目前并没有报告队列中的请求数量,也没有计划添加此功能。但如果有足够的兴趣或用例,他们可能会考虑这个功能。
- 其他用户也对这个功能表示了兴趣,并提出了一些与此相关的用例。
后记
参考链接
- https://github.com/huggingface/text-generation-inference
- https://github.com/ELS-RD/transformer-deploy
- https://github.com/ELS-RD/kernl
- https://github.com/vllm-project/vllm
- https://github.com/InternLM/lmdeploy/tree/main
- GitHub - huggingface/text-generation-inference: Large Language Model Text Generation Inference