目前主流的大模型部署框架以llama.cpp、Ollama和Vllm为主。
llama.cpp是使用没有任何依赖关系的纯 C/C++ 实现,性能很高,可定制化和优化项非常多,但也因为底层是C语言,直接导致上手难度极高。Ollama之所以能够受到最多开发者的关注,一个最根本的原因就是其部署和使用太简单了,兼容多种操作系统,且都提供了一键安装、单命令启动的快捷方式,可以极大降低初学者使用开源大模型的门槛,同时Ollama框架提供原生REST API和OpenAI API兼容性,也可以非常轻松的接入到其他的客户端中,ollama的底层还是llama.cpp,其使用简单,但是在灵活性和拓展性方面较差。vllm是基于pytorch开发的,既没有llama.cpp那么底层,也没有ollama那么高的封装度,优先考虑性能和可扩展性的大模型部署框架,其核心的优化点在高效内存管理、持续批处理功能和张量并行性,从而在生产环境中的高吞吐量场景中表现极佳。
从各种基准测试数据来看,同等配置下,使用 vLLM 框架与 Transformer 等传统推理库相比,其吞吐量可以提高一个数量级,这归功于以下几个特性:
- 高级 GPU 优化 :利用
CUDA和PyTorch最大限度地提高GPU利用率,从而实现更快的推理速度。Ollama其实是对CPU-GPU的混合应用,但vllm是针对纯GPU的优化。 - 高级内存管理 :通过
PagedAttention算法实现对KV cache的高效管理,减少内存浪费,从而优化大模型的运行效率。 - 批处理功能:支持连续批处理和异步处理,从而提高多个并发请求的吞吐量。
- 安全特性 :内置
API密钥支持和适当的请求验证,不像其他完全跳过身份验证的框架。 - 易用性 :
vLLM与HuggingFace模型无缝集成,支持多种流行的大型语言模型,并兼容OpenAI的API服务器。
Vllm 框架仅支持Linux 操作系统
Linux 操作系统部署vllm
- 创建Python虚拟环境,可以使用conda的虚拟环境,也可以使用uv的虚拟环境。
bash
conda create --name vllm python=3.12
- 激活虚拟环境,安装vllm,
pip install vllm
大模型推理服务和调用
在安装完成Vllm 框架后,就可以开始进行大模型推理服务的启动和调用了。Vllm 框架提供了两种启动并调用大模型生成推理服务的方式,分别是离线推理和在线推理。其中:
- 离线推理 : 离线推理类似于使用
Pytorch模块一样,当我们需要使用大模型生成推理服务时,先加载模型,然后使用输入数据运行该模型,并获取输出结果。 - 在线推理: 在线推理类似于有一个服务器,可以先启动大模型,然后等待来自客户端的请求,一旦接收到请求,就会使用大模型生成推理服务,并返回结果,并且可以同时处理多个请求。
这是两种不同推理方式最本质的区别,毫无疑问在线推理是更加符合实际生产环境的。
离线推理
- 下载大模型权重,通过modelscope,hugging face等
modelscope download --model deepseek-ai/DeepSeek-R1-Distill-Qwen-7B --cache_dir './models/'
无论是在线推理还是离线推理,都采用的是API 接口的方式来调用大模型生成推理服务。对离线推理来说,其加载模型、传输数据、获取结果的API 调用形式如下所示:
python
from vllm import LLM
# 加载模型
llm = LLM(model="deepseek-ai/DeepSeek-R1-Distill-Qwen-7B")
# 传输数据
outputs = llm.generate("你好,请介绍一下你自己")
# 获取结果
for output in outputs:
prompt = output.prompt
generated_text = output.outputs[0].text
print(f"Prompt: {prompt!r}, Generated text: {generated_text!r}")
- 需要注意的是,加载模型如果不在本地,会自动从huggingface查找拉取
如果需要本地使用服务器的内容,方案如下
以配置jupyter lab为例,按照常见的步骤进行配置,在本地打开对应jupyter lab的地址,输入密码即可访问vllm环境对应的kernel,其他使用方式和上面一样
bash
llm = LLM(model="/home/08_vllm/deepseek-ai/DeepSeek-R1-Distill-Qwen-7B",
trust_remote_code=True,
tensor_parallel_size=2,
gpu_memory_utilization=0.8,
max_model_len=4096,
)
model:指定要加载的模型的路径。这里是一个本地路径,指向DeepSeek-R1-Distill-Qwen-7B模型(DeepSeek 的 R1 蒸馏版 Qwen-7B 模型)。如果该路径包含模型权重文件和配置文件,vLLM会从该位置读取模型。trust_remote_code=True:允许加载模型时执行来自远程(或本地)的自定义代码。某些模型(如 Hugging Face 上的模型)可能包含自定义的模型结构或预处理代码,需要执行这些代码才能正确加载。设置True表示信任这些代码并执行。tensor_parallel_size=2:张量并行大小,即使用的 GPU 数量。这里设置为 2,表示将模型切分到 2 块 GPU 上进行推理,以利用多卡并行加速。适用于模型太大无法单卡装载或需要更高吞吐量的场景。gpu_memory_utilization=0.8:GPU 内存利用率。指定每个 GPU 最多可使用显存的 80%。剩余 20% 留给其他开销或避免 OOM(显存溢出)。合理的设置有助于稳定运行。max_model_len=4096:模型支持的最大输入+输出长度(以 token 为单位)。这里设置为 4096,即一次请求中上下文和生成的总长度不能超过 4096 token。如果输入或输出超出此限制,vLLM 会拒绝或截断。
- 需要注意的是gpu_memory_utilization可以不是模型实际占用的内存,但是该进程会占用0.8的显存不给其他进程使用。
- 这里要引入一个
SamplingParams的概念。默认情况下,vLLM的离线推理API会使用模型原生定义的采样参数,也就是本地存储权重中的generation_config.json文件中的配置,如果某些开源的模型权重中没有提供该文件,Vllm会根据其定义的SamplingParams类自动指定默认值。不进行定义SamplingParams类,Vllm默认的maxtoken的长度很短,只有16,导致模型输出内容不完全。
最后,完整可用的调用代码如下:
python
from vllm import LLM
from vllm import SamplingParams
# 加载模型
llm = LLM(model="deepseek-ai/DeepSeek-R1-Distill-Qwen-7B")
# 根据 DeepSeek 官方的建议,temperature应在 0.5-0.7,推荐 0.6
sampling_params = SamplingParams(max_tokens=8192, temperature=0.6, top_p=0.95)
# 传输数据
outputs = llm.generate("你好,请你介绍一下你自己。", sampling_params=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}")
除了传入字符串,当输入一个列表时,.generate方法会执行批量推理,针对每一个prompt执行一次推理,并返回一个RequestOutput对象的列表。
python
from vllm import LLM
from vllm import SamplingParams
# 加载模型
llm = LLM(model="deepseek-ai/DeepSeek-R1-Distill-Qwen-7B")
# 根据 DeepSeek 官方的建议,temperature应在 0.5-0.7,推荐 0.6
sampling_params = SamplingParams(max_tokens=8192, temperature=0.6, top_p=0.95)
# 传输数据
text = [
"你好,请你介绍一下你自己",
"请问什么是机器学习",
"请问如何理解大模型?"
]
outputs = llm.generate(text, sampling_params=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}")
对于推理模型可以通过切分think部分,得到自己想要答案部分。
在线推理
离线推理服务中的大模型生命周期是:仅当发生实际的调用请求时,大模型资源才会被加载到显存中,当调用请求结束后,大模型会被立即卸载 ,这种工作模式会导致每产生一轮新的调用都需要重新加载大模型,产生非常大的响应延迟。 因此从工作模式上看,离线推理通常会用于非实时性任务,比如数据预处理、批量文本生成、模型评估等不要求实时返回响应结果,但需要高吞吐量的场景。
对于像实时问答、聊天机器人、AI 助手等对实时性要求较高的场景,企业级应用需处理数千 QPS(每秒查询数),如金融交易、搜索引擎等高并发服务,离线推理显然是无法满足需求的。因此,我们需要掌握vLLM的在线推理方法,这种工作模式才是我们在实际构建大模型应用及高效使用vLLM框架启动的模型服务进行项目开发时,最常用且最优的应用方案。
1.vllm服务器配置参数
对于在线推理服务,我们需要先启动vLLM模型服务,然后才能通过http协议来访问模型服务。启动Http服务器vLLM 提供的是vllm serve命令,可以通过vllm serve --help命令查看详细、可以在启动时指定模型服务启动的参数。
- 首先,从底层架构上来看,
vLLM使用的是FastAPI托管其http服务器。FastAPI服务器在启动时,其可传递的参数定义如下:
python
parser = FlexibleArgumentParser()
parser.add_argument("--host", type=str, default=None)
parser.add_argument("--port", type=parser.check_port, default=8000)
parser.add_argument("--ssl-keyfile", type=str, default=None)
parser.add_argument("--ssl-certfile", type=str, default=None)
parser.add_argument("--ssl-ca-certs",
type=str,
default=None,
help="The CA certificates file")
parser.add_argument(
"--enable-ssl-refresh",
action="store_true",
default=False,
help="Refresh SSL Context when SSL certificate files change")
parser.add_argument(
"--ssl-cert-reqs",
type=int,
default=int(ssl.CERT_NONE),
help="Whether client certificate is required (see stdlib ssl module's)"
)
parser.add_argument(
"--root-path",
type=str,
default=None,
help="FastAPI root_path when app is behind a path based routing proxy")
parser.add_argument("--log-level", type=str, default="debug")
parser = AsyncEngineArgs.add_cli_args(parser)
vLLM 模型服务部署参数
| 参数 | 类型 | 默认值 | 描述 |
|---|---|---|---|
--host |
str |
None |
服务器主机地址。 |
--port |
int |
8000 |
服务器端口,使用 parser.check_port 进行验证。 |
--ssl-keyfile |
str |
None |
SSL 密钥文件路径。 |
--ssl-certfile |
str |
None |
SSL 证书文件路径。 |
--ssl-ca-certs |
str |
None |
CA 证书文件路径。 |
--enable-ssl-refresh |
store_true |
False |
当 SSL 证书文件更改时,刷新 SSL 上下文。 |
--ssl-cert-reqs |
int |
0 |
是否需要客户端证书(参考标准库 ssl 模块)。 |
--root-path |
str |
None |
当应用程序在基于路径的路由代理后面时的 FastAPI root_path。 |
--log-level |
str |
debug |
日志级别 |
vLLM 在线推理服务启动参数
| 参数 | 描述 | 默认值 |
|---|---|---|
| --host | 服务器主机地址。 | None |
| --port | 服务器端口,使用 parser.check_port 进行验证。 | 8000 |
| --api_key | 访问模型服务的API密钥。 | None |
| --served-model-name | 模型服务启动后,在代码环境下通过http协议访问时需要指定的模型名称。 |
无默认值 |
| --trust_remote_code | 信任来自 Hugging Face 的远程代码。 | 无默认值 |
| --tensor_parallel_size | 张量并行组的数量。 | 1 |
| --device | vLLM 执行使用的设备类型:auto, cuda, neuron, cpu, tpu, xpu, hpu。 | "auto" |
这里我们就以Qwen2.5-7B-Instruct模型为例,按照如上参数启动vLLM的在线推理服务。在服务器终端执行如下命令:
bash
vllm serve Qwen2.5-7B-Instruct --served-model-name qwen2.5-7b --api_key edward --host 192.168.110.131 --port 9000 --trust_remote_code --tensor_parallel_size 2 --device cuda
这里的核心参数定义规则是:vllm serve <模型名称> --served-model-name <模型别名> --api_key <API密钥> --host <服务器地址> --port <端口号> --trust_remote_code --tensor_parallel_size <张量并行组数量> --device <设备类型>。其中 vllm serve 后不使用任何参数,直接指定本地模型的存储路径,而--served-model-name 参数则用于指定模型服务启动后,在代码环境下通过http协议访问时需要指定的模型名称。api_key是自己可以随意设置的,相当于一种校验,其他人使用该服务,需要填写设置好的api_key。
当HTTP服务器启动后,首先可以访问 http://localhost:9000/docs (具体地址根据实际部署的模型服务地址来填写,本地就是localhost) 来查看Swagger UI界面,也就是常见的fastapi的界面。
/completions主要用于处理基本的文本生成任务,模型会在给定的提示后生成一段文本。这种类型的任务通常用于生成文章、故事、邮件等。而/chat/completions则主要用于处理面向对话的任务,模型需要理解和生成对话。
python
# ! pip install openai
from openai import OpenAI
client = OpenAI(
base_url="http://localhost:9000/v1", # 这里是 --host + --port 的组合
api_key="edward", # 这里是 --api_key 的参数
)
completion = client.chat.completions.create(
model="qwen2.5-7b", # 这里是 --served-model-name 的参数
messages=[
{"role": "user", "content": "你好,请你介绍一下你自己"},
]
)
print(completion.choices[0].message.content)
2.vLLM模型推理参数
控制模型输出采样参数列表
| 参数 | 描述 |
|---------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---|
| n | 要返回的输出序列数量。 |
| best_of | 从提示生成的输出序列数量。从这些 best_of 序列中返回前 n 个序列。best_of 必须大于或等于 n。默认情况下,best_of 设置为 n。警告:此功能仅在 V0 中支持。 |
| presence_penalty | 浮点数,根据新 token 是否出现在已生成的文本中对其进行惩罚。值 > 0 鼓励模型使用新 token,值 < 0 鼓励模型重复 token。 |
| frequency_penalty | 浮点数,根据新 token 在已生成文本中的频率对其进行惩罚。值 > 0 鼓励模型使用新 token,值 < 0 鼓励模型重复 token。 |
| repetition_penalty | 浮点数,根据新 token 是否出现在提示和已生成文本中对其进行惩罚。值 > 1 鼓励模型使用新 token,值 < 1 鼓励模型重复 token。 |
| temperature | 浮点数,控制采样的随机性。较低的值使模型更确定,较高的值使模型更随机。零表示贪婪采样。 |
| top_p | 浮点数,控制考虑的 top token 的累积概率。必须在 (0, 1] 之间。设置为 1 以考虑所有 token。 |
| top_k | 整数,控制考虑的 top token 数量。设置为 -1 以考虑所有 token。 |
| min_p | 浮点数,表示相对于最可能 token 的概率,考虑 token 的最小概率。必须在 [0, 1] 之间。设置为 0 以禁用此功能。 |
| seed | 用于生成的随机种子。 |
| stop | 停止生成的字符串列表。当生成这些字符串时,生成将停止。返回的输出将不包含停止字符串。 |
| stop_token_ids | 停止生成的 token 列表。当生成这些 token 时,生成将停止。返回的输出将包含停止 token,除非停止 token 是特殊 token。 |
| bad_words | 不允许生成的单词列表。更准确地说,只有当下一个生成的 token 可以完成序列时,才不允许对应 token 序列的最后一个 token。 |
| include_stop_str_in_output | 是否在输出文本中包含停止字符串。默认值为 False。 |
| ignore_eos | 是否忽略 EOS token,并在生成 EOS token 后继续生成 token。 |
| max_tokens | 每个输出序列生成的最大 token 数量。 |
| min_tokens | 每个输出序列生成的最小 token 数量,直到可以生成 EOS 或 stop_token_ids。 |
| logprobs | 每个输出 token 返回的 log 概率数量。当设置为 None 时,不返回概率。如果设置为非 None 值,结果将包括指定数量的最可能 token 的 log 概率,以及选择的 token。注意,实施遵循 OpenAI API:API 将始终返回采样 token 的 log 概率,因此响应中可能有多达 logprobs+1 个元素。 |
| prompt_logprobs | 每个提示 token 返回的 log 概率数量。 |
| detokenize | 是否对输出进行反分词。默认值为 True。 |
| skip_special_tokens | 是否在输出中跳过特殊 token。 |
| spaces_between_special_tokens | 是否在输出中的特殊 token 之间添加空格。默认值为 True。 |
| logits_processors | 修改 logits 的函数列表,基于先前生成的 token,并可选地将提示 token 作为第一个参数。 |
| truncate_prompt_tokens | 如果设置为整数 k,将仅使用提示的最后 k 个 token(即左截断)。默认值为 None(即不截断)。 |
| guided_decoding | 如果提供,引擎将根据这些参数构建引导解码 logits 处理器。默认值为 None。 |
| logit_bias | 如果提供,引擎将构建一个应用这些 logit 偏置的 logits 处理器。默认值为 None。 |
| allowed_token_ids | 如果提供,引擎将构建一个 logits 处理器,仅保留给定 token ids 的分数。默认值为 None。 |
| extra_args | 任意额外参数,可供自定义采样实现使用。未被任何树内采样实现使用。 | |
在不改变vllm serve启动参数的情况下,在调用时,我们可以通过extra_body参数来控制模型推理的生成参数
python
# ! pip install openai
from openai import OpenAI
client = OpenAI(
base_url="http://192.168.110.131:9000/v1", # 这里是 --host + --port 的组合
api_key="muyu", # 这里是 --api_key 的参数
)
completion = client.chat.completions.create(
model="qwen2.5-7b", # 这里是 --served-model-name 的参数
messages=[
{"role": "user", "content": "你好,请你介绍一下你自己"},
],
extra_body={
"temperature": 0.5,
"top_p": 0.9,
"top_k": 10,
"max_tokens": 10,
"repetition_penalty": 1.0,
"length_penalty": 1.0,
"stop": ["\n"]
}
)
print(completion.choices[0].message.content)
3. vLLM模型启动参数
vLLM 框架作为目前在实际生产中使用最为广泛的模型推理框架,在实际使用中是一定需要根据不同的业务需求来调整模型推理的启动参数才能达到一个符合预期的推理效果,并不是仅仅靠默认的启动参数就能够达到最佳的性能,而是由所使用的硬件设备、模型大小、推理需求等共同决定的。因此我们需要简单理解vLLm框架在提供推理服务时其基本的底层工作原理,才能根据实际需求来调整模型推理的启动参数。
首先,当请求通过POST发送到generate接口发送后,会触发AsyncLLMEngine引擎中的add_request函数;
处理新请求的过程与AsyncLLMEngine引擎的操作完全独立。它的唯一目的是将请求入队。AsyncLLMEngine引擎会在另一个线程(或协程)中并发运行,从队列中获取请求。 其中:
- 使用
partial创建一个可以中止请求的函数,并创建一个AsyncStream实例,表示这个请求的流。 - 将请求流和请求参数放入
_new_requests队列中,准备在下一个循环中处理。 - 设置
new_requests_event,通知后台循环有新的请求需要处理。
AsyncLLMEngine引擎的初始化工作会包括 Create worker、Cache engine、Scheduler 的三个初始化。vLLM 引擎会为每个 GPU 分配一个工作线程,从而保证高效的并行性。比如如果我们使用4个GPU,它将创建4个对应的工作线程。Worker 负责 GPU 相关的任务,在AsyncLLMEngine引擎初始化时,会将大模型权重加载到 GPU 上。
每个Worker都有独立的缓存引擎,其中每个缓存引擎管理其 GPU 中分配给 KV 缓存存储的内存。这里的KV缓存,指的就是vLLM 框架提出的PagedAttention机制。 Transformer 架构在解码过程中最大的计算瓶颈就是会对输入的每个token计算注意力,即使用成对的键值张量。这些张量都必须存储在内存中,随着大模型接收越来越长的输入,这些张量消耗的内存会变得非常大。简单地将所有张量存储在内存中会导致内存过度预留和碎片化。这种碎片化会使内存访问效率非常低下,尤其是对于较长的标记序列。为了缓解这些问题,vLLM团队(来自加州大学伯克利分校)提出了PagedAttention。
需要理解的是:PagedAttention 背后的想法是创建映射到GPU内存中物理块的连续虚拟块,通过虚拟内存管理来解决GPU内存碎片化的问题。它把键值对分成固定大小的块,这些块在虚拟地址空间中是连续的,但物理内存中可以分散。这样在推理时,只需要按需加载需要的块,减少内存浪费,提高效率。
因此,PagedAttention会将KV缓存分割为固定大小的块(默认是16个token/块),内存按块分配而非连续预留。同时物理块可分散存储,逻辑上通过映射表维护连续性。所以为了能存储kv缓存,所以需要预留一定量的GPU显存。vLLM 的计算方法是:可用的显存等于总 GPU 显存减去模型权重的大小、中间激活大小和缓冲区(默认为总内存的 10%)。模型大小已知,但中间激活大小未知,即推理过程中中间激活占用的最大内存,vLLM 通过运行虚拟数据然后分析内存消耗来确定此数字。虚拟数据的大小由配置中的参数决定,默认情况下设置为模型支持的最大上下文长度。
vLLM缓存配置参数列表
| 参数 | 描述 | 默认值 |
|---|---|---|
block_size |
连续缓存块的大小(以 token 数量为单位)。Neuron设备设置为 --max-model-len,CUDA 设备仅支持最大为 32 的块大小,HPU 设备默认 128。 |
16 |
gpu_memory_utilization |
模型执行器使用的 GPU 内存比例,范围从 0 到 1。例如,0.5 表示使用 50% 的 GPU 内存。默认值为 0.9。 | 0.9 |
swap_space |
每个 GPU 的 CPU 交换空间大小(以 GiB 为单位)。 | 4 |
cache_dtype |
KV 缓存存储的数据类型。如果为 "auto",将使用模型数据类型。CUDA 11.8+ 支持 fp8(=fp8_e4m3)和 fp8_e5m2。 | "auto" |
is_attention_free |
模型是否为无注意力模型。主要在 ModelConfig 中设置,该值应手动复制到此处。 |
False |
num_gpu_blocks_override |
要使用的 GPU 块数量。如果指定,则覆盖已分析的 num_gpu_blocks。如果为 None,则无效。用于测试抢占。 |
None |
sliding_window |
KV 缓存的滑动窗口大小。主要在 ModelConfig 中设置,该值应手动复制到此处。 |
None |
enable_prefix_caching |
是否启用前缀缓存。V0 默认禁用,V1 默认启用。 | None |
prefix_caching_hash_algo |
设置前缀缓存的哈希算法:"builtin" 是 Python 的内置哈希,"sha256" 是抗碰撞的,但有一定开销。 |
"builtin" |
cpu_offload_gb |
每个 GPU 的 CPU 上要卸载的空间(以 GiB 为单位)。默认值为 0,表示不卸载。 | 0 |
calculate_kv_scales |
启用时动态计算 k_scale 和 v_scale,当 kv_cache_dtype 为 fp8 时有效。如果为 False,则从模型检查点加载。 |
False |
num_gpu_blocks |
分配给 GPU 内存的块数量 | None |
num_cpu_blocks |
分配给 CPU 内存的块数量 | None |
max_model_len,影响kv_cache内存占用大小。
核心代码块如下:
python
@dataclass
class AttentionSpec(KVCacheSpec):
num_kv_heads: int
head_size: int
dtype: torch.dtype
use_mla: bool
@property
def page_size_bytes(self) -> int:
# For MLA we only store a single latent vector
coef = 1 if self.use_mla else 2
return coef * self.block_size * self.num_kv_heads * self.head_size \
* get_dtype_size(self.dtype)
@dataclass
class FullAttentionSpec(AttentionSpec):
@property
def type_id(self) -> str:
return f"full_attention_{self.block_size}_{self.page_size_bytes}"
def max_memory_usage_bytes(self, vllm_config: VllmConfig) -> int:
max_model_len = vllm_config.model_config.max_model_len
return cdiv(max_model_len, self.block_size) * self.page_size_bytes
cdiv 是向上取整除法,计算需要多少个块来存储最大模型Token长度,所以根据源码计算公式为:ceil(max_model_len / block_size) * page_size_bytes,其中:
-
page_size_bytes = 2 * block_size * num_kv_heads * head_size * dtype_size : 块大小 * kv 头数量 * 隐藏层数量 * 数据类型大小 * 2
-
max_model_len :模型能够处理的最大输入序列长度(token 数量);
-
block_size :块的大小,
CUDA下默认配置是 16; -
num_kv_heads: 键值头的数量,即用于存储键值对的注意力头的数量。
-
head_size :隐藏层的大小;
-
dtype_size :使用的 PyTorch 数据类型
其中用于计算的模型架构参数,来源于所加载模型权重文件中的config.json配置
因此,对于 KV Cache 的初始化显存占用配置是根据加载的模型进行计算的,其中:
- max_model_len = 131072 (max_position_embeddings)
- block_size = 16 (默认值)
- num_kv_heads = 4
- hidden_size = 3584
- num_attention_heads = 28
- dtype_size = 2 (bfloat16)
计算方法如下:
bash
单个层的内存需求:
需要内存 = ceil(131072 / 16) * 2 * 16 * 4 * (3584 / 28) * 2
= 8192 * 2 * 16 * 4 * 128 * 2
= 8192 * 32768
= 268,435,456 字节
≈ 256 MB
对于有 28 层的模型(num_hidden_layers = 28),总的 KV 缓存内存需求是:
总需要内存 = 28 * 268,435,456
= 7,516,192,768 字节
≈ 7.00 GB
理解了这个计算公式,已经基本能够解决大家在实际使用vLLM 框架启动模型的过程中对显存占用的疑惑。这也就是为什么同样参数量的模型,为什么在启动时所需预留的显存大小会有差异,根本原因就是在于模型的架构参数不同,会导致KV Cache 的初始化显存预留空间不一样。
能够看到,第一个关键点是max_model_len 的值是不可以超过模型在训练过程中设置的max_position_embeddings 的值的,否则会直接报错。其次,我们需要逐步降低 max_model_len 的值,便可以使 kv cache 的内存需求降低,从而在有限的显存上加载。
max_num_seqs 该参数用来指定并发请求上限:max_num_seqs 会限制AsyncLLMEngine引擎同时处理的请求数量(即 batch_size 的最大值)。 其底层的执行逻辑是:AsyncLLMEngine 会根据 max_num_seqs 将多个请求动态合并为一个批处理(如 4 个请求合并为 1 个 batch),但总序列数不能超过此值。 当进行高并发时,超过 max_num_seqs 会被阻塞(排队等待),直到有槽位释放。
所以,如果我们把max_model_len 设置的很大,比如 100000,那么 max_num_seqs 就需要降低(默认是256),可以这样设置
max_num_seqs 默认值是256。取值越大,能处理的请求数量就会越大,但提升也会有上限,不一定是越大越好,一个测试的经验是:
- 2卡时,max_num_seqs设置为1024,相较于256,速度提升19%。
- 4卡时,max_num_seqs设置为2048,相较于256,速度提升35%;max_num_seqs设置为4096,相较于256,速度提升33%。
所以在有限的硬件资源下,如果想获得极限的并发,我们先把max_num_seqs 设置成较优的1024, 调低max_model_len 的值,然后使用--tensor-parallel-size 来指定双卡运行,确保能正常启动。
以上提及的max_model_len 和 max_num_seqs 的参数需要大家重点关注。从严格意义上来讲,max_num_seqs 和 max_model_len 都属于vLLM 的调度器配置,在执行vllm serve 的最后一个环节,会初始化调度器,
调度器会创建一个块空间管理器来管理逻辑键值缓存ID与其物理存储位置之间的映射。它负责分配和交换内存。此外,调度器还会创建三个队列:运行队列、等待队列和已交换队列,
vLLM底层运行逻辑的最小运行单位是Step,其作用是:用于生成新的 token,或者处理新的提示。 其对应的就是解码和预填充的过程。预填充是指使用提示运行模型,并填充提示标记的键值缓存。解码是指使用先前标记的现有键值缓存生成下一个标记。注意:vLLM 调度程序会根据两个因素来决定是否预填充或解码:是否有请求交换到 CPU,以及是否有新请求。vLLM 遵循 FCFS(先来先服务)规则,优先处理已交换的请求,而不是新请求。
4. 推理类模型的在线推理
vLLM 框架支持 DeepSeek R1系列、Qwen QwQ模型和IBM Granite 3.2 language models 的推理类模型接口的兼容。但是注意:只有QWQ模型目前支持Function Calling功能。与对话类模型不同,推理类模型因存在思考过程,所以在启动推理模型时,需要按照vLLM的规范指定--enable-reasoning 和 --reasoning-parser deepseek_r1 参数。
css
vllm serve DeepSeek-R1-Distill-Qwen-7B --max_model_len 50000 --served-model-name deepseek-r1:7b --gpu_memory_utilization 0.90 --tensor-parallel-size 2 --api_key dragon --host localhost --port 9000 --trust_remote_code --enable-reasoning --reasoning-parser deepseek_r1
特别注意:--reasoning-parser 参数的值必须为deepseek_r1,这指定的vllm内部针对推理类模型兼容的解析器,如果使用QwQ模型,仍然指定的是deepseek_r1。