用 HTTP 服务化 vLLM:OpenAI 兼容 server¶
基线:vLLM 0.26.0 · 模型 Qwen2.5-7B-Instruct · 单张 RTX 4090
已按 ADR-0004 用 Context7 对照 vLLM 0.26.0 核实:用 vllm serve <model> 启动 server(--host / --port / --uds,--api-key 或环境变量 VLLM_API_KEY——可传多个 key 做轮换,--served-model-name 设置对外的 model id)。它暴露 OpenAI 路由 /v1/chat/completions、/v1/completions、/v1/models,外加工具路由 /health(引擎活着返回 200、死了返回 503)、/ping、/version、/load、/tokenize / /detokenize,以及 Prometheus 的 /metrics。容量由你已熟悉的引擎参数塑造——--max-num-seqs、--max-num-batched-tokens、--max-model-len、--gpu-memory-utilization。本节所有数字均为示例 / 量级参考。
1 · 直觉 & 为什么重要¶
到 Part 7 为止,我们造出并调优了一个引擎——一个把 prompt 变成 token、快到硬件极限的 Python 对象。但没人上线一个 Python 对象。生产服务是一个 HTTP server:客户端从网络发请求,server 把它们复用到引擎上,再把 token 流式发回。本节把引擎变成那个 server。
vLLM 的 server 说 OpenAI API。就这一个决定,让 vLLM 极易上手:任何早已针对 api.openai.com 写好的工具、SDK 或 app——openai Python client、LangChain、LlamaIndex、一个聊天 UI——只要改一行 base_url,就能对着你的 vLLM server 跑。你不是在发明协议;你是在扮演那个支持最广的协议。
面试官期望你真的懂、而非只会比划的两件事:
- server 是什么。 它是同一个引擎核心(scheduler + workers,见架构地图)前面一层薄薄的 FastAPI/uvicorn 前端。前端做 HTTP、鉴权、请求校验,并套用聊天模板 (chat template);引擎核心做批处理与 GPU 计算。知道哪个盒子干哪件事,就知道 bug 或瓶颈住在哪。
- 哪些旋钮是 server 旋钮、哪些是引擎旋钮。
--port、--api-key、--served-model-name塑造接口。--max-num-seqs、--gpu-memory-utilization、--max-model-len塑造容量——它们设的天花板,你会在下一节去测。同一个二进制,两类 flag。
所以:server 暴露什么(endpoints 与鉴权),以及你怎么跟它说话(OpenAI client + 流式)。→ 术语见 术语表 的 SLO、Goodput。
2 · 心智模型¶
一个二进制,两半:一个从 curl 就能看到的 HTTP 前端,和你在 Part 4–7 调过的引擎核心。
vllm serve Qwen/Qwen2.5-7B-Instruct --port 8000
─────────────────────────────────────────────────────────────────────────────
client ──HTTP──▶ API SERVER (FastAPI/uvicorn) ──▶ ENGINE CORE(架构地图)
(openai SDK, HTTP 前端: SCHEDULER
curl, app) • /v1/chat/completions(chat template) │ continuous batching
▲ • /v1/completions (纯文本) ▼
│ • /v1/models (服务 id+LoRA) WORKERS ──▶ GPU
│ • /health /metrics /ping /version /load (PagedAttention)
└─────────────── token 流式返回 (SSE) ◀──────────────┘
接口旋钮 → --port --api-key --served-model-name (客户端怎么跟你说话)
容量旋钮 → --max-num-seqs --max-num-batched-tokens
--max-model-len --gpu-memory-utilization ← 设你要测的天花板
上面的前端/后端划分是一张拓扑(ASCII,按 ADR-0005)。而一条请求穿过这两半的生命周期是一次交互,故用 Mermaid sequenceDiagram(图内标签按 ADR-0005 保持英文):
sequenceDiagram
participant C as Client (openai SDK)
participant S as API server (FastAPI)
participant E as Engine core (scheduler + workers)
C->>S: POST /v1/chat/completions (Bearer key, stream=true)
Note over S: auth, validate, apply chat template
S->>E: add request to the running batch
E->>E: continuous batching — prefill then decode
E-->>S: first token (at ~TTFT)
S-->>C: SSE data chunk
E-->>S: next tokens (paced by TPOT)
S-->>C: SSE data chunks ... then data [DONE]
三个要记住的形状:
- server 是前端;引擎是后端。 HTTP 层便宜、近乎无状态(鉴权、JSON 解析、chat-template 渲染、SSE 流式)。昂贵、有状态的部分——KV cache、运行中的 batch——住在引擎核心。延迟飙升时,几乎从不是 FastAPI 层;是 scheduler 前面的队列(下一节)。
- OpenAI 兼容意味着即插即用。 把
openaiclient 的base_url指向http://your-host:8000/v1,传任意非空api_key,同一份调 GPT 的代码就在调你的 Qwen。/v1/chat/completions套用模型的 chat template;/v1/completions是纯文本进、纯文本出。 /health≠「能接流量了」。/health告诉你引擎进程活着(200)还是死了(503)。它不说「模型加载完了」或「还有余量」。那些是不同的信号——其中一个,你会在两节课后接进自动扩缩器。
3 · 原理¶
3.1 启动 server¶
那一条命令:
vllm serve <model> 启动引擎(下载/加载权重、profile KV-cache block pool),然后起一个 uvicorn server。好用的接口 flag(均在 0.26.0 上核实):
--host/--port—— 绑到哪里。0.0.0.0暴露在所有接口上(见 §6 的安全坑);只给本机用的 dev server 用127.0.0.1。--uds /tmp/vllm.sock绑 Unix 域套接字而非 TCP。--api-key KEY(或环境变量VLLM_API_KEY)—— 要求每个请求带这个 bearer token。可多次传这个 flag 一次授权多个 key,这就是不停机轮换 key 的做法。--served-model-name NAME—— 客户端要在"model"字段里发、且/v1/models会报告的 model id。默认是 HF 路径(Qwen/Qwen2.5-7B-Instruct);设一个稳定别名,客户端就不用硬编码 checkpoint 路径。
3.2 endpoints¶
面试官可能让你列举的路由:
| Endpoint | 方法 | 干什么 |
|---|---|---|
/v1/chat/completions |
POST | 用角色消息 (role messages) 对话;server 套用模型的 chat template。主 endpoint。 |
/v1/completions |
POST | 纯文本进、文本出——不套 chat template。 |
/v1/models |
GET | 列出所服务的模型(id = --served-model-name)+ 任何已加载的 LoRA adapter(multi-LoRA)。 |
/health |
GET | 引擎活着返回 200、死了(EngineDeadError)返回 503。存活性 (liveness)。 |
/ping |
GET/POST | SageMaker 期望的健康检查名。 |
/version |
GET | vLLM 版本——把你的文档/复现钉在它上(ADR-0004)。 |
/load |
GET | server 负载指标(比全套 Prometheus 更轻的一瞥)。 |
/tokenize、/detokenize |
POST | token↔文本、不生成——方便客户端侧数 token。 |
/metrics |
GET | Prometheus 指标(vllm:num_requests_running、vllm:num_requests_waiting、KV-cache 用量、TTFT/ITL 直方图)。可观测性 + 自动扩缩的数据源。 |
3.3 流式¶
默认一次 completion 完整返回一次。传 "stream": true,server 切到 Server-Sent Events (SSE):每个生成的 token(或小 chunk)作为独立 data: 事件到达,以 data: [DONE] 结束。这就是聊天 UI 逐字打印的原理,也是 TTFT(Part 0)成为用户可见数字的原因——有了流式,用户在 prefill 一完成就看到第一个 token,而不是等整段答复完成。
3.4 接口旋钮 vs 容量旋钮¶
同一条 vllm serve 命令带两族 flag。上面的接口旋钮塑造客户端怎么跟你说话。容量旋钮——你调引擎时见过的——塑造你能服务多少:
--max-num-seqs—— 一个 batch 里的最大并发序列数(运行 batch 宽度)。--max-num-batched-tokens—— 每个 scheduler step 的 token 预算(chunked-prefill 的旋钮)。--max-model-len—— 最大上下文长度;封顶单请求 KV cache。--gpu-memory-utilization—— vLLM 可占的 VRAM 比例;越大 → KV-cache block pool 越大 → 并发越高。
这四个设一个实例的天花板。下一节完全在讲测天花板在哪;再下一节讲靠多开实例抬高它。
3.5 在 vLLM 源码里读它(v0.26.0)¶
「引擎核心之上的一层薄前端」就是实打实的文件布局(ADR-0002:读懂 + 会推,不重写):
- 前端是
vllm/entrypoints/openai/api_server.py——挂载路由并流式 SSE 的 FastAPI app。它注册了一个EngineDeadError异常处理器,这正是引擎死掉时/health返回 503 的机制(§2)。 - 每个 endpoint 都是一个 serving handler。
/v1/chat/completions→OpenAIServingChat(chat_completion/serving.py)——注意它的chat_template构造参数:这就是应用 chat template 的类(§3.2)。/v1/completions→OpenAIServingCompletion(completion/serving.py)——原始文本,不套模板。/v1/models→OpenAIServingModels(models/serving.py)。 - 接口 flag(
--api-key、--served-model-name、--host/--port)定义在vllm/entrypoints/openai/cli_args.py。
先打开 api_server.py,再跳到 chat_completion/serving.py——「HTTP/模板」与「引擎核心」的划分是一条文件边界,不只是一张图。
4 · 完整可跑代码 + 逐行讲解¶
启动 server,再用三种方式跟它说话:OpenAI Python client(非流式 + 流式),以及 curl 打运维 endpoint。
# 1) 启动 server(在一个终端里留着运行)
vllm serve Qwen/Qwen2.5-7B-Instruct \
--host 0.0.0.0 --port 8000 \
--api-key sk-demo-key \ # 要求这个 bearer token;重复该 flag 可加更多
--served-model-name qwen2.5-7b \ # 客户端必须发的对外 model id
--max-num-seqs 256 \ # 容量旋钮:运行 batch 宽度
--gpu-memory-utilization 0.90 # 容量旋钮:多少 VRAM → KV cache
"""用标准 openai SDK 跟 vLLM OpenAI 兼容 server 说话。
读代码是离线安全的;要命中网络需上面的 server。"""
from openai import OpenAI
client = OpenAI(
base_url="http://localhost:8000/v1", # 那「一行」:把 OpenAI client 指向 vLLM
api_key="sk-demo-key", # 必须匹配某个 --api-key 值(没设时任意非空串即可)
)
# (a) 非流式对话——server 对这些角色消息套用 Qwen 的 chat template
resp = client.chat.completions.create(
model="qwen2.5-7b", # 必须等于 --served-model-name,不是 HF 路径
messages=[
{"role": "system", "content": "You are a terse assistant."},
{"role": "user", "content": "Name three GPUs good for LLM inference."},
],
max_tokens=64,
temperature=0.7,
)
print(resp.choices[0].message.content) # 完整 completion,一次返回
# (b) 流式——token 作为 SSE 事件到达;第一个 chunk 约在 TTFT 落地
stream = client.chat.completions.create(
model="qwen2.5-7b",
messages=[{"role": "user", "content": "Count to five."}],
stream=True, # 切到 Server-Sent Events
)
for chunk in stream: # 每个 chunk 带下一个 token
delta = chunk.choices[0].delta.content
if delta:
print(delta, end="", flush=True) # 像聊天 UI 一样逐字打印
print()
# (c) 运维 endpoint——health/metrics 不需鉴权;/v1/* 需要鉴权
curl -s http://localhost:8000/health # -> HTTP 200(引擎活着)或 503(死了)
curl -s http://localhost:8000/v1/models \
-H "Authorization: Bearer sk-demo-key" # -> 列出 "qwen2.5-7b"(+ 任何 LoRA adapter)
curl -s http://localhost:8000/metrics | grep -E "num_requests_(running|waiting)"
# vllm:num_requests_running{...} 3.0 # 此刻在运行 batch 里
# vllm:num_requests_waiting{...} 0.0 # 排队、等一个槽位
逐行讲解:
--api-key sk-demo-key—— server 现在拒绝任何不带Authorization: Bearer sk-demo-key的/v1/*请求。重复该 flag(--api-key k1 --api-key k2)可在轮换期间接受多个 key。/health和/metrics保持免鉴权,好让探针和 Prometheus 够得着。--served-model-name qwen2.5-7b—— 把对外 id 与 checkpoint 路径解耦。客户端发"model": "qwen2.5-7b";不匹配就返回模型未找到错误(头号首请求错误,§6)。base_url=".../v1"—— 整件事的关键。openaiSDK 默认打api.openai.com/v1;这一行把它重定向到你的机器。api_key须匹配某个--api-key值;没设--api-key时,任意非空串(惯例"EMPTY")都行。chat.completions.create(..., stream=True)—— 同一个调用,切到 SSE。for chunk in stream循环收到delta.content片段;第一个约在 TTFT 到达,其余由 TPOT(Part 0)配速。非流式会等整段答复完成才返回。/v1/models—— 返回所服务的 id 和任何已加载的 LoRA adapter;客户端靠它发现能问什么。grep num_requests_(running|waiting)—— 概括负载的两个 gauge:现在 batch 里有多少序列、排了多少。waiting > 0且在涨,就是实例饱和的签名——knee 与自动扩缩信号。
5 · Lab —— 立起 server 并把每个 endpoint 都练一遍¶
GPU Lab(单卡)
- 最低显存:
Qwen2.5-7B-InstructBF16、适中--max-model-len约 18–20 GB;24 GB 的 RTX 4090 从容装下。紧张?服务 INT4 量化 checkpoint 并调高--gpu-memory-utilization。 - 建议 AutoDL 卡型: 单张 RTX 4090 (24 GB)——默认主线卡(ADR-0001)。本节不需多卡。
- 预估耗时 / 花费: 上手约 15–25 分钟 · 4090 常见时价下 约 ¥1–3(示例)。先在 无卡模式(纯 CPU)下把模型下好,再开 GPU 服务。
- 平台: NVIDIA CUDA(默认)。非 NVIDIA: server 是纯 Python/FastAPI,各处一致;只有引擎后端不同(AMD ROCm 版 vLLM 暴露相同的 endpoints 和 flag)。
从启动做到可观测:
- 启动。 跑上面的
vllm serve …。看日志报告模型加载与 KV-cache block pool 大小——那个数就是你的并发预算。 - 先存活、后就绪。
curl /health在模型完全就绪、能服务好之前就返回 200;注意它是存活性探针,不是「热好了」信号。确认/v1/models列出qwen2.5-7b。 - 先对话,再流式。 跑
client.py。确认 (a) 返回一整块、(b) 逐字打印。留意第一个流式 token 前那段可感知的延迟——那就是 TTFT。 - 看负载。 循环
curl /metrics | grep num_requests。并发打几个请求(同时开几个client.py),看num_requests_running上升、超过 batch 宽度时num_requests_waiting变正。做完关机。
6 · 常见坑 / 反直觉点¶
- 设了
--served-model-name却发 HF 路径。 若你以--served-model-name qwen2.5-7b启动,带"model": "Qwen/Qwen2.5-7B-Instruct"的请求返回模型未找到。"model"字段必须等于你所选的服务名。(没设--served-model-name时才是 HF 路径。) - 忘了 API key——或以为有。 设了
--api-key,每个/v1/*调用都需Authorization: Bearer <key>,否则 401。没设--api-key时,server 是开放的——任意非空 key 串都过。别把「客户端发了EMPTY」当成「server 已加固」。 - 无鉴权就在不可信网络上绑
0.0.0.0。--host 0.0.0.0把 server 暴露在每个接口。在共享/租来的机器上、无--api-key时,谁够得着端口谁就能烧你的 GPU。本地开发绑127.0.0.1,或在暴露前设 API key(加防火墙)。 - 把
/health当成「能接流量」。/health是存活性(引擎活/死),不是就绪性(加载完 + 有余量),也不是负载信号。把负载均衡就绪与自动扩缩挂在/metrics的 gauge(num_requests_waiting)上,别挂/health。 - 用
/v1/completions却纳闷 chat 格式去哪了。/v1/completions是纯文本——它不套 chat template。角色消息与 system prompt 只在/v1/chat/completions上有效。把裸指令发给/completions会跳过模型被调过的模板,质量下降。 - 把延迟怪到 FastAPI。 HTTP 前端是微秒级开销。若 p99 延迟差,那是引擎队列(并发超过
--max-num-seqs,或 prefill 饿死 decode)——去测 knee,别 profile uvicorn。 - 负载均衡/代理缓冲破坏流式。 中间代理(nginx、部分云 LB)若缓冲响应,会把所有 SSE 事件收齐后一次发出——毁掉逐字效果、抬高感知 TTFT。给流式路由关掉响应缓冲。
- 以为 chat template 到处都套。 模板是在
OpenAIServingChat(chat_completion/serving.py)内部套的,不在OpenAIServingCompletion里——它们是不同路由后的不同类。所以/v1/chat/completions会用模型的 turn marker 框住你的 role messages,而/v1/completions原样透传文本。把 chat 风格的输入发给/v1/completions不会报错;它只是悄悄跳过了 instruct 模型期望的模板。
7 · 面试连线¶
- HTTP 服务化:OpenAI 兼容 server 及其 endpoints —— 本节为你准备的高频题:
vllm serve暴露什么、/v1/chat/completions与/v1/completions的区别、/health保证与不保证什么、鉴权怎么工作、哪些 flag 塑造接口 vs 容量。
8 · 小结 & 延伸阅读¶
一句话: vllm serve <model> 把引擎核心包进一层薄 FastAPI 前端、说 OpenAI API——一行 base_url 就能重定向任何 OpenAI client——暴露 /v1/chat/completions(套 chat template)、/v1/completions(纯文本)、/v1/models(服务 id + LoRA adapter)、/health(存活性:200 活 / 503 死)、/metrics(Prometheus 数据源)与工具路由;鉴权是 --api-key/VLLM_API_KEY(可重复以轮换);接口旋钮(--port、--api-key、--served-model-name)与设天花板的容量旋钮(--max-num-seqs、--max-num-batched-tokens、--max-model-len、--gpu-memory-utilization)相互独立,后者的天花板你接着去测。
延伸阅读:
- vLLM
docs/serving/openai_compatible_server.md—— 完整 endpoint 列表、请求字段、采样参数。 - vLLM
docs/cli/README.md—— 每个vllm serveflag(host/port/uds、api-key、served-model-name)。 - vLLM
docs/usage/security.md—— 工具 endpoint(/health、/ping、/version、/load、/tokenize)与加固说明。 - vLLM 源码(v0.26.0):
vllm/entrypoints/openai/api_server.py(FastAPI app +EngineDeadError→503)、chat_completion/serving.py(OpenAIServingChat、chat template)、completion/serving.py(OpenAIServingCompletion)、cli_args.py(各 flag)——§3.5 的前端。 - vLLM 架构地图 —— 这个前端背后的「引擎核心」到底干什么。
- 下一节 —— 把这个 server 变成一条测出来的吞吐曲线。
9 · 自测小问¶
客户端发 POST /v1/chat/completions、带 {\"model\": \"Qwen/Qwen2.5-7B-Instruct\", ...},却得到模型未找到——即便那正是你在服务的 checkpoint。为什么?
因为 server 启动时把 --served-model-name 设成了别的(比如 qwen2.5-7b),而 "model" 字段必须匹配服务名、不是 Hugging Face checkpoint 路径。--served-model-name 刻意把对外 id 与 checkpoint 解耦,好让客户端不硬编码路径;代价是你对外公布的 id(在 /v1/models 可见)才是唯一能解析的。修法:发 "model": "qwen2.5-7b",或去掉 --served-model-name 让 HF 路径成为 id。
你的 uptime 监控轮询 /health、看到 200、就标记实例就绪——但头几个真实请求慢得离谱或永远排队。误解在哪?
/health 是存活性探针:引擎进程活着返回 200、死了(EngineDeadError)返回 503。它对就绪性(权重加载完并热好)与余量(batch 可能已满、队列很长)只字不提。拿它当就绪/流量信号,会把负载发给一个服务不好的实例。就绪与自动扩缩应读 /metrics 的 gauge——尤其 vllm:num_requests_waiting(队列深度)与 vllm:num_requests_running——它们真正反映实例能不能接更多活。那个队列深度信号,正是驱动路由与自动扩缩的东西。
/v1/chat/completions 与 /v1/completions 的实际区别是什么?发错哪个会悄悄伤到质量?
/v1/chat/completions 收角色消息(system/user/assistant),server 套用模型的 chat template——即 instruct 模型被微调时那套精确的特殊 token 格式。/v1/completions 是纯文本进、文本出、不套模板。若你把一条裸指令发给 instruct 模型的 /v1/completions,它在没有模板 system 框架与轮次标记的情况下运行,模型看到分布外的 prompt,输出质量悄悄劣化——不报错,只是答得更差。instruct/chat 模型用 /v1/chat/completions;/v1/completions 留给 base 模型、或你有意自己控制原始 prompt 时。