状态:草案(2026-11-15),实测。 本文中的数字是由作者于 2026-11-15 亲自测量的值。基于 Kubernetes v1.34、Dynamo v1.6.2 的
vllm-runtime:1.6.2镜像(vLLM 0.31.0)、Qwen3-Embedding-0.6B 模型以及单张 8GB 级别的笔记本 GPU。由于对比对象并非通用产品,而是作者自制的服务器,因此这些数字不能直接照搬到其他服务器上。 编写规则:以实测数据为基础,未能测量的数据标记为“未实测”,无法确认的内容标记为“未确认”。
将嵌入服务器迁移至 Dynamo
目录
1. 为什么是嵌入
第 3 篇和 第 4 篇是通过测试用聊天模型进行测量的。这一次,我们将 Dynamo 应用到了实际使用的服务中。然而,在仔细查看正在使用的服务时,发现它并不是聊天模型。这是一个由自制的 FastAPI 服务器组成的系统,在单张 GPU 上由同一个进程提供用于搜索(RAG)的嵌入、重排序以及语音识别服务。
Dynamo 的代表性功能在这个服务器上毫无用武之地。由于嵌入不生成 Token,因此前缀缓存、KV 路由和分离式服务均不适用。Dynamo 的代码也印证了这一点。嵌入工作进程的帮助文档中写着它会跳过 KV 事件和 KV 路由器的注册(v1.6.2, components/src/dynamo/vllm/backend_args.py).
因此,问题变了。 是否可以移除自制服务器并将其替换为 Dynamo 嵌入工作进程,如果这样做会有什么改变?这就是我们的问题。
2. 架构
Dynamo 的 vLLM 后端通过 --embedding-worker 参数变身为嵌入专用工作进程。在 vLLM 侧,同时传入了 --runner pooling 。
command: ["python3", "-m", "dynamo.vllm"]
args:
- --model
- Qwen/Qwen3-Embedding-0.6B
- --embedding-worker
- --runner
- pooling
- --max-model-len
- "1024"
- --gpu-memory-utilization
- "0.4"
架构由一个前端和一个工作进程组成。模型预先下载了与现有服务器相同的修订版本(Revision),工作进程通过 HF_HUB_OFFLINE=1 启动。 --max-model-len 1024 是与现有服务器的输入限制相匹配的值。
对比步骤如下:
- 向现有服务器发送请求,收集向量和延迟数据(共 3 次)。
- 关闭现有服务器,在相同的 GPU 上部署 Dynamo 架构。
- 向 Dynamo 前端发送相同的请求(共 3 次)。
- 关闭 Dynamo 架构并重新启动现有服务器。
请求内容为 40 个文档(包含韩语和英语,平均每个文档 84 个 Token),每 8 个一组进行批处理。这是因为现有服务器的客户端实际上就是按每 8 个一组打包发送的。延迟测量了 30 次顺序请求以及并发 4 下的 40 次请求,并取 3 次重复测量结果的中位数。
3. 向量是相同的
| 项目 | 现有服务器 | Dynamo 嵌入工作进程 |
|---|---|---|
| 维度 | 1024 | 1024 |
| 第一个向量的长度 (L2) | 1.0 | 1.0 |
| 40个文档的 Token 总数 | 3,375 | 3,375 |
| 各文档的余弦相似度 | 最小 0.9996 · 中位数 0.9998 · 最大 0.9999 |
Token 数量完全相同,向量在小数点后第四位开始出现分歧。由于现有服务器直接使用 transformers 进行推理,而 Dynamo 侧使用 vLLM 进行推理,因此它们的计算顺序和精度处理有所不同。我们并未单独测量这是否是足以改变搜索排名的差异(未实测)。我们不能仅凭这些数字就断定是否可以在不重新构建索引的情况下混用它们。
4. 无法直接无缝替换
两个服务器都开启了 /v1/embeddings 。因为名字相同,看起来似乎只要改一下地址就行,但对于相同的请求,有四个地方给出了不同的响应。
| 请求 | 现有服务器 | Dynamo 嵌入工作进程 |
|---|---|---|
dimensions: 256 |
200,256维 | 500 |
input_type: "query" (查询指令) |
200,附加指令后共 28 个 Token | 200, 忽略指令,共 9 个 Token |
| 超出限制的长输入 | 200,截断为 1,024 个 Token | 500 |
encoding_format: "base64" |
忽略并返回数组 | base64 字符串 |
dimensions. 工作进程日志中留下了原因。vLLM 认为该模型是不支持 Matryoshka 嵌入的模型,当存在 dimensions 时会拒绝请求。现有客户端在所有请求中都带有 dimensions 发送。即使该值与模型的原始维度相同,也会触发“不应存在”的检查,因此如果不修改客户端,所有请求都会失败。虽然我知道 vLLM 中存在将模型作为 Matryoshka 处理的重写选项,但本文未对此进行测试(未实测)。
查询指令。 这种情况更加危险,因为不会报错。Qwen3-Embedding 是一个设计为在查询前附加指令的模型,现有服务器通过接收 input_type 或 instruction 字段来代为处理该工作。而 Dynamo 侧则会丢弃不认识的字段并直接按原文字面量进行嵌入。两方针对相同查询生成的向量的余弦相似度为 0.82 。如果不带指令进行对比,则是 0.9995。如果仅仅更换地址,所有请求都会返回 200,但搜索质量会悄然下降。指令必须由客户端在文本中自行拼接后发送。
长输入。 现有服务器会在限制处进行截断,而 vLLM 在超出限制时会直接拒绝。很难说哪一方是正确的。不过,如果客户端一直将截断工作交由服务器处理,那么就必须把这部分逻辑接过来。
错误代码。 dimensions 和长输入都属于客户端请求错误的情况,但前端返回了带有 Internal server error 的 500 错误。工作进程日志中留下了 vLLM 的验证错误及其原因。由于许多客户端将 500 视为需要重试的对象,照此下去,客户端会不断重复发送无法修复的错误请求。这是在 v1.6.2 中观察到的行为,最新版本是否已更改尚未确认(未确认)。
5. 延迟与吞吐量
这是在同款 GPU(8GB 级别)上测量的值。它是包含 8 个批处理请求的单个请求的延迟,取 3 次重复测量结果的中位数。双方的错误发生次数均为 0.
| 顺序 p50 | 顺序 p99 | 并发 4 p50 | 并发 4 p99 | 并发 4 吞吐量 | |
|---|---|---|---|---|---|
| 现有服务器 | 54.3ms | 76.0ms | 225.9ms | 245.9ms | 141.0文档/s |
| Dynamo 嵌入工作进程 | 36.9ms | 40.5ms | 77.4ms | 108.9ms | 395.8文档/s |
顺序延迟为 0.68 倍,并发 4 时的吞吐量为 2.8 倍。每次重复的吞吐量方面,现有服务器为 139.4·141.0·141.2,Dynamo 为 409.1·395.8·351.9 文档/s。
并发 4 下出现这种现象的原因似乎在于现有服务器的架构。现有服务器用一个全局锁将推理过程包裹起来,一次只能处理一个请求。并发 4 的延迟(226ms)约为顺序延迟(54ms)的四倍,这便是痕迹所在。而 vLLM 会将同时到达的请求打包成一个批次(Batch)。不过,由于我们并未通过释放锁或关闭批处理进行单独测量,因此该解释并非经过验证的结论,而是契合数字的推测。
在阅读本表时有三点需要注意。
- 条件并非完全一致。 现有服务器在同一张显卡上还部署了重排序模型和语音识别模型(显卡显存占用 3.7GB)。而 Dynamo 工作进程仅部署了一个嵌入模型(3.1GB)。
- 现有服务器中可能混入了实际的流量。 在测量期间并未阻止其他请求。
- 对比对象是自制服务器。 不应将其解读为“Dynamo 快了 2.8 倍”,而应解读为“如果将逐个处理请求的服务器更换为具备批处理功能的引擎,就会产生这样的差异”。同样的差异大概率在不使用 Dynamo、仅部署 vLLM 的情况下也会出现。我们并未与仅使用 vLLM 的情况进行对照(未实测)。
6. 无法迁移的部分
现有服务器执行三项任务,而本文尝试迁移的只有一项。
| 功能 | Dynamo v1.6.2 的 vLLM 后端 | 在本文中 |
|---|---|---|
| 嵌入 | --embedding-worker |
实测 |
| 重排序 | 仅在 SGLang 后端侧的代码中找到了重排序处理器 | 未实测 |
| 语音识别 | 代码中存在实时转录处理器 | 未实测 |
如果只迁移嵌入,服务器数量就会从一个增加到两个。现有服务器将三个模型放在一个进程中运行,共享一张 8GB 的显卡;而如果将嵌入独立出来,该显卡上就会有两个进程,并且还会多出一个前端 Pod。如果只有一个工作进程,那么 Dynamo 前端所做的事情就只是将请求转发给该工作进程而已。
7. 总结
- Dynamo 的嵌入工作进程通过两个参数(
--embedding-worker,--runner pooling)成功启动,并在相同的模型中生成了与现有服务器相同的向量(余弦相似度最小 0.9995)。 - 在相同的 GPU 上,顺序延迟从 54ms 降至 37ms,并发 4 时的吞吐量从 141文档/s 提升至 396文档/s。大部分差异似乎源于逐个处理请求的架构与批处理架构之间的区别。
- 仅修改地址的替换将会失败。
dimensions会导致 500 错误,查询指令会被无误地忽略从而导致向量改变(余弦相似度 0.82),长输入不会被截断而是触发 500 错误。 - 在迁移之前,需要在客户端做一些工作。
dimensions必须移除,指令需在文本中拼接,长度需提前截断,并且应避免对 500 错误进行重试。 - 对于只有一个工作进程的嵌入服务而言,Dynamo 所带来的增益微乎其微。所获得的收益大概率来自于将引擎更换为 vLLM,而这即使没有 Dynamo 也能做到。
结合本系列前面的文章来看,其结论如下:KV 路由在存在前缀重叠的聊天请求中发挥作用(第 3 篇)。分离式服务需要具备快速的网络和大模型(第 4 篇)。对于像嵌入这样没有生成(Generation)过程的任务,这两者都不适用,剩下的问题在于 API 契约是否匹配(本文)。
参考
- NVIDIA Dynamo v1.6.2,
components/src/dynamo/vllm/backend_args.py的--embedding-worker帮助文档以及embedding_worker_processes.py. - vLLM 文档:Pooling Models 与 OpenAI 兼容的 Embeddings API。
- Qwen3-Embedding 模型卡片中的指令(Instruction)使用方法。
- 清单、对照脚本(
embed_probe.py)、中位数汇总位于代码仓库的docs/experiments/2026-11-15-dynamo-embedding/中。