vLLM生产栈同时部署对话模型与Embedding模型的完整实践

发布时间:2026/9/29 6:48:57
vLLM生产栈同时部署对话模型与Embedding模型的完整实践 对话模型和向量模型放进同一套 vLLM 生产栈听起来像是不需要纠结的事但实际操作过的朋友应该都有体会同一个 GPU 上既要跑 chat 又要跑 embedding端口怎么分、显存怎么切、镜像版本怎么选、调度器会不会打架每一样都够你折腾一阵子。最近我正好在一台 80G 显存的服务器上把 DeepSeek 风格的中等规模对话模型和 Qwen3-Embedding-0.6B 向量模型同时托了起来中间经历了镜像版本踩坑、显存 OOM、调度参数调整最后把整条链路用 Nginx 统一成了对外单个入口。这篇文章就围绕这次实践把我对 vLLM production stack 的思考和完整部署过程整理出来。1. 为什么对话模型和向量模型会在同一个生产栈里相遇1.1 RAG 只是起点场景倒逼架构很多团队最开始接触对话模型 向量模型的组合都是因为要做知识库问答。用户提一个问题系统先把问题转成向量到向量数据库里召回相关片段再把片段拼进 prompt 里交给对话模型生成答案。这条链路跑通之后大家很快会发现与其维护两套独立的推理服务不如在一个统一的推理栈里把它们一起托起来。理由很直接运维成本低、资源可以动态调配、接口风格还能保持一致。但“一起托起来”不是简单的“同时启动两个进程”。对话模型和向量模型在生产负载特征上差异非常大。对话模型是典型的 decode-heavy 任务每次请求生成几十到几百个 token延迟敏感KV cache 会持续占用显存向量模型则是 encode-only 任务一次前向推理直接输出 embedding 向量不涉及自回归生成大部分场景下请求很短、并发很高。把这两种负载混在一个调度器里如果没有明确的任务隔离互相抢占资源会导致两边都不舒服。1.2 vLLM 在推理栈中的定位vLLM 最早给人留下的印象是“把大模型推理跑得很快”依赖 PageAttention 和连续批处理continuous batching把 GPU 利用率拉上去。后来它的定位逐渐从一个“LLM 推理引擎”扩展成了一个“生产级模型服务框架”也就是你标题里说的 production stack。它内置了 OpenAI 兼容的 API 服务支持 chat、completion、embedding 等多种任务类型提供--task参数来声明当前进程服务的是什么任务。这个能力让 vLLM 可以作为统一的推理底座把对话、向量、甚至排序模型用相同的方式对外暴露。理解这一点很重要vLLM 应对对话加向量混合场景核心思路不是“让一个进程同时处理两种任务”而是“在一个 GPU 集群/一台服务器上用多个 vLLM 实例分别承载不同任务再通过统一的网关对外提供服务”。这是一种更符合生产环境的物理隔离思路。1.3 常见误区一个进程跑多个模型我见过不少刚接触 vLLM 的朋友默认认为可以像 Python 导入库一样在一个服务里把多个模型加载进来然后由路由层决定哪个模型处理请求。实际上 vLLM 当前的官方 serving 方式是一个进程加载一个模型虽然模型内部可以有多个 LoRA adapter但跨模型的并发混跑不在标准 server 模式的支持范围内。所以生产栈的正确姿势通常是每个模型一个 vLLM 实例每个实例一个端口实例之间通过环境变量或网关注入配置。对话模型实例监听 8000向量模型实例监听 8001Nginx 根据请求路径把/v1/chat/completions和/v1/embeddings分别转发到对应端口。用户看到的仍然是一个统一入口背后是清晰隔离的服务进程。2. 两种模型同栈的三个核心决策2.1 镜像版本选型的逻辑这次我用的是docker vllm/vllm-openai:v0.27.1这个镜。像 tag 本身迭代速度非常快可能每隔两周就有新版本很多社区教程里的版本号放到今天不一定还是最新。选择镜像版本有一个基本判断标准优先保证对话模型和向量模型同时被官方支持。具体到实践我会先确认两个模型的架构是否在当前 vLLM 版本的MODEL_ARCHITECTURE支持列表里。对话模型一般问题不大主流系列都有支持向量模型则容易踩坑尤其是比较新的模型。比如Qwen3-Embedding-0.6B这套架构老版本 vLLM 是不认的直接启动会报ValueError: Unknown model format或者Unsupported model architecture。遇到这种报错不要急着改参数先检查版本是不是太旧换新镜像往往就解决了。还有一个很多人忽略的细节vLLM 的 docker 镜像里面只有推理框架不带具体的模型权重。镜像本身不会“自带模型”模型文件需要挂载进去或者在启动命令里通过--model指向 HuggingFace 缓存目录。这点下文会细说。2.2 任务参数背后的调度逻辑差异vLLM 启动命令里的--task参数是整个混部方案里最关键的一个开关。对话模型用--task generate向量模型用--task embed。这两个任务在 vLLM 内部走的是不同的调度路径。对话模型会走完整的 decode loop调度器需要管理 KV cache、做 continuous batching、处理 preemption抢占请求可能因为生成长度超过策略而被暂停、交换或重启。向量模型没有 KV cache调度器只需要把请求 batch 起来做一次前向推理处理长度差异的方式也不同。正因如此把向量请求丢给一个generate任务的服务轻则返回无效结果重则直接报错。我在生产部署里更倾向于不依赖--task auto而是显式指定。自动检测有时候会把一些同时有lm_head和pooler的模型识别错尤其兼容类模型容易出现“模型加载成功但接口 501”的情况。2.3 显存分配多实例 GPU 共享的边界vLLM 默认会尽量吃掉单卡显存--gpu-memory-utilization默认是 0.9。如果你在同一块 GPU 上同时跑两个 vLLM 实例不做显存规划两个进程启动时各自认为自己可以用 90% 的显存第二个进程大概率直接 OOM。我这次的做法是手动切分对话模型给--gpu-memory-utilization 0.55向量模型给--gpu-memory-utilization 0.25剩下约 20% 留给 CUDA context、驱动和少量余量。这个比例不是拍脑袋定的是看显存峰值算出来的。对话模型在服务长对话时 KV cache 需求会涨0.55 已经能覆盖中等并发向量模型虽然单次推理不吃显存但并发高而且需要同时驻留模型权重和激活中间态0.25 也能比较充裕地工作。这里要特别注意两个实例共用一块物理 GPU但它们是独立的 CUDA context。即使按比例分好了CUDA context 本身也会占一部分显存这部分不算在 vLLM 的gpu_utilization里。所以两个实例的分配值加在一起最好控制在 0.85 以下别真把显存算满。3. 可复现的部署全过程3.1 环境与目录准备开始之前先把模型文件准备好。我的习惯是所有模型放在一个统一目录下mkdir -p /data/models cd /data/models # 对话模型 git-lfs clone https://hf-mirror.com/your-org/your-chat-model # 向量模型 git-lfs clone https://hf-mirror.com/Qwen/Qwen3-Embedding-0.6B模型下载完成后检查目录里是否包含config.json、权重文件、tokenizer 文件。常见的问题是权重文件没下全vLLM 加载时只会报一个笼统的“模型加载失败”容易误导排查方向。3.2 启动对话模型服务以 DeepSeek 的蒸馏系列为例启动命令大概是这样的docker run --runtime nvidia --gpus all \ -v /data/models:/models \ -p 8000:8000 \ vllm/vllm-openai:v0.27.1 \ --model /models/deepseek-ai/DeepSeek-R1-Distill-Qwen-7B \ --served-model-name deepseek-r1 \ --task generate \ --port 8000 \ --gpu-memory-utilization 0.55 \ --max-model-len 16384 \ --max-num-seqs 64 \ --trust-remote-code逐个参数说下用意。--served-model-name是对外暴露的模型名客户端请求时在model字段里填这个名字就行和实际路径不需要一致。--max-model-len控制最长上下文我这边业务场景需要 16K如果设得过大KV cache 会吃满vLLM 可能启动时直接拒绝分配所以要根据业务真实需要来。--max-num-seqs是单批次最大并发序列数64 对于和外部系统对接的场景已经不算低再大就要小心显存和延迟的平衡。--trust-remote-code这里说一下。有些模型仓库的代码不在 vLLM 官方架构列表里需要信任远端 Python 代码来加载。这个参数会执行模型仓库里的自定义代码存在安全风险生产环境尽量只用官方或可信仓库里的模型要么提前把自定义代码审查一遍。3.3 启动 qwen3-embedding-0.6b 向量服务向量模型服务和对话模型流程相似但有几个参数完全不同docker run --runtime nvidia --gpus all \ -v /data/models:/models \ -p 8001:8000 \ vllm/vllm-openai:v0.27.1 \ --model /models/Qwen/Qwen3-Embedding-0.6B \ --served-model-name qwen3-embedding \ --task embed \ --port 8000 \ --gpu-memory-utilization 0.25 \ --max-model-len 8192 \ --max-num-seqs 512 \ --trust-remote-code启动后可以先确认日志里有没有Starting vLLM server和Embedding model loaded之类的关键信息。这里有个细节容器内默认监听 8000 端口但对外映射成了宿主机的 8001。假如你用了桥接网络容器的--port 8000和宿主机的-p 8001:8000是两码事别搞混淆。注意--max-num-seqs 512这个值在向量模型场景下可以比对话模型大得多。原因很简单embedding 请求不需要 decode上下文短则显存占用小批得越多吞吐越高。但也不要贪多batch 过大会增加前向推理的单次耗时在 GPU 压满之后收益反而下降。3.4 用代码验证 chat 与 embedding 双接口服务起来之后先用 Python 验证两个接口都正常。vLLM 的 OpenAI 兼容接口可以直接用openaiSDK 来调from openai import OpenAI chat_client OpenAI( base_urlhttp://127.0.0.1:8000/v1, api_keyEMPTY ) resp chat_client.chat.completions.create( modeldeepseek-r1, messages[ {role: user, content: 介绍一下 vLLM 的连续批处理机制} ], max_tokens1024, temperature0.7, ) print(resp.choices[0].message.content) embed_client OpenAI( base_urlhttp://127.0.0.1:8001/v1, api_keyEMPTY ) emb embed_client.embeddings.create( modelqwen3-embedding, input对话模型和向量模型如何共存 ) print(len(emb.data[0].embedding))如果一切正常chat 接口会返回一段文本embedding 接口会打印出向量的维度一般是 1024 或 2048 这类数值。这里要留意几个点其一vLLM 不会自动对 embedding 向量做归一化保存到向量数据库之前建议自己算一下vec / np.linalg.norm(vec)其二input支持传字符串列表一批可以传多个文本比单个请求更省开销其三不同向量模型对超出max_model_len的输入会报错或截断建议在调用层做文本长度预检。3.5 加一道统一网关两个端口对外暴露不好管理我是用 Nginx 统一收口。这样下游只需要记一个地址不需要关心对话和向量分别是哪个端口upstream chat_backend { server 127.0.0.1:8000; keepalive 32; } upstream embed_backend { server 127.0.0.1:8001; keepalive 32; } server { listen 9000; client_max_body_size 16m; location ^~ /v1/embeddings { proxy_pass http://embed_backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /v1/chat/completions { proxy_pass http://chat_backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /v1/models { proxy_pass http://chat_backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这里用^~是因为/v1/embeddings加了前缀匹配避免被后面的/当成普通前缀请求转发到对话模型。假如不加这个Nginx 会按最长前缀匹配一般也会选中^~这个 location但显式声明能少踩很多坑。统一入口除了路由转发还可以附带鉴权。vLLM 本身支持--api-key但只针对当前实例跨实例需要统一 token可以在 Nginx 层用auth_request或者简单的 header 校验实现。4. 生产环境里的调度与性能调优4.1 从 PagedAttention 到 continuous batchingvLLM 调度器谈到生产栈应对混合负载绕不开 vLLM Scheduler。它和传统静态 batching 最大的区别是调度粒度细化到了请求级别。继续有新的请求进来时只要当前 step 有闲置的 KV cache 块或 slot调度器就会把新请求插进去已经完成的请求立刻退出 batch把位置让给后面排队的人。这样一个请求生成 1 个 token 和另一个请求生成 512 个 token可以在同一个迭代周期里互相不拖累太多。对话模型在这种机制下受益明显。不同用户的问题长度、回复长度差异很大continuous batching 能把 GPU 的空闲时间压到很低。但要注意对话模型的调度器在显存紧张时会有 preemption 逻辑也就是把低优先级请求的 KV cache 交换出去或重新计算。生产里如果大量请求被 preemptP99 延迟会很难看。我在部署时通常通过--max-num-seqs控制并发上限从源头降低 preemption 概率。向量模型的调度相对“短平快”因为没有 decode 循环请求可以大批量进入单次前向推理。vLLM 对 embedding 任务的处理同样走 continuous batching区别在于请求很快完成调度开销占比更大所以有时候增加 batch 比增加并发更能提升吞吐。4.2 针对 embedding 吞吐的参数调整把向量模型和对话模型放在同一台机器上最大的矛盾可能是显存分配但真正需要仔细调的是向量服务的吞吐参数。我实践下来的经验是--max-num-seqs可以调到 512 甚至 1024但要先压测观察 GPU 利用率和延迟曲线。--max-model-len不要盲目给大。向量模型主要是文本向量化如果文档片段一般不超过 1000 token把上限设成 2048 就够了。设置过大会让显存按最大长度预留 KV 或激活空间白白浪费。如果业务里有大量短文本几十 token可以考虑在客户端做小批量合并提交但注意批量提交的单次响应延迟会略有上升。压测实际数据出来之后我这边 80G 显卡上向量服务开 512 并发、每请求平均 200 token 的情况下吞吐大概能到几千 QPS 级别对话服务 P99 稳定在 2 秒内两者互不干扰。这个数字不能照搬到你自己的环境不同 GPU、不同模型、不同上下文长度差异极大。4.3 压测指标与预期生产栈上线前一定要做负载测试至少看三个指标TTFT首 token 延迟、TPS每秒生成 token 数、错误率。对向量服务还要额外看单请求 P95/P99 延迟和吞吐上限。vLLM 自带一个压测脚本python -m vllm.benchmarks.benchmark_serving。虽然它有模型和 dataset 两种模式可选但我在混合场景下更喜欢自己写个异步脚本固定并发数随机长短 query统计成功率和延迟分位数。这样更接近实际用户行为也方便在对话和向量服务之间模拟同时打流看资源竞争最激烈时两边是否还能守住 SLA。5. 常见问题与排障速查表5.1 镜像里到底带不带模型这是新人问得最多的问题。vLLM 的 docker 镜像只包含推理框架、算子库和依赖环境不包含任何模型权重。容器启动时--model指向的本地路径如果不存在vLLM 会尝试到 HuggingFace 下载。生产环境尽可能把模型放到本地目录并用-v挂载进容器一是下载速度快二是模型版本可以锁死不会因为远端仓库更新导致权重变化。我还见过一种情况同一个模型目录挂载到容器后vLLM 报找不到tokenizer_config.json。排查下来是挂载路径权限问题容器内用户对目录没有读权限。启动时加-e USERroot或者调整目录权限都能解决但对生产环境来说更好的做法是显式创建普通用户并授权。5.2 端口、密钥、接口路径问题两个 vLLM 实例最容易出的问题就是端口冲突。8000 被对话服务占了第二个服务不加-p或是映射错误就会直接失败。另外vLLM 服务默认不校验 API key加--api-key之后客户端必须带着同样的 key而且对话和向量各自独立外部调用要保证两个 key 一致。接口路径上vLLM 的 embedding 接口是严格对标 OpenAI 的/v1/embeddingschat 接口是/v1/chat/completions。如果用的是老 SDK 或者自定义客户端把路径写成/v1/embedding就会得到 404。这类问题排查时先看请求是不是真的打到了对应端口再盯着返回错误码别一上来就怀疑模型。5.3 CUDA Out of Memory两个 vLLM 实例同时跑在同一块 GPU 上显存问题是最常见的。先nvidia-smi看进程显存占用确认是不是有一个实例吃掉了超过自己额定的量。vLLM 有个特性如果你设置了较高的--gpu-memory-utilization在启动阶段它会把对应显存全部预先占用预先为 KV cache 分配 cache 空间所以两个实例加起来超过物理显存就会 OOM。我的原则是先在单卡上分别观察两个服务的峰值占用再按真实数据分配gpu-memory-utilization留出至少 10% 的余量。还有一个辅助技巧对话模型可以加--enforce-eager减少 CUDA graph 的显存开销但会牺牲一点性能只有在显存非常紧张时才考虑。5.4 新模型架构与镜像版本兼容这问题集中出现在两个场景一个是 GLM 这类有新架构的模型一个是 Qwen3-Embedding 这类刚推出的向量模型。报错通常是ValueError: Unsupported model architecture ...。我看到网上不少针对 GLM 的问题问“glm5.3 应该用 vLLM 哪个版本的镜像”。这类问题其实没有一个万能答案因为新架构从官方支持到稳定要经过若干版本迭代。我的建议是如果模型发布方在文档里指定了 vLLM 版本就按指定版本来如果没指定先到 vLLM 对应版本的 release note 里找该架构的支持记录。比如--trust-remote-code可以绕过架构检测但只能算临时方案长期还是要等官方适配。5.5 Ollama 装了向量模型怎么用跟 vLLM 怎么选有人问 Ollama 装完向量模型后怎么用。这个其实也能用/api/embed接口可以做简单文本向量化。但它和 vLLM 的定位差异很大Ollama 偏单机开发和快速体验模型切换方便资源占用可控vLLM 面向高并发生产OpenAI 兼容性好支持负载压测和精细调度。如果你只是在本机做 RAG 原型装 Ollama 完全够用一条命令就能跑起来。一旦要考虑多服务混合部署、统一鉴权、并发 SLAvLLM 的 production stack 会更合适。两者不是替代关系是不同阶段的工具选择。6. 写在最后的一些体会这套“对话 向量双服务同栈”的部署方式我实际踩过的坑比这篇文字能呈现的多。版本问题是最容易劝退人的尤其新模型出来的时候社区教程里的镜像版本可能已经过期你需要自己去验证。我的经验是多看 vLLM 官方 release note少盲从网上的配置。另外显存管理一定要从第一步就算清楚别让两个实例进入“启动时看着正常业务高峰突然 OOM”的状态。混合负载生产栈的本质不是搞什么黑科技而是把三种不同负载生成、Embedding、未来可能的排序模型在物理资源、调度策略、对外接口三个维度上做清晰的边界划分。边界清楚了整个服务栈自然就稳了。最后再分享一个小技巧两个 vLLM 实例都用同一个镜像和同一个模型目录挂载方式升级时只需要替换镜像 tag 重新启动回滚也简单。我把对话、向量服务的启动命令都写成了脚本每次修改只动参数文件不碰业务代码。这套习惯帮我省掉了大量反复排查问题的时间也推荐给你。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询