embedding、pooling 与 rerank 执行路径#

到这里为止,execution model 章节几乎都围绕 token generation loop 展开:forward、sampling、finish、output processing、grammar mask、penalties。这样的主线当然是对的,但如果一本更厚的技术书只讲生成路径,它仍然会留下一个很真实的空白:同一套 runtime 还支持 embedding、classification、score、rerank,这些非生成请求在 execution layer 里到底怎么走?

这章真正要补齐的,就是这条非生成执行支线。它的意义不只是“再补一种请求类型”,而是证明 execution model 从来都不只是“token 怎样被生成”,而是“同一套执行壳怎样根据请求人格切换不同后半段语义”。

先把非生成路径放回同一套执行壳#

很多读者会自然把 embedding / rerank 理解成“生成路径减掉 sampler”。源码给出的现实比这更精确:它们会显式改写

  • request object:EmbeddingReqInput
  • scheduler 对 batch 的理解:is_prefill_only
  • worker 的执行入口:forward_batch_embedding(...)
  • 最终输出对象:EmbeddingBatchResult
  • 以及 layers/pooler.py 里的池化 / score head 行为

也就是说,这已经不是“主路径上的轻微变体”,而是 execution model 的一条正式分支。下面这张图就专门用来把这条分支和生成主线拆开:

flowchart LR
    Req["EmbeddingReqInput / cross-encoder pairs"] --> Batch["ModelWorkerBatch (prefill-only)"]
    Batch --> Fwd["ModelRunner.forward()"]
    Fwd --> Pool["pooler / score head / embeddings"]
    Pool --> Out["EmbeddingBatchResult"]

图里最关键的一点是:非生成路径的输出语义并不在 sampler,而在 pooling / score 这一层。

Scheduler 已经明确承认:并不是所有 batch 都要产出 next token#

这是非常值得技术书主动点明的一层事实。scheduler.pyrun_batch 附近的逻辑本身就把路径切开了:

  • generation path 会走 GenerationBatchResult
  • 否则会进入 “embedding or reward model” 分支,调用 self.tp_worker.forward_batch_embedding(...)

这说明 execution model 到 scheduler 这一层就已经承认:同样是 batch,并不总要产出 next token ids。对整本书来说,这一点非常重要,因为它把第 5 节从“生成模型手册”重新拉回“统一 runtime 的执行模型”。

TpModelWorker.forward_batch_embedding(...) 是最短的执行证明#

如果只读一个函数,我会优先推荐它。因为它把这条支线压得非常清楚:

  1. ForwardBatch.init_new(...) 准备 forward 输入
  2. self.model_runner.forward(forward_batch).logits_output
  3. 从结果里取出 embeddings

这说明 embedding 路径并没有完全绕开执行壳。它和生成路径共享:

  • ForwardBatch
  • ModelRunner.forward(...)

真正分叉发生在:

  • forward 之后取什么
  • 后面是否继续走 sampler

这类“共享前半段、分叉后半段”的写法,非常适合系统书强调,因为它比列几个 API 名字更能说明 runtime 的内部统一性。

EmbeddingBatchResult 代表的是另一套后半段结果语义#

这也是一本书必须说透的地方。EmbeddingBatchResult 对应的不是文本块,也不是 token ids,而是更靠近 embedding / score 语义的结果容器。这意味着生成路径后半段里常见的这些东西:

  • output processor
  • detokenizer
  • streaming merge

并不会原样照搬到这里。也就是说,一旦进入非生成人格,execution model 后半段就已经换了一套结果语义,而不是只是在主路径上删掉一个 sampler 调用。

layers/pooler.py 在非生成路径里的地位,很像 sampler 在生成路径里的地位#

这个文件特别值得被拉进 execution model 章节。因为它直接回答了:

  • 什么是 PoolingType.LAST
  • 什么是 PoolingType.CLS
  • normalize 什么时候生效
  • cross-encoder path 怎样产出 score
  • multi-item scoring delimiter 场景怎样特殊处理

从 execution model 的角度看,它和 sampler 很像:都是“把 forward 产物翻成最终可返回语义”的边界层。区别只在于:

  • 生成路径最终产出的是 token
  • 非生成路径最终产出的是 embedding 或 score

只要这一层类比关系成立,读者就更容易接受:execution model 的真正主角不是某个单一输出类型,而是一套共享前半段、分叉后半段的执行壳。

score_and_pool(...) 特别像这条支线的最短证明#

这段逻辑很值得拿来当“非生成后半段”的最小证明,因为它把模式分支压得很清楚:

  • 如果是 multi-item scoring delimiter 场景,就先找 delimiter 前的位置,再过 score head
  • 否则走标准分类路径:先过 score head,再 pool

这说明非生成执行路径同样也有“模式人格”,并不是固定一种 pool 策略。这和前面 execution model 一再强调的结论完全一致:同一个 runtime 下,后半段路径会根据请求人格切换,而不是只有生成模型才配称得上“有执行模型”。

CrossEncodingPooler 让 rerank 这条支线真正立起来#

它特别值得单独提,因为它打破了一个很常见的误解:rerank 并不总是“先拿 embedding,再做余弦相似度”。在 cross-encoder 路径里,它更像一条直接的判分类或评分执行链,也正因此:

  • EmbeddingReqInput 还能承载 cross-encoder request
  • rerank 路径在 execution model 里更像 classification path,而不是 embedding extraction 的附属品

这让这条支线更值得成章,因为它不是“输出形态不同”这么简单,而是后半段执行人格本身就不同。

这条支线和前文的回扣非常多#

它天然回扣了几条前面的主线:

  • request lifecycle:解释非生成请求到底在 execution 里去了哪里
  • runtime architecture:解释 ModelRunner 为什么不只服务生成模型
  • codebase walkthrough:解释 layers/pooler.py、embedding / rerank handler 为什么重要

这正是一本“像书”的技术作品特别重要的地方:前文建立的边界,不断在不同执行人格里被验证,而不是每章都重造一套世界观。

最容易出现的三种误判#

第一,误把 embedding 路径看成“去掉 sampler 的生成路径”。
真正变化的不只是 sampler 消失,而是后半段结果语义整体换了。

第二,误把 rerank 完全理解成 API 层逻辑。
实际上 execution 层已经明确存在 pooler / score head 路径。

第三,误以为 ForwardBatch 只服务生成。
embedding path 同样会经过 ForwardBatch,只是后半段不再产出 token。

真正顺着非生成执行路径读源码时,更稳的顺序#

建议按下面顺序:

  1. 先看 EmbeddingReqInput
  2. 再看 scheduler 如何分叉到 EmbeddingBatchResult
  3. 再看 TpModelWorker.forward_batch_embedding(...)
  4. 最后看 layers/pooler.py

这样读的好处很直接:你先有主线,再进入具体 pooling / score 细节,就不会把非生成执行路径误看成一堆零散例外。

小结#

这一章真正补齐的,是 execution model 长期会被忽略的一条支线:同一套执行壳不只服务 token generation,embedding / rerank / score 路径共享前半段执行壳,但在后半段会转向完全不同的结果语义。

把这条支线讲清楚之后,第 5 节才更像完整 runtime 的执行模型,而不只是生成模型手册。