读 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 来说,这样很容易读散。更稳的顺序反而是:

  1. 先看 __init__(),确认这个 runner 当前被初始化成什么人格。
  2. 再看 initialize(pre_model_load_memory),看执行壳怎样被真正装起来。
  3. 然后回头读 load_model()init_memory_pool()init_attention_backend()
  4. 最后再去读 forward()sample()

这个顺序的理由很朴素:forward() 里看到的很多分支,并不是当场临时决定的,而是在更早的构造和初始化阶段就已经被钉住了。只有先知道它“是什么”,再看它“怎么干活”,你才不会把一个大文件读成很多偶然分支的堆叠。

第一层:构造器先决定了执行人格#

__init__() 最值得抓的,不是字段多,而是字段分组。它在很早就钉住了几类执行人格:

  • model_config
  • server_args
  • spec_algorithm
  • is_draft_worker
  • req_to_token_pool
  • token_to_kv_pool_allocator
  • use_mla_backend
  • is_generation
  • is_multimodal

这说明 ModelRunner 并不是“每次前向时再现想自己是谁”,而是在初始化时就已经决定了大部分运行人格。因此后面很多分支,更像构造时人格的兑现,而不是一棵任意生长的条件树。

从读书的角度,这一点尤其重要。因为它会把很多看似分散的问题统一起来:为什么这个 runner 会走草稿模型路径,为什么会走多模态路径,为什么 attention backend 会选成这样。答案往往不在 forward() 里,而在更早的初始化人格里。

第二层:initialize() 才是整棵树真正的装配中心#

如果说 __init__() 定义了人格,那么 initialize(pre_model_load_memory) 就是在把这个人格真正装成一个可工作的执行壳。它通常会串起:

  1. 远端 transfer engine 的必要初始化
  2. expert location metadata 与 expert distribution recorder
  3. EPLB / Elastic EP 等与 MoE 或并行人格相关的状态
  4. self.sampler = create_sampler()
  5. self.load_model()
  6. 远端 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 分支,而是先抓几个稳定步骤:

  1. 根据 forward_mode 判断当前执行人格。
  2. attn_backend 初始化这轮真正需要的 metadata。
  3. 在需要时处理 DP attention、padding、split prefill 等执行现实。
  4. forward_batch 交给真正的模型层。
  5. 拿回 LogitsProcessorOutputPPProxyTensors

这样读,你就不会再把 forward() 看成“模型自己在跑”,而会看到它是前面装配逻辑真正开始兑现的地方。

sample() 让它从执行壳变成真正的语义壳#

如果 forward() 负责产出候选,那么 sample(logits_output, forward_batch) 负责把这些候选变成运行时真正接受的 token 与返回语义。它会继续把前面多章已经提到的对象重新串起来:

  • _preprocess_logits(...)
  • SamplingBatchInfo
  • return_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 的边界#

这章最容易和前面章节看起来重叠,但它们实际回答的问题不同:

而这一章补的是:当这些问题都已经讲过之后,model_runner.py 这棵具体源码树怎样把它们重新汇到同一个地方。这就是代码导读章和原理章最不一样的地方:它不是再造概念,而是把概念重新落回源文件的真实组织。

读这棵树时最容易出现的误判#

第一,误以为 ModelRunner 只是 model.forward() 的薄封装。
实际上它同时承担了模型加载、KV、attention backend、sampling、旁路返回和 expert recorder 汇合点的职责。

第二,误以为 forward() 里看到的分支都是当场临时判断。
很多人格早在 __init__()initialize() 里就已经被决定了。

第三,误以为 hidden states / routed experts / MoE recorder 是外围附属功能。
源码里它们都直接挂在执行壳主链上。

真正顺着源码读时,推荐的骨架#

如果你准备真正把这棵树打开读,最稳的顺序仍然是:

  1. __init__():确认 runner 是什么人格
  2. initialize():确认执行壳怎样被装起来
  3. load_model()init_memory_pool()init_attention_backend()
  4. forward()
  5. sample()

这样读,你先知道“它是什么”,再知道“它怎么被装起来”,最后才看“它怎样真正干活”。这比盯着一个大函数来回滚动更容易形成稳定地图,也更符合一本好技术书在代码导读部分应该提供的帮助。

小结#

model_runner.py 值得单独成章,不是因为它很大,而是因为它是执行壳里真正的汇流点:模型、KV、attention backend、sampling 和旁路证据都在这里重新接起来。

只要这棵树读稳了,前面运行时架构和执行模型里的很多抽象就不再只是互相引用,而会在源码层真正落到同一个中枢上。