读 model_runner.py:执行壳的真正汇合点#
前面的运行时架构和执行模型已经反复把 ModelRunner 当成核心汇合点来讲,但如果你真的打开 python/sglang/srt/model_executor/model_runner.py,很容易第一眼就被它淹没。因为这棵树同时缠着几类逻辑:
- 模型加载
- KV cache 初始化
- attention backend 初始化
- distributed / MoE / expert record
forward()/sample()- hidden states 与 routed experts 的旁路返回
很多人因此会得出一个看似合理、其实很弱的判断:这是个大文件,先记住它很重要,等以后再说。代码导读章节的任务,恰恰不是让你停在这种印象上,而是给出一条更稳的阅读骨架,让你知道这棵树应该先抓什么,后抓什么,哪些分支其实属于同一层。
先把 ModelRunner 放回全书位置#
如果把整本书压成一条主链,ModelRunner 之所以值得单独成章,不是因为它“又是一个重要文件”,而是因为它站在几条主线真正交叉的地方:
- 对运行时架构来说,它是 worker 壳里面真正的执行汇合点
- 对调度与内存来说,它把 batch 视图落成可执行状态
- 对执行模型来说,
forward()/sample()的正式入口就在这里 - 对旁路返回语义来说,hidden states、routed experts、expert recorder 也都在这里重新汇流
也就是说,这不是一章“读一个大文件”,而是一章“把全书前面几条主线重新对到同一棵源码树上”。
先看这张图,再决定往哪读#
下面这张图的作用,不是把 model_runner.py 再抽象一遍,而是帮你避免一个常见误读:把它看成 model.forward() 的薄封装。更准确的理解是,它是执行壳内部真正的装配与汇流层。
flowchart TD
Load["load_model()"] --> KV["init_memory_pool()"]
KV --> Attn["init_attention_backend()"]
Attn --> Fwd["forward(forward_batch)"]
Fwd --> Sample["sample(logits_output, forward_batch)"]
Fwd --> Side["hidden states / routed experts / expert recorder"]这张图比目录式描述多解释的一件事是:ModelRunner 不只站在执行后半段,而是从模型、缓存、后端,到输出与旁路证据都在同一处汇流。
更稳的阅读顺序,不是先看 forward()#
很多人面对执行文件时会本能地先跳 forward()。对 model_runner.py 来说,这样很容易读散。更稳的顺序反而是:
- 先看
__init__(),确认这个 runner 当前被初始化成什么人格。 - 再看
initialize(pre_model_load_memory),看执行壳怎样被真正装起来。 - 然后回头读
load_model()、init_memory_pool()和init_attention_backend()。 - 最后再去读
forward()和sample()。
这个顺序的理由很朴素:forward() 里看到的很多分支,并不是当场临时决定的,而是在更早的构造和初始化阶段就已经被钉住了。只有先知道它“是什么”,再看它“怎么干活”,你才不会把一个大文件读成很多偶然分支的堆叠。
第一层:构造器先决定了执行人格#
__init__() 最值得抓的,不是字段多,而是字段分组。它在很早就钉住了几类执行人格:
model_configserver_argsspec_algorithmis_draft_workerreq_to_token_pooltoken_to_kv_pool_allocatoruse_mla_backendis_generationis_multimodal
这说明 ModelRunner 并不是“每次前向时再现想自己是谁”,而是在初始化时就已经决定了大部分运行人格。因此后面很多分支,更像构造时人格的兑现,而不是一棵任意生长的条件树。
从读书的角度,这一点尤其重要。因为它会把很多看似分散的问题统一起来:为什么这个 runner 会走草稿模型路径,为什么会走多模态路径,为什么 attention backend 会选成这样。答案往往不在 forward() 里,而在更早的初始化人格里。
第二层:initialize() 才是整棵树真正的装配中心#
如果说 __init__() 定义了人格,那么 initialize(pre_model_load_memory) 就是在把这个人格真正装成一个可工作的执行壳。它通常会串起:
- 远端 transfer engine 的必要初始化
- expert location metadata 与 expert distribution recorder
- EPLB / Elastic EP 等与 MoE 或并行人格相关的状态
self.sampler = create_sampler()self.load_model()- 远端 transfer engine 信息的注册
这一步特别值得技术书单独强调,因为它揭示了一个很重要的工程事实:执行壳不是从“调用 forward()”开始的,而是从“把所有必要支撑条件先装起来”开始的。
这也是为什么把 ModelRunner 理解成“只包一层模型前向”会严重低估它的职责。单从执行语义看,forward() 很重要;但从系统组织看,initialize() 往往更能说明这棵树为什么长成现在这样。
load_model() 不是单纯 I/O,init_* 也不是可有可无#
读到这里时,很容易又把 load_model() 看成“把权重搬进来”的 I/O 动作,把 init_memory_pool() 和 init_attention_backend() 看成“装配细节”。更稳的理解是:这三步正好把执行壳的地基铺完。
load_model()让 runner 第一次真正“有内容”init_memory_pool()让之后的 forward 不再面对抽象请求,而面对可落地的缓存现实init_attention_backend()则决定很多后面看起来像forward()内部分支的真正人格来源
这也解释了为什么前面第 4 节和第 5 节必须分开写。前者讲的是 cache 和 batch 的资源现实,后者讲的是执行人格和后半段状态推进;而在源码层,这两部分恰恰就在 ModelRunner 这里重新接起来。
forward() 不是一个调用点,而是装配后果的兑现#
虽然不建议一开始就跳进 forward(),但这仍然是这棵树最核心的正式入口。更稳的读法,不是试图记住每个 backend 分支,而是先抓几个稳定步骤:
- 根据
forward_mode判断当前执行人格。 - 让
attn_backend初始化这轮真正需要的 metadata。 - 在需要时处理 DP attention、padding、split prefill 等执行现实。
- 把
forward_batch交给真正的模型层。 - 拿回
LogitsProcessorOutput或PPProxyTensors。
这样读,你就不会再把 forward() 看成“模型自己在跑”,而会看到它是前面装配逻辑真正开始兑现的地方。
sample() 让它从执行壳变成真正的语义壳#
如果 forward() 负责产出候选,那么 sample(logits_output, forward_batch) 负责把这些候选变成运行时真正接受的 token 与返回语义。它会继续把前面多章已经提到的对象重新串起来:
_preprocess_logits(...)SamplingBatchInforeturn_logprob- token-id logprobs
因此更稳的理解方式是:
forward()产出执行结果sample()再把执行结果翻译成 next token 选择与可返回证据
如果把两者拆开读,你就很容易人为地把“模型前向”和“执行语义”切断。但在源码里,它们在同一执行壳里自然相接。
为什么旁路返回语义也必须回到这里#
前面的章节已经分别讲过 hidden states、routed experts 和 expert distribution record。放回 ModelRunner 再看一次,你会发现它们不是外围系统顺手接出来的功能,而是只有在这一层才自然成立:
- 这里只有这里同时掌握模型本体
- 这里只有这里同时看见
forward_batch - 这里只有这里同时连接 attention backend、sampling 和输出后半段
也正因为这样,ModelRunner 才是全书里最像“执行中枢”的单文件入口。把旁路返回和 expert recorder 重新压回这棵树,读者就更容易理解:它们不是额外特性,而是执行壳内部证据的不同出口。
这章和 7.15、3.x、5.x 的边界#
这章最容易和前面章节看起来重叠,但它们实际回答的问题不同:
- 7.15 读 models/ 与 layers/:模型定义与执行砖块
回答的是模型树和执行砖块树怎么读。 - 运行时架构里关于 worker / runner 的章节
回答的是边界怎么切。 - 第 5 节执行模型
回答的是运行时后半段怎样工作。
而这一章补的是:当这些问题都已经讲过之后,model_runner.py 这棵具体源码树怎样把它们重新汇到同一个地方。这就是代码导读章和原理章最不一样的地方:它不是再造概念,而是把概念重新落回源文件的真实组织。
读这棵树时最容易出现的误判#
第一,误以为 ModelRunner 只是 model.forward() 的薄封装。
实际上它同时承担了模型加载、KV、attention backend、sampling、旁路返回和 expert recorder 汇合点的职责。
第二,误以为 forward() 里看到的分支都是当场临时判断。
很多人格早在 __init__() 和 initialize() 里就已经被决定了。
第三,误以为 hidden states / routed experts / MoE recorder 是外围附属功能。
源码里它们都直接挂在执行壳主链上。
真正顺着源码读时,推荐的骨架#
如果你准备真正把这棵树打开读,最稳的顺序仍然是:
__init__():确认 runner 是什么人格initialize():确认执行壳怎样被装起来load_model()、init_memory_pool()、init_attention_backend()forward()sample()
这样读,你先知道“它是什么”,再知道“它怎么被装起来”,最后才看“它怎样真正干活”。这比盯着一个大函数来回滚动更容易形成稳定地图,也更符合一本好技术书在代码导读部分应该提供的帮助。
小结#
model_runner.py 值得单独成章,不是因为它很大,而是因为它是执行壳里真正的汇流点:模型、KV、attention backend、sampling 和旁路证据都在这里重新接起来。
只要这棵树读稳了,前面运行时架构和执行模型里的很多抽象就不再只是互相引用,而会在源码层真正落到同一个中枢上。
叶王 © 2013-2026 版权所有。如果本文档对你有所帮助,可以请作者喝饮料。