预计阅读 7 分钟

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. 自检

  1. 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" 直接回流路径,少一跳。
  1. 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` 覆盖参数。
  1. Matryoshka embedding 的 dimensions 有什么作用?
答案 Matryoshka 模型在训练时让向量前缀也保持可用语义。请求中的 `dimensions` 将完整向量 截断到模型声明支持的维度,从而用一定召回质量换取更小的向量库和更快的相似度计算。 服务端会依据 `matryoshka_dimensions` 或 `is_matryoshka` 校验;普通 embedding 模型不能 任意截断后仍假定质量成立。
  1. 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。
  1. 为什么 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. 下一步

上游源码:sglang/python/sglang/srt/ embedding 相关分布在 managers / models 各处。