<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Sglang on vishctl</title><link>https://vishctl.dev/tags/sglang/</link><description>Recent content in Sglang on vishctl</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Mon, 12 Oct 2026 03:00:00 +0800</lastBuildDate><atom:link href="https://vishctl.dev/tags/sglang/index.xml" rel="self" type="application/rss+xml"/><item><title>SGLang vs. vLLM: RadixAttention, Structured Outputs, and When to Pick Which</title><link>https://vishctl.dev/posts/sglang-vs-vllm/</link><pubDate>Mon, 12 Oct 2026 03:00:00 +0800</pubDate><guid>https://vishctl.dev/posts/sglang-vs-vllm/</guid><category>sglang</category><category>vllm</category><category>inference</category><category>kv-cache</category><category>ai-infra</category><category>kubernetes</category><description>A simple, diagram-first comparison of SGLang and vLLM: how RadixAttention works, how jump-forward decoding speeds up JSON outputs, how their schedulers compare, and which engine fits your workload.</description><content:encoded><![CDATA[<p>If you self-host open-weight LLMs today, your choice almost always comes down to two engines: <strong><a href="https://github.com/vllm-project/vllm">vLLM</a></strong> and <strong><a href="https://github.com/sgl-project/sglang">SGLang</a></strong>.</p>
<p>Both started in the same UC Berkeley research circle. Both expose an OpenAI-compatible API. Both store the KV cache in pages, and both plug into Kubernetes routers like <a href="https://vishctl.dev/posts/llm-d-kubernetes-inference-routing/">llm-d</a>.</p>
<p>Yet one engine often beats the other on a real workload. Why? Because they were built to solve two different problems first:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">vLLM    = built like OS virtual memory: pack many independent users onto any GPU
</span></span><span class="line"><span class="cl">SGLang  = built for agent programs: reuse branching prompts and force fast JSON outputs
</span></span></code></pre></div><figure class="mermaid-figure">
  <div class="mermaid">flowchart TD
    subgraph V["vLLM: many chats"]
        direction LR
        U1["User 1<br/>summarize email"] a1@--> G1["Shared GPU<br/>Paged KV pool"]
        U2["User 2<br/>write SQL"] a2@--> G1
        U3["User 3<br/>translate text"] a3@--> G1
    end

    subgraph S["SGLang: one agent"]
        direction LR
        A0["Agent<br/>6k-token runbook"] b1@--> B1["Check<br/>pod logs"]
        A0 b2@--> B2["Check<br/>node events"]
        A0 b3@--> B3["Return<br/>strict JSON"]
    end

    V ~~~ S
    a1@{ animate: true }
    a2@{ animate: true }
    a3@{ animate: true }
    b1@{ animate: true }
    b2@{ animate: true }
    b3@{ animate: true }

    classDef input fill:#13264a,stroke:#38bdf8,color:#e6f6ff,stroke-width:2px;
    classDef gpu fill:#173f59,stroke:#22d3ee,color:#ecfeff,stroke-width:2px;
    classDef agent fill:#281d48,stroke:#a78bfa,color:#f1edff,stroke-width:2px;
    class U1,U2,U3 input;
    class G1 gpu;
    class A0 agent;
    class B1,B2,B3 gpu;</div>
  <figcaption>Top: vLLM started with many separate callers sharing one GPU. Bottom: SGLang started with one agent branching into sub-tasks and returning strict JSON.</figcaption>
</figure>

<p>Over the past year, both engines have borrowed each other&rsquo;s best ideas. Still, three core pieces explain why they behave differently in practice:</p>
<ol>
<li><strong>RadixAttention vs. block hash tables</strong> (how they reuse the KV cache)</li>
<li><strong>Jump-forward decoding</strong> (how they generate strict JSON)</li>
<li><strong>Overlapped CPU scheduling</strong> (how they keep the GPU from waiting on Python)</li>
</ol>
<p>Let&rsquo;s walk through each one with simple examples.</p>
<h2 id="1-kv-cache-reuse-block-hash-tables-vs-radixattention">1. KV cache reuse: block hash tables vs. RadixAttention</h2>
<p>First, clear up a common myth: <strong>both vLLM and SGLang use paged GPU memory.</strong> When a model reads a prompt, both engines store the resulting attention state (<a href="https://vishctl.dev/posts/llm-basics-visual-guide/#5-context-is-the-input-kv-cache-saves-work">the KV cache</a>) in small physical memory blocks on the GPU.</p>
<p>The difference is <strong>how they organize and look up those blocks</strong> when the next request arrives.</p>
<h3 id="how-vllm-does-it-a-hash-table-of-fixed-blocks">How vLLM does it: a hash table of fixed blocks</h3>
<p>When you turn on <a href="https://docs.vllm.ai/en/latest/features/automatic_prefix_caching/">Automatic Prefix Caching</a> in vLLM, it slices your prompt into fixed-size chunks (usually 16 tokens per block).</p>
<p>It hashes Block 0, then hashes Block 0 + Block 1, and stores those hashes in a lookup table:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Block 0 (tokens 1..16)   → hash_A → GPU page 12
</span></span><span class="line"><span class="cl">Block 1 (tokens 17..32)  → hash_B → GPU page 45
</span></span><span class="line"><span class="cl">Block 2 (tokens 33..48)  → hash_C → GPU page 08
</span></span></code></pre></div><p>If a new request starts with the exact same 16-token blocks, vLLM finds the hashes in its table and reuses those GPU pages.</p>
<h3 id="how-sglang-does-it-radixattention-a-shared-family-tree">How SGLang does it: RadixAttention (a shared family tree)</h3>
<p>Instead of a flat hash table, SGLang stores cached token sequences in a <strong>radix tree</strong> (a compressed tree where shared prefixes form a single trunk, and differences split into branches).</p>
<figure class="mermaid-figure">
  <div class="mermaid">flowchart TD
    subgraph VLLM["vLLM: hash table"]
        direction LR
        H1["Block 0<br/>16 tokens<br/>hash 9f2a"] a1@--> H2["Block 1<br/>16 tokens<br/>hash 4c1b"]
        H2 a2@--> H3["Block 2A<br/>16 tokens<br/>hash 7e8d"]
        H2 a3@--> H4["Block 2B<br/>16 tokens<br/>hash 1a3f"]
    end

    subgraph SGL["SGLang: Radix tree"]
        direction LR
        R0["Trunk<br/>System prompt +<br/>6k-token runbook"] b1@--> B1["Branch 1<br/>Pod OOM check<br/>+ answer"]
        R0 b2@--> B2["Branch 2<br/>Node disk check<br/>+ answer"]
        B1 b3@--> L1["Leaf 1A<br/>Suggest memory limit"]
        B1 b4@--> L2["Leaf 1B<br/>Draft PR patch"]
    end

    VLLM ~~~ SGL
    a1@{ animate: true }
    a2@{ animate: true }
    a3@{ animate: true }
    b1@{ animate: true }
    b2@{ animate: true }
    b3@{ animate: true }
    b4@{ animate: true }

    classDef input fill:#13264a,stroke:#38bdf8,color:#e6f6ff,stroke-width:2px;
    classDef tree fill:#281d48,stroke:#a78bfa,color:#f1edff,stroke-width:2px;
    classDef hit fill:#173f59,stroke:#22d3ee,color:#ecfeff,stroke-width:2px;
    classDef leaf fill:#302410,stroke:#fbbf24,color:#fff3cf,stroke-width:2px;
    class H1,H2,H3,H4 input;
    class R0 tree;
    class B1,B2 hit;
    class L1,L2 leaf;</div>
  <figcaption>Top: vLLM matches fixed 16-token blocks in a hash table. Bottom: SGLang keeps prompts and answers in a branching Radix tree.</figcaption>
</figure>

<h3 id="example-1-one-incident-agent-four-steps">Example 1: One incident agent, four steps</h3>
<p>Imagine an on-call Kubernetes agent that starts with a <strong>6,000-token runbook</strong> and runs four steps. Here is how SGLang&rsquo;s radix tree handles it:</p>
<div class="table-scroll">
  <table>
    <thead>
        <tr>
            <th scope="col">Step</th>
            <th scope="col">What the agent sends</th>
            <th scope="col">What happens in the Radix tree</th>
            <th scope="col">Prefill work saved</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td><strong>1</strong></td>
            <td><code>Runbook + &quot;Why did payment-worker crash?&quot;</code></td>
            <td>No match yet. Creates one node holding the runbook, question, and the model&rsquo;s answer.</td>
            <td>0 tokens (cold start)</td>
        </tr>
        <tr>
            <td><strong>2</strong></td>
            <td><code>Runbook + &quot;Did the node run out of disk?&quot;</code></td>
            <td>Splits the tree right after <code>Runbook</code>. Adds a second branch for the disk question.</td>
            <td><strong>Reuses 6,000 runbook tokens</strong></td>
        </tr>
        <tr>
            <td><strong>3</strong></td>
            <td><code>Step 1 chat history + &quot;Raise memory limit to 2Gi&quot;</code></td>
            <td>Matches the trunk <em>and</em> Branch 1 (including the model&rsquo;s Step 1 answer). Adds a leaf.</td>
            <td><strong>Reuses runbook + Step 1 Q&amp;A</strong></td>
        </tr>
        <tr>
            <td><strong>4</strong></td>
            <td>GPU memory gets full</td>
            <td>Evicts the oldest unused <strong>leaf</strong> first (<code>Step 2</code>), keeping the shared <code>Runbook</code> trunk alive.</td>
            <td>Trunk stays warm for future calls</td>
        </tr>
    </tbody>
  </table>
</div>
<p>Watch how the tree splits and evicts from the leaves inward:</p>
<figure class="mermaid-figure">
  <div class="mermaid">flowchart TD
    T["Shared Trunk<br/>6,000-token Runbook<br/>(kept warm)"] e1@--> B1["Branch 1<br/>Q1: payment-worker crash?<br/>+ Model Answer 1"]
    T e2@--> B2["Branch 2<br/>Q2: node disk full?<br/>(evicted first when idle)"]
    B1 e3@--> L1["Leaf<br/>Q3: Raise limit to 2Gi"]

    e1@{ animate: true }
    e2@{ animate: true }
    e3@{ animate: true }

    classDef trunk fill:#281d48,stroke:#a78bfa,color:#f1edff,stroke-width:3px;
    classDef warm fill:#173f59,stroke:#22d3ee,color:#ecfeff,stroke-width:2px;
    classDef evict fill:#302410,stroke:#fbbf24,color:#fff3cf,stroke-dasharray:5 4;
    class T trunk;
    class B1,L1 warm;
    class B2 evict;</div>
  <figcaption>Step 1 builds a single branch. Step 2 splits the edge at the shared runbook. Step 3 extends Branch 1. When memory fills up, unused leaves get evicted before the shared trunk.</figcaption>
</figure>

<p>And here is what happens across a multi-turn chat when both the prompt and the model&rsquo;s generated reply stay in the tree:</p>
<figure class="mermaid-figure">
  <div class="mermaid">sequenceDiagram
    participant A as Agent
    participant S as SGLang Radix Tree
    participant G as GPU

    A->>S: 1. Runbook + Check pod status
    S->>G: Prefill Runbook + Q1, decode Answer 1
    G-->>S: Save Runbook + Q1 + Answer 1 in tree
    S-->>A: Answer 1 (Pod was OOMKilled)

    A->>S: 2. Turn 1 history + Write YAML fix
    Note over S: Full cache hit on Runbook + Q1 + Answer 1
    S->>G: Prefill only Write YAML fix
    G-->>A: Answer 2 (YAML patch)</div>
  <figcaption>In SGLang, Turn 1 stores both your prompt and the model&#39;s answer in the radix tree. Turn 2 only computes the new follow-up line.</figcaption>
</figure>

<p>One bonus trick: because SGLang can see the whole tree, its scheduler <strong>reorders waiting requests</strong> so requests sharing the same branch run back-to-back before their cached KV pages get evicted.</p>
<h2 id="2-structured-outputs-why-json-schemas-slow-models-down-and-how-jump-forward-fixes-it">2. Structured outputs: why JSON schemas slow models down (and how Jump-Forward fixes it)</h2>
<p>When an <a href="https://vishctl.dev/posts/ai-agent-basics/">AI agent</a> calls a tool or updates a ticket, it cannot return a chatty paragraph. Your code needs strict JSON:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span><span class="nt">&#34;pod_name&#34;</span><span class="p">:</span> <span class="s2">&#34;payment-worker&#34;</span><span class="p">,</span> <span class="nt">&#34;oom_killed&#34;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="nt">&#34;memory_mb&#34;</span><span class="p">:</span> <span class="mi">2048</span><span class="p">}</span>
</span></span></code></pre></div><p>If you only ask for JSON in the system prompt, the model will eventually add markdown backticks or miss a quote. <strong>Guided (or constrained) decoding</strong> guarantees valid JSON by blocking every invalid token at each step.</p>
<h3 id="the-slow-way-checking-one-token-at-a-time">The slow way: checking one token at a time</h3>
<p>Normally, a model generates text one token at a time. With naive guided decoding, every single step looks like this:</p>
<ol>
<li>Check the JSON schema to see which tokens from the 128,000-word vocabulary are legal right now.</li>
<li>Mask out all illegal tokens.</li>
<li>Run a full GPU forward pass to pick <strong>one</strong> token.</li>
</ol>
<h3 id="example-2-count-the-wasted-gpu-steps-in-one-json-line">Example 2: Count the wasted GPU steps in one JSON line</h3>
<p>Look at our JSON output again:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">{&#34;pod_name&#34;: &#34;payment-worker&#34;, &#34;oom_killed&#34;: true, &#34;memory_mb&#34;: 2048}
</span></span></code></pre></div><p>Once the model starts the object, does it really have a choice about typing <code>{&quot;pod_name&quot;: &quot;</code>?</p>
<p>No! The schema requires <code>pod_name</code> first. And after the model generates <code>payment-worker</code>, the very next characters <strong>must</strong> be <code>&quot;, &quot;oom_killed&quot;: </code>.</p>
<p>In a standard token-by-token loop, the GPU still runs a separate decode forward pass for <code>{</code>, <code>&quot;</code>, <code>pod</code>, <code>_name</code>, <code>&quot;:</code>, and <code>&quot;</code>. You are using a massive neural network as an expensive typewriter for fixed punctuation.</p>
<h3 id="the-fast-way-sglangs-jump-forward-decoding">The fast way: SGLang&rsquo;s Jump-Forward Decoding</h3>
<p>SGLang turns your JSON schema into a <strong>Compressed Finite-State Machine (FSM)</strong>. Whenever the schema leaves only one valid path, SGLang <strong>jumps forward</strong>: it inserts the entire fixed string (<code>{&quot;pod_name&quot;: &quot;</code>) at once without running step-by-step decode passes, and only asks the GPU to sample when there is a real decision to make.</p>
<figure class="mermaid-figure">
  <div class="mermaid">flowchart TD
    subgraph SLOW["Token-by-token decode"]
        direction LR
        T1["open<br/>brace"] --> T2["pod"] --> T3["_name"] --> T4["colon +<br/>quote"] --> T5["payment-<br/>worker"] --> T6["...and<br/>so on"]
    end

    subgraph FAST["Jump-Forward decode"]
        direction LR
        J1["Jump at once<br/>pod_name key"] b1@--> J2["GPU sample<br/>payment-worker"]
        J2 b2@--> J3["Jump at once<br/>oom_killed key"]
        J3 b3@--> J4["GPU sample<br/>true"]
    end

    SLOW ~~~ FAST
    b1@{ animate: true }
    b2@{ animate: true }
    b3@{ animate: true }

    classDef waste fill:#302410,stroke:#fbbf24,color:#fff3cf,stroke-width:2px;
    classDef jump fill:#281d48,stroke:#a78bfa,color:#f1edff,stroke-width:2px;
    classDef sample fill:#173f59,stroke:#22d3ee,color:#ecfeff,stroke-width:2px;
    class T1,T2,T3,T4,T6 waste;
    class T5 sample;
    class J1,J3 jump;
    class J2,J4 sample;</div>
  <figcaption>Top: token-by-token decoding runs a GPU step for every forced key and quote. Bottom: Jump-Forward pastes the fixed schema text at once and only samples the values.</figcaption>
</figure>

<h3 id="what-about-xgrammar">What about XGrammar?</h3>
<p>To make the vocabulary masks themselves fast, the SGLang / CMU team also built <strong><a href="https://arxiv.org/abs/2411.15100">XGrammar</a></strong>, which precomputes grammar masks in CPU cache so checking valid tokens takes microseconds instead of milliseconds.</p>
<p>Good news if you use vLLM: <strong>vLLM V1 now uses XGrammar too.</strong> Today, both engines give you fast grammar masking through the standard OpenAI <code>response_format</code> field, while SGLang adds jump-forward decoding and its own Python DSL for multi-step templates.</p>
<h2 id="3-cpu-scheduling-why-fast-gpus-sit-idle-waiting-on-python">3. CPU scheduling: why fast GPUs sit idle waiting on Python</h2>
<p>You might think LLM serving is 100% GPU work. On modern hardware, it isn&rsquo;t.</p>
<h3 id="example-3-the-10-ms-gpu-step-and-the-5-ms-python-pause">Example 3: The 10 ms GPU step and the 5 ms Python pause</h3>
<p>Suppose a batch decode step on an H100 takes <strong>10 ms</strong> on the GPU.</p>
<p>Between every step, the Python process on the CPU has to:</p>
<ul>
<li>Check which sequences finished</li>
<li>Look up prefix cache blocks or radix tree nodes</li>
<li>Build page tables for the next step</li>
<li>Turn token IDs back into text strings for streaming</li>
</ul>
<p>Suppose that CPU bookkeeping takes <strong>5 ms</strong>. If the engine runs the CPU step and the GPU step one after the other, every 10 ms of GPU work is followed by 5 ms of silence:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Sequential:  [CPU 5ms] → [GPU 10ms] → [CPU 5ms (GPU idle!)] → [GPU 10ms]
</span></span><span class="line"><span class="cl">Result:      The GPU sits idle 33% of the time.
</span></span></code></pre></div><h3 id="the-fix-overlapped-scheduling">The fix: Overlapped Scheduling</h3>
<p>SGLang solved this with its <strong><a href="https://lmsys.org/blog/2024-12-04-sglang-v0-4/">Zero-Overhead Batch Scheduler</a></strong>: while the GPU is busy running Step 1, the CPU is <em>already</em> preparing the metadata for Step 2 in parallel. By the time the GPU finishes Step 1, Step 2 is waiting at the door.</p>
<figure class="mermaid-figure">
  <div class="mermaid">flowchart TD
    subgraph SEQ["Without overlap"]
        direction LR
        C1["CPU prep 1<br/>5 ms"] --> G1["GPU run 1<br/>10 ms"] --> C2["CPU prep 2<br/>GPU idle 5 ms"] --> G2["GPU run 2<br/>10 ms"]
    end

    subgraph OVL["With overlap"]
        direction LR
        OC1["CPU prep 1"] b1@--> OG1["GPU run 1<br/>10 ms"]
        OG1 b2@--> OG2["GPU run 2<br/>10 ms"]
        OC1 b3@--> OC2["CPU prep 2<br/>in parallel"]
        OC2 b4@--> OG2
    end

    SEQ ~~~ OVL
    b1@{ animate: true }
    b2@{ animate: true }
    b3@{ animate: true }
    b4@{ animate: true }

    classDef cpu fill:#281d48,stroke:#a78bfa,color:#f1edff,stroke-width:2px;
    classDef gpu fill:#173f59,stroke:#22d3ee,color:#ecfeff,stroke-width:2px;
    classDef stall fill:#302410,stroke:#fbbf24,color:#fff3cf,stroke-width:2px;
    class C1,OC1,OC2 cpu;
    class G1,G2,OG1,OG2 gpu;
    class C2 stall;</div>
  <figcaption>Top: without overlap, the GPU waits every time the CPU prepares the next batch. Bottom: SGLang and vLLM V1 prepare Step 2 on the CPU while Step 1 is still running on the GPU.</figcaption>
</figure>

<p>vLLM saw the exact same bottleneck and rewrote its core loop in <strong><a href="https://blog.vllm.ai/2025/01/27/v1-alpha-release.html">vLLM V1</a></strong> to overlap CPU scheduling with GPU execution as well. That is why gap benchmarks between vLLM v0.x and SGLang narrowed significantly once vLLM V1 became the default.</p>
<h2 id="where-do-they-fit-in-a-kubernetes-cluster">Where do they fit in a Kubernetes cluster?</h2>
<p>If you read my earlier posts on <a href="https://vishctl.dev/posts/llm-d-kubernetes-inference-routing/">llm-d</a> and <a href="https://vishctl.dev/posts/why-lmcache-is-needed/">LMCache</a>, it helps to see how all of these tools fit together. They do not replace each other; they sit at different layers:</p>
<figure class="mermaid-figure">
  <div class="mermaid">flowchart TD
    C["Client / Agent<br/>OpenAI API request"] e1@--> R["llm-d Router<br/>Picks cache-warm pod"]
    R e2@--> P1["Pod A: vLLM or SGLang<br/>Local GPU KV cache"]
    R -.-> P2["Pod B: vLLM or SGLang<br/>Local GPU KV cache"]
    P1 e3@--> L["LMCache / Mooncake<br/>Shared CPU or remote KV pool"]
    P2 -.-> L

    e1@{ animate: true }
    e2@{ animate: true }
    e3@{ animate: true }

    classDef input fill:#13264a,stroke:#38bdf8,color:#e6f6ff,stroke-width:2px;
    classDef router fill:#281d48,stroke:#a78bfa,color:#f1edff,stroke-width:2px;
    classDef pod fill:#173f59,stroke:#22d3ee,color:#ecfeff,stroke-width:2px;
    classDef store fill:#302410,stroke:#fbbf24,color:#fff3cf,stroke-width:2px;
    class C input;
    class R router;
    class P1,P2 pod;
    class L store;</div>
  <figcaption>llm-d routes requests to the right pod. Inside that pod, you run either vLLM or SGLang. LMCache or Mooncake can sit underneath to share KV cache across pods.</figcaption>
</figure>

<ul>
<li><strong>llm-d</strong> sits <strong>above</strong> the pods and routes a request to whichever pod already has the prefix warm.</li>
<li><strong>vLLM or SGLang</strong> runs <strong>inside</strong> each pod to execute the model and manage that pod&rsquo;s GPU KV cache.</li>
<li><strong>LMCache or Mooncake</strong> sits <strong>beside/below</strong> the pods to offload or move KV tensors between pods when one pod&rsquo;s GPU memory isn&rsquo;t enough.</li>
</ul>
<h2 id="hands-on-run-the-same-json-test-on-both-servers">Hands-on: run the same JSON test on both servers</h2>
<p>Because both servers speak the OpenAI API, you can swap one for the other without touching your client code.</p>
<h3 id="1-start-vllm">1. Start vLLM</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">vllm serve Qwen/Qwen3-8B <span class="se">\
</span></span></span><span class="line"><span class="cl">  --port <span class="m">8000</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  --enable-prefix-caching <span class="se">\
</span></span></span><span class="line"><span class="cl">  --structured-outputs-config <span class="s1">&#39;{&#34;backend&#34;: &#34;xgrammar&#34;}&#39;</span>
</span></span></code></pre></div><h3 id="2-start-sglang">2. Start SGLang</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">python3 -m sglang.launch_server <span class="se">\
</span></span></span><span class="line"><span class="cl">  --model-path Qwen/Qwen3-8B <span class="se">\
</span></span></span><span class="line"><span class="cl">  --port <span class="m">30000</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  --grammar-backend xgrammar
</span></span></code></pre></div><h3 id="3-send-the-exact-same-structured-output-request-to-either-port">3. Send the exact same structured-output request to either port</h3>
<p>Point this <code>curl</code> command at <code>localhost:8000</code> (vLLM) or <code>localhost:30000</code> (SGLang):</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -sS http://localhost:30000/v1/chat/completions <span class="se">\
</span></span></span><span class="line"><span class="cl">  -H <span class="s2">&#34;Content-Type: application/json&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -d <span class="s1">&#39;{
</span></span></span><span class="line"><span class="cl"><span class="s1">    &#34;model&#34;: &#34;Qwen/Qwen3-8B&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s1">    &#34;messages&#34;: [
</span></span></span><span class="line"><span class="cl"><span class="s1">      {
</span></span></span><span class="line"><span class="cl"><span class="s1">        &#34;role&#34;: &#34;system&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s1">        &#34;content&#34;: &#34;You are a Kubernetes incident responder. Classify the alert into the required JSON schema.&#34;
</span></span></span><span class="line"><span class="cl"><span class="s1">      },
</span></span></span><span class="line"><span class="cl"><span class="s1">      {
</span></span></span><span class="line"><span class="cl"><span class="s1">        &#34;role&#34;: &#34;user&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s1">        &#34;content&#34;: &#34;Pod payment-worker-7d9f8b-x2k9p is in CrashLoopBackOff with exit code 137 (OOMKilled).&#34;
</span></span></span><span class="line"><span class="cl"><span class="s1">      }
</span></span></span><span class="line"><span class="cl"><span class="s1">    ],
</span></span></span><span class="line"><span class="cl"><span class="s1">    &#34;response_format&#34;: {
</span></span></span><span class="line"><span class="cl"><span class="s1">      &#34;type&#34;: &#34;json_schema&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s1">      &#34;json_schema&#34;: {
</span></span></span><span class="line"><span class="cl"><span class="s1">        &#34;name&#34;: &#34;incident_triage&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s1">        &#34;strict&#34;: true,
</span></span></span><span class="line"><span class="cl"><span class="s1">        &#34;schema&#34;: {
</span></span></span><span class="line"><span class="cl"><span class="s1">          &#34;type&#34;: &#34;object&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s1">          &#34;properties&#34;: {
</span></span></span><span class="line"><span class="cl"><span class="s1">            &#34;pod&#34;: { &#34;type&#34;: &#34;string&#34; },
</span></span></span><span class="line"><span class="cl"><span class="s1">            &#34;reason&#34;: { &#34;type&#34;: &#34;string&#34;, &#34;enum&#34;: [&#34;OOMKilled&#34;, &#34;ImagePullBackOff&#34;, &#34;CrashLoopBackOff&#34;, &#34;NodeNotReady&#34;] },
</span></span></span><span class="line"><span class="cl"><span class="s1">            &#34;oom_killed&#34;: { &#34;type&#34;: &#34;boolean&#34; },
</span></span></span><span class="line"><span class="cl"><span class="s1">            &#34;recommended_action&#34;: { &#34;type&#34;: &#34;string&#34; }
</span></span></span><span class="line"><span class="cl"><span class="s1">          },
</span></span></span><span class="line"><span class="cl"><span class="s1">          &#34;required&#34;: [&#34;pod&#34;, &#34;reason&#34;, &#34;oom_killed&#34;, &#34;recommended_action&#34;],
</span></span></span><span class="line"><span class="cl"><span class="s1">          &#34;additionalProperties&#34;: false
</span></span></span><span class="line"><span class="cl"><span class="s1">        }
</span></span></span><span class="line"><span class="cl"><span class="s1">      }
</span></span></span><span class="line"><span class="cl"><span class="s1">    }
</span></span></span><span class="line"><span class="cl"><span class="s1">  }&#39;</span>
</span></span></code></pre></div><h3 id="4-check-the-cache-hit-metrics">4. Check the cache hit metrics</h3>
<p>Send the request a second time with a different pod name, then check <code>/metrics</code> to confirm the system prompt hit the cache:</p>
<div class="table-scroll">
  <table>
    <thead>
        <tr>
            <th scope="col">Engine</th>
            <th scope="col">Metric to check</th>
            <th scope="col">What you want to see</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td><strong>vLLM</strong></td>
            <td><code>vllm:prefix_cache_hits_total</code> ÷ <code>vllm:prefix_cache_queries_total</code></td>
            <td>Hit ratio climbs on repeated system prompts</td>
        </tr>
        <tr>
            <td><strong>SGLang</strong></td>
            <td><code>sglang:cache_hit_rate</code> and <code>sglang:cached_tokens_total</code></td>
            <td>Radix tree hit rate climbs and prefill time drops</td>
        </tr>
    </tbody>
  </table>
</div>
<p>Both servers also return the cached token count directly in the API response under <code>usage.prompt_tokens_details.cached_tokens</code>.</p>
<h2 id="side-by-side-comparison">Side-by-side comparison</h2>
<div class="table-scroll">
  <table>
    <thead>
        <tr>
            <th scope="col">Feature</th>
            <th scope="col">vLLM</th>
            <th scope="col">SGLang</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td><strong>Main strength</strong></td>
            <td>Broadest model and hardware support; default Kubernetes serving engine</td>
            <td>Branching prefix reuse (RadixAttention), structured outputs, and frontier MoE speed</td>
        </tr>
        <tr>
            <td><strong>KV cache lookup</strong></td>
            <td>Block hash table (<code>--enable-prefix-caching</code>)</td>
            <td><strong>RadixAttention</strong> tree with edge splitting and leaf-first LRU eviction</td>
        </tr>
        <tr>
            <td><strong>Structured JSON</strong></td>
            <td>XGrammar / <code>llguidance</code></td>
            <td>XGrammar / <code>llguidance</code> + <strong>Jump-Forward Decoding</strong></td>
        </tr>
        <tr>
            <td><strong>CPU scheduler</strong></td>
            <td>Overlapped multiprocess loop (vLLM V1)</td>
            <td>Zero-overhead overlapped batch scheduler</td>
        </tr>
        <tr>
            <td><strong>DeepSeek / MoE tuning</strong></td>
            <td>Strong support (MLA, DeepEP, P/D disaggregation)</td>
            <td>Often first to ship new DeepSeek-V3/R1 kernels (MLA, Two-Batch Overlap, MTP)</td>
        </tr>
        <tr>
            <td><strong>Hardware support</strong></td>
            <td><strong>Widest:</strong> NVIDIA, AMD ROCm, Google TPU, Intel Gaudi/XPU, AWS Neuron, Ascend, Apple Silicon, CPU</td>
            <td>Primarily NVIDIA and AMD ROCm, with growing Ascend/TPU/CPU support</td>
        </tr>
        <tr>
            <td><strong>Model variety</strong></td>
            <td><strong>200+ architectures:</strong> LLMs, vision, audio/Whisper, embeddings, rerankers</td>
            <td>Major production LLMs and vision-language models</td>
        </tr>
    </tbody>
  </table>
</div>
<h2 id="when-to-pick-which">When to pick which</h2>
<p>Follow the arrows based on your hardware and traffic:</p>
<figure class="mermaid-figure">
  <div class="mermaid">flowchart TD
    Q1{"Running on non-NVIDIA hardware<br/>(TPU, Gaudi, Neuron) or serving<br/>embeddings / audio / niche models?"}
    Q1 e1@-->|"Yes"| V1["Pick vLLM<br/>Widest hardware and model support"]
    Q1 e2@-->|"No (NVIDIA / AMD + standard LLM)"| Q2{"What does your workload<br/>do most?"}

    Q2 e3@-->|"Agent loops, branching prompts,<br/>multi-turn forks, strict JSON outputs"| S1["Pick SGLang<br/>RadixAttention + Jump-Forward"]
    Q2 e4@-->|"Huge MoE models<br/>(DeepSeek-V3/R1, Qwen3-MoE)"| S2["Test SGLang first<br/>then compare against vLLM V1"]
    Q2 e5@-->|"General chat API or standard<br/>Kubernetes recipes (llm-d, KServe)"| V2["Pick vLLM<br/>Default across K8s tooling"]

    e1@{ animate: true }
    e2@{ animate: true }
    e3@{ animate: true }
    e4@{ animate: true }
    e5@{ animate: true }

    classDef question fill:#281d48,stroke:#a78bfa,color:#f1edff,stroke-width:2px;
    classDef vllm fill:#13264a,stroke:#38bdf8,color:#e6f6ff,stroke-width:3px;
    classDef sglang fill:#173f59,stroke:#22d3ee,color:#ecfeff,stroke-width:3px;
    class Q1,Q2 question;
    class V1,V2 vllm;
    class S1,S2 sglang;</div>
  <figcaption>Start at the top. Hardware and model breadth point to vLLM; branching agent trees, heavy JSON schemas, and large MoE throughput point to SGLang.</figcaption>
</figure>

<h3 id="pick-vllm-when">Pick vLLM when:</h3>
<ul>
<li><strong>You run many different model types or hardware chips.</strong> If one platform team serves chat models, embeddings, rerankers, and Whisper across mixed GPUs (or TPUs and Gaudi), vLLM handles all of them with one tool.</li>
<li><strong>You want plug-and-play Kubernetes guides.</strong> Projects like <a href="https://vishctl.dev/posts/llm-d-kubernetes-inference-routing/">llm-d</a>, <a href="https://vishctl.dev/posts/why-lmcache-is-needed/">LMCache</a>, and KServe test their default recipes on vLLM first.</li>
<li><strong>Your traffic is mostly straightforward chat or independent prompts</strong> where fixed-block prefix caching is plenty.</li>
</ul>
<h3 id="pick-sglang-when">Pick SGLang when:</h3>
<ul>
<li><strong>You run multi-step agents or branching workflows.</strong> If one request forks into sub-tasks, reuses multi-turn answers, and demands strict JSON on every call, RadixAttention and Jump-Forward decoding are built for your exact traffic.</li>
<li><strong>You serve large MoE models like DeepSeek-V3/R1 or Qwen3-MoE on NVIDIA GPUs.</strong> SGLang has consistently led on specialized MoE kernels, Multi-head Latent Attention (MLA), and Two-Batch Overlap.</li>
</ul>
<h2 id="cheat-sheet">Cheat sheet</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">PagedAttention        = store KV cache in small GPU memory pages (both use this)
</span></span><span class="line"><span class="cl">vLLM APC              = find reusable prefixes using a hash table of 16-token blocks
</span></span><span class="line"><span class="cl">RadixAttention        = SGLang&#39;s shared prefix tree; splits branches and evicts leaves first
</span></span><span class="line"><span class="cl">Constrained decoding  = block invalid tokens so the model cannot break your JSON schema
</span></span><span class="line"><span class="cl">Jump-Forward          = paste fixed JSON keys at once instead of decoding token by token
</span></span><span class="line"><span class="cl">XGrammar              = fast grammar mask engine now shared by both SGLang and vLLM
</span></span><span class="line"><span class="cl">Overlap scheduler     = prepare Step 2 on the CPU while Step 1 is running on the GPU
</span></span><span class="line"><span class="cl">vLLM&#39;s sweet spot     = model variety, hardware portability, Kubernetes ecosystem defaults
</span></span><span class="line"><span class="cl">SGLang&#39;s sweet spot   = branching agent prompts, heavy JSON outputs, large MoE speed
</span></span></code></pre></div><p>Because both engines expose the same <code>/v1/chat/completions</code> endpoint, switching is a container-image change, not an application rewrite. To see how a smart Kubernetes router sits in front of either engine, read my <a href="https://vishctl.dev/posts/llm-d-kubernetes-inference-routing/">guide to llm-d</a> or try the <a href="https://vishctl.dev/posts/simulate-gpu-vllm-llm-d-on-laptop/">laptop GPU simulation lab</a>.</p>
]]></content:encoded></item></channel></rss>