借着热点,简单聊聊大模型的部署方案,作为一个只搞过CV部署的算法工程师,在最近LLM逐渐改变生活的大背景下,猛然意识到LLM部署也是很重要的。大模型很火,而且确实有用(很多垂类场景可以针对去训练),并且和Vision结合的大模型也逐渐多了起来。所以怎么部署大模型是一个超级重要的工程问题,很多公司也在紧锣密鼓的搞着。
目前效果最好讨论最多的开源实现就是LLAMA,所以我这里讨论的也是基于LLAMA的魔改部署。
基于LLAMA的finetune模型有很多,比如效果开源最好的vicuna-13b和较早开始基于llama做实验的alpaca-13b,大家可以看:
- https://huggingface.co/spaces/HuggingFaceH4/open_llm_leaderboard 开源LLM
- https://lmsys.org/blog/2023-05-03-arena/ 基于llama的开源对比
- https://github.com/camenduru/text-generation-webui-colab 一些开源LLM的notebook
至于为啥要选LLAMA,因为当前基于LLAMA的模型很多,基础的llama的效果也不错,当然RWKV也很不错,我也一直在看。
具体的可以看这里,https://huggingface.co/spaces/HuggingFaceH4/open_llm_leaderboard
Github上较多的是实现是直接python推理然后基于gradio的网页端展示,还不能被当成服务调用(或者说不大优雅),一般来说作为服务:
- 响应速度要快,每秒的tokens越多越好
- 稳定性,出现core了不会影响推理或者可以及时恢复
- 支持各种形式,http、sse、grpc等形式
其实最重要的一点还是要快,其次必须要量化,因为大模型的权重往往很大,量化可以有效减小权重(省显卡),是非常必要的。要做成服务的话,需要稍稍花一点功夫和魔改一下,幸运的是网上已经有不错的demo实现供我们尝试,这篇文章主要是总结下以LLAMA为例的LLM部署方案,希望能抛砖引玉。
PS:我们常用的ChatGPT据说是居于Python服务搭建的,相比模型的耗时,python环境的开销可以忽略不计。
以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模式,就是出一点返回一点,类似于打字机的赶脚。
部署方案讨论
这部分是本篇文章主要想说的地方,也是想和大家讨论,一起想想方案,抛砖引玉。
对于部署像LLAMA这种的大语言模型,我之前也没有经验,浏览了一些开源的仓库和资料,大概也有些思路,简单总结总结,有那么几个方案:
依赖Python实现的方案
和普通的CV模型一样,python实现肯定是最简单也是实现最多的,我们可以基于现有的开源实现直接包一层服务框架,类似于flask的服务即可,但是也需要有一定的可靠性。
所以这里可以选择triton-inference-server的python后端,自己包一层前后处理,可以支持stream模式(使用grpc)。
这个实现起来比较简单,多注意输入输出即可,相对于CV来说,我们的输入可以是text或者是input_ids,要和图像的unchar区别开,加速部分只能依赖python实现,同时也依赖python环境。
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上。
后记
暂时先说这些,这篇文章之后也会随时更新。目前只是简单列了列我自己的想法,大家如果有好的想法也欢迎跟老潘一起交流。
每天出的新东西新技术太多了,看不过来,这篇文章也拖了好久,上述的这三种方案我自己都尝试过了,都是可行的,大家感兴趣可以试试,不过有个消息是TensorRT已经在默默支持多卡推理了,最快可能下个月(6月)就会出来(可能是对外release),不知道TensorRT大模型版本怎么样?