我不是没踩过坑。之前用 Ollama 部署公司内部的知识库问答模型,图的就是省事,但等真实业务流量一上来,问题全暴露了:并发一高,响应慢得像爬,显存占用忽上忽下,稍不留神直接 OOM。后来我把推理引擎换成 vLLM,同一张 4090 显卡、同一个模型,吞吐量差不多翻了三倍。这篇就是把我从 0 到 1 部署 vLLM、再用 FastAPI 把推理能力接进业务系统的完整过程写下来,包括环境准备、模型加载、启动参数、接口调用和排障思路。适合两类人看:一类是第一次部署大模型的新手,另一类是已经在用其他推理方案但想提升吞吐量的老手。
1. 为什么最终选择 vLLM:从吞吐量痛点说起
1.1 大模型推理的三条路线对比
部署大模型本地推理,现在主流有三条路:直接用 HuggingFace Transformers 跑推理、用 Ollama 这类傻瓜式工具、用 vLLM 这类高性能推理引擎。我全部试过一遍,说说真实体感。
HuggingFace Transformers 最灵活,写一段推理脚本就能跑,但它默认是"每来一个请求,独立走过一遍完整的前向计算",而且 KV Cache 不做复用和显存管理。模型不大还好,模型一上 7B,并发一高,显存直接爆炸,吞吐量也上不去。Ollama 省心不少,一条命令就能拉起服务,底层还自带显存优化,但它的自定义空间有限,你要做动态 batch、前缀缓存、细粒度显存控制,Ollama 基本帮不上忙。vLLM 站在了中间位置:部署不算复杂,但提供了极高的吞吐量和丰富的生产级参数,这也是它成为目前大模型服务化部署主流选择的原因。
我在本地 4090(24G 显存)上做过简单对比。同一个 7B 模型,Transformers 默认推理,同时来 8 个请求基本就快 OOM 了,响应时间也飙升;换成 vLLM 之后,同样显存下并发几十个请求还能保持稳定,延迟没有明显劣化,吞吐量差距非常明显。如果你的场景是"内部工具、几个开发者自己用",Ollama 够用;但如果你的场景是"业务系统要接 API、并发请求来自多个用户",vLLM 更值得投入。
1.2 PagedAttention 解开了显存束缚
vLLM 吞吐量高的核心秘密是它的 PagedAttention 机制,这个机制值得多花两分钟理解,因为它直接决定了你部署时的显存参数怎么调。
推理过程中,模型需要缓存历史 token 的 Key 和 Value,缓存之间的空间碎片问题就来了。传统的 KV Cache 是按请求长度预先分配一整块连续显存的,但请求长度动态变化,有的请求先长后短,有的先短后长,分配不对就造成大量碎片和浪费。vLLM 借鉴了操作系统虚拟内存的分页思路,把 KV Cache 切分成固定大小的块(block),按需分配,用完释放,碎片几乎为零。
这带来的实际效果是:同样的显存,vLLM 能装下更多的请求并发,因为它的 KV Cache 空间利用率远高于传统方式。我在部署后观察 nvidia-smi,显存占用曲线比以前平稳很多,不再出现"某个时刻突然爆显存"的情况。理解了 PagedAttention,你也就理解了为什么 vLLM 官方一直强调--gpu-memory-utilization这个参数要留给 KV Cache 足够的空间,因为你给模型权重留完显存之后,剩下的是 KV Cache 的可用池子,池子越大,并发能力越强。
2. 部署前的环境准备与版本选择
2.1 硬件、驱动与 CUDA 的硬性要求
vLLM 本质是一个"重依赖"的推理引擎,对底层环境要求比一般 Python 项目高不少。先说硬件:NVIDIA GPU 基本是必须的,AMD GPU 也支持一部分,但体验不如 NVIDIA。推荐显存规格:7B 模型 fp16 推理,建议 16GB 以上显存;如果做量化(INT8、INT4),8GB 显存也能跑,但吞吐量会受影响。我自己用的是 RTX 4090 24G,跑 7B 模型余量很足,跑 13B 量化模型也凑合。
驱动和 CUDA 版本是第一个大坑。vLLM 0.6.x 系列对 CUDA 12.1 和 12.4 支持比较好,但不是说你机器上装哪个 CUDA 版本都行,它实际依赖的是 NVIDIA 驱动自带的 CUDA runtime。先检查:
nvidia-smi这条命令看的是显卡驱动信息,右上角的 CUDA Version 就是当前驱动支持的 CUDA 版本上限。再检查编译器版本:
nvcc --version如果 nvcc 找不到,说明你没装 CUDA Toolkit,但 vLLM 很多场景下只要驱动够新也能跑(因为 PyTorch 会自带 CUDA runtime)。我的经验是:nvcc 有没有都不影响 vLLM 的 pip 安装和运行,只要 PyTorch 装的版本和你的驱动匹配就行。但如果你打算从源码编译 vLLM,那 nvcc 必须装好。
显卡算力也要留意。vLLM 官方要求 GPU 计算能力不低于 7.0,太老的卡(比如 GTX 10 系)跑不起来。GeForce 30 系、40 系都是 8.x,完全没问题。如果驱动版本太老,建议先升级驱动,否则 vLLM 运行时可能出现奇怪的 CUDA error,排查起来很浪费时间。
2.2 Python 环境与安装 vLLM 的注意事项
准备工作一定要做干净,我因为偷懒吃过亏。第一步,用 conda 创建独立环境:
conda create -n vllm-env python=3.10 conda activate vllm-envPython 版本建议 3.10 或 3.11,太旧依赖装不全,太新可能和部分算子编译不兼容。然后升级 pip 并安装 vLLM:
pip install --upgrade pip pip install vllm国内网络环境下载慢的话,建议把 pip 源换成国内镜像,否则装到一半超时会很窝火:
pip install vllm -i https://pypi.tuna.tsinghua.edu.cn/simple装完之后验证一下能不能 import:
python -c "import vllm; print(vllm.__version__)"如果这条命令报错,最常见的原因是 PyTorch 和 CUDA 版本不匹配。vLLM 默认会拉取最新兼容的 PyTorch 版本,但如果你环境里之前有旧版 PyTorch,非常容易产生冲突。我建议在纯净 conda 环境里安装,不要和已有项目混在一起。另外,vLLM 对 FlashAttention 也有依赖,如果你显卡驱动太老,FlashAttention 可能在运行时提示设备不支持,这种情况通常还是驱动版本问题。
2.3 用 Docker 方式部署的额外取舍
除了 pip 安装,还有 Docker 路线。官方提供了镜像vllm/vllm-openai,比如热搜上出现的vllm/vllm-openai:v0.27.1这种 tag。Docker 方式的最大优势是环境隔离,宿主机上 Python 乱成一锅粥也不影响 vLLM 运行。缺点是 NVIDIA 容器需要额外装nvidia-container-toolkit,否则容器里根本认不到 GPU。
我的实际建议是:本地开发调试用 pip 安装就够了,部署到生产服务器用 Docker 更省心,因为生产环境一旦多服务共存,pip 依赖冲突几乎是必然的。Docker 启动命令大致是:
docker run --gpus all \ -v /data/models:/models \ -p 8000:8000 \ vllm/vllm-openai:v0.27.1 \ --model /models/qwen2.5-7b-instruct注意挂载路径,模型权重文件要放到宿主机目录再挂载进容器,否则每次重建容器都要重新下载模型。Docker 方式还有个好处:镜像里已经配好了 CUDA、FlashAttention 这些底层依赖,你几乎不需要关心宿主机驱动之外的任何环境问题。
3. 模型下载与 vLLM 服务启动的完整流程
3.1 模型下载渠道与路径配置
vLLM 本身不负责下载模型,它需要你先把模型权重准备好。最常见的两个渠道是 HuggingFace 和 ModelScope。国内用户我强烈建议走 ModelScope,速度快不止一个量级,而且不需要额外配置代理。
用 ModelScope 下载模型,可以写一行 Python 脚本:
from modelscope import snapshot_download model_dir = snapshot_download( 'Qwen/Qwen2.5-7B-Instruct', local_dir='/data/models/qwen2.5-7b-instruct' ) print(model_dir)参数local_dir可以指定你要保存的路径,建议统一放在一个目录下,比如/data/models,方便多模型管理。下载完成后检查一下目录里有没有config.json、model-00001-of-0000X.safetensors、tokenizer.json这几个关键文件,缺少任何一个都说明下载不完整。
下载模型容易忽略的是 tokenizer 相关文件。有时候你看到config.json和权重都在,但 vLLM 启动时报 tokenizer 加载失败,就是因为少了tokenizer_config.json或者vocab.json。所以下载完最好把整个目录文件列表和 HuggingFace 仓库对照一下,别只盯着 safetensors 大文件。
3.2 启动命令与关键启动参数
vLLM 启动服务有两种方式:老版本用python -m vllm.entrypoints.openai.api_server,新版本(0.6.x 之后)直接有vllm serve子命令。我在 0.6.9 上用的是:
vllm serve /data/models/qwen2.5-7b-instruct \ --port 8000 \ --tensor-parallel-size 1 \ --gpu-memory-utilization 0.9 \ --max-model-len 8192这个命令背后发生了什么值得说一下。vLLM 会先加载模型权重到显存,然后初始化 KV Cache 池,最后启动一个 OpenAI 兼容的 HTTP 服务。启动日志里会出现显存分配情况,比如 "GPU KV cache size: xxx tokens",这个数字代表 KV Cache 能容纳的 token 总数,直接影响你的并发上限。
几个关键参数逐个说:
--port:服务端口,默认 8000,注意别和已有服务冲突。--tensor-parallel-size:多卡并行数。单卡就设 1,双卡设 2。这里有个常见误解,不是设得越大越好,它会把模型切分到多张卡,通信开销也大,单卡能跑就别开。--gpu-memory-utilization:vLLM 能使用的显存比例上限,默认 0.9。建议保留至少 10% 余量给 CUDA context 和其他开销,设成 0.95 以上容易踩坑。--max-model-len:模型支持的最大输入+输出长度。设短了,长文本请求会被截断;设长了,KV Cache 预分配空间就大,并发能力下降。
如果你的场景有明确的输入输出长度需求,比如知识库问答需要支撑长上下文,那--max-model-len就得认真调。我之前在 24G 显存上把 7B 模型 max-model-len 设到 16384,并发明显下降;调回 8192 之后吞吐量提升了不少。这是个 trade-off,没有绝对最优值,要根据实际请求长度分布来定。
3.3 验证服务是否正常工作的三条命令
启动完了别急着写业务代码,先验证服务状态。/v1/models接口可以列出当前加载的模型:
curl http://localhost:8000/v1/models返回 JSON 里会有模型 ID,通常是你在启动命令里传入的路径或模型名。这个接口很重要,因为它能直接确认模型有没有加载成功。
第二条命令是直接测一次最基本的对话补全,用/v1/chat/completions接口:
curl http://localhost:8000/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "/data/models/qwen2.5-7b-instruct", "messages": [{"role": "user", "content": "你好,介绍一下你自己"}], "max_tokens": 100 }'第三条命令是看服务日志。vLLM 启动后日志会输出每次请求的处理时间和 tokens/s 吞吐量。比如 "1 prompt, 100 generated tokens at 78.5 tokens/s" 这种信息,说明请求处理成功了。如果这里的显存分配数值和你预期差距很大,再回头调参数。
4. 基于 OpenAI 兼容接口的推理调用
4.1 为什么 vLLM 能直接用 OpenAI SDK
vLLM 自带一个 OpenAI 兼容的服务端实现,这是它对比其他推理引擎的一大优势。它实现了/v1/chat/completions、/v1/completions、/v1/embeddings这些接口,所以你不需要写任何额外的适配层,可以直接用 OpenAI 官方 SDK 来调用。这意味着你现有的、原本对接 OpenAI API 的代码,只需要改一下 base_url 就能切到本地 vLLM。
这个设计对生产系统帮助很大。我接过的业务系统里,不少项目要求"以后可能切回云端 API",因为 vLLM 接口完全兼容 OpenAI 格式,切换成本几乎为零。调用方只需要改环境变量里的 base_url,代码逻辑一行都不用动。
要注意的是,vLLM 默认并不校验 API key,但 OpenAI SDK 客户端要求必须传一个非空字符串,否则会直接报错。我习惯传一个固定占位值,比如"not-needed",然后把 base_url 指到本地端口。
4.2 curl 测试与 Python 客户端调用示例
我用 curl 测试都是先发一个简单请求,确认服务通不通,再上 SDK。Python 这边的 OpenAI SDK 写法如下:
from openai import OpenAI client = OpenAI( base_url="http://localhost:8000/v1", api_key="not-needed" ) response = client.chat.completions.create( model="/data/models/qwen2.5-7b-instruct", messages=[ {"role": "user", "content": "用三句话解释一下什么是数据库索引"} ], temperature=0.7, max_tokens=512 ) print(response.choices[0].message.content)model参数要和启动 vLLM 时的模型名或路径保持一致。如果你是用vllm serve Qwen/Qwen2.5-7B-Instruct启动的,这边就传Qwen/Qwen2.5-7B-Instruct;如果传的是本地路径启动的,就传那个路径。不一致会报 model not found。
这里还容易忽略一个点:OpenAI SDK 会默认从环境变量读 API key,如果你环境里正好设置过 OpenAI 的 key,它可能会发到错误地址。我建议在代码里显式传api_key="not-needed",从根本上避免这个问题。
4.3 流式输出与采样参数的注意点
vLLM 支持流式输出,把请求里的stream参数设为true,服务端就会按 SSE(Server-Sent Events)格式逐 token 返回。流式输出对用户体验影响很大,尤其在长文本生成场景下,用户不用干等十几秒,而是看到内容一个字一个字蹦出来。
普通调用和流式调用在 SDK 里的写法也不同:
stream = client.chat.completions.create( model="/data/models/qwen2.5-7b-instruct", messages=[{"role": "user", "content": "写一篇关于春天的短文"}], stream=True, max_tokens=1024 ) for chunk in stream: if chunk.choices[0].delta.content is not None: print(chunk.choices[0].delta.content, end="")这里有个小坑:流式返回时最后一个 chunk 的choices[0].delta.content通常为None,它只携带finish_reason字段,所以代码里一定要判空,否则会多打印一行 "None"。
采样参数方面,temperature控制随机性,值越低输出越保守越稳定,适合知识库问答、代码生成;值越高输出越发散,适合创意写作。top_p也是控制多样性的,一般和 temperature 配合,要么动一个,别两个一起乱调。max_tokens决定生成的最大长度,这个值不是越大越好,它会受 vLLM 启动时--max-model-len的限制,超出了会报错。
5. 手写 FastAPI 服务:让业务系统真正接入 vLLM
5.1 FastAPI 项目目录结构
vLLM 自带的服务能满足"能跑"的要求,但它暴露的是最底层的模型接口,缺少业务逻辑、权限校验、参数校验、日志统计这些。所以生产环境中,我通常在 vLLM 前面再套一层 FastAPI 服务,让业务系统只和 FastAPI 打交道。
目录结构我习惯这样组织:
fastapi_app/ ├── app.py ├── config.py ├── routers/ │ └── chat.py ├── services/ │ └── vllm_client.py ├── models/ │ └── schemas.py ├── requirements.txt └── .envapp.py负责创建 FastAPI 实例、注册路由、加载配置;routers/chat.py放业务路由;services/vllm_client.py封装对 vLLM 的 HTTP 调用;models/schemas.py定义请求和响应的 Pydantic 模型。
这样的分层好处是:如果哪天你把 vLLM 换成了别的推理引擎,只需要改vllm_client.py一个文件,路由层和业务层完全不用动。我在项目中实际经历过把 vLLM 换成 SGLang 的情况,真的只改了 service 层,其他代码原封不动。
5.2 异步调用 vLLM 的核心代码
FastAPI 相比 Flask 最大的优势就是原生支持异步。vLLM 本身是一个 HTTP 服务,所以 FastAPI 需要作为客户端去调用它,这一步我用httpx的AsyncClient来实现,整条链路保持异步,不阻塞事件循环。
先写 vLLM 客户端封装:
import httpx from typing import AsyncGenerator VLLM_BASE_URL = "http://localhost:8000/v1" _client: httpx.AsyncClient | None = None def get_client() -> httpx.AsyncClient: global _client if _client is None or _client.is_closed: _client = httpx.AsyncClient( base_url=VLLM_BASE_URL, timeout=httpx.Timeout(connect=10.0, read=120.0, write=30.0, pool=10.0) ) return _client async def chat_completion( messages: list[dict], max_tokens: int = 512, temperature: float = 0.7, stream: bool = False ): payload = { "model": "/data/models/qwen2.5-7b-instruct", "messages": messages, "max_tokens": max_tokens, "temperature": temperature, "stream": stream } client = get_client() if not stream: resp = await client.post("/chat/completions", json=payload) resp.raise_for_status() return resp.json() # 流式情况在下一节单独处理为什么用全局单例的AsyncClient而不是每次请求都新建?因为 httpx 连接池可以复用 TCP 连接,避免反复握手。vLLM 处理一个请求可能耗时几十秒,而连接建立只有几毫秒,但在高并发场景下,频繁创建和销毁连接放大的开销就很可观了。我一开始没做连接复用,压测的时候看到大量 TIME_WAIT 连接,改成全局单例之后明显改善。
然后在路由里调用:
from fastapi import APIRouter, HTTPException from models.schemas import ChatRequest, ChatResponse router = APIRouter() @router.post("/chat") async def chat(req: ChatRequest): try: result = await chat_completion( messages=req.messages, max_tokens=req.max_tokens, temperature=req.temperature ) return ChatResponse( reply=result["choices"][0]["message"]["content"], usage=result.get("usage", {}) ) except Exception as e: raise HTTPException(status_code=502, detail=f"vLLM 调用失败: {str(e)}")这里有几个实操细节。请求体最好用 Pydantic 模型来定义和校验,比如ChatRequest里强制messages必须是列表且不能为空。vLLM 的返回结构里choices[0].message.content是最核心的生成文本,usage里有 token 统计,可以用于计费或监控。异常处理一定要兜住,vLLM 服务重启或者超时都会导致请求失败,业务层要能拿到明确的错误信息,而不是 500 页面。
5.3 通过 StreamingResponse 实现流式返回
如果你的业务要给用户展示打字机效果,FastAPI 这边就要把 vLLM 的流式响应再转发给前端。这个功能的实现并不复杂,但对异步生成器的理解有一定要求。
vLLM 返回的是 SSE 格式流,每行一个data: {...}片段,最后以data: [DONE]结束。FastAPI 侧用StreamingResponse逐块转发:
from fastapi.responses import StreamingResponse import json async def sse_stream(messages: list[dict]) -> AsyncGenerator[str, None]: payload = { "model": "/data/models/qwen2.5-7b-instruct", "messages": messages, "stream": True } client = get_client() async with client.stream("POST", "/chat/completions", json=payload) as resp: async for line in resp.aiter_lines(): if not line.startswith("data:"): continue data = line[5:].strip() if data == "[DONE]": break chunk = json.loads(data) if chunk["choices"][0]["delta"].get("content"): yield f"data: {json.dumps(chunk, ensure_ascii=False)}\n\n" yield "data: [DONE]\n\n" @router.post("/chat/stream") async def chat_stream(req: ChatRequest): return StreamingResponse( sse_stream(req.messages), media_type="text/event-stream", headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"} )这里易踩的坑是 middleware 或者反向代理把响应缓冲了,导致流式变成"等全部生成完才一次性返回",用户体验原地消失。X-Accel-Buffering: no是给 Nginx 看的,告诉它别缓冲这个响应。很多后端开发只写了 StreamingResponse,却忘了在 Nginx 层关闭缓冲,结果前端拿到的根本不是流,排查半天。
5.4 超时控制、并发与连接复用
FastAPI 接入 vLLM 之后,超时控制是第一个要面对的问题。大模型生成是慢操作,一个 1024 token 的响应在推理速度 50 tokens/s 时就要 20 秒,默认的 5 秒超时肯定不够。我用的是前面提到的httpx.Timeout(connect=10.0, read=120.0, write=30.0, pool=10.0),connect 是建连超时,read 是等待响应的时间,120 秒给足余量。如果模型比较慢或者并发高,read 超时可以再调大。
并发方面要注意的是,FastAPI 的异步并不等于无限并发。每个请求最终都会打到 vLLM,vLLM 内部会做 continuous batching,但如果请求太多超过它的调度能力,排队时间就会变长。FastAPI 这层可以加一个简单的信号量做限流,防止流量洪峰把 vLLM 压垮:
import asyncio _semaphore = asyncio.Semaphore(32) async def chat_completion_with_limit(messages, **kwargs): async with _semaphore: return await chat_completion(messages, **kwargs)32 的并发上限是我在 4090 单卡上测试比较稳定的值,具体要根据你模型大小、显存、max-model-len 来调整。加限流还有一个好处:vLLM OOM 通常不是瞬间显存爆掉,而是排队请求太多,KV Cache 池被耗尽。提前在上游限流比在 vLLM 层等报错更优雅。
6. 部署中的常见问题排查与性能调优
6.1 显存不足与模型加载失败的排查链路
vLLM 部署最常见的故障就是 OOM。我梳理一条排查链路,照着走基本能定位问题。
第一步,看启动日志。vLLM 启动时会打印模型权重占用显存和 KV Cache 大小,如果日志里提示 "Cannot allocate memory" 或 "CUDA out of memory",说明--gpu-memory-utilization设得太高,或者 max-model-len 设得太大。先降到 0.8 试,还不行再降 max-model-len。
第二步,看 nvidia-smi。确认有没有别的进程占了显存。你机器上可能同时跑着别的服务,如果显存已经被占了几个 G,vLLM 实际可用的就变少了。我遇到过最无语的场面是服务器上有个残留的 Python 僵尸进程一直占着 10G 显存,排查了半天才发现。
第三步,检查--tensor-parallel-size。如果你设置了大于 1 的值,但实际没有那么多 GPU 可用,vLLM 启动就会报错。单卡机器务必设 1。
第四步,看 CUDA 错误日志。vLLM 报错里如果带 "no kernel image is available for execution on the device",说明你的 GPU 算力和编译时的目标算力不匹配,通常是显卡太老。这种问题只能换卡或者换老版本 vLLM,调参数解决不了。
模型加载失败的另一种常见原因是路径问题。你传了一个相对路径给vllm serve,服务却是在别的目录下启动的,模型自然找不到。我建议统一用绝对路径,并且在启动脚本里写死,别靠相对路径糊弄。
6.2 吞吐量优化参数的实际效果
服务跑通之后就该考虑性能调优了。vLLM 有几个参数直接影响吞吐量,我给一份实测结论。
--max-num-seqs控制单次 batch 中最多同时处理的序列数,默认 256。调低这个值可以减少单 batch 的显存压力,但吞吐量会下降;调高则吞吐量上升,前提是显存够用。我在 24G 显存上跑 7B 模型,设置 128 到 256 之间的区别不大,但设成 512 就开始出问题。
--enable-prefix-caching这个参数值得关注。它会把请求中相同的前缀 token 对应的 KV Cache 缓存下来,下次遇到同样前缀就直接复用,节省重复计算。对于知识库类场景,多轮对话和图片描述这种有很多公共前缀的请求,效果非常明显。实测我开这个参数之后,相同前缀聚合的请求场景吞吐量提升了接近 30%。注意,前缀缓存和 PagedAttention 是两回事,前缀缓存是在 block 粒度上的复用,非常有价值。
还有一个容易被忽略的参数是--cpu-offload-gb,它可以额外使用 CPU 内存来辅助存储 KV Cache。对于显存不够但 CPU 内存充足的机器,适当开启能提升一下并发能力,但 CPU 和 GPU 之间数据搬运有开销,过度使用反而拖慢速度。我只在 8G 显存的机器上用过,算是应急方案。
总结一下性能调优的顺序:先确认 gpu-memory-utilization 和 max-model-len 的搭配是否合理(这是大头),再开启 prefix caching(如果是知识库场景),最后才考虑 max-num-seqs 这些细调参数。别一上来就纠结小参数,主次颠倒。
6.3 生产环境部署的最佳实践
如果这个服务要真正上线,我建议按下面这套配置走。
第一,用 Docker 部署 vLLM。理由前面说过了,隔离依赖、方便回滚。
第二,给 FastAPI 配置进程守护。FastAPI 本身可以用 uvicorn 起,但生产上建议用 systemd 或者 supervisor 托管,崩溃了能自动拉起。由于 FastAPI 调用 vLLM 是异步 I/O,单进程就够用,不需要像同步框架那样开一堆 worker。
第三,日志和监控不能省。FastAPI 侧记录请求耗时、模型名、token 消耗;vLLM 自带的日志已经包含每次请求的吞吐和延迟,可以采集到 Prometheus 这类监控系统。我吃过日志丢失的亏,uvicorn 默认配置下 access log 和业务日志混在一起,排查问题非常痛苦。建议用logging.config.dictConfig把业务日志单独输出到独立文件,并格式化加入 trace_id,这样一条请求从 FastAPI 到 vLLM 的完整链路都能串起来。
第四,配置环境变量管理。模型路径、vLLM 端口、并发上限这些不要硬编码在代码里,用.env文件加 pydantic-settings 管理。换环境部署时只需要改配置,不需要动代码。
第五,测试要充分。上线前至少做一轮并发压测,确定服务的吞吐上限和延迟分布,这样才能给调用方一个合理的限流阈值。我之前压测发现,vLLM 在并发 40 左右时延迟开始明显爬升,所以把 FastAPI 的信号量上限固定在了 32,留一点余量。
部署完成之后,我建议把启动命令和关键参数写进 README,尤其要写清楚为什么选这些参数。大模型部署涉及的因素太多,光靠记忆很容易忘。
从实际项目经验来看,vLLM 部署最核心的心得就是一句话:显存利用率和模型长度是全局的开关,多花时间把这两个参数调明白,比折腾任何花活都管用。而 FastAPI 那层封装,别贪多,保持薄薄的、清晰的边界,让业务代码始终只面对一个简单的"发消息、拿回复"接口,后面的引擎怎么变都不影响你的系统。