如何利用vLLM框架快速部署LLama2
vLLM 是一个领先的 LLM 推理与服务库,它不仅高速,还非常易于使用。
vLLM的亮点包括:
- 业界领先的服务吞吐量。
- 利用 PagedAttention 技术,高效地管理注意力机制中的键和值。
- 能够连续批处理接入的请求。
- 充分优化的 CUDA 核心,确保速度与效率。
- 与热门的 HuggingFace 模型完美融合。
- 提供多种解码算法的高效服务,如并行采样、波束搜索等。
- 针对分布式推理提供张量并行支持。
- 实时的流式输出。
- 具备与 OpenAI 兼容的 API 接口。
此外,vLLM 支持众多的 Huggingface 模型架构,例如 Aquila、Baichuan、BLOOM 和 Falcon,以及包括 GPT-2、GPT-J、GPT-NeoX 在内的多款热门模型。
Quick Start
部署模型之前首先需要下载好模型权重,因为llama2模型权重动则10GB左右,在国内因为网络不稳定可能下载有困难。具体下载教程可以参考我之前的文章:
我们使用的是中文llama2模型,模型保存在'/mlx/users/xingzheng.daniel/playground/model/chinese-alpaca-2-7b'目录下:
我们可以利用pip安装vllm
# (Optional) Create a new conda environment.
conda create -n myenv python=3.8 -y
conda activate myenv
# Install vLLM.
pip install vllm
pip国内源可以使用清华源:mirrors.tuna.tsinghua.edu.cn/help/pypi/
批量离线推理
先准备好我们的脚本:
# 导入 vLLM 所需的库
from vllm import LLM, SamplingParams
# 定义输入提示的列表,这些提示会被用来生成文本
prompts = [
"[INST] <<SYS>>\nYou are a helpful assistant. 你是一个乐于助人的助手。\n<</SYS>>\n\n [/INST] 你好,我叫",
"[INST] <<SYS>>\nYou are a helpful assistant. 你是一个乐于助人的助手。\n<</SYS>>\n\n [/INST] 美国的总统是",
"[INST] <<SYS>>\nYou are a helpful assistant. 你是一个乐于助人的助手。\n<</SYS>>\n\n [/INST] 法国的首都是",
"[INST] <<SYS>>\nYou are a helpful assistant. 你是一个乐于助人的助手。\n<</SYS>>\n\n [/INST] 人工智能的未来是",
]
# 定义采样参数,temperature 控制生成文本的多样性,top_p 控制核心采样的概率
sampling_params = SamplingParams(temperature=0.8, top_p=0.95, max_tokens=512)
# 初始化 vLLM 的离线推理引擎,这里选择的是 "/root/chinese-llama2" 模型
llm = LLM(model="/mlx/users/xingzheng.daniel/playground/model/chinese-alpaca-2-7b")
# 使用 llm.generate 方法生成输出文本。
# 这会将输入提示加入 vLLM 引擎的等待队列,并执行引擎以高效地生成输出
outputs = llm.generate(prompts, sampling_params)
# 打印生成的文本输出
for output in outputs:
prompt = output.prompt # 获取原始的输入提示
generated_text = output.outputs[0].text # 从输出对象中获取生成的文本
print(f"Prompt: {prompt!r}, Generated text: {generated_text!r}")
构建Prompts的时候需要注意,根据不同模型预训练时的提示词格式,需要构建对应格式的prompt。格式不匹配的话模型可能会出现未定义的行为。
运行我们的脚本,得到以下结果:
OpenAI 风格的服务器
vLLM也可以支持部署满足OpenAI API风格的在线服务
python -m vllm.entrypoints.openai.api_server --model /mlx/users/xingzheng.daniel/playground/model/chinese-alpaca-2-7b
运行后如下:
发送一个请求测试一下
curl http://localhost:8000/v1/completions \
-H "Content-Type: application/json" \
-d '{
"model": "/mlx/users/xingzheng.daniel/playground/model/chinese-alpaca-2-7b",
"prompt": "[INST] <<SYS>>\nYou are a helpful assistant. 你是一个乐于助人的助手。\n<</SYS>>\n\n [/INST] 美国的总统是",
"max_tokens": 7,
"temperature": 0
}'
响应:
{"id":"cmpl-6a971f9a46ce4870bcf965611b2c0508","object":"text_completion","created":1694439858,"model":"/mlx/users/xingzheng.daniel/playground/model/chinese-alpaca-2-7b","choices":[{"index":0,"text":"乔·拜登。","logprobs":null,"finish_reason":"stop"}],"usage":{"prompt_tokens":39,"total_tokens":45,"completion_tokens":6}}
vLLM性能
推理系统常常需要被部署为在线服务,因此服务的稳定性、延迟、性能等指标的量化非常关键。vllm项目下benchmark目录提供了测试脚本供我们进行实验。
首先需要启动服务,与第一小节不同的是,脚本并不支持openai风格的接口
python -m vllm.entrypoints.api_server --model /mlx/users/xingzheng.daniel/playground/model/chinese-alpaca-2-7b
然后运行脚本得到以下输出
(torch2) ➜ benchmarks git:(main) python3 benchmark_serving.py --dataset ShareGPT_V3_unfiltered_cleaned_split.json --tokenizer /mlx/users/xingzheng.daniel/playground/model/chinese-alpaca-2-7b --request-rate 40
Namespace(backend='vllm', host='localhost', port=8000, dataset='ShareGPT_V3_unfiltered_cleaned_split.json', tokenizer='/mlx/users/xingzheng.daniel/playground/model/chinese-alpaca-2-7b', best_of=1, use_beam_search=False, num_prompts=1000, request_rate=40.0, seed=0, trust_remote_code=False)
Total time: 165.50 s
Throughput: 6.04 requests/s
Average latency: 77.68 s
Average latency per token: 0.27 s
Average latency per output token: 1.03 s
输出的 token 总数 / 总时间: 1348.35 tokens/s
脚本逻辑总结来说,通过异步IO的方式发送N个网络请求,记录每一个单个请求所消耗的时长(latency), prompt token数量,output的token数量。最后根据这个数据计算一些统计指标。
指标 | 计算方式 |
---|---|
Total Time | 所有请求完成时间 - 开始发送第一个请求时间 |
Throughput | 总共发送的Prompt数量 / Total Time |
Average latency | 求平均(单个请求结束时间 - 单个请求开始时间) |
Average latency per token | 求平均((单个请求Prompt Token + 输出Token)/ 单个请求延迟) |
Average latency per output | 求平均((单个请求输出Token)/ 单个请求延迟) |
输出的 token 总数 / 总时间 | 所有请求输出Token总数 / Total Time |
说明 Total Time和Throughput可能和request-rate相关,默认是不会限制每分钟发送的请求数量。但由于我的环境问题,因此限制为20,可能会造成该数值偏低。但Average latency、Average latency per token、Average latency per output由于是先分别计算每个请求、再求平均,因此不会收到影响。
vLLM高速的秘密 - PagedAttention
疑问:实验时发现,vllm部署服务(7B)时,显存占用非常高,这是为什么?
根据vllm的官方博客,我们大致找到了问题的原因,问题出在vllm的机制上。
vllm的作者们观察到,对于LLM来说,所有输入的令牌都会产生它们的注意力键和值张量,并这些张量会被保存在GPU内存中以生成下一个令牌,这被称为KV缓存。然而,这种缓存的特点是大型的和动态的,例如在LLaMA-13B中,单个序列的KV缓存可能高达1.7GB。更重要的是,其大小取决于序列的长度,这个长度是难以预测和有很大变化的。这种情况对KV缓存的有效管理带来了巨大挑战。实际上,现有的系统由于内存的碎片化和过度预留,浪费了60% - 80%的内存资源。为了解决这个问题,他们提出了PagedAttention,这是一种新的注意力算法。它受到了虚拟内存和操作系统中的分页思想的启发。与传统的注意力算法不同,PagedAttention在非连续的内存空间中存储连续的键和值。PagedAttention的工作原理是,它将每个序列的KV缓存分成若干块,每块负责固定数量的令牌的键和值。在进行注意力计算时,PagedAttention算法能够高效地识别并获取这些块,从而提高了内存使用的效率。
vllm的作者们深入分析了,由于这些块在内存中不需要是连续的,我们可以像操作系统中的虚拟内存那样更灵活地管理这些键和值:可以将块视为页面,令牌视为字节,序列视为进程。一个序列的连续逻辑块通过块表映射到非连续的物理块。这些物理块根据新生成的令牌的需求进行分配。
在PagedAttention中,内存浪费只发生在序列的最后一个块。在实际应用中,这导致了接近最优的内存使用率,浪费率仅在4%以下。这种内存效率的提高非常有益:它允许系统将更多的序列批量处理,增加GPU的使用率,显著提高了吞吐量。
PagedAttention还有另一个关键优点:高效的内存共享。例如,在并行采样中,从同一提示生成多个输出序列。在这种情况下,提示的计算和内存可以在输出序列之间共享。PagedAttention通过其块表自然地实现了内存共享。类似于进程如何共享物理页面,PagedAttention中的不同序列可以通过将它们的逻辑块映射到同一个物理块来共享块。为了确保安全的共享,PagedAttention跟踪物理块的引用计数,并实现了写时复制机制。PageAttention的内存共享大大减少了复杂采样算法的内存开销,如并行采样和波束搜索,将它们的内存使用率减少了高达55%。这可以转化为高达2.2倍的吞吐量提升。这使得在LLM服务中,这些采样方法变得实用。
回到我们最开始问题,显存之所以占用那么多,可能是vllm激进的内存占用导致的,这些内存可能并未被实际使用,但被vllm占用了。