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.py 在 run_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(...) 是最短的执行证明#
如果只读一个函数,我会优先推荐它。因为它把这条支线压得非常清楚:
- 用
ForwardBatch.init_new(...)准备 forward 输入 self.model_runner.forward(forward_batch).logits_output- 从结果里取出
embeddings
这说明 embedding 路径并没有完全绕开执行壳。它和生成路径共享:
ForwardBatchModelRunner.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。
真正顺着非生成执行路径读源码时,更稳的顺序#
建议按下面顺序:
- 先看
EmbeddingReqInput - 再看 scheduler 如何分叉到
EmbeddingBatchResult - 再看
TpModelWorker.forward_batch_embedding(...) - 最后看
layers/pooler.py
这样读的好处很直接:你先有主线,再进入具体 pooling / score 细节,就不会把非生成执行路径误看成一堆零散例外。
小结#
这一章真正补齐的,是 execution model 长期会被忽略的一条支线:同一套执行壳不只服务 token generation,embedding / rerank / score 路径共享前半段执行壳,但在后半段会转向完全不同的结果语义。
把这条支线讲清楚之后,第 5 节才更像完整 runtime 的执行模型,而不只是生成模型手册。
叶王 © 2013-2026 版权所有。如果本文档对你有所帮助,可以请作者喝饮料。