<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Mini-SGLang: reading the scheduler of an LLM serving framework on Jaehun's Blog</title><link>https://jaehun.me/en/series/sgl-project--mini-sglang/</link><description>Recent content in Mini-SGLang: reading the scheduler of an LLM serving framework on Jaehun's Blog</description><generator>Hugo</generator><language>en-US</language><atom:link href="https://jaehun.me/en/series/sgl-project--mini-sglang/index.xml" rel="self" type="application/rss+xml"/><item><title>1. The Processes One Request Passes Through</title><link>https://jaehun.me/en/posts/code-series-sgl-project--mini-sglang-01/</link><pubDate>Fri, 18 Sep 2026 00:00:00 +0900</pubDate><guid>https://jaehun.me/en/posts/code-series-sgl-project--mini-sglang-01/</guid><description>&lt;p&gt;Open an LLM serving framework for the first time and you will probably go&#10;looking for the scheduler. But no amount of reading scheduler code answers the&#10;question that actually blocks you: who calls this function, and who puts things&#10;in that queue? Mini-SGLang is not a single-process program. One HTTP request&#10;passes through at least four kinds of process before it becomes an answer, and&#10;every gap between them is crossed by a message and nothing else.&lt;/p&gt;</description></item><item><title>2. Req and Batch: the ledger for one request</title><link>https://jaehun.me/en/posts/code-series-sgl-project--mini-sglang-02/</link><pubDate>Fri, 18 Sep 2026 00:00:00 +0900</pubDate><guid>https://jaehun.me/en/posts/code-series-sgl-project--mini-sglang-02/</guid><description>&lt;p&gt;In chapter 1, what reached the scheduler was a 1-D int32 tensor on the CPU. While&#10;that token sequence grows into an answer, the system has to write down somewhere&#10;how far this request has got. That ledger is &lt;code&gt;Req&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;Calling it a ledger is deliberate, and not because it has many fields. It is&#10;because progress is expressed by &lt;strong&gt;three lengths&lt;/strong&gt; whose ordering is pinned down&#10;as an invariant. This chapter is about what those three numbers mean, and who&#10;moves them when.&lt;/p&gt;</description></item><item><title>3. The budget behind a prefill batch, and chunked prefill</title><link>https://jaehun.me/en/posts/code-series-sgl-project--mini-sglang-03/</link><pubDate>Fri, 18 Sep 2026 00:00:00 +0900</pubDate><guid>https://jaehun.me/en/posts/code-series-sgl-project--mini-sglang-03/</guid><description>&lt;p&gt;In chapter 2 a request became a ledger expressed by three lengths. Now there is&#10;one question left: with several requests waiting, &lt;strong&gt;what goes into this step, and&#10;how much of it&lt;/strong&gt;.&lt;/p&gt;&#10;&lt;p&gt;The answer is two budgets and five checks. And in how a prompt larger than the&#10;budget is handled, this repository makes one more tidy choice.&lt;/p&gt;&#10;&lt;h2 id="there-are-two-budgets"&gt;There are two budgets&lt;a href="#there-are-two-budgets" class="heading-anchor" aria-label="Link to this section"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Whatever builds the batch constructs a fresh &lt;code&gt;PrefillAdder&lt;/code&gt; every time. Its first&#10;two arguments are the budgets.&lt;/p&gt;</description></item><item><title>4. Paged allocation, and a page table that holds raw locations</title><link>https://jaehun.me/en/posts/code-series-sgl-project--mini-sglang-04/</link><pubDate>Fri, 18 Sep 2026 00:00:00 +0900</pubDate><guid>https://jaehun.me/en/posts/code-series-sgl-project--mini-sglang-04/</guid><description>&lt;p&gt;In chapter 3 a request &amp;ldquo;secured its space&amp;rdquo;. What that space actually is, is this&#10;chapter&amp;rsquo;s subject.&lt;/p&gt;&#10;&lt;p&gt;The KV cache is managed in pages. Yet open the &lt;code&gt;page_table&lt;/code&gt; that each request&#10;owns a row of, and you find not page numbers but &lt;strong&gt;the location of every single&#10;token&lt;/strong&gt;. Why write token locations into the table when allocation happens in&#10;pages? The repository answers that in a one-line comment.&lt;/p&gt;&#10;&lt;h2 id="the-table-holds-raw-locations"&gt;The table holds raw locations&lt;a href="#the-table-holds-raw-locations" class="heading-anchor" aria-label="Link to this section"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;div&#10; class="code-block-container border-border bg-card my-6 overflow-hidden rounded-xl border shadow-sm transition-all duration-200 ease-out hover:-translate-y-0.5 hover:shadow-md"&#10; data-code-block&#10; data-code-id="code-0"&#10; data-collapsible="true"&#10; data-default-state="expanded"&#10; data-collapsed="false"&#10; data-auto-collapse-lines="30"&#10; data-auto-collapse-height="400"&#10; data-collapsed-height="120"&#10;&gt;&#10; &#10; &lt;div&#10; class="code-block-header bg-muted/30 border-border flex items-center justify-between border-b px-4 py-3"&#10; &gt;&#10; &#10; &lt;div class="flex items-center gap-2"&gt;&#10; &lt;div class="text-muted-foreground shrink-0"&gt;&#10; &lt;svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;&#10; &lt;path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" /&gt;&#10;&lt;/svg&gt;&#10; &lt;/div&gt;&#10; &lt;span class="text-muted-foreground text-sm font-medium"&gt;&#10; PYTHON&#10; &lt;/span&gt;&#10; &lt;/div&gt;&#10;&#10; &#10; &lt;div class="flex items-center gap-2"&gt;&#10; &lt;button&#10; class="collapse-code-btn text-muted-foreground hover:text-primary hover:bg-primary/10 focus:ring-primary/20 flex items-center gap-1.5 rounded-md px-2 py-1 text-xs font-medium transition-all duration-200 ease-out focus:ring-2 focus:outline-none"&#10; type="button"&#10; data-code-action="toggle-collapse"&#10; data-label-expand="Expand"&#10; data-label-collapse="Collapse"&#10; title="Collapse"&#10; aria-label="Collapse"&#10; aria-controls="code-0"&#10; aria-expanded="true"&#10; &gt;&#10; &lt;span class="collapse-chevron transition-transform duration-200 ease-out"&gt;&#10; &lt;svg class="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;&#10; &lt;path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" /&gt;&#10;&lt;/svg&gt;&#10; &lt;/span&gt;&#10; &lt;span class="collapse-text hidden sm:inline"&gt;Collapse&lt;/span&gt;&#10; &lt;/button&gt;&#10; &lt;button&#10; class="copy-code-btn text-muted-foreground hover:text-primary hover:bg-primary/10 focus:ring-primary/20 flex items-center gap-1.5 rounded-md px-2 py-1 text-xs font-medium transition-all duration-200 ease-out focus:ring-2 focus:outline-none"&#10; type="button"&#10; data-code-action="copy"&#10; data-label-copy="Copy"&#10; data-label-copied="Copied"&#10; title="Copy"&#10; aria-label="Copy"&#10; &gt;&#10; &lt;span class="copy-icon"&gt;&#10; &lt;svg class="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;&#10; &lt;path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" /&gt;&#10;&lt;/svg&gt;&#10; &lt;/span&gt;&#10; &lt;span class="copy-text hidden sm:inline"&gt;Copy&lt;/span&gt;&#10; &lt;/button&gt;&#10; &lt;/div&gt;&#10; &lt;/div&gt;&#10;&#10; &#10; &lt;div class="code-block-content relative" id="code-0"&gt;&#10; &lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# ======================= Page table initialization ========================&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# NOTE: 1. aligned to 128 bytes; 2. store raw locations instead of pages&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_seq_len&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_seq_len&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num_tokens&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;aligned_max_seq_len&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_align_up_32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_seq_len&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;page_table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;page_table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;zeros&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="c1"&gt;# + 1 for dummy request&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_running_req&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;aligned_max_seq_len&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;int32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&#10; &lt;div hidden data-code-source&gt; # ======================= Page table initialization ========================&#10; # NOTE: 1. aligned to 128 bytes; 2. store raw locations instead of pages&#10; self.max_seq_len = min(config.max_seq_len, num_tokens)&#10; aligned_max_seq_len = _align_up_32(self.max_seq_len)&#10; self.ctx.page_table = self.page_table = torch.zeros( # &amp;#43; 1 for dummy request&#10; (config.max_running_req &amp;#43; 1, aligned_max_seq_len),&#10; dtype=torch.int32,&#10; device=self.device,&#10; )&lt;/div&gt;&#10; &#10; &lt;div&#10; class="collapse-overlay to-card/90 pointer-events-none absolute inset-0 bg-linear-to-b from-transparent via-transparent opacity-0 transition-opacity duration-300"&#10; hidden&#10; &gt;&#10; &lt;button&#10; class="collapse-overlay-btn text-muted-foreground bg-card/80 border-border/50 hover:bg-primary/10 hover:text-primary hover:border-primary/30 absolute bottom-4 left-1/2 flex -translate-x-1/2 items-center justify-center rounded-full border p-2 backdrop-blur-sm transition-all duration-200"&#10; type="button"&#10; data-code-action="expand"&#10; aria-label="Expand"&#10; title="Expand"&#10; &gt;&#10; &lt;svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"&gt;&#10; &lt;path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" /&gt;&#10;&lt;/svg&gt;&#10; &lt;/button&gt;&#10; &lt;/div&gt;&#10; &lt;/div&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;— &lt;a&#10; href="https://github.com/sgl-project/mini-sglang/blob/9a91cfafe754aa85daee49998176275667eb58f2/python/minisgl/engine/engine.py#L65-L73"target="_blank"&#10; class="inline-flex items-center gap-1"&#10; &gt;&lt;code&gt;python/minisgl/engine/engine.py:65-73&lt;/code&gt;&lt;svg class="h-3 w-3 flex-shrink-0" id="external-link" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"&gt;&lt;path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 3h6v6m-11 5L21 3m-3 10v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/&gt;&lt;/svg&gt;&#10; &lt;/a&gt;&lt;/p&gt;</description></item><item><title>5. The radix cache that reuses prefixes, and eviction</title><link>https://jaehun.me/en/posts/code-series-sgl-project--mini-sglang-05/</link><pubDate>Fri, 18 Sep 2026 00:00:00 +0900</pubDate><guid>https://jaehun.me/en/posts/code-series-sgl-project--mini-sglang-05/</guid><description>&lt;p&gt;Chapter 4 showed how one request secures its space. But if a hundred requests&#10;arrive with the same system prompt and each repeats the same computation, that&#10;space runs out fast.&lt;/p&gt;&#10;&lt;p&gt;So already-computed prefixes get reused. Once you start reusing, two new&#10;questions appear: how do you quickly find &lt;strong&gt;how far two sequences agree&lt;/strong&gt;, and how&#10;do you know &lt;strong&gt;what may be thrown away&lt;/strong&gt; when space runs short?&lt;/p&gt;&#10;&lt;h2 id="walk-the-tree-and-split-where-it-diverges"&gt;Walk the tree, and split where it diverges&lt;a href="#walk-the-tree-and-split-where-it-diverges" class="heading-anchor" aria-label="Link to this section"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Prefix lookup starts at the root and follows children down.&lt;/p&gt;</description></item><item><title>6. Hiding CPU scheduling behind two streams</title><link>https://jaehun.me/en/posts/code-series-sgl-project--mini-sglang-06/</link><pubDate>Fri, 18 Sep 2026 00:00:00 +0900</pubDate><guid>https://jaehun.me/en/posts/code-series-sgl-project--mini-sglang-06/</guid><description>&lt;p&gt;Across five chapters we have watched what the scheduler does. It receives&#10;messages, computes budgets, walks a tree, allocates pages, fills a table. All of&#10;it is &lt;strong&gt;CPU work&lt;/strong&gt;.&lt;/p&gt;&#10;&lt;p&gt;What is the GPU doing meanwhile? If the answer is nothing, then however fast the&#10;model runs, the gaps between runs are empty. This chapter reads the loop that&#10;fills those gaps.&lt;/p&gt;&#10;&lt;h2 id="put-the-two-loops-side-by-side"&gt;Put the two loops side by side&lt;a href="#put-the-two-loops-side-by-side" class="heading-anchor" aria-label="Link to this section"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;There are two loops in the same file. The simple one first.&lt;/p&gt;</description></item><item><title>7. Where the ledger becomes kernel arguments, and CUDA graphs</title><link>https://jaehun.me/en/posts/code-series-sgl-project--mini-sglang-07/</link><pubDate>Fri, 18 Sep 2026 00:00:00 +0900</pubDate><guid>https://jaehun.me/en/posts/code-series-sgl-project--mini-sglang-07/</guid><description>&lt;p&gt;At the end of chapter 6 we listed the seven lines of &lt;code&gt;_prepare_batch&lt;/code&gt; and left two&#10;of them alone: &lt;code&gt;pad_batch&lt;/code&gt; and &lt;code&gt;prepare_metadata&lt;/code&gt;. This chapter opens both.&lt;/p&gt;&#10;&lt;p&gt;Everything built so far has been Python objects: three lengths on a &lt;code&gt;Req&lt;/code&gt;, a slot&#10;number, a row of the page table. Kernels know none of that. What a kernel receives&#10;is a handful of tensors. This chapter is about where and how that conversion&#10;happens, and how a CUDA graph pins those tensors down.&lt;/p&gt;</description></item><item><title>8. How several ranks share one ledger</title><link>https://jaehun.me/en/posts/code-series-sgl-project--mini-sglang-08/</link><pubDate>Fri, 18 Sep 2026 00:00:00 +0900</pubDate><guid>https://jaehun.me/en/posts/code-series-sgl-project--mini-sglang-08/</guid><description>&lt;p&gt;Chapter 1 showed that one scheduler starts per TP rank, and the six chapters since&#10;read what happens inside &lt;strong&gt;one&lt;/strong&gt; of them. Now all of it gets replicated once per&#10;rank.&lt;/p&gt;&#10;&lt;p&gt;That creates a problem. The scheduler makes decisions every step: which requests&#10;enter the batch, which pages to take, what to discard and when. If ranks decide&#10;independently and arrive at different answers, GPUs holding slices of the same&#10;model compute different things. How do they reach the same answer?&lt;/p&gt;</description></item></channel></rss>