<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <id>https://streamazure.github.io/</id>
  <title type="text">StreamAzure 的笔记</title>
  <subtitle type="text">读源码的记录</subtitle>
  <updated>2026-05-19T00:00:00.000Z</updated>
  <author><name>StreamAzure</name></author>
  <link rel="alternate" href="https://streamazure.github.io/"/>
  <link rel="self" href="https://streamazure.github.io/atom.xml"/>
  <generator uri="https://github.com/CuteLeaf/Firefly">Firefly v6.16.8</generator>
    <entry>
      <id>https://streamazure.github.io/posts/mini-sglang-request-flow/</id>
      <title type="text">Mini-SGLang 源码解析：从 HTTP 到 SSE，5 次消息投递</title>
      <published>2026-05-19T00:00:00.000Z</published>
      <updated>2026-05-19T00:00:00.000Z</updated>
      <author><name>StreamAzure</name></author>
      <link rel="alternate" href="https://streamazure.github.io/posts/mini-sglang-request-flow/"/>
      <summary type="text">从 HTTP 请求进入 API Server 开始，经 ZMQ 投递、tokenizer 分词、scheduler 调度计算，到增量 token 以 SSE chunk 返回客户端的完整链路。</summary>
      <content type="html"><![CDATA[<section><h2>总体流程<a href="#总体流程"><span>#</span></a></h2><p></p><figure><img alt="API Server 请求处理总体流程：用户请求经 API Server、Tokenizer、Detokenizer 与多个 Scheduler 流转" loading="lazy" width="714" height="734" src="/_astro/01-api-server-overview.BZbexuxY_1hq4fb.webp" /><figcaption>API Server 请求处理总体流程：用户请求经 API Server、Tokenizer、Detokenizer 与多个 Scheduler 流转</figcaption></figure><p></p></section>
<section><h2>API Server<a href="#api-server"><span>#</span></a></h2><p>以 <strong>Qwen/Qwen3-0.6B</strong> 模型为例，启动 Mini-SGLang 推理服务，并向服务发送一个用户请求：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>curl</span><span> </span><span>-X</span><span> </span><span>POST</span><span> </span><span>http://localhost:8000/v1/chat/completions</span><span> </span><span>\</span></div></div><div><div><div>2</div></div><div><span>  </span><span>-H</span><span> </span><span>"Content-Type: application/json"</span><span> </span><span>\</span></div></div><div><div><div>3</div></div><div><span>  </span><span>-d</span><span> </span><span>'{</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>"model": "Qwen/Qwen3-0.6B",</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>"messages": [{"role": "user", "content": "Hello"}],</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>"max_tokens": 64,</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>"stream": true</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>}'</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>该请求首先被 <code>server/api_server.py</code> 中定义的 FastAPI 服务接口处理。</p><section><h3>用户请求投递与响应 token 分发<a href="#用户请求投递与响应-token-分发"><span>#</span></a></h3><p>在理解 LLM 服务的 API Server 设计逻辑之前，我们需要回顾一下传统 Web API 设计。</p><p>在传统 Web 服务中，请求进来后，服务器从线程池里取一个线程来处理它，最后完整返回响应结果，并将线程归还到线程池中。在这样的处理模型下，响应结果与其对应的请求天然绑定，不需要额外的判断。</p><p>但 LLM 推理引擎的计算特性（即，将多个请求拼接成一个大矩阵一起计算，作为计算结果的 token 可能交错返回：刚刚返回的 token 可能属于请求 1，下一个 token 可能属于请求 2），使我们在设计 API Server 层时，必须考虑这样一个问题：</p><p><strong>1. 如何将后端交错返回的 token，正确绑定到对应的请求上？</strong></p><p>在 API Server 中，我们可以为每个请求分配一个编号：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>@app</span><span>.</span><span>post</span></span><span>(</span><span>"/v1/chat/completions"</span><span>)</span></div></div><div><div><div>2</div></div><div><span>async</span><span> </span><span>def</span><span> </span><span>v1_completions</span><span>(</span><span>req</span><span>:</span><span> OpenAICompletionRequest</span><span>,</span><span><span> </span><span>request</span></span><span>:</span><span> Request</span><span>):</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>state </span><span>=</span><span> </span><span>get_global_state</span><span>() </span><span># 全局状态管理</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>...</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>uid </span><span>=</span><span> state.</span><span>new_user</span><span>()</span></div></div><div><div><div>6</div></div><div><span>    </span><span># 为新请求分配编号、分配缓冲区 ack_map、分配异步事件 asyncio.Event() 实例</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>...</span></div></div><div><div><div>8</div></div><div><span>    </span><span>return</span><span><span> </span><span>StreamingResponse</span><span>(</span></span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>state.</span><span>stream_with_cancellation</span><span>(state.</span><span>stream_chat_completions</span><span>(uid), request, uid),</span></div></div><div><div><div>10</div></div><div><span>        </span><span># 给 StreamingResponse 中的 stream 绑定 uid</span></div></div><div><div><div>11</div></div><div><span>        </span><span>media_type</span><span>=</span><span>"text/event-stream"</span><span>,</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这个 <code>uid</code> 将贯穿整个处理链路：API Server → tokenizer → scheduler → detokenizer → API Server。</p><p>在整条处理链路的最后一步，后端将 uid、增量文本、结束标记返回给 API Server：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@dataclass</span></div></div><div><div><div>2</div></div><div><span>class</span><span><span> </span><span>UserReply</span><span>(</span><span>BaseFrontendMsg</span><span>)</span></span><span>:</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>uid: </span><span>int</span><span> </span><span># 关联请求的 uid</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>incremental_output: </span><span>str</span><span> </span><span># 增量文本</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>finished: </span><span>bool</span><span> </span><span># 是否结束</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>为请求分配编号之后，还需要考虑：</p><ol>
<li><strong>如何全局维护编号与请求的对应关系以及其他相关信息？即，如何维护 API Server 的全局状态？</strong></li>
</ol><p>FastAPI 提供的接口方法 <code>v1_completions</code> 应当是无状态的，我们需要在接口方法之外，创建一个能够跨请求共享的运行时管理器，即 <code>FrontendManager</code>，如图所示。</p><p></p><figure><img alt="FrontendManager 全局状态管理器持有的字段：config、uid_counter、send/recv_tokenizer、ack_map、event_map" loading="lazy" width="925" height="234" src="/_astro/02-frontend-manager-state.Byza2t0f_Z1U5Lvk.webp" /><figcaption>FrontendManager 全局状态管理器持有的字段：config、uid_counter、send/recv_tokenizer、ack_map、event_map</figcaption></figure><p></p><p>除了管理请求编号以外，它还包含其他需要被跨请求共享的信息：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@dataclass</span></div></div><div><div><div>2</div></div><div><span>class</span><span><span> </span><span>FrontendManager</span></span><span>:</span></div></div><div><div><div>3</div></div><div><span>    </span><span># 推理服务全局配置，如模型路径、最大并发请求数等</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>config: ServerArgs</span></div></div><div><div><div>5</div></div><div><span>    </span><span># send/recv_tokenizer 是 API Server 与后端之间数据交互的通道</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>send_tokenizer: ZmqAsyncPushQueue[BaseTokenizerMsg]</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>recv_tokenizer: ZmqAsyncPullQueue[BaseFrontendMsg]</span></div></div><div><div><div>8</div></div><div><span>    </span><span># 编号计数</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>uid_counter: </span><span>int</span><span><span> </span><span>=</span><span> </span></span><span>0</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>initialized: </span><span>bool</span><span><span> </span><span>=</span><span> </span></span><span>False</span></div></div><div><div><div>11</div></div><div><span>    </span><span># 维护当前正在处理的多个请求的响应 token 数据和事件</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>ack_map: Dict[</span><span>int</span><span><span>, List[UserReply]] </span><span>=</span><span> </span><span>field</span><span>(</span></span><span>default_factory</span><span>=</span><span>dict</span><span>)</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>event_map: Dict[</span><span>int</span><span><span>, asyncio.Event] </span><span>=</span><span> </span><span>field</span><span>(</span></span><span>default_factory</span><span>=</span><span>dict</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>当多个请求（要求流式响应）到达时，API Server 需要维护多个 HTTP 长连接。为了避免多个长连接持续轮询是否有新 token 产生，每个请求都在 <code>state.new_user</code> 方法中向 <code>FrontendManager</code> 注册一个 <code>asyncio.Event</code>，并在 <code>ack_map</code> 中注册获取自己的缓冲区 ，以“生产者-消费者”模式处理后端返回的 token，如图所示：</p><p></p><figure><img alt="请求在 new_user() 中获取 uid，并注册缓冲区 ack_map 与监听事件 event_map" loading="lazy" width="1216" height="410" src="/_astro/03-new-user-registration.BF-tacWn_ZCscwR.webp" /><figcaption>请求在 new_user() 中获取 uid，并注册缓冲区 ack_map 与监听事件 event_map</figcaption></figure><p></p><ol>
<li><strong>如何将用户请求投递给后端？</strong></li>
</ol><p>请求通过 <code>state.new_user()</code> 完成注册、获得 <code>uid</code> 后，将被 API Server 进一步包装为 <code>TokenizeMsg</code> 对象，并通过 <code>send_one</code> 方法，将请求数据通过 <code>send_tokenizer</code> 投递到后端，如图所示。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>await</span><span><span> state.</span><span>send_one</span><span>(</span></span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>TokenizeMsg</span><span>(</span></div></div><div><div><div>3</div></div><div><span>        </span><span>uid</span><span><span>=</span><span>uid,</span></span></div></div><div><div><div>4</div></div><div><span>        </span><span>text</span><span><span>=</span><span>prompt,</span></span></div></div><div><div><div>5</div></div><div><span>        </span><span>sampling_params</span><span><span>=</span><span>SamplingParams</span><span>(</span><span>...</span><span>),</span></span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>7</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p></p><figure><img alt="请求投递：await state.send_one(TokenizeMsg(...)) 经 self.send_tokenizer.put(msg) 发往后端" loading="lazy" width="1206" height="376" src="/_astro/04-request-dispatch-send-one.Bz2BQvex_Z2fs2bi.webp" /><figcaption>请求投递：await state.send_one(TokenizeMsg(...)) 经 self.send_tokenizer.put(msg) 发往后端</figcaption></figure><p></p><p>在等待后端返回 token 期间，请求在 <code>stream_chat_completions(uid)</code> 方法内部的<code>wait_for_ack()</code> 方法中调用 <code>await event.wait()</code> 挂起，直到事件通知。</p><p></p><figure><img alt="后端 token 返回链路：listen() 写入 ack_map 并唤醒事件，stream_chat_completions 取出增量文本封装为 SSE chunk" loading="lazy" width="1213" height="506" src="/_astro/05-stream-response-path.DHNhXmsw_ksS3d.webp" /><figcaption>后端 token 返回链路：listen() 写入 ack_map 并唤醒事件，stream_chat_completions 取出增量文本封装为 SSE chunk</figcaption></figure><p></p><p><strong>4. 后端返回 token 后，如何将其分发给对应请求？</strong></p><p>请求所等待的事件通知由 <code>FrontendManager</code> 触发，整体流程如图所示。</p><p><code>FrontendManager</code> 实例通过 <code>listen()</code> 方法为所有请求统一监听 token 返回事件。它通过 <code>recv_tokenizer</code> 与后端直接通信。读到后端返回的一个 <code>UserReply</code> 对象后，它将该对象放进 <code>ack_map[uid]</code>，再调用 <code>event_map[msg.uid].set()</code> 方法唤醒对应的请求。</p><p>请求醒来后，继续执行<code>wait_for_ack()</code>方法：从缓冲区 <code>ack_map[uid]</code> 中取出数据，清空缓冲区，将数据 <code>yield</code> 给上一层 <code>stream_chat_completions(uid)</code> 方法。该方法拿到数据后，解析处理增量文本 <code>ack.incremental_output</code>，将其包装成 OpenAI 格式的 SSE chunk，然后继续 <code>yield</code> 给 <code>StreamingResponse</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>async</span><span> </span><span>for</span><span> ack </span><span>in</span><span> </span><span>self</span><span><span>.</span><span>wait_for_ack</span><span>(uid):</span></span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>...</span></div></div><div><div><div>3</div></div><div><span>    </span><span>if</span><span> ack.incremental_output:</span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>delta[</span><span>"content"</span><span><span>] </span><span>=</span><span> ack.incremental_output</span></span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>chunk </span><span>=</span><span> {</span></div></div><div><div><div>7</div></div><div><span>        </span><span>"id"</span><span>: </span><span>f</span><span>"cmpl-</span><span>{</span><span>uid</span><span>}</span><span>"</span><span>,</span></div></div><div><div><div>8</div></div><div><span>        </span><span>"object"</span><span>: </span><span>"text_completion.chunk"</span><span>,</span></div></div><div><div><div>9</div></div><div><span>        </span><span>"choices"</span><span>: [{</span><span>"delta"</span><span>: delta, </span><span>"index"</span><span>: </span><span>0</span><span>, </span><span>"finish_reason"</span><span>: </span><span>None</span><span>}],</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>11</div></div><div><span>    </span><span>yield</span><span> </span><span>f</span><span>"data: </span><span>{</span><span><span>json.</span><span>dumps</span><span>(chunk)</span></span><span>}</span><span>\n\n</span><span>"</span><span><span>.</span><span>encode</span><span>()</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>最后，<code>StreamingResponse</code> 是最终的流式响应，作为接口方法 <code>v1_completions</code> 的返回值返回给用户请求。</p><p><strong>整体上，API Server 的设计可以总结为下图：</strong></p><p></p><figure><img alt="API Server 设计总结：new_user 注册、send_one 投递、listen 唤醒与 StreamResponse 返回的完整链路" loading="lazy" width="1211" height="486" src="/_astro/06-api-server-design-summary.BRY1OHaJ_rTypz.webp" /><figcaption>API Server 设计总结：new_user 注册、send_one 投递、listen 唤醒与 StreamResponse 返回的完整链路</figcaption></figure><p></p></section><section><h3>ZMQ：与后端 tokenizer 进程通信<a href="#zmq与后端-tokenizer-进程通信"><span>#</span></a></h3><p>在前文中，我们笼统地将负责处理用户请求的部分称为后端。实际上，用户请求首先抵达分词器 tokenizer，经过分词处理后，才会进一步递交给推理引擎进行 token 计算；计算完毕的 token 还需要经过反分词处理，转换成自然语言文本后再返回给 API Server。</p><p>API Server 的通信对象是 tokenizer/detokenizer 两个进程，它使用 ZMQ（ZeroMQ）这一消息队列框架作为通信组件。<code>FrontendManager</code> 所持有的<code>send_tokenizer</code> 和  <code>recv_tokenizer</code> 分别是 <code>ZmqAsyncPushQueue</code> 对象和 <code>ZmqAsyncPullQueue</code> 对象，即 API Server 这一侧持有的 ZMQ 通信端口。</p><p>API Server 发给 tokenizer 的数据被封装为 <code>TokenizeMsg</code> 对象，包含 uid，请求文本和采样参数：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@dataclass</span></div></div><div><div><div>2</div></div><div><span>class</span><span><span> </span><span>TokenizeMsg</span><span>(</span><span>BaseTokenizerMsg</span><span>)</span></span><span>:</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>uid: </span><span>int</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>text: </span><span>str</span><span><span> </span><span>|</span><span> List[Dict[</span></span><span>str</span><span>, </span><span>str</span><span>]]</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>sampling_params: SamplingParams</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>以本章开头提供的请求参数为例，被封装为 <code>TokenizeMsg</code> 对象再经序列化之后的内容如下所示：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span></div></div><div><div><div>2</div></div><div><span>    </span><span>"__type__"</span><span>: </span><span>"TokenizeMsg"</span><span>,</span></div></div><div><div><div>3</div></div><div><span>    </span><span>"uid"</span><span>: </span><span>3</span><span>,</span></div></div><div><div><div>4</div></div><div><span>    </span><span>"text"</span><span>: [</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>{</span><span>"role"</span><span>: </span><span>"user"</span><span>, </span><span>"content"</span><span>: </span><span>"hello"</span><span>}</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>],</span></div></div><div><div><div>7</div></div><div><span>    </span><span>"sampling_params"</span><span>: {</span></div></div><div><div><div>8</div></div><div><span>        </span><span>"__type__"</span><span>: </span><span>"SamplingParams"</span><span>,</span></div></div><div><div><div>9</div></div><div><span>        </span><span>"temperature"</span><span>: </span><span>1.0</span><span>,</span></div></div><div><div><div>10</div></div><div><span>        </span><span>"top_k"</span><span>: </span><span>-1</span><span>,</span></div></div><div><div><div>11</div></div><div><span>        </span><span>"top_p"</span><span>: </span><span>1.0</span><span>,</span></div></div><div><div><div>12</div></div><div><span>        </span><span>"ignore_eos"</span><span>: </span><span>False</span><span>,</span></div></div><div><div><div>13</div></div><div><span>        </span><span>"max_tokens"</span><span>: </span><span>16</span><span>,</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>},</span></div></div><div><div><div>15</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>除了正常的用户 Prompt 以外，用户还可能发送取消请求。取消请求将被封装为 <code>AbortMsg</code>。取消请求不会被分配新的 <code>uid</code>，而是直接携带被取消的请求的 <code>uid</code>，同样通过 ZMQ 发送给 tokenizer。</p><p>值得注意的是，后端通过 ZMQ 向 API Server 返回消息时，并非对称地以 <code>DetokenizeMsg</code> 对象格式返回，而是使用 <code>BatchFrontendMsg</code> 对象，其<code>data</code> 字段包含多个用户的结果（<code>List[UserReply]</code>） ：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@dataclass</span></div></div><div><div><div>2</div></div><div><span>class</span><span><span> </span><span>BatchFrontendMsg</span><span>(</span><span>BaseFrontendMsg</span><span>)</span></span><span>:</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>data: List[BaseFrontendMsg]</span></div></div><div><div><div>4</div></div><div><span>    </span><span># UserReply 是 BaseFrontendMsg 的子类</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>FrontendManager</code> 的 <code>listen</code> 方法将 <code>data</code> 字段拆开，按 uid 将 <code>Reply</code> 对象放回到对应请求的 <code>ack_map</code> 中。</p><p>那么 <code>DetokenizeMsg</code> 在哪里被使用呢？事实上，它存在于后端，是 <code>scheduler</code> 发给 <code>detokenizer</code> 的数据封装格式，不会被 API Server 直接收到：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@dataclass</span></div></div><div><div><div>2</div></div><div><span>class</span><span><span> </span><span>DetokenizeMsg</span><span>(</span><span>BaseTokenizerMsg</span><span>)</span></span><span>:</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>uid: </span><span>int</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>next_token: </span><span>int</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>finished: </span><span>bool</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>detokenizer</code> 负责将其中的 <code>next_token</code> 转换为可读文本，包装成 <code>UserReply</code>后再发回给 API Server。</p><p></p><figure><img alt="前端管理器视角的完整消息链路：注册缓冲区、send_tokenizer 投递、接收返回 token、yield 增量文本返回 chunk" loading="lazy" width="1276" height="741" src="/_astro/07-full-message-chain.D-4vEgO2_vCyns.webp" /><figcaption>前端管理器视角的完整消息链路：注册缓冲区、send_tokenizer 投递、接收返回 token、yield 增量文本返回 chunk</figcaption></figure><p></p><p>综上，API Server 与后端通信的完整消息链路如图所示：</p><ol>
<li>API Server 收 HTTP JSON 请求后，将其内容封装为 <code>TokenizeMsg</code> 对象，通过 ZMQ 发给 tokenizer。</li>
<li>tokenizer 把其中的文本内容转成 <code>input_ids</code>，包装为 <code>UserMsg</code> 对象，通过 ZMQ 发给 scheduler。</li>
<li>scheduler 推理出 next_token 后，封装为 <code>DetokenizeMsg</code> 对象，通过 ZMQ 发给 detokenizer。</li>
<li>detokenizer 把 token id 转换为可读文本，并封装为 <code>UserReply</code> 对象后，通过 ZMQ 发回 API Server。</li>
<li>最后 API Server 通过 <code>recv_tokenizer</code> 取出 <code>BatchFrontendMsg</code>，拆出其中的多个 <code>UserReply</code> 对象，处理成 SSE chunk 后，最终返回给 HTTP 客户端。</li>
</ol></section></section>
<section><h2>Tokenizer &amp; Detokenizer<a href="#tokenizer--detokenizer"><span>#</span></a></h2><p>默认情况下，tokenizer 和 detokenizer 属于同一个进程，在 <code>tokenizer/server.py</code> 中的 <code>tokenize_worker</code> 一并初始化（即 <code>TokenizeManager</code> 和 <code>DetokenizeManager</code>）。</p><p><code>tokenize_worker</code> 管理一个消息循环，持续监听来自 ZMQ 的消息。为减少读取开销，它采用批量收集的方式，尽量多地收取队列中已有的消息后再进行处理，但不会刻意凑满某个消息数量：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>while</span><span> </span><span>len</span><span><span>(pending_msg) </span><span>&lt;</span><span> local_bs </span></span><span>and</span><span> </span><span>not</span><span><span> recv_listener.</span><span>empty</span><span>():</span></span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>pending_msg.</span><span>extend</span><span>(</span><span>_unwrap_msg</span><span>(recv_listener.</span><span>get</span><span>()))</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>消息数量达到 <code>local_bs</code>（推理服务启动时指定，默认为 1），或队列已空时，停止消息收集，开始处理。</p><p>它接收的消息分为以下三类：</p><ol>
<li>来自 API Server 的 <code>tokenize_msg</code>；</li>
<li>来自 API Server 的 <code>abort_msg</code>；</li>
<li>来自 Scheduler 的 <code>detokenize_msg</code>。</li>
</ol><p>对于 <code>tokenize_msg</code> 和 <code>detokenize_msg</code>，<code>tokenize_worker</code> 分别调用 <code>TokenizeManager.tokenize()</code> 和 <code>DetokenizeManager.detokenize()</code> 进行处理，并将 token 结果分别封装为 <code>UserMsg</code> 和 <code>UserReply</code> ，多个 token 结果合并到一个消息对象，通过 ZMQ 投递到对应接收方。</p><p><code>abort_msg</code> 的处理方式与 <code>tokenize_msg</code> 基本相同，但不会被封装为 <code>UserMsg</code>，而是封装为 <code>AbortBackendMsg</code>，该对象内只包含一个 <code>uid</code> 字段，用以表示要被取消的目标请求。</p></section>
<section><h2>Scheduler<a href="#scheduler"><span>#</span></a></h2><section><h3>一个请求的调度与计算流程<a href="#一个请求的调度与计算流程"><span>#</span></a></h3><p>假设一个用户请求经过 API Server 与 tokenizer 的处理，以 <code>UserMsg</code> 格式经由 ZMQ 抵达 scheduler。在 scheduler 中，它需要经过 prefill 阶段（并在该阶段建立、利用 KV Cache），完成用户输入 prompt 的注意力矩阵计算后，再进入 decode 阶段，逐个产生新 token，并持续以 <code>DetoknizeMsg</code> 的格式返回给 detokenizer。</p><p><strong>预处理</strong>。scheduler 在调度循环中，从 ZMQ 取出上文所述的 <code>UserMsg</code>。<code>UserMsg</code> 首先在 <code>_process_one_msg</code> 方法中被处理。我们需要保证用户输入的 prompt 长度 + 最大生成长度不会超过模型能承受的最大序列长度。例如，模型最大上下文为 8192，用户 prompt 已有 8000 个 token，则最多只能再生成 192 个 token。即使用户请求中已指定 <code>max_tokens=1024</code>，scheduler 也只从模型最大上下文角度考虑。此外，如果用户 prompt 本身 token 数就已经超过模型最大上下文，则会被直接丢弃，不做处理：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>input_len, max_seq_len </span><span>=</span><span> </span></span><span>len</span><span>(msg.input_ids), </span><span>self</span><span>.engine.max_seq_len</span></div></div><div><div><div>2</div></div><div><span><span>max_output_len </span><span>=</span><span> max_seq_len </span><span>-</span><span> input_len</span></span></div></div><div><div><div>3</div></div><div><span>if</span><span><span> max_output_len </span><span>&lt;=</span><span> </span></span><span>0</span><span>:</span></div></div><div><div><div>4</div></div><div><span>    </span><span># 输入 prompt 本身已经超过最大上下文长度，无生成 token 的空间，直接丢弃</span></div></div><div><div><div>5</div></div><div><span>    </span><span>return</span><span><span> logger.</span><span>warning_rank0</span><span>(</span></span></div></div><div><div><div>6</div></div><div><span>        </span><span>f</span><span>"Input sequence length </span><span>{</span><span>input_len</span><span>}</span><span> exceeds </span><span>{</span><span>max_seq_len</span><span>}</span><span>, "</span></div></div><div><div><div>7</div></div><div><span>        </span><span>f</span><span>"request </span><span>{</span><span>msg.uid</span><span>}</span><span> is dropped."</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>9</div></div><div><span>if</span><span><span> msg.sampling_params.max_tokens </span><span>&gt;</span><span> max_output_len:</span></span></div></div><div><div><div>10</div></div><div><span>    </span><span># 输入 prompt 未超最大上下文长度，但无法满足用户指定的 max_tokens，</span></div></div><div><div><div>11</div></div><div><span>    </span><span># 以 max_output_len 为 max_tokens</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>msg.sampling_params.max_tokens </span><span>=</span><span> max_output_len</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>logger.</span><span>warning_rank0</span><span>(</span></div></div><div><div><div>14</div></div><div><span>        </span><span>f</span><span>"Adjust max_tokens to </span><span>{</span><span>max_output_len</span><span>}</span><span> for request </span><span>{</span><span>msg.uid</span><span>}</span><span>."</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><strong>Prefill 调度</strong>。通过上下文长度检查的 <code>UserMsg</code> 不会马上进行 prefill 计算，而是先被转换为 <code>PendingReq</code> 对象，放入到 pending 队列中，归属<code>PrefillManager</code> 管理，等待 scheduler 的下一批次调度。当 <code>schedule_next_batch</code> 方法被调用时，pending 队列中的每一个 pending 请求都会被尝试加入（ <code>try_add_one(pending_req)</code> ）到本轮 prefill batch 中。</p><p>不是每个 pending 请求都能一次性完成 prefill 计算。对于长 prompt，可能要分多个轮次计算，每次只 prefill 一部分。因此，<code>try_add_one</code>方法考虑两种情况：</p><ul>
<li>
<p><code>pending_req</code> 是之前已处理过一部分的长 prompt 请求。此时，该请求的相关 KV Cache 资源已经被分配过，则直接复用，继续处理当前部分。</p>
</li>
<li>
<p><code>pending_req</code> 是全新的请求。新请求要进入 prefill 阶段，必须先通过 <code>_try_allocate_one()</code> 申请资源：</p>
<ul>
<li>用 <code>CacheManager</code> 查 <code>prefix cache</code>，得到 <code>cache_handle</code>；</li>
<li>向 <code>TableManager</code> 申请 <code>table_idx</code>；</li>
<li>判断 KV cache 空间是否充足；</li>
<li>如果有命中的 prefix，还会把匹配的 token/page 信息写进 token pool/page table。</li>
</ul>
<p>若资源不足，比如 <code>TableManager</code> 已无空闲槽位，或 KV Cache 空间不足，则该方法返回 <code>None</code>，最终结束本轮 prefill 调度，将调度结果包装为 <code>Batch(reqs=reqs, phase="prefill")</code> 返回。</p>
</li>
</ul><p>接下来，prefill batch 送入 <code>_prepare_batch</code> 方法中的预处理管线，转换成 GPU 可执行的 <code>ForwardInput</code>。该预处理管线能处理 prefill 和 decode 两类请求 batch，步骤包括：</p><ol>
<li><code>pad_batch</code>：将 batch 内的请求 padding 到统一对齐长度，方便 CUDA Graph 以固定 shape 进行录制和回放。它只影响 decode batch。</li>
<li><code>allocate_paged</code>：为每个请求在 paged KV Cache 中分配物理页。</li>
<li><code>_make_positions</code>：生成每个 token 的位置编码，用于 attention 计算过程中的 RoPE 等位置编码计算。</li>
<li><code>_make_input_tuple</code>：构建输入 token 的逻辑索引。</li>
<li><code>batch.out_loc</code>：通过页表将逻辑索引映射到实际 KV Cache 的物理地址。</li>
<li><code>_make_write_tuple</code>：构建输出 token 要写回的逻辑索引（即新生成的 token 写回到 token pool 的位置）。</li>
<li><code>prepare_metadata</code>：准备 attention kernel 需要的元数据，如 page table、cache 长度等，供 kernel 调度使用。</li>
</ol><p>上述处理完成后，attention 层可以知道这批请求应该怎么读对应 cache、怎么计算 RoPE、怎么计算 prefill/decode。最后，返回一个 <code>ForwardInput</code> 对象，包含请求 batch、请求的采样参数、input_tuple、write_tuple。</p><p><strong>Prefill 批处理</strong>。<code>Engine.forward_batch()</code> 方法负责 prefill 的实际计算。Engine 将当前 batch 放入全局上下文，从而模型层可以通过 <code>get_global_ctx()</code> 看到当前 batch 是 prefill batch 还是 decode batch。对于 prefill batch，直接进行模型前馈计算（<code>model.forward()</code>）。</p><p>举个例子，假设当前计算的 prefill batch 只包含用户请求 A 和 B，其中：</p><ul>
<li>请求 A 的 prompt 有 4 个 token：<code>A0 A1 A2 A3</code></li>
<li>请求 B 的 prompt 有 3 个 token：<code>B0 B1 B2</code></li>
</ul><p>为了计算效率，这些 token 会被拼接成一个输入序列 <code>A0 A1 A2 A3 B0 B1 B2</code>。Scheduler 所准备的元数据将被同时传入，以区分每个请求的 token 边界，如 <code>[0, 4, 7]</code>，表示请求 A 的 token 范围为 <code>[0, 4)</code>，请求 B 的 token 范围为 <code>[4, 7)</code>。Attention kernel 会按这些边界分别计算两条序列。请求 A 的 token 只能看到请求 A 内部的历史 token；请求 B 的 token 只能看到请求 B 内部的历史 token，不会看到请求 A 的 token。</p><p>在 prefill 阶段，我们要为每个请求生成第一个输出 token。根据 Attention 计算原理，只需要每个请求最后一个位置的 hidden state：请求 A 用 <code>h3</code>（即模型看完 <code>A0...A3</code> 后的状态），请求 B 用 <code>h6</code>（即模型看完 <code>B0...B2</code> 后的状态）。然后把 <code>h3</code> 和 <code>h6</code> 分别送进 <code>lm_head</code>（Attention 架构中的线性转换层），得到两个词表 <code>logits</code>：第一个 <code>logits</code> 用来采样请求 A 的下一个 token，第二个 <code>logits</code> 用来采样请求 B 的下一个 token。</p><p>从而，<code>model.forward()</code> 最终返回 <code>logits</code>。<code>logits</code> 随后被送入采样流程，确定生成的 token，并将新 token 按先前确定的写回位置 <code>write_tuple</code> 写回到 GPU 上的 token_pool 中，以备进行下一轮 decode。</p><p><code>forward</code> 完毕后，当前 prefill batch 中还能继续 decode （如还未达到最大长度）的请求将会被纳入到 <code>decode_manager</code> 管理，在下一轮 decode batch 调度中执行 decode 计算，继续生成新的 token。</p><p>注意，这一轮 prefill 计算中，每个请求都生成了一个新的 token。按前文所述，这个新 token 需要被 detokenizer 处理后转换为增量文本，返回给 API Server。因此，最后还需要执行 <code>_process_last_data</code>。</p><p><strong>新 token 返回</strong>。在 <code>_process_last_data</code> 中，它按 batch 顺序遍历请求，完成新 token 与原始请求的绑定：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>for</span><span> i, req </span><span>in</span><span> </span><span>enumerate</span><span>(batch.reqs):</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>next_token </span><span>=</span><span> next_tokens_cpu[i]</span></div></div><div><div><div>3</div></div><div><span>    </span><span># 获取当前请求的新 token</span></div></div><div><div><div>4</div></div><div><span>    </span><span># next_tokens_cpu[i] 和 batch.reqs[i] 是一一对应的</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>req.</span><span>append_host</span><span>(next_token.</span><span>unsqueeze</span><span>(</span><span>0</span><span>))</span></div></div><div><div><div>6</div></div><div><span>    </span><span># 将新 token 追加到该请求在 CPU 侧的 token 序列中</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>next_token </span><span>=</span><span> </span><span>int</span><span><span>(next_token.</span><span>item</span><span>())</span></span></div></div><div><div><div>8</div></div><div><span>    </span><span># 将 tensor 形式的 token id 转换为普通 int</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>finished </span><span>=</span><span> </span><span>not</span><span> req.can_decode</span></div></div><div><div><div>10</div></div><div><span>    </span><span># 是否达到长度限制，要结束生成</span></div></div><div><div><div>11</div></div><div><span>    </span><span>if</span><span> </span><span>not</span><span> req.sampling_params.ignore_eos:</span></div></div><div><div><div>12</div></div><div><span>        </span><span># 用户是否忽略 EOS，如果不忽略，则模型生成 EOS 时，视为全部内容已经生成完毕</span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>finished </span><span>|=</span><span> next_token </span><span>==</span><span> </span><span>self</span><span>.eos_token_id</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>reply.</span><span>append</span><span>(</span><span>DetokenizeMsg</span><span>(</span><span>uid</span><span><span>=</span><span>req.uid, </span></span><span>next_token</span><span><span>=</span><span>next_token, </span></span><span>finished</span><span><span>=</span><span>finished))</span></span></div></div><div><div><div>15</div></div><div><span>    </span><span># 将 token 封装为发给 detokenizer 的消息</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h3>运行时状态管理<a href="#运行时状态管理"><span>#</span></a></h3><p>Scheduler 作为负责调度所有请求的 token 计算。某一时刻，Scheduler 的调度循环中同时存在以下处于不同处理阶段的请求：</p><ul>
<li>请求刚刚开始处理，处于 prefill 阶段；</li>
<li>请求已进入 decode 阶段（正在逐 token 生成）；</li>
<li>请求将要被取消。</li>
<li>请求已结束，需要释放 KV cache。</li>
</ul><p>Scheduler 需要管理这些请求所处的阶段、资源占用情况、并决定下一轮计算应执行哪些请求。SGLang 的核心机制 Radix Cache 也在 Scheduler 的 KV cache 管理中体现。</p></section></section>]]></content>
    </entry>
</feed>
