Embedding & Pooling:用 SGLang 跑非生成任务
谁该读这一篇? 要在 SGLang 上同时服务 chat / embedding / rerank 任务的工程师。 前置阅读:
00-prerequisites.md。 耗时: 25 分钟 学完能: 1. 用 SGLang 跑 Qwen3 / BGE / E5 / Jina 嵌入模型; 2. 解释 pooling 策略(mean / cls / last); 3. 区分 embedding 和 generation 在调度 / KV 上的差异; 4. 跑 rerank 任务; 5. 评估是否值得让 chat 和 embedding 共用 server。
1. Embedding 任务是什么
Input text ─▶ Tokenize ─▶ Transformer ─▶ Hidden states ─▶ Pool ─▶ Vector [hidden_dim]
- 输出不是 token id,是 fixed-dim vector。
- 用于:相似度计算、retrieval、RAG。
- 没有 decode 循环,只跑一次 forward。
2. SGLang 启动 embedding server
sglang serve Qwen/Qwen3-Embedding-4B \
--is-embedding \
--tp 1 \
--port 30000
--is-embedding 为 decoder-style embedding checkpoint 表达任务意图;原生 encoder
embedding 架构和 EmbeddingGemma 可自动识别。
OpenAI 兼容 API:
from openai import OpenAI
client = OpenAI(base_url="http://localhost:30000/v1", api_key="EMPTY")
resp = client.embeddings.create(
model="Qwen/Qwen3-Embedding-4B",
input=["text 1", "text 2"]
)
print(resp.data[0].embedding[:5]) # [0.012, -0.034, ...]
3. Pooling 策略
不同模型用不同 pooling:
| 策略 | 公式 | 用模型 |
|---|---|---|
| CLS | 取 [CLS] token 的 hidden state |
BERT 系 |
| Mean | 平均所有 token | E5 / BGE |
| Last | 取最后一 token | GPT-style embedding(Llama embedding) |
| Weighted | attention-weighted | 自定义 |
Pooling 是模型正确性的一部分。SGLang 通过模型实现与
embedding_model_spec.py
解析 CLS/mean/last/model-defined 策略;当前没有通用的 --pool CLI 覆盖项。自定义模型
必须在 model config/实现中声明正确 pooler,不能靠启动时猜测。
4. Embedding 在 SGLang 内部
sequenceDiagram
participant C as Client
participant T as TokenizerManager
participant S as Scheduler
participant W as ModelWorker
participant TM as TokenizerManager
C->>T: POST /v1/embeddings
T->>S: EmbeddingReqInput
S->>W: forward(extend mode, output_hidden=True)
W-->>S: hidden_states
S->>S: pool → embedding vector
S-->>T: BatchEmbeddingOutput
T-->>C: JSON 响应
差异:
- 不走 detokenizer(没有 token 流)。
- 不需要 KV cache 累计(一次 forward 结束)。
- Scheduler 用
forward_mode=extend,但跑完不进 decode。
5. 与 chat 模型共用 server?
一个 SGLang server 进程加载一套已解析的模型与任务能力。生产上应使用:
- chat server 一个进程。
- embedding server 另一个进程。
- 客户端 / router 按任务类型路由。
理由:
- 模型架构差异(embedding 模型一般小、是 encoder-only)。
- 调度策略不一样(embedding 不需 cache-aware)。
- 资源配比不同。
若确实要用 decoder backbone 产 embedding,应以 embedding 模式启动经过训练/验证的 decoder-style embedding checkpoint;这并不等于同一进程还能同时提供正常 chat generation。
6. Matryoshka 与稀疏 embedding
当前 OpenAI embedding API 支持用请求字段 dimensions 截断 Matryoshka 模型的输出;
模型 config 已包含 matryoshka_dimensions/is_matryoshka 时会自动校验,也可通过
--json-model-override-args 为自定义 checkpoint 声明。SGLang 还包含 sparse pooler 支持,
但没有通用的 --enable-multi-vector 参数,也不要假定一个 BGE-M3 请求会自动同时返回
dense/sparse/ColBERT 三套表示;应以当前 supported-models 文档和实际响应 schema 为准。
7. Rerank 任务
输入:query + N 个候选 document,输出:每个 doc 的相关性 score。
# /v1/rerank 不是 OpenAI 标准端点,但 SGLang 提供兼容实现
resp = requests.post(
"http://localhost:30000/v1/rerank",
json={
"model": "BAAI/bge-reranker-large",
"query": "什么是 RadixAttention?",
"documents": ["doc 1...", "doc 2...", "doc 3..."]
}
)
for item in resp.json():
print(item["index"], item["score"])
Cross-encoder 内部把 (query, doc) 拼起来做 forward,输出分数。 延迟比 dense embedding 高(每对 query-doc 一次 forward)。
8. 性能特征
Embedding 没有逐 token decode,通常更适合大 batch;但 latency/throughput 会随模型、输入 长度、batch、精度、attention backend 和 GPU 显著变化,不能复用固定的 H100 数字。用当前 内置基准在目标 workload 上测量:
python -m sglang.bench_serving \
--backend sglang-embedding \
--base-url http://127.0.0.1:30000 \
--model Qwen/Qwen3-Embedding-4B \
--dataset-name random --random-input-len 512 --random-output-len 0 \
--num-prompts 1000 --max-concurrency 64
9. 批量请求
embedding 天然适合大 batch:
client.embeddings.create(
model="...",
input=[t1, t2, t3, ..., t256] # 一次 256
)
SGLang 内部 batch 一次 forward 跑完,throughput 极高。
10. 模型选型
| 任务 | 推荐 |
|---|---|
| 通用英文 | BGE-large-en-v1.5 |
| 通用中文 | BGE-large-zh-v1.5 |
| 多语言 | multilingual-e5;其它模型先查当前 supported-models 列表 |
| 长文档 | nomic-embed-text-v1.5 (8k context) |
| Rerank | BGE-reranker-large |
| 极致质量 | Voyage / Cohere 闭源 |
11. 常见问题
| 现象 | 排查 |
|---|---|
| 启动报 "not embedding model" | 模型架构不是 encoder-only;加 --is-embedding 强制 |
| Embedding 维度不对 | 检查 model config;可能加了 projection |
| 同输入输出不一致 | 数值精度问题(fp16);用 fp32 验证 |
| 慢 | batch 太小;增 input list |
| 输出全 0 | 模型加载失败;查启动日志 |
12. 监控
当前 collector 没有 sglang:embedding_throughput 或
sglang:embedding_latency_ms。使用 sglang:num_requests_total 与
sglang:e2e_request_latency_seconds 观察请求级行为;一个请求可含多个 input,若要计算
vectors/s,还需在 API gateway 记录每次请求的 input item 数。p99 和吞吐目标应来自上面的
目标硬件基准。
13. 小结
- Embedding 任务一次 forward 出 vector,无 KV cache 累计。
- Pooling 策略 cls / mean / last 各对应不同模型。
- 原生 encoder embedding 架构可自动识别;decoder-style embedding checkpoint 仍用
--is-embedding表达任务意图。 - Rerank 是 cross-encoder,延迟更高。
- 通常 chat 和 embedding 独立部署,按业务路由。
14. 自检
- Embedding 为什么不进 detokenizer?
答案
Embedding 任务的输出是 **fixed-dim vector**(如 `[1024]` float),不是 token 流。 Detokenizer 的工作是 token id → 字符串(增量 BPE 解码),对 vector 输出没意义。 Embedding 流程: (1) TM tokenize prompt → token id 列表。 (2) Scheduler 一次 forward 拿 hidden states。 (3) Pooling(cls / mean / last)→ 单个 vector。 (4) Scheduler 直接发 `BatchEmbeddingOutput` 给 TM(**跳过 Detokenizer**)。 (5) TM 返回 JSON 给客户端。 架构上 Embedding 走的是 "Scheduler → TM" 直接回流路径,少一跳。- CLS / mean / last pooling 各对应什么类型模型?
答案
- **CLS pooling**:取 `[CLS]` token 的 hidden state。**BERT 系**(encoder-only,bidirectional):BERT、RoBERTa、早期 sentence-BERT。CLS token 在训练时被设计为 "represent the whole sentence"。 - **Mean pooling**:所有 token hidden state 平均。**E5 / BGE 系**(modern 中英文 embedding 模型):训练时 in-batch negative + cosine similarity loss,mean 比 CLS 经验上更稳。 - **Last pooling**:取最后一个 token 的 hidden state。**GPT-style / LLM-based embedding**:Llama embedding、Mistral embedding、E5-Mistral。LLM 是 decoder-only causal,只有最后位置看到全文,自然成为"整段摘要"。 选错 pooling:embedding 完全错(数值合理但语义不对),cosine similarity 接近随机。 SGLang 按模型实现/spec 选择;自定义模型应实现或声明正确 pooler,不存在通用 `--pool` 覆盖参数。- Matryoshka embedding 的
dimensions有什么作用?
答案
Matryoshka 模型在训练时让向量前缀也保持可用语义。请求中的 `dimensions` 将完整向量 截断到模型声明支持的维度,从而用一定召回质量换取更小的向量库和更快的相似度计算。 服务端会依据 `matryoshka_dimensions` 或 `is_matryoshka` 校验;普通 embedding 模型不能 任意截断后仍假定质量成立。- Rerank vs dense embedding 在 RAG 里怎么配?
答案
两阶段检索是 RAG 标准做法: - **阶段 1 - Dense embedding 召回**:业务向量库(如 Milvus)按 cosine similarity 召回 top-100 文档;快(百万级文档毫秒)。 - **阶段 2 - Cross-encoder rerank**:对召回的 100 个 (query, doc) 对跑 cross-encoder(如 BGE-reranker-large),输出相关性 score,按 score 排序取 top-5。 差别: - Dense(bi-encoder):query 和 doc 独立编码,速度快但精度低(特别是细微匹配)。 - Rerank(cross-encoder):(query, doc) 拼起来一次 forward 算 attention,通常能改善细粒度匹配,但收益必须在业务 eval 上测量。 开销:rerank 需要对 query-document pair 联合编码;批量送 SGLang `/v1/rerank`,并在目标长度与 batch 上实测。 配置:dense 取宽(top-100)保 recall,rerank 取窄(top-5)保 precision。- 为什么 chat 与 embedding 通常要拆成不同 server?
答案
一个 server 只加载一套模型/任务能力,不能同时装独立的 chat 与 embedding checkpoint。 即便使用同一 decoder backbone 的不同部署方式,两类 workload 也有明显差异:chat 需要 KV cache、prefix 复用和持续 decode;embedding 更适合大 prefill batch,通常不累计 KV。 两者的模型架构、batch 形状和资源配置不同。 推荐:**分独立副本组**。embedding 使用按其 batch/长度调优的副本,chat 使用按 KV cache 和 decode 调优的副本;Gateway 按模型/endpoint 路由。容量边界应由压测决定, 不使用固定 QPS 阈值。15. 下一步
07-hands-on/02-benchmark-throughput.md— 跑 embedding 基准。08-production-deployment/— 生产部署。- 源码:
srt/managers/tokenizer_manager.pyembedding 路径、srt/entrypoints/openai/embedding API。
上游源码:
sglang/python/sglang/srt/embedding 相关分布在 managers / models 各处。