4、vLLM核心概念:LLMEngine、SamplingParams、AsyncLLMEngine的职责与关系
好,咱们今天来啃vLLM最核心的几个概念。说实话,我刚接触vLLM的时候,也被LLMEngine、AsyncLLMEngine、SamplingParams这几个东西绕得有点晕。它们到底谁管谁?为什么非要搞两个Engine?
别急,咱们一个一个拆开看。你想想看,vLLM本质上是一个推理服务框架,它要解决的核心问题就两个:怎么高效跑模型,以及怎么优雅地对外提供服务。LLMEngine和AsyncLLMEngine,就是分别对应这两个问题的答案。
4.1 LLMEngine:推理的“发动机”
LLMEngine是vLLM最底层的推理引擎。说白了,它就是那个真正干活的人——加载模型、管理KV Cache、调度请求、执行推理。
我个人习惯把LLMEngine理解成一个单线程的推理调度器。它内部维护了一个请求队列,每次调用它的step()方法,它就会从队列里取出一个batch的请求,执行一次前向传播,然后返回结果。
它的核心职责可以归纳为三点:
- 模型加载与管理:负责把HuggingFace的模型加载进来,并转换成vLLM内部的执行格式。我在项目中遇到过一个问题,就是模型加载时显存分配策略没选对,导致OOM。后来发现LLMEngine的
max_num_seqs参数可以控制并发请求数,调小一点就解决了。 - 请求调度与批处理:vLLM最牛的地方就是动态批处理。LLMEngine会持续收集请求,等到攒够一个batch,或者有请求超时了,就触发一次推理。这个调度策略是vLLM吞吐量高的关键。
- KV Cache管理:这是vLLM的内存管理核心。LLMEngine会为每个请求分配一块连续的显存空间来存KV Cache,并且通过PagedAttention技术来避免内存碎片。嗯,这里要注意,如果请求太长,KV Cache可能会撑爆显存,所以
max_model_len这个参数要设好。
下面这张图展示了LLMEngine的内部工作流程:
4.2 SamplingParams:控制“怎么生成”
SamplingParams,名字听着挺唬人,其实就是一组采样参数。它告诉模型:你生成下一个token的时候,用什么样的策略?
我刚开始用的时候,经常把temperature和top_p搞混。后来我总结了一个记忆方法:temperature控制“随机性”,top_p控制“候选集”。
常用的参数有这些:
| 参数名 | 作用 | 典型值 |
|---|---|---|
temperature |
控制输出的随机性。值越大,输出越多样;值越小,输出越确定。 | 0.7(创意写作),0.1(代码生成) |
top_p |
核采样。只从累积概率达到top_p的token中采样。 | 0.9 |
top_k |
只从概率最高的k个token中采样。 | 50 |
max_tokens |
生成的最大token数。 | 1024 |
stop |
停止词列表。遇到这些词就停止生成。 | ["\n", "。"] |
frequency_penalty |
频率惩罚。值越大,越避免重复已生成的词。 | 0.0 ~ 2.0 |
4.3 AsyncLLMEngine:异步的“服务层”
好,现在问题来了。LLMEngine是单线程的,它一次只能处理一个batch。但我们的Web服务通常要同时处理几百个请求,怎么办?
答案就是AsyncLLMEngine。它本质上是一个异步包装器,把LLMEngine包了一层,让它能跑在异步事件循环里。
AsyncLLMEngine的核心设计思路是:请求进来后,先丢进一个异步队列,然后立刻返回一个Future对象。后台的LLMEngine会不断从队列里取请求,执行推理,推理完成后把结果塞回Future里。这样,前端就不用傻等着了。
我曾经踩过一个坑:直接用LLMEngine去处理Web请求,结果并发一上来,请求全堵住了。后来换成AsyncLLMEngine,配合FastAPI的异步接口,吞吐量直接翻了5倍。
4.4 三者的关系:一张图说清楚
这三者的关系,我用一个比喻来解释:
- LLMEngine = 工厂里的生产线。它负责把原材料(请求)加工成产品(输出)。但生产线一次只能处理一批货。
- AsyncLLMEngine = 工厂的接单窗口。客户下单后,窗口给一个取货号(Future),然后客户就可以去干别的事了。窗口会协调生产线,等货好了通知客户。
- SamplingParams = 产品规格书。客户下单时告诉窗口:我要红色的、带花纹的、尺寸是XXL的。生产线就按这个规格来生产。
下面这张图展示了它们之间的协作关系:
4.5 代码示例:怎么用
说了这么多理论,咱们直接看代码。下面是一个完整的示例,展示了怎么用AsyncLLMEngine配合FastAPI搭建一个推理服务:
from vllm import AsyncLLMEngine, SamplingParams
from vllm.engine.arg_utils import AsyncEngineArgs
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import asyncio
# 初始化引擎参数
engine_args = AsyncEngineArgs(
model="Qwen/Qwen2.5-7B-Instruct",
max_model_len=8192,
max_num_seqs=32,
gpu_memory_utilization=0.9
)
# 创建异步引擎
engine = AsyncLLMEngine.from_engine_args(engine_args)
app = FastAPI()
@app.post("/generate")
async def generate(prompt: str):
# 设置采样参数
sampling_params = SamplingParams(
temperature=0.7,
top_p=0.9,
max_tokens=1024,
stop=["\n\n"]
)
# 异步生成
results_generator = engine.generate(prompt, sampling_params)
# 流式返回
async def stream_results():
async for result in results_generator:
yield result.outputs[0].text
return StreamingResponse(stream_results(), media_type="text/plain")
4.6 避坑指南
最后,分享几个我实战中遇到的坑:
- 显存泄漏:如果你发现服务跑着跑着显存越来越高,很可能是LLMEngine的KV Cache没有正确释放。检查一下
max_num_seqs和max_model_len是否设置合理。 - 请求超时:AsyncLLMEngine的异步队列如果积压太多请求,后面的请求可能会等很久。我建议设置一个
request_timeout参数,超时的请求直接返回错误,别让用户一直等着。 - 采样参数冲突:temperature和top_p同时设置时,vLLM会先应用top_p,再应用temperature。如果你发现输出不符合预期,可以试试只设置其中一个。
好了,这一章的内容就到这里。核心就是记住:LLMEngine是干活的,AsyncLLMEngine是接单的,SamplingParams是告诉它怎么干的。搞清楚了这三者的关系,vLLM的异步推理架构你就掌握了一大半。
公众号:蓝海资料掘金营,微信deep3321