<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[# SGLang 开 MTP 后 KV 池被砍到 1/5：一个上游 bug 的完整定位与修复，附同机四方实测总表]]></title><description><![CDATA[<p dir="auto">上一篇我还在说"SGLang 开 MTP 只能跑 32K 上下文，因为它在显存分配上留了 8GB 结构性预留"。</p>
<p dir="auto"><strong>那个结论是错的。</strong> 后来插了几行日志一查，真相是 SGLang 把 MTP 的层数识别成了 64（实际只有 1 层），导致每 token 的 KV 成本被高估 <strong>5 倍</strong>，KV 池被砍到 1/5 —— 不是"显存不够"，是"算错了"。</p>
<p dir="auto">修好之后，<strong>单路 128K + MTP 直接跑通</strong>（池子 44913 → 211357 token）。这篇把定位过程、修复、前后实测和更新后的四方总表一次写完。</p>
<h2>1. 先给三张结论表</h2>
<h3>1.1 修复前后（单变量 A/B：只切换补丁）</h3>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>指标</th>
<th>修复前</th>
<th><strong>修复后</strong></th>
<th>变化</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>cell_size</code></td>
<td>163840 B/token</td>
<td><strong>32768 B/token</strong></td>
<td>÷5</td>
</tr>
<tr>
<td><strong>KV 池 token</strong></td>
<td><strong>44913</strong></td>
<td><strong>211357</strong></td>
<td><strong>×4.71</strong></td>
</tr>
<tr>
<td><strong>单条输入上限</strong></td>
<td><strong>45417 token</strong></td>
<td><strong>~211000 token</strong></td>
<td>×4.65</td>
</tr>
<tr>
<td>127K 单路请求</td>
<td><img src="https://lcz.me/assets/plugins/nodebb-plugin-emoji/emoji/android/274c.png?v=efcae6a46b1" class="not-responsive emoji emoji-android emoji--x" style="height:23px;width:auto;vertical-align:middle" title="❌" alt="❌" /> 被拒（HTTP 400）</td>
<td><img src="https://lcz.me/assets/plugins/nodebb-plugin-emoji/emoji/android/2705.png?v=efcae6a46b1" class="not-responsive emoji emoji-android emoji--white_check_mark" style="height:23px;width:auto;vertical-align:middle" title="✅" alt="✅" /> <strong>成功</strong></td>
<td>—</td>
</tr>
<tr>
<td>短上下文 code</td>
<td>86.1 t/s</td>
<td>86.1 t/s</td>
<td><strong>0</strong></td>
</tr>
<tr>
<td>短上下文 抽取</td>
<td>85.9 t/s</td>
<td>85.9 t/s</td>
<td><strong>0</strong></td>
</tr>
<tr>
<td>短上下文 创作</td>
<td>59.8 t/s</td>
<td>59.8 t/s</td>
<td><strong>0</strong></td>
</tr>
<tr>
<td>双流聚合</td>
<td>170.1 t/s</td>
<td>171.8 t/s</td>
<td>+1%</td>
</tr>
<tr>
<td>运行显存</td>
<td>26182 MiB（余 6016）</td>
<td>30626 MiB（余 1572）</td>
<td>预算用满</td>
</tr>
</tbody>
</table>
<h3>1.2 MTP 到底值多少（同权重同参数，只切投机）</h3>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>场景</th>
<th>无 MTP</th>
<th>MTP</th>
<th>增益</th>
</tr>
</thead>
<tbody>
<tr>
<td>短上下文 code</td>
<td>41.5 t/s</td>
<td><strong>86.1 t/s</strong></td>
<td><strong>+107%</strong></td>
</tr>
<tr>
<td>短上下文 抽取原文</td>
<td>40.5 t/s</td>
<td><strong>85.9 t/s</strong></td>
<td><strong>+112%</strong></td>
</tr>
<tr>
<td>短上下文 中文创作</td>
<td>41.5 t/s</td>
<td><strong>59.8 t/s</strong></td>
<td>+44%</td>
</tr>
<tr>
<td>双流聚合</td>
<td>82.9 t/s</td>
<td><strong>171.8 t/s</strong></td>
<td><strong>+107%</strong></td>
</tr>
<tr>
<td>128K 单路 decode</td>
<td>34.4 t/s</td>
<td>31.5 t/s（另一次 37.8）</td>
<td><strong>−8% ~ +10%，基本持平</strong></td>
</tr>
<tr>
<td>接受长度</td>
<td>—</td>
<td>短上下文 2.5-3.7 / <strong>128K 仅 1.68</strong></td>
<td>—</td>
</tr>
</tbody>
</table>
<p dir="auto"><strong>MTP 的价值全在短/中上下文；128K 下接受长度塌到 1.6-2.0，验证开销把收益吃光 —— 长上下文别指望 MTP。</strong></p>
<h3>1.3 更新后的四方总表</h3>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>项目</th>
<th>① llama.cpp&lt;br&gt;Q6_K+MTP</th>
<th>② llama.cpp&lt;br&gt;unsloth Q4_K_M+MTP</th>
<th>③ SGLang&lt;br&gt;INT4 无投机</th>
<th>④ SGLang&lt;br&gt;INT4+NEXTN</th>
</tr>
</thead>
<tbody>
<tr>
<td>权重</td>
<td>20.89 GB</td>
<td><strong>15.33 GB</strong></td>
<td>17.7 GB</td>
<td>17.7 GB</td>
</tr>
<tr>
<td>code 解码</td>
<td>80.9 t/s</td>
<td><strong>101.2</strong>（nmax4）</td>
<td>41.5 t/s</td>
<td>86.1 t/s</td>
</tr>
<tr>
<td>tool 解码</td>
<td>81.8 t/s</td>
<td><strong>91.1</strong>（nmax4）</td>
<td>40.5 t/s</td>
<td>85.9 t/s</td>
</tr>
<tr>
<td>prose 解码</td>
<td>52.9 t/s</td>
<td><strong>59.7</strong>（nmax3）</td>
<td>41.5 t/s</td>
<td>59.8 t/s</td>
</tr>
<tr>
<td>双路总吞吐</td>
<td>95-160</td>
<td><strong>161.4</strong></td>
<td>82.9 t/s</td>
<td><strong>171.8 t/s</strong></td>
</tr>
<tr>
<td><strong>可用上下文</strong></td>
<td>128K × 2</td>
<td><strong>256K × 2</strong></td>
<td>128K</td>
<td><strong>128K（单路，修复前只有 45K）</strong></td>
</tr>
<tr>
<td>显存</td>
<td>28.4 GB（含视觉）</td>
<td>27.6 GB</td>
<td>30.4 GB</td>
<td>26.2-30.6 GB</td>
</tr>
<tr>
<td>MTP 接受率</td>
<td>未记录</td>
<td>tool 87-100% / code 87-89%</td>
<td>—</td>
<td>0.87-0.94 / 0.90 / 0.50（128K 时 0.42）</td>
</tr>
</tbody>
</table>
<blockquote>
<p dir="auto">口径提醒：①② 是 Windows 11 + <code>-c 262144/131072</code>、temp 0.7；③④ 是 Ubuntu + fp8 KV、贪婪解码。④ 的双路吞吐 171.8 是在 <strong>32K 上下文</strong>下测的，② 的 161.4 是在 <strong>256K</strong> 下测的，论"每单位上下文的速度"② 更强。</p>
</blockquote>
<h2>2. 现象：MTP 一开，KV 池从 28 万掉到 4.5 万</h2>
<p dir="auto">模型是 <code>RedHatAI/Qwen3.8-27B-INT4</code>（17.7 GB，自带 <code>model_mtp.safetensors</code>），同一个启动命令只差投机开关：</p>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>运行</th>
<th>KV 池</th>
<th>128K 请求</th>
</tr>
</thead>
<tbody>
<tr>
<td>不开投机</td>
<td>279214</td>
<td><img src="https://lcz.me/assets/plugins/nodebb-plugin-emoji/emoji/android/2705.png?v=efcae6a46b1" class="not-responsive emoji emoji-android emoji--white_check_mark" style="height:23px;width:auto;vertical-align:middle" title="✅" alt="✅" /> 能跑</td>
</tr>
<tr>
<td>开 NEXTN</td>
<td><strong>44913</strong></td>
<td><img src="https://lcz.me/assets/plugins/nodebb-plugin-emoji/emoji/android/274c.png?v=efcae6a46b1" class="not-responsive emoji emoji-android emoji--x" style="height:23px;width:auto;vertical-align:middle" title="❌" alt="❌" /> <code>Input length (125837 tokens) exceeds the maximum allowed length (60967 tokens)</code></td>
</tr>
</tbody>
</table>
<h2>3. 排查：先以为显存不够，后来发现是算错</h2>
<p dir="auto">第一反应当然是"显存被 MTP 吃掉了"，于是把所有能想到的旋钮都试了一遍：</p>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>尝试</th>
<th>KV 池</th>
<th>结论</th>
</tr>
</thead>
<tbody>
<tr>
<td>基线 mf 0.92</td>
<td>44913</td>
<td>—</td>
</tr>
<tr>
<td>mf 0.97</td>
<td>55228</td>
<td>涨得很少</td>
</tr>
<tr>
<td>mf 0.99</td>
<td>59150</td>
<td>还是很少</td>
</tr>
<tr>
<td>mf 0.97 + 并发降到 1 + prefill 压到 2048 + hicache 缩到 4</td>
<td><strong>60973（上限）</strong></td>
<td>到顶了</td>
</tr>
<tr>
<td><code>--max-total-tokens 262144</code> 强制指定</td>
<td>55228</td>
<td><strong>参数被忽略</strong></td>
</tr>
<tr>
<td>关 CUDA graph</td>
<td>不变</td>
<td>无关</td>
</tr>
<tr>
<td>投机步数从 3 降到 1</td>
<td>+2.9K</td>
<td>无关</td>
</tr>
</tbody>
</table>
<p dir="auto"><strong>参数调到天亮都没用</strong>。直到我注意到一个矛盾：</p>
<pre><code>Memory pool end. avail mem=7.90 GB      ← 分配完还剩 7.9GB 空闲
KV Cache is allocated. #tokens: 44913   ← 却只给了 1.37GB
</code></pre>
<p dir="auto"><strong>显存明明够，它却不用。</strong> 那就不是"不够"，是"算错"。</p>
<p dir="auto">于是往 sizing 代码里插了几行日志（<code>_profile_available_bytes</code> 和 <code>_compute_cell_size</code>），一次就露馅了：</p>
<pre><code>[KV_SIZING] avail=11.359GB slack=2.387GB rest_after_mamba=6.853GB pre_load=29.837GB
            reserve_mb=3712 mf=0.92 post_capture=False
→ 交给池子的预算是 6.85GB，却只分配了 1.37GB

[KV_CELL_DEBUG]  cell_size=32768B (32.0KiB/token) num_layers_arg=16
                 mambaish=True full_attn=16 spec=SpeculativeAlgorithm.EAGLE
                 eagle_draft_layers=64                      ← 这里！
[KV_CELL_DEBUG2] EAGLE 缩放生效: eagle_draft_layers=64 num_layers=16
                 -&gt; cell_size=163840B                       ← 每 token 成本被放大 5 倍
</code></pre>
<p dir="auto">顺带还排除了一个我原本以为的元凶：日志里 <code>post_capture=False</code>，说明那个"混合 mamba 模型的 pre_capture 激活预留"（3712 MB）<strong>根本没生效</strong>，跟这事无关。</p>
<h2>4. 根因：MTP 层数识别错误，×5 的缩放</h2>
<p dir="auto">两处代码组合出来的。</p>
<p dir="auto"><strong>第一处</strong>，<code>spec_aux_hidden_state.py</code> 里推导 draft 层数：</p>
<pre><code class="language-python">num_nextn_predict_layers = draft_model_config.num_nextn_predict_layers
if num_nextn_predict_layers is not None:
    config.eagle_draft_num_layers = int(num_nextn_predict_layers)
else:
    config.eagle_draft_num_layers = int(max(
        draft_model_config.num_hidden_layers,      # ← 拿到 64
        draft_model_config.num_attention_layers,
    ))
</code></pre>
<p dir="auto"><strong>Qwen3.5/3.8 的 MTP 层数写在 <code>text_config.mtp_num_hidden_layers</code> 里（=1）</strong>，不是 DeepSeek/GLM 用的 <code>num_nextn_predict_layers</code>。字段认不出来就走了 fallback，而 <code>--speculative-draft-model-path</code> 指向的正是模型自己的目录 —— 于是这个"draft 模型配置"读到的就是整模型的 <strong>64 层</strong>。</p>
<p dir="auto">（证据：<code>model_mtp.safetensors</code> 里只有 <code>mtp.layers.0.*</code>，就一层。）</p>
<p dir="auto"><strong>第二处</strong>，<code>pool_configurator.py</code> 拿这个数字去缩放每 token 成本：</p>
<pre><code class="language-python"># EAGLE/STANDALONE: scale cell_size to account for draft model KV cache
self._cell_size = int(
    self._cell_size * (1 + int(eagle_draft_num_layers) / int(num_layers))
)
</code></pre>
<p dir="auto">而混合注意力模型（48 层 GDN + 16 层全注意力）里 <code>num_layers</code> 取的是<strong>真正带 KV 的层数 = 16</strong>，于是缩放系数变成：</p>
<pre><code>1 + 64 / 16 = 5
</code></pre>
<p dir="auto"><strong>每 token 成本高估 5 倍，KV 池就被砍到 1/5。</strong></p>
<h2>5. 修法：一处改动</h2>
<p dir="auto">最地道的改法不是去改那个 fallback（那只修了一处），而是在 <code>ModelConfig</code> 里就把字段补齐 —— 所有依赖 <code>num_nextn_predict_layers</code> 的地方都自动受益：</p>
<pre><code class="language-diff"># sglang/srt/configs/model_config.py
         self.num_nextn_predict_layers = getattr(
             self.hf_text_config, "num_nextn_predict_layers", None
         )
+        if self.num_nextn_predict_layers is None:
+            # Qwen3.5/3.8 name the MTP depth `mtp_num_hidden_layers`
+            self.num_nextn_predict_layers = getattr(
+                self.hf_text_config, "mtp_num_hidden_layers", None
+            )
</code></pre>
<p dir="auto"><strong>验证（只改这一处，把之前打在 fallback 上的补丁撤销）</strong>：</p>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>mem-fraction</th>
<th>修复前</th>
<th><strong>只改 ModelConfig 后</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td>0.92</td>
<td>44913</td>
<td><strong>211355</strong></td>
</tr>
<tr>
<td>0.97</td>
<td>59150</td>
<td><strong>257367</strong></td>
</tr>
</tbody>
</table>
<p dir="auto"><code>mf 0.97</code> 时池子 257367 token，双路 128K（需要 262144）已经接近可行。</p>
<h2>6. 修复前后实测：单变量 A/B</h2>
<p dir="auto">方法：只切换这一个补丁，vocab 占位补丁两边都开，其他启动参数逐字符相同。</p>
<p dir="auto">公共参数：</p>
<pre><code class="language-bash">--model-path &lt;Qwen3.8-27B-INT4&gt; --language-only \
--context-length 131072 --max-running-requests 2 --mem-fraction-static 0.92 \
--kv-cache-dtype fp8_e4m3 --page-size 1 \
--max-mamba-cache-size 16 --mamba-ssm-dtype bfloat16 \
--enable-hierarchical-cache --hicache-size 12 --hicache-write-policy write_through \
--speculative-algorithm NEXTN --speculative-draft-model-path &lt;同目录&gt; \
--speculative-eagle-topk 1 --speculative-num-steps 3 --speculative-num-draft-tokens 4
</code></pre>
<h3>127K 单路请求（本轮唯一真实差异）</h3>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th></th>
<th>修复前</th>
<th>修复后</th>
</tr>
</thead>
<tbody>
<tr>
<td>结果</td>
<td><strong>HTTP 400</strong>：<code>exceeds the maximum allowed length (45417 tokens)</code></td>
<td><strong><img src="https://lcz.me/assets/plugins/nodebb-plugin-emoji/emoji/android/2705.png?v=efcae6a46b1" class="not-responsive emoji emoji-android emoji--white_check_mark" style="height:23px;width:auto;vertical-align:middle" title="✅" alt="✅" /> 成功</strong></td>
</tr>
<tr>
<td>prompt</td>
<td>—</td>
<td><strong>128180 token</strong></td>
</tr>
<tr>
<td>prefill</td>
<td>—</td>
<td>112.9 s（<strong>1136 tok/s</strong>）</td>
</tr>
<tr>
<td>解码 @128K</td>
<td>—</td>
<td>31.5 t/s（接受长度 1.68）</td>
</tr>
</tbody>
</table>
<h3>短上下文 decode：两边逐位相同（对照组）</h3>
<pre><code>修复前:  code 86.1 t/s accept 3.56 | echo 85.9 accept 3.66 | prose 59.8 accept 2.51
        双流聚合 90.9 + 79.2 = 170.1 t/s
修复后:  code 86.1 t/s accept 3.56 | echo 85.9 accept 3.66 | prose 59.8 accept 2.51
        双流聚合 90.9 + 80.9 = 171.8 t/s
</code></pre>
<p dir="auto"><strong>逐位相同是预期结果</strong>：那个字段只被 KV 池尺寸估算消费，不参与任何 kernel、attention 或采样逻辑。这也正好反证了补丁是纯内存修复 —— 如果有人报告"打了补丁速度变了"，那一定是别的原因。</p>
<h3>顺带一修的另一个浪费：draft 的 vocab 层（2.4 GiB）</h3>
<p dir="auto">MTP draft 在 <code>__init__</code> 里会建自己的 <code>embed_tokens</code> 和 <code>lm_head</code>（各 248320×5120×2 byte ≈ 2.5 GB），但运行时 <code>set_embed_and_head()</code> 会把它们 <code>del</code> 掉、换成目标模型的张量引用 —— <strong>而 KV 池是在这之前就按"draft 还占着 5.53GB"算好尺寸的</strong>，之后释放的显存就白空着。</p>
<p dir="auto">用"0 尺寸占位"规避（上游对应 PR #37155 把这步释放提前了）：</p>
<pre><code>draft 加载显存:  5.53 GB → 0.79 GB
KV 池:          14161 → 45423 token
</code></pre>
<p dir="auto"><strong>两个修复加起来</strong>：池子 14161 → 45423（vocab 层）→ 211357（层数），总共 <strong>×14.9</strong>。</p>
<h2>7. MTP 参数怎么扫（同 llama.cpp 扫 n-max 的规律）</h2>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>steps/draft</th>
<th>code</th>
<th>抽取</th>
<th>创作</th>
<th>接受率（code/抽取/创作）</th>
</tr>
</thead>
<tbody>
<tr>
<td>1/2</td>
<td>62.0</td>
<td>60.5</td>
<td>54.6</td>
<td>0.97 / 0.95 / 0.74</td>
</tr>
<tr>
<td>2/3</td>
<td>73.5</td>
<td>77.8</td>
<td><strong>60.9</strong></td>
<td>0.87 / 0.98 / 0.64</td>
</tr>
<tr>
<td><strong>3/4</strong></td>
<td><strong>86-89</strong></td>
<td><strong>86</strong></td>
<td>59.8</td>
<td>0.87-0.94 / 0.90 / 0.50</td>
</tr>
<tr>
<td>5/6</td>
<td><strong>108.7</strong></td>
<td>—（显存不足）</td>
<td>59.6</td>
<td>0.91 / — / 0.42</td>
</tr>
</tbody>
</table>
<p dir="auto">规律和 llama.cpp 完全一致：<strong>steps 越大，可预测任务（代码/工具）越快，创作类接受率反而下降</strong>。而且<strong>上下文越长接受长度越低</strong>：短上下文 3.76，128K 时只有 1.68。</p>
<h2>8. 三方/四方横评（更新后）</h2>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>项目</th>
<th>① llama.cpp Q6_K+MTP</th>
<th>② llama.cpp Q4_K_M+MTP</th>
<th>③ SGLang INT4 无投机</th>
<th>④ SGLang INT4+NEXTN</th>
</tr>
</thead>
<tbody>
<tr>
<td>系统</td>
<td>Windows 11</td>
<td>Windows 11</td>
<td>Ubuntu</td>
<td>Ubuntu</td>
</tr>
<tr>
<td>权重</td>
<td>20.89 GB</td>
<td>15.33 GB</td>
<td>17.7 GB</td>
<td>17.7 GB</td>
</tr>
<tr>
<td>code</td>
<td>80.9</td>
<td><strong>101.2</strong></td>
<td>41.5</td>
<td>86.1</td>
</tr>
<tr>
<td>tool</td>
<td>81.8</td>
<td><strong>91.1</strong></td>
<td>40.5</td>
<td>85.9</td>
</tr>
<tr>
<td>prose</td>
<td>52.9</td>
<td><strong>59.7</strong></td>
<td>41.5</td>
<td>59.8</td>
</tr>
<tr>
<td>双路聚合</td>
<td>95-160</td>
<td>161.4</td>
<td>82.9</td>
<td><strong>171.8</strong></td>
</tr>
<tr>
<td>上下文</td>
<td>128K×2</td>
<td><strong>256K×2</strong></td>
<td>128K</td>
<td><strong>128K 单路</strong>（修复前 45K）</td>
</tr>
<tr>
<td>显存</td>
<td>28.4 GB 含视觉</td>
<td>27.6 GB</td>
<td>30.4 GB</td>
<td>26.2-30.6 GB</td>
</tr>
<tr>
<td>prefill</td>
<td>未测</td>
<td>报告未列</td>
<td><strong>1680-1915 tok/s</strong></td>
<td>~1775 tok/s</td>
</tr>
</tbody>
</table>
<p dir="auto"><strong>结论</strong>：</p>
<ol>
<li>llama.cpp + Q4_K_M + MTP 仍是"最快 + 最长上下文"的综合最优（256K×2 还能 161 t/s），它的 KV 池是<strong>按实际使用分配</strong>；</li>
<li>SGLang + MTP 修好之后，解码反超 llama.cpp（+6%~13%），双流聚合最高，而且<strong>单路 128K 保住了</strong>；</li>
<li>SGLang 无投机的唯一优势是 prefill（1680-1915 tok/s，123K 时还有 1199）和 HiCache 前缀复用（17.9K 前缀 10.2s → 1.25s）；</li>
<li>两者取舍的核心差异从"显存分配策略"变成了"<strong>MTP 在长上下文下收益趋零</strong>"这个共同的物理限制。</li>
</ol>
<h2>9. 给同是小白的提醒</h2>
<ol>
<li><strong>池子异常小的时候，先怀疑"算错"而不是"不够"</strong>。判据很简单：<strong>分配完成后还剩多少显存</strong>。像这次分完还剩 7.9GB，就说明预算没被用满，参数调到天亮都没用。</li>
<li><strong>定位靠日志，别靠猜</strong>：在 sizing 路径（<code>_profile_available_bytes</code>、<code>_compute_cell_size</code>）插几行 <code>print</code>，把中间值打出来，一次就能看到 <code>cell_size</code> 被放大了 5 倍。改完记得把调试补丁撤掉（env 变量门控更安全）。</li>
<li><strong>换带 MTP 的权重先确认两件事</strong>：它带 <code>model_mtp.safetensors</code>；<code>model.safetensors.index.json</code> 里引用了它。</li>
<li><strong>Xet 存储的仓库必须 <code>HF_HUB_DISABLE_XET=1</code></strong>，否则 hf-mirror 等于没用，还会 401。</li>
<li><strong><code>max_running_requests is capped to 1</code> 这条日志一定要看</strong>：混合 mamba 模型 + 投机下每请求要 5 个 mamba 槽，槽不够就变排队（我实测 2×64K 墙钟 84.4s ≈ 2×42s，是串行不是并行）。</li>
<li><strong><code>--max-mamba-cache-size</code> 别乱调大</strong>：<code>bf16 + 16 槽</code> 下跑 2×60K 长并发，两个请求都能完成但随后 <code>AssertionError: Can not alloc mamba cache</code>（<code>mamba_component.py:479</code>）→ SIGQUIT → <strong>整个服务退出</strong>。长上下文档建议用 <code>--max-mamba-cache-size 6</code>（默认 float32）。</li>
<li><strong>投机参数要自己扫</strong>，而且<strong>别忘了它只在短上下文值钱</strong>：短上下文 +107%，128K 下 −8%~+10%（基本白干）。</li>
</ol>
<h2>10. 还没解决的</h2>
<ul>
<li><strong>双路长并发的 mamba 槽位断言崩溃</strong>（见提醒 6），要么等上游修，要么避开 bf16 + 大槽位组合。</li>
<li><strong>128K 下 MTP 收益趋零</strong>：接受长度塌到 1.68，属于投机解码在长上下文下的通用问题，不是这个 bug 带来的。</li>
<li><strong>SGLang 的 L3 磁盘层只写不读</strong>（混合模型上从不触发预取），三级缓存实际只能用两级。</li>
</ul>
<hr />
<p dir="auto">上游已按第 5 节的方案提交了 issue 和最小复现脚本（<code>repro_mtp_draft_layers.py</code>：Part 1 不需要 GPU，只读 config.json 和权重就能证明误判；Part 2 起两次服务对比池子）。这个 bug 对<strong>所有用 <code>mtp_num_hidden_layers</code> 的 Qwen3.5/3.8 混合模型</strong>都成立，池子会被砍 <code>1 + 总层数/带KV层数</code> 倍；就算是全注意力模型（每层都带 KV）也会被砍 2 倍。</p>
<p dir="auto">感谢论坛里讨论投机解码和显存分配的各位，这次少走了很多弯路。</p>
]]></description><link>https://lcz.me/topic/1631</link><generator>RSS for Node</generator><lastBuildDate>Tue, 22 Sep 2026 16:09:42 GMT</lastBuildDate><atom:link href="https://lcz.me/topic/1631.rss" rel="self" type="application/rss+xml"/><pubDate>Fri, 11 Sep 2026 15:44:46 GMT</pubDate><ttl>60</ttl><item><title><![CDATA[Reply to # SGLang 开 MTP 后 KV 池被砍到 1/5：一个上游 bug 的完整定位与修复，附同机四方实测总表 on Fri, 11 Sep 2026 16:04:29 GMT]]></title><description><![CDATA[<p dir="auto">这个 ×5 的定位很漂亮，我这边有一段要跟着改口：我在隔壁 1622 说过「SGLang 侧因 MTP 预留只剩 ~45K」，用的就是「8GB 结构性预留」那个说法——来源是你上一篇的结论，现在被你自己推翻了，那句撤回，改记成这一版：不是预留，是 sizing 把 draft 层数从 1 算成了 64。</p>
<p dir="auto">借这个案例固化三条，以后再遇到同类（不报错但某个数字差整数倍）能少走夜路：</p>
<p dir="auto"><strong>1. 派生量要和真值对账，别只跟参数对账</strong></p>
<p dir="auto">你试的那一圈旋钮（mf、并发、hicache、--max-total-tokens、关 CUDA graph）全是「喂给推导公式的输入」，公式本身错了，输入怎么调都没用。真正的破局点其实不是日志，是那两行自相矛盾的数字：</p>
<ul>
<li>avail mem = 7.90 GB</li>
<li>却只分配了 1.37 GB</li>
</ul>
<p dir="auto">凡是「资源够但不用」，优先怀疑「算错」而不是「不够」。</p>
<p dir="auto"><strong>2. 唯一真值在权重文件的 header 里</strong></p>
<p dir="auto">config 是元数据，可以缺字段——你这份就是 <code>num_nextn_predict_layers</code> 为 None 才走的 fallback；safetensors 的 header 才是事实。所以拿到任何带 draft/MTP 的第三方包，先读 header 把 draft 那一块的层数数出来，跟日志里的 <code>eagle_draft_layers</code> 对一次，两者不等就先别看性能数字：</p>
<pre><code>import json, struct
with open('model_mtp.safetensors', 'rb') as f:
    n = struct.unpack('&lt;Q', f.read(8))[0]
    hdr = json.loads(f.read(n))
print(len(hdr), list(hdr)[:5])
</code></pre>
<p dir="auto">（层号前缀按你这份权重的实际命名取，要紧的是「数出来的层数」，不是「config 声称的层数」。）</p>
<p dir="auto"><strong>3. 修复优先走配置，不走 patch</strong></p>
<p dir="auto">你的 traceback 已经把答案给出来了：第一分支读的是 <code>draft_model_config.num_nextn_predict_layers</code>，只有它是 None 才 fallback 到 <code>num_hidden_layers</code>。所以在模型目录的 config.json 里显式写上这个字段（值 = header 里数出来的真实 MTP 层数），比打上游 patch 稳：SGLang 换 wheel 不会把你打回原形，也不用维护 diff。上游那个 fallback 值得顺手提个 issue——拿 <code>num_hidden_layers</code> 当 draft 层数在结构上就不成立，这类单层 MTP head 永远不是 N 层。</p>
<p dir="auto"><strong>一个可以顺手验的预测</strong>：修好后 32768 B/token ÷ 16 层 ÷ 2(KV) = 1024 B/层/token，正好是 fp8 下 kv_heads × head_dim = 1024 的布局。如果你哪天为了精度把 KV 换成 bf16，池子会再掉一半回到 10 万 token 出头——「128K 单路 + MTP」就又放不下了。也就是说这份配置的可跑性是被 fp8 KV 撑起来的，换精度前先把这条账算清。</p>
<p dir="auto">最后，你 1.2 表里那条结论我认为是整帖最值钱的部分：128K 下接受长度塌到 1.68、decode 31.5 对无 MTP 的 34.4——说明长上下文里 MTP 的收益会被验证开销吃光。<strong>长上下文的显存应该给 KV 池，不是给 draft 权重。</strong> 这条和引擎无关，llama.cpp 那边也是同一个方向，比「SGLang 行不行」有用得多。</p>
]]></description><link>https://lcz.me/post/17367</link><guid isPermaLink="true">https://lcz.me/post/17367</guid><dc:creator><![CDATA[Xiaote]]></dc:creator><pubDate>Fri, 11 Sep 2026 16:04:29 GMT</pubDate></item></channel></rss>