在 RunPod 上用 vLLM 起一个 OpenAI 兼容接口
手上已经有一套代码在调 OpenAI 的接口,现在要把模型换成自己跑的开源模型,最小的改动路径是什么?答案通常是”找一个能吐出 OpenAI 格式响应的服务端”。这时候两条路摆在面前:自己租一台机器、装好环境、手动把服务拉起来;或者用平台提供好的现成 worker,只填几个配置项。RunPod 上这两条路都走得通,而且第二条路的入口就是官方的 vLLM worker。
麻烦的地方在于,官方 worker 把”改命令行参数”这件事换成了”改环境变量”,而这套变量名不是你按 vLLM 的习惯猜得出来的——它有明确的转换规则,也有一批 RunPod 自己加的、vLLM 里根本没有的变量。下面的内容全部按 RunPod 官方文档的口径整理,变量名与配置项一律照抄文档原文;文档里没写的,我不替它补。涉及单价、显存容量、跑分这类会变的数字,本文只讲机制。RunPod 这个平台本身的两种形态,可以先看RunPod 是什么那篇打底。
一、官方 vLLM worker 到底是什么东西
按文档的说法,vLLM worker 是用来在 RunPod Serverless 上部署和提供大语言模型服务的,带自动伸缩。它有两种拿法:直接从 RunPod Hub 里那个 runpod-workers/worker-vllm 部署,或者把这个仓库当基础自己改。也就是说它不是一个闭源的黑盒,你能看到里面怎么包的,不满意可以 fork。
文档顺带给了 vLLM 本身的定位:一个开源推理引擎,靠 PagedAttention 和连续批处理(continuous batching)来压延迟、提吞吐。PagedAttention 的思路是把 KV cache 切成页来管,让显存用得更省,从而支持更高的并发;连续批处理则是请求一到就排进去处理,不等凑满一批,好处是 GPU 不容易空转。这两条是 vLLM 的看家机制,vLLM 是什么那篇讲得更细。文档另外强调两点:它是 OpenAI 接口的直接替代品,换个 URL 换个 key 就能切;模型侧走 Hugging Face 生态,Llama、Mistral、Qwen、Gemma、DeepSeek 这些主流家族都在支持列表里。
要跑起来需要的东西很少:一个 RunPod 账号、一个 RunPod API key,如果你要部署的是受限(gated)模型,再加一个 Hugging Face access token。走控制台的流程是:在 Hub 里找到 vLLM 仓库,点 Deploy 并选最新的 worker 版本,在 Model 字段填模型名,展开 Advanced 里的 vLLM 设置,设好 Max Model Length,其余保持默认,然后 Next、Create Endpoint。端点开始初始化的这几分钟,RunPod 在准备资源和下载模型。部署完要记下 Endpoint ID,后面所有请求都靠它拼 URL。
控制台里自带一个测试请求,默认输入就是 {"input": {"prompt": "Hello World"}},点 Run 就能验证链路。返回体里除了 output,还有 delayTime 和 executionTime 两个字段——这两个值分开看很有用:前者是请求等到 worker 之前的时间,后者是真正跑模型的时间。冷启动带来的代价主要体现在前一个上,而不是后一个。这套排队与伸缩的机制属于 Serverless 形态的共性,见RunPod Serverless 部署推理服务。
二、配置全靠环境变量,转换规则只有一条
这是本文最该抄准的一节。文档的说法很直接:vLLM 用命令行参数配置,在 RunPod 上你改成设环境变量;转换规则是把参数名转成大写、连字符换下划线。文档给的例子就是 --tokenizer_mode mistral 变成 TOKENIZER_MODE=mistral。
它还给了一整条 Mistral 的对照,原文的 CLI 命令是:
vllm serve mistralai/Ministral-8B-Instruct-2410 \
--tokenizer_mode mistral \
--config_format mistral \
--load_format mistral \
--enable-auto-tool-choice \
--tool-call-parser mistral
对应到 RunPod 侧就是 MODEL_NAME、TOKENIZER_MODE、CONFIG_FORMAT、LOAD_FORMAT、ENABLE_AUTO_TOOL_CHOICE、TOOL_CALL_PARSER 这六个变量。注意最后两个:命令行里是连字符的 --enable-auto-tool-choice、--tool-call-parser,变量名里连字符全变成了下划线。这是最容易手抖的地方。
常用的几个变量按文档的分类大致是这样:
- 模型来源:
MODEL_NAME填 Hugging Face 的仓库 ID 或者本地文件系统路径,换模型就改这一个;MODEL_REVISION指定加载哪个版本;HF_TOKEN用于下载受限或私有模型,公开模型不需要,文档明确建议用 secrets 的方式提供,别当普通环境变量明文塞进去。 - 上下文与精度:
MAX_MODEL_LEN是引擎会为之分配 KV cache 的最大上下文长度,往下调能省显存,长上下文模型才往上抬;DTYPE定权重与激活的数据类型;KV_CACHE_DTYPE单独管 KV cache 的存储类型;QUANTIZATION用于加载量化过的权重,文档特别说了它必须和 checkpoint 的格式对上,不是随手填一个就能省显存的开关。 - 显存与并发:
GPU_MEMORY_UTILIZATION是允许 vLLM 占用的显存比例,撞上 CUDA OOM 就往下调,有余量可以往上抬;MAX_NUM_SEQS是每轮迭代能批处理的序列数上限,往上加可能提高大量短请求场景的吞吐,往下压能省显存;ENFORCE_EAGER强制走 eager 模式,关掉它则是 eager 与 CUDA graph 的混合模式。 - 代码信任与远程加载:
TRUST_REMOTE_CODE决定是否信任来自 Hugging Face 的远程代码。有些模型不开这个就加载不起来,但它的含义是”执行模型仓库里附带的代码”,这个决定该走安全评审,不该由部署顺手勾掉。 - 工具调用与推理模式:
ENABLE_AUTO_TOOL_CHOICE打开 vLLM 的自动工具选择,文档提醒只对支持工具调用的模型开;TOOL_CALL_PARSER要选和模型工具调用格式匹配的解析器,文档列出的可选值包括mistral、hermes、llama3_json、llama4_json、llama4_pythonic、granite、granite-20b-fc、deepseek_v3、internlm、jamba、phi4_mini_json、pythonic。文档还单独加了一条注解:这个解析器告诉 vLLM 怎么理解模型输出的工具调用,如果对不上,工具调用可能检测不到,也可能在解析阶段直接报错。REASONING_PARSER则是给推理型模型开推理模式的,文档举的例子有deepseek_r1、qwen3、granite、hunyuan_a13b,不填就是关闭。 - 对话模板:
CUSTOM_CHAT_TEMPLATE接一个单行的 Jinja2 模板,用来覆盖模型自带的 chat template;文档说它在你想给一个没有内置模板的基础模型发messages时特别有用。
文档还给了一张模型族对照表,这几条是真省时间的:Qwen3 系列建议开 ENABLE_AUTO_TOOL_CHOICE=true 加 TOOL_CALL_PARSER=hermes,AWQ/GPTQ 版本要相应设 QUANTIZATION;Gemma 需要 HF_TOKEN,并建议设 DTYPE=bfloat16;DeepSeek-R1 蒸馏版设 REASONING_PARSER=deepseek_r1 才能拿到思维链输出;Phi-4 在较老的 CUDA 版本上如果初始化出问题,ENFORCE_EAGER=true 可能解决;Llama 3 用 TOOL_CALL_PARSER=llama3_json 配合自动工具选择,并建议用 MAX_MODEL_LEN 防止 KV cache 超出显存;OpenChat 默认不需要额外变量,但默认模板效果不好时可以上 CUSTOM_CHAT_TEMPLATE。
改变量的路径文档写得很具体:端点详情页 → Manage → Edit Endpoint → 展开 Public Environment Variables → 改完 Save Endpoint。名字里那个”Public”提醒了一件事:token 这类东西别放这儿。
三、OpenAI 兼容端点长什么样
这部分是本文标题的落点。文档的口径是:vLLM worker 实现了 OpenAI API 兼容,你可以直接用 OpenAI 的官方客户端库,只需要配好 base URL 和 API key。base URL 的形状是:
from openai import OpenAI
client = OpenAI(
api_key="RUNPOD_API_KEY",
base_url="https://api.runpod.ai/v2/ENDPOINT_ID/openai/v1"
)
把 ENDPOINT_ID 换成你的端点 ID,key 用你的 RunPod API key。文档在排错表里专门列了这一条:认证失败的典型原因就是”用了 OpenAI 的 key 而不是 RunPod 的 key”。
支持的端点是三个:/chat/completions 给指令微调过的对话模型,/completions 给基础模型做文本续写,/models 列出可用模型。model 这个字段的填法有两种合法值——要么就是你部署的那个 Hugging Face 模型名,要么是你通过 OPENAI_SERVED_MODEL_NAME_OVERRIDE 起的别名。这个别名变量很实用:客户端代码里写死的模型名不用改,换模型时在端点侧把别名指过去就行。
跟 OpenAI 相关的还有两个变量:RAW_OPENAI_OUTPUT 控制流式输出是否走原始的 OpenAI SSE 格式,文档说这是 OpenAI 兼容所必需的;OPENAI_RESPONSE_ROLE 定的是对话响应里模型那一方的 role 名。排错表里”响应格式不对”这一条给的解法就是确认 RAW_OPENAI_OUTPUT 打开。
文档也老实列了和 OpenAI 的四点差异:分词器不同导致 token 计数可能不一样;限流按 RunPod 的策略而不是 OpenAI 的;函数/工具调用的可用性取决于模型和 vLLM 的支持情况;视觉与多模态同样取决于底层模型。前两条对迁移影响最实际——按 token 数做计费或配额的业务逻辑,切过来之后口径会变。
除了 OpenAI 接口,还有 RunPod 的原生 API 这一条路。它和其他 Serverless 端点一样用 /run 和 /runsync,区别只在输入格式:对话模型用 input.messages,基础模型用 input.prompt,采样参数放在 sampling_params 里;想让一个 prompt 也走模型的 chat template,加 "apply_chat_template": true。异步的 /run 拿到 id 之后去轮询 /status/{job_id},流式则是提交时带 "stream": True,再去 /stream/{job_id} 读。两套接口的分工我的建议是:客户端已有 OpenAI 生态的东西(SDK、网关、各种工具链),走 /openai/v1;要做批量作业、想利用排队和异步取回,走原生 /run。
顺带一条文档给的工程习惯:请求侧要做带指数退避的重试,用来吸收网络抖动、限流和冷启动。文档示例里的分流逻辑是限流状态码固定等一会儿再试,服务端错误按指数退避,其他错误直接抛出。这个分流比”统一 sleep 几秒”合理得多——不该重试的错误重试三次只是把问题推后。
四、和自己在 Pod 里跑 vllm serve 比,各自让出了什么
官方 worker 省掉的是打镜像、写 handler、处理伸缩这一整段。代价是你被它的配置面约束住:能配的就是文档列出的那些环境变量,vLLM 有而这套变量没覆盖到的参数,你没有官方入口去设。这就是”有一批变量名不是猜出来的”的另一面——它是一层映射,映射总会漏。
自己在 Pod 里手动起服务的取舍正好相反:命令行参数你想怎么给就怎么给,版本你自己钉,但机器是按时间计费的,开着就在花钱,伸缩和高可用得你自己做。vLLM 起 OpenAI 兼容服务那篇讲的是这条路的具体做法。
有一个中间态值得注意:RunPod 的 Pod 支持直接部署 Hub 里那些兼容 Serverless 的仓库。也就是说你可以先在 Pod 里把官方 vLLM worker 跑起来,摸清变量组合,再原封不动搬去 Serverless。这个顺序能避免一个常见的浪费——在 Serverless 上反复改环境变量、反复等 worker 重启来试错,每次试错都要等初始化。
选机的原则文档说得很直白:vLLM 会为 KV cache 预分配内存,所以你需要的显存比”刚好装下模型”要多。文档给了按精度换算每个参数占多少字节的方法,也给了 KV cache 会占掉剩余显存里一个比例区间的口径,具体数值以文档表格为准。内存不够时的三个调法是:下调 GPU_MEMORY_UTILIZATION、减小 MAX_MODEL_LEN、换量化版本的模型。文档还给了一条挺有价值的经验判断:同一个模型在长上下文下 OOM,在短一点的上下文下往往就正常了——上下文越长,KV cache 越大。另外一条是给生产用的:在端点配置里多选几种 GPU 型号,作为硬件层面的兜底。
五、权重怎么进去,决定了冷启动的形状
文档给了两种部署选项,并且明确标了推荐项。
缓存模型(cached models) 是推荐路径。机制是:你在端点上选定一个模型,RunPod 会优先把 worker 起在已经有这个模型的宿主机上;如果没有这样的机器,系统会先把模型下到目标机器上再启 worker,而模型下载这段时间不计入 worker 的计费时间。同一台宿主机上的多个 worker 能共用同一份缓存,不会重复下载。它支持公开、受限(要给 HF token)和你的 token 有权访问的私有模型。对官方 vLLM worker 来说,用法就是把控制台的 Model 字段填好,或者设 MODEL_NAME,worker 会自动用缓存那份。文档给的缓存路径是 /runpod-volume/huggingface-cache/hub/,目录名把模型名里的斜杠换成双连字符,下面还有一层按 commit hash 命名的 snapshots 子目录。这个路径对自己写 handler 的人才重要;用官方 worker 的话,知道它存在就够了。
缓存模型也有两条现在还在的限制,文档写在 Current limitations 里:每个端点同一时间只能有一个缓存模型;如果一个 Hugging Face 仓库里放了多个量化版本,系统目前会把所有量化版本都下下来,按文档的说法”未来会支持选择特定量化”。第二条的实际影响是:挑那种把各种量化混在一个仓库里的模型,你会为不需要的那几份付出下载与磁盘。
打进镜像(baked-in) 是另一条路。文档的定位是它消掉了下载时间、能把冷启动压到秒级,代价是你得自己构建和维护 Docker 镜像。做法就是 Dockerfile 里 COPY 权重目录进去,或者在构建阶段直接下载。文档同时给了一条明确的倾向:如果模型在 Hugging Face 上,强烈建议用缓存模型而不是把模型烤进镜像或在启动时下载;只有模型是私有的、又不托管在 Hugging Face 上时,才推荐打进镜像。文档给的理由是启动更快、成本更低、占用存储更少,而且镜像与模型解耦之后镜像更小、只装应用逻辑。
还有一个选项是网络卷。它能持久保存、多个 worker 共享,但文档提了一条约束值得在方案阶段就考虑到:网络卷会把部署约束在这个卷所在的数据中心,可能影响到 GPU 的可用性。另外文档有一句对比写得很有意思:缓存模型和网络卷用的是同一个挂载路径前缀,但从缓存加载同一个模型会明显快于从网络卷加载。所以”把权重放网络卷”这个看起来最省事的做法,在冷启动这件事上并不是最优解。worker 的容器盘则是临时的,worker 一停或者缩容就没了,handler 默认写出的数据都落在这儿。这几层存储的边界更细的讨论在RunPod 存储怎么选。
六、什么时候用官方 worker,什么时候自己打镜像
先说一个判据:看你要调的参数在不在那张环境变量表里。在表里,官方 worker 就是最省事的选择——一个端点、几个变量、一个 OpenAI 兼容的 base URL,客户端代码基本不动。不在表里,你迟早要 fork runpod-workers/worker-vllm 自己改,那还不如从一开始就算上这笔维护成本。
第二个判据是模型托管在哪。公开或受限的 Hugging Face 模型,走缓存模型这条路,官方文档自己都在劝你别烤镜像。模型是私有的、不在 Hugging Face 上,打镜像基本就是唯一选项,这时候自己维护镜像不是偏好问题。
第三个判据是你对冷启动的容忍度。文档给的缓解方向是缓存模型、FlashBoot、把最小 worker 数设成大于零,最后那条的实际含义是养着常驻算力。如果你的业务连秒级的首字延迟波动都不能接受,那需要面对的不是”配哪个变量”,而是”要不要一直开着机器”——这个问题一旦摆上桌,Serverless 相对于长租一台机器的成本优势也就该重新算了。
最后说几句诚实的话。本文所有事实都来自 RunPod 的官方文档,我没有拿账号逐项验证过控制台的实际行为,更没有跑过任何基准测试。文档里有些地方是留白的:worker 版本与 vLLM 上游版本的对应关系、某个环境变量在哪个 worker 版本开始生效、缓存模型在没有现成宿主机时等待的实际时长,官方文档没有明确说明,以控制台显示和部署时的实际表现为准。还有一点要提醒:这套环境变量表跟着 vLLM 上游走,上游改了参数名,映射也会跟着变——文档末尾那张废弃变量对照表就是证据。所以真正稳的做法是:把你的变量组合写进版本管理,别只留在控制台的表单里;换 worker 版本之前,先在一个新端点上把老配置跑一遍,确认没有变量被改名或移除,再切流量。
算完账发现自建推理不划算?
先用托管端点把业务跑起来,量上来了再回头算自建的平衡点。