<?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[把只支持 5090 的推理引擎跑在 4090/4080 上，性能全面超过 SGLang]]></title><description><![CDATA[<h1>在 Ada 上跑 NInfer：RTX 4090 + 4080 双卡部署记</h1>
<blockquote>
<p dir="auto">NInfer 是一个为 RTX 5090 单卡硬编码的手写 C++/CUDA 推理引擎，CMake 第 9 行就拒绝任何非 <code>sm_120a</code> 架构。借助一个被上游拒收的社区移植分支，我们把它跑在了 RTX 4090 与 4080 上，两张卡都全面超过原先的 SGLang 部署。</p>
</blockquote>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td>日期</td>
<td>2026-09-15</td>
</tr>
<tr>
<td>主机</td>
<td>Ubuntu 24.04.4 · 内核 7.0.0-31-generic</td>
</tr>
<tr>
<td>模型</td>
<td><a href="https://huggingface.co/fullmetaljackass/Qwen3.8-27B-Uncensored-NInfer" rel="nofollow ugc">Qwen3.8-27B-Uncensored-NInfer</a> · groupwise-int · 18.21 GB</td>
</tr>
<tr>
<td>状态</td>
<td>已接入生产客户端</td>
</tr>
</tbody>
<tbody>
<tr>
<td></td>
<td>RTX 4090</td>
<td>RTX 4080</td>
</tr>
<tr>
<td>---</td>
<td>---</td>
<td>---</td>
</tr>
<tr>
<td>显存</td>
<td>49,140 MiB</td>
<td>32,760 MiB</td>
</tr>
<tr>
<td>端口 / model-id</td>
<td>:8001 / <code>4090</code></td>
<td>:8002 / <code>4080</code></td>
</tr>
<tr>
<td>并发</td>
<td>3</td>
<td>2</td>
</tr>
<tr>
<td><strong>KV 池</strong></td>
<td><strong>786,432</strong></td>
<td><strong>350,208</strong></td>
</tr>
<tr>
<td>可同时吃满 262K 的通道</td>
<td><strong>3</strong></td>
<td>1</td>
</tr>
<tr>
<td>decode 单路</td>
<td><strong>115.5 tok/s</strong></td>
<td><strong>88.2 tok/s</strong></td>
</tr>
<tr>
<td>decode 满并发</td>
<td>207.5 tok/s（3 路）</td>
<td>128.1 tok/s（2 路）</td>
</tr>
<tr>
<td>prefill（长上下文）</td>
<td>1,068 tok/s</td>
<td>701 tok/s</td>
</tr>
<tr>
<td>权重 / 启动后空闲</td>
<td>16.9 / 1.98 GiB</td>
<td>16.9 / 1.13 GiB</td>
</tr>
<tr>
<td>启动耗时</td>
<td>12.0 s</td>
<td>12.0 s</td>
</tr>
</tbody>
</table>
<hr />
<h2>一、环境与构建</h2>
<p dir="auto">上游 <code>Neroued/ninfer</code> 的 CMake 对非 <code>sm_120a</code> 直接 <code>FATAL_ERROR</code>，官方只支持 RTX 5090。下面用的是社区的 Ada 移植分支 <strong><code>memoriaru/ninfer</code> 的 <code>sm89-ada-port</code></strong>（1 个 commit、18 个文件，上游以「难以维护」为由拒收，<strong>已落后上游 93 个 commit</strong>，想跟进得自己 rebase）。</p>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>组件</th>
<th>版本 / 值</th>
</tr>
</thead>
<tbody>
<tr>
<td>OS / 内核</td>
<td>Ubuntu 24.04.4 LTS · 7.0.0-31-generic</td>
</tr>
<tr>
<td>GPU 0</td>
<td>RTX 4090 · 49,140 MiB · UUID <code>GPU-aaaaaaaa-…</code>（<code>nvidia-smi -L</code> 查）</td>
</tr>
<tr>
<td>GPU 1</td>
<td>RTX 4080 · 32,760 MiB · UUID <code>GPU-bbbbbbbb-…</code></td>
</tr>
<tr>
<td>驱动</td>
<td>580.159.04（<strong>只支持到 CUDA 13.0</strong>）</td>
</tr>
<tr>
<td>CUDA 工具链</td>
<td>13.1.115（NInfer 硬要求 ≥ 13.1）</td>
</tr>
<tr>
<td>源码</td>
<td><code>memoriaru/ninfer</code> @ <code>baf9feb3e41039c7ce92323b9f4f0f732e120866</code></td>
</tr>
<tr>
<td>二进制</td>
<td><code>ninfer-serve</code> 177 MiB</td>
</tr>
<tr>
<td>模型</td>
<td><code>fullmetaljackass/Qwen3.8-27B-Uncensored-NInfer</code> · 18.21 GB · groupwise-int</td>
</tr>
</tbody>
</table>
<blockquote>
<p dir="auto"><strong>驱动版本差一档，但能跑。</strong> 驱动 580.159.04 报告支持到 CUDA 13.0，而引擎要求工具链 ≥ 13.1。靠 CUDA <em>minor version compatibility</em>——13.1 编出的二进制能跑在支持 13.0 的驱动上——实测无问题，全程零运行期错误。不需要升级驱动。</p>
</blockquote>
<pre><code class="language-bash"># 1. 工具链（与系统原有 CUDA 12.8 并存，不动 /usr/local/cuda 符号链接）
sudo apt-get install -y cuda-toolkit-13-1
sudo apt-get install -y pkg-config libavformat-dev libavcodec-dev \
                        libavutil-dev libswscale-dev libcurl4-openssl-dev

# 2. 源码
git clone --depth 1 --branch sm89-ada-port \
    https://github.com/memoriaru/ninfer.git src

# 3. 配置 —— 打印 "arch=89 NVFP4=OFF" 即为正确
cmake -S src -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \
  -DCMAKE_CUDA_ARCHITECTURES=89 \
  -DCMAKE_CUDA_COMPILER=/usr/local/cuda-13.1/bin/nvcc \
  -DCUDAToolkit_ROOT=/usr/local/cuda-13.1 \
  -DNINFER_BUILD_APPS=ON

# 4. 编译 —— 288 步，零错误
ninja -C build -j6
</code></pre>
<p dir="auto">FFmpeg 那几个 dev 包是视频/多模态路径的硬依赖，缺了 CMake 直接失败。系统里有运行时库不够，必须有头文件和 <code>.pc</code>。Ubuntu 24.04 自带的 FFmpeg 6.1.1 正好满足版本下限（libavformat 60 / libavcodec 60 / libavutil 58 / libswscale 7）。</p>
<p dir="auto">编译耗时最久的单个文件是 <code>src/ops/linear/w8/w8_small_t.cu</code>——正是移植里被改造的那个（136 个 kernel 实例从静态共享内存挪到动态）。它编过了，就说明那套绕开 48 KB 限制的办法在 Ada 上成立。</p>
<hr />
<h2>二、跑的是什么模型</h2>
<p dir="auto"><strong>下载地址</strong>：<a href="https://huggingface.co/fullmetaljackass/Qwen3.8-27B-Uncensored-NInfer" rel="nofollow ugc">https://huggingface.co/fullmetaljackass/Qwen3.8-27B-Uncensored-NInfer</a></p>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td>格式 / 体积</td>
<td><code>.ninfer</code> 单文件 · <strong>18.21 GB</strong> · <code>weights_id: groupwise-int</code></td>
</tr>
<tr>
<td>基座</td>
<td><code>orcarouter/Qwen3.8-27B-Uncensored</code> ← <code>Qwen/Qwen3.8-27B</code></td>
</tr>
<tr>
<td>能力</td>
<td>文本 + <strong>视觉</strong> + <strong>MTP 投机解码</strong> · 上下文 262,144 · 中英</td>
</tr>
<tr>
<td>许可</td>
<td>Apache-2.0</td>
</tr>
</tbody>
</table>
<p dir="auto">架构是 Qwen3.8-27B：64 层 dense，其中 <strong>16 层 full-attention + 48 层 linear-attention（GDN）</strong>——这个混合结构决定了 KV 只要 32.4 KB/token。</p>
<p dir="auto">三点要注意：</p>
<ul>
<li>仓库里<strong>只有 3 个文件</strong>（<code>.gitattributes</code> / <code>README.md</code> / <code>.ninfer</code>），没有 safetensors、config.json、tokenizer——全部内嵌在单文件里。<strong>这个格式只能给 NInfer 用</strong>，喂不了 vLLM / SGLang / llama.cpp。</li>
<li>标签里写着 <code>RTX-5090</code>：<strong>打包者的目标平台是 Blackwell</strong>。Ada 能跑是本文移植的结果，不是发布者的承诺。</li>
<li>★<strong>必须选 <code>groupwise-int</code> 的 artifact</strong>。同一个模型的 NVFP4 版本在 Ada 上永远不能用（FP4 张量核是 Blackwell 硬件）。判别方法：<code>.ninfer</code> 文件开头是明文 JSON manifest，用 HTTP Range 取前 1.5 MB 就能读出 <code>identity.weights_id</code>，不必下整个 18 GB。</li>
</ul>
<hr />
<h2>三、RTX 4090：NInfer 对比 SGLang</h2>
<p dir="auto">同一天、同一张卡、同一段 prompt、同一套计时方法，512 token 输出，关闭 thinking。这是本文里最干净的一组对照。</p>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>项目</th>
<th>SGLang 0.5.19</th>
<th>NInfer</th>
<th>变化</th>
</tr>
</thead>
<tbody>
<tr>
<td>模型</td>
<td>twolven abliterated W4-AWQ · 19 GB</td>
<td>Uncensored groupwise-int · 18.2 GB</td>
<td>换了基座</td>
</tr>
<tr>
<td>KV 数据类型</td>
<td>fp8_e4m3</td>
<td>int8</td>
<td>—</td>
</tr>
<tr>
<td>KV 池</td>
<td>451,959</td>
<td><strong>786,432</strong></td>
<td><strong>+74%</strong></td>
</tr>
<tr>
<td>单请求上下文</td>
<td>262,144</td>
<td>262,144</td>
<td>持平</td>
</tr>
<tr>
<td>并发上限</td>
<td>2</td>
<td><strong>3</strong></td>
<td>+1</td>
</tr>
<tr>
<td><strong>可同时吃满 262K 的通道</strong></td>
<td>1</td>
<td><strong>3</strong></td>
<td><strong>×3</strong></td>
</tr>
<tr>
<td>投机解码</td>
<td>NEXTN 树形 topk4/steps5/draft16</td>
<td>MTP 线性 draft3 + lm-head-draft</td>
<td>降级为线性</td>
</tr>
<tr>
<td>启动耗时</td>
<td>~82 s</td>
<td><strong>12.0 s</strong></td>
<td><strong>−85%</strong></td>
</tr>
<tr>
<td>启动后空闲显存</td>
<td>4.16 GB</td>
<td>1.98 GiB</td>
<td>更紧</td>
</tr>
<tr>
<td>decode 单路</td>
<td>101.4</td>
<td><strong>115.5</strong></td>
<td><strong>+14%</strong></td>
</tr>
<tr>
<td>decode 双路合计</td>
<td>173.2</td>
<td><strong>178.3</strong></td>
<td>+3%</td>
</tr>
<tr>
<td>decode 三路合计</td>
<td>配置上限 2，未测</td>
<td><strong>207.5</strong></td>
<td>—</td>
</tr>
<tr>
<td>prefill（长上下文）</td>
<td>~1,115 @207K</td>
<td>1,068 @249,952</td>
<td>基本持平</td>
</tr>
</tbody>
</table>
<p dir="auto"><strong>一个漂亮的整除</strong>：<code>--max-concurrency 3</code> 配 <code>--kv-capacity auto</code>，引擎解出的 KV 池是 <strong>786,432 = 3 × 262,144</strong>，页表 <code>12,288 / 12,288</code> 也刚好用满。3 是「每一路都能吃满满上下文」的最大档，4 路需要 1,048,576，差 26 万装不下。</p>
<p dir="auto">对比之下 SGLang 的 451,959 连两路满上下文（524,288）都装不下——第二路会在准入阶段被 <code>AddReqResult.NO_TOKEN</code> 直接拒绝。</p>
<p dir="auto"><strong>两处口径说明</strong>：</p>
<ul>
<li>SGLang 的三路那一栏不是「测了跑不动」，是配置上做不到——<code>--max-running-requests 2</code> 写死了。短请求场景下它未必输，把上限调到 3 再测大概率也能跑。NInfer 站得住的优势是长上下文：786,432 的池子能让三路同时吃满 262K。</li>
<li>SGLang 那个 prefill 数字采集时有其它客户端并发流量，被污染过，可比性一般。</li>
</ul>
<hr />
<h2>四、RTX 4080：NInfer 对比 SGLang</h2>
<p dir="auto">4080 上原本跑的是另一个模型（cyankiwi AWQ-INT4），所以这组对照不如 4090 那组严格，配置差异已逐项列出。</p>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>项目</th>
<th>SGLang 0.5.19</th>
<th>NInfer</th>
<th>变化</th>
</tr>
</thead>
<tbody>
<tr>
<td>模型</td>
<td>cyankiwi AWQ-INT4</td>
<td>Uncensored groupwise-int</td>
<td>换了模型</td>
</tr>
<tr>
<td>KV 池</td>
<td>151,419</td>
<td><strong>350,208</strong></td>
<td><strong>+131%</strong></td>
</tr>
<tr>
<td>单请求上下文</td>
<td>147,456</td>
<td><strong>262,144</strong></td>
<td><strong>+78%</strong></td>
</tr>
<tr>
<td>并发上限</td>
<td>1</td>
<td><strong>2</strong></td>
<td>+1</td>
</tr>
<tr>
<td>投机解码</td>
<td>NEXTN 链式 topk1/steps5/draft6</td>
<td>MTP 线性 draft3 + lm-head-draft</td>
<td>—</td>
</tr>
<tr>
<td>启动后空闲显存</td>
<td>2.97 GB</td>
<td>1.13 GiB</td>
<td>更紧</td>
</tr>
<tr>
<td>decode 单路</td>
<td>未测该模型</td>
<td><strong>88.2</strong></td>
<td>见下</td>
</tr>
<tr>
<td>decode 双路合计</td>
<td>并发上限 1</td>
<td><strong>128.1</strong></td>
<td>—</td>
</tr>
<tr>
<td>prefill（长上下文）</td>
<td>未测</td>
<td>701 @200,932</td>
<td>—</td>
</tr>
</tbody>
</table>
<blockquote>
<p dir="auto"><strong>4080 的速度对照要打折看。</strong> 被替换掉的那个 SGLang 服务（cyankiwi AWQ-INT4）我们没有测过它的 tok/s。同一张卡上另一个模型 twolven-AWQ 的历史记录是 <strong>56–64 tok/s（峰值 68，2026-08-21，上下文 131,072）</strong>。拿它和 NInfer 的 88.2 比是 <strong>+40~55%</strong>——但那是<em>不同模型、不同日期、不同上下文长度</em>的数字，只能当指示性参考，不能当严格 A/B。4090 那一组才是同日同条件的干净对照。</p>
</blockquote>
<hr />
<h2>五、启动器与服务单元</h2>
<p dir="auto">两张卡用同一个二进制、同一个 artifact，靠 <code>CUDA_VISIBLE_DEVICES</code> 绑 UUID 区分，端口与 <code>--model-id</code> 沿用原 SGLang 的值，客户端零改动。</p>
<p dir="auto"><strong><code>/data/launchers/start-ninfer-4090.sh</code></strong></p>
<pre><code class="language-bash">#!/bin/bash
export CUDA_VISIBLE_DEVICES=GPU-aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa
export LD_LIBRARY_PATH=/usr/local/cuda-13.1/lib64:$LD_LIBRARY_PATH
exec /data/ninfer-build/build/apps/ninfer-serve \
    /data/ninfer-models/qwen3_8_27b_uncensored.ninfer \
    --host 0.0.0.0 --port 8001 --model-id 4090 \
    --max-context 262144 --kv-capacity auto --max-concurrency 3 \
    --kv-dtype int8 --vision \
    --spec mtp --draft-tokens 3 --lm-head-draft \
    --pending-timeout-ms 600000 \
    --request-log-jsonl /data/ninfer-build/reqlog-4090.jsonl \
    --log-stats-interval-ms 5000
</code></pre>
<p dir="auto"><strong><code>/data/launchers/start-ninfer-4080.sh</code></strong></p>
<pre><code class="language-bash">#!/bin/bash
export CUDA_VISIBLE_DEVICES=GPU-bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb
export LD_LIBRARY_PATH=/usr/local/cuda-13.1/lib64:$LD_LIBRARY_PATH
exec /data/ninfer-build/build/apps/ninfer-serve \
    /data/ninfer-models/qwen3_8_27b_uncensored.ninfer \
    --host 0.0.0.0 --port 8002 --model-id 4080 \
    --max-context 262144 --kv-capacity auto --max-concurrency 2 \
    --kv-dtype int8 --vision \
    --spec mtp --draft-tokens 3 --lm-head-draft \
    --pending-timeout-ms 600000 \
    --request-log-jsonl /data/ninfer-build/reqlog-4080.jsonl \
    --log-stats-interval-ms 5000
</code></pre>
<h3>参数逐项说明</h3>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>参数</th>
<th>作用与取值理由</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>--model-id</code></td>
<td>覆盖 artifact 自报的 <code>identity.model_id</code>。沿用原 SGLang 的 <code>served-model-name</code>（4090 / 4080），客户端配置一行都不用改</td>
</tr>
<tr>
<td><code>--max-context</code></td>
<td>单条序列的逻辑上限。超出会明确报 <code>context_length_exceeded</code> 而不是静默截断</td>
</tr>
<tr>
<td><code>--kv-capacity auto</code></td>
<td>按「权重之后的剩余显存 − 1 GiB headroom」反推最大合法容量。所以启动后空闲显存必然落在 1 GiB 出头，那是设计值不是意外</td>
</tr>
<tr>
<td><code>--max-concurrency</code></td>
<td>启动时固定，运行期不可变，引擎硬上限是 <strong>1–8</strong>。4090 取 3 是为了保证每路都能吃满 262,144</td>
</tr>
<tr>
<td><code>--kv-dtype int8</code></td>
<td>可选 <code>bf16 / int8 / fp8 / nvfp4 / k8v4</code>。nvfp4 在 Ada 上不可用</td>
</tr>
<tr>
<td><code>--vision</code></td>
<td><strong>必须显式加</strong>，否则不加载视觉的 GPU 分配，多模态请求会失败</td>
</tr>
<tr>
<td><code>--spec mtp --draft-tokens 3</code></td>
<td>MTP 投机解码，draft window 范围 0–5。作者全部基准都用 3</td>
</tr>
<tr>
<td><code>--lm-head-draft</code></td>
<td>启用优化过的 proposal head。该 artifact 内含 <code>text/draft_head</code> 张量</td>
</tr>
<tr>
<td><code>--pending-timeout-ms</code></td>
<td><strong>默认只有 30,000（30 秒）</strong>，覆盖 preparation + 媒体获取 + 引擎 FIFO 准入的全过程。长 prompt 场景必须调大，理由见第七节</td>
</tr>
<tr>
<td><code>--request-log-jsonl</code></td>
<td>机器可读的测量日志。含 <code>prefix_cache_hit_tokens</code> 与 <code>prefix_reuse_path</code>，是排查缓存的唯一直接手段。<strong>纯元数据，不含 prompt 文本或图片</strong></td>
</tr>
</tbody>
</table>
<p dir="auto"><strong><code>/etc/systemd/system/ninfer-4090.service</code></strong>（4080 的同构，仅 Conflicts 与 ExecStart 不同）</p>
<pre><code class="language-ini">[Unit]
Description=NInfer 4090 Qwen3.8-27B-Uncensored groupwise-int+MTP+Vision
After=network-online.target
Wants=network-online.target
Conflicts=sglang-4090-twolven.service sglang-4090-heretic.service
StartLimitIntervalSec=0

[Service]
Type=simple
User=&lt;你的用户名&gt;
ExecStart=/data/launchers/start-ninfer-4090.sh
Restart=always
RestartSec=10
TimeoutStartSec=600
KillSignal=SIGTERM
TimeoutStopSec=60
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target
</code></pre>
<p dir="auto"><code>Conflicts=</code> 让新旧引擎互斥：启动任意一个会自动停掉另一个，回滚就是一条 <code>systemctl start</code>。旧的 SGLang launcher 与 unit 全部保留，只是 <code>disable</code>。</p>
<hr />
<h2>六、启动日志：容量账怎么读</h2>
<p dir="auto"><strong>RTX 4090</strong></p>
<pre><code>engine ready  | qwen3.8-27b/groupwise-int | total 12.0s | weights 16.9 GiB
capacity      | KV 786,432 tokens, int8, auto | pages 12,288/12,288 | runtime 28.2 GiB | free 1.98 GiB
context cache | 3 active + 3 cached device states | host 8 states, 8.00 GiB KV | private 6 | shared 4 | anchors 2
media         | 12 preprocess workers | cache 1.00 GiB | live 2.00 GiB
listening on http://0.0.0.0:8001 | model 4090 | auth disabled
</code></pre>
<p dir="auto"><strong>RTX 4080</strong></p>
<pre><code>engine ready  | qwen3.8-27b/groupwise-int | total 12.0s | weights 16.9 GiB
capacity      | KV 350,208 tokens, int8, auto | pages 5,472/8,192 | runtime 13.3 GiB | free 1.13 GiB
context cache | 2 active + 2 cached device states | host 8 states, 8.00 GiB KV | private 4 | shared 4 | anchors 2
media         | 12 preprocess workers | cache 1.00 GiB | live 2.00 GiB
listening on http://0.0.0.0:8002 | model 4080 | auth disabled
</code></pre>
<ul>
<li><strong>device states</strong> 是 GDN recurrent state 槽位，投机解码回滚要用。这个模型一份 state 是 <code>48 GDN 层 × 48 value heads × 128×128 FP32 ≈ 144 MiB</code>，默认 active 与 cached 各等于并发数，所以每加一路并发约多吃 288 MiB。</li>
<li><strong>host 8 states / 8.00 GiB KV</strong> 在主机内存里，不占显存，是 Device/Host 两级缓存的第二级。长上下文前缀被挤出显存时落到这里，比重算快得多。</li>
<li><strong>pages 12,288/12,288</strong>：4090 的页表刚好用满，4080 的 <code>5,472/8,192</code> 还有余量——那是 <code>--max-context 262144</code> 预留的寻址空间，不是浪费。</li>
</ul>
<hr />
<h2>七、上线之后才暴露的三件事</h2>
<p dir="auto">跑通只是开始。下面三条在基准测试里完全看不到，只有真实客户端接上去才会撞上，也是这次部署里最值钱的经验。</p>
<h3>7.1 前缀缓存决定一切：226 秒对 0.4 秒</h3>
<p dir="auto">把 <code>--request-log-jsonl</code> 打开后，<code>request_done</code> 事件里有 <code>prefix_cache_hit_tokens</code> 和 <code>prefix_reuse_path</code> 两个字段。同一个会话连续几轮的真实记录：</p>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th style="text-align:right">消息数</th>
<th style="text-align:right">prompt</th>
<th style="text-align:right">命中</th>
<th style="text-align:right">命中率</th>
<th style="text-align:right">真算</th>
<th>reuse_path</th>
<th style="text-align:right">TTFT</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:right">174</td>
<td style="text-align:right">107,124</td>
<td style="text-align:right">0</td>
<td style="text-align:right">0.0%</td>
<td style="text-align:right">107,124</td>
<td><code>root</code></td>
<td style="text-align:right">226.2s</td>
</tr>
<tr>
<td style="text-align:right">176</td>
<td style="text-align:right">107,451</td>
<td style="text-align:right">107,429</td>
<td style="text-align:right"><strong>100.0%</strong></td>
<td style="text-align:right">22</td>
<td><code>private_endpoint</code></td>
<td style="text-align:right"><strong>0.4s</strong></td>
</tr>
<tr>
<td style="text-align:right">114</td>
<td style="text-align:right">171,957</td>
<td style="text-align:right">0</td>
<td style="text-align:right">0.0%</td>
<td style="text-align:right">171,957</td>
<td><code>root</code></td>
<td style="text-align:right">145.0s</td>
</tr>
<tr>
<td style="text-align:right">116</td>
<td style="text-align:right">173,779</td>
<td style="text-align:right">173,721</td>
<td style="text-align:right"><strong>100.0%</strong></td>
<td style="text-align:right">58</td>
<td><code>private_endpoint</code></td>
<td style="text-align:right"><strong>0.6s</strong></td>
</tr>
<tr>
<td style="text-align:right">118</td>
<td style="text-align:right">174,291</td>
<td style="text-align:right">174,125</td>
<td style="text-align:right">99.9%</td>
<td style="text-align:right">166</td>
<td><code>private_endpoint</code></td>
<td style="text-align:right"><strong>0.6s</strong></td>
</tr>
</tbody>
</table>
<p dir="auto"><strong>17.4 万 token 的 prompt，第二轮只真算了 58 个 token，TTFT 从 145 秒降到 0.6 秒。</strong> 首轮走 <code>root</code>（全量重算）是不可避免的冷启动成本，之后每轮几乎免费。</p>
<blockquote>
<p dir="auto"><strong>直接推论：别轻易重启。</strong> 每次重启引擎都会清空前缀缓存，所有活跃会话的下一轮都要重付一次全量 prefill。两个 17 万 token 的会话同时冷启动 = 大约 4 分钟不可用。调参时把改动攒到一起，一次重启改完。</p>
</blockquote>
<h3>7.2 prefill 是串行的，<code>--max-concurrency</code> 给的是 decode 通道</h3>
<pre><code>throughput | 5.0s | prefill 1.43k tok/s | decode 4.2 tok/s | running 2 (prefill 1, decode-ready 1)
</code></pre>
<p dir="auto">注意 <code>prefill 1</code>：并发设成 3，同时在跑的请求可以有 3 个，但<strong>任一时刻只有一条在 prefill</strong>。两个客户端同时发大请求时，第二个的 TTFT = 自己的 prefill + 前面那个的<em>全部</em> prefill。实测一条请求 <code>queue 2m 20.9s</code>，纯粹是在等前一条 prefill 完。</p>
<p dir="auto"><strong>加并发数解决不了这个</strong>——瓶颈是 prefill 吞吐（本机 1.2~1.4k tok/s），不是通道数。</p>
<h3>7.3 <code>--pending-timeout-ms</code> 默认 30 秒，长 prompt 场景必炸</h3>
<pre><code>req#104 failed during generation | HTTP 503 | request queue timeout   # 等了 30 秒
req#105 failed during generation | HTTP 503 | request queue timeout   # 等了 30 秒
req#106 failed during generation | HTTP 503 | request queue timeout   # 等了 31 秒
</code></pre>
<p dir="auto">这个计时<strong>从 preparation 之前就开始</strong>，覆盖媒体获取和引擎 FIFO 准入的全过程。只要有一条大请求在 prefill（十几万 token = 2 分钟以上），排在它后面的一律 30 秒判死。<strong>改成 600,000（10 分钟）后，那条等了 2 分 21 秒的请求正常完成了。</strong></p>
<p dir="auto">这三条是连环的：串行 prefill 造成长队列 → 30 秒默认超时把队列里的人杀掉 → 而如果缓存命中，这些长 prefill 根本不会发生。<strong>先保证缓存能用，再把超时调大兜底。</strong></p>
<hr />
<h2>八、视觉的隐藏上限：32,768 merged token</h2>
<p dir="auto">这条在任何文档的显眼处都找不到，但多图会话一定会撞上。接上真实客户端后出现的第二个 400：</p>
<pre><code>{"code":"media_budget_exceeded","message":"vision raw patches exceed processor budget"}
req#78 rejected during prepare | HTTP 400 | media budget exceeded | messages 162 | media 20 | tools 27
</code></pre>
<p dir="auto">根因是两个编译期常量（<code>prepared_prompt.h</code>）：</p>
<pre><code class="language-cpp">inline constexpr std::uint64_t kMaximumPromptVisionTokens = 32'768;   // 整请求
inline constexpr std::uint64_t kMaximumVisionItemTokens   = 16'384;   // 单张图
inline constexpr std::uint64_t kRawPatchesPerVisionToken  = 4;

// frontend.cpp
vision_tokens = min(options.max_context, kMaximumPromptVisionTokens);   // → 32,768
</code></pre>
<p dir="auto">注意那个 <code>min()</code>：<strong>纯文本能到 <code>--max-context</code> 的 262,144，但一旦带图，视觉部分的上限就是 32,768。</strong> 这两个值<strong>没有任何命令行参数可以调</strong>。</p>
<p dir="auto">换算（<code>patch_size 16</code> + <code>merge_size 2</code>，即每个 merged token = 32×32 像素）：</p>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>图片</th>
<th style="text-align:right">像素</th>
<th style="text-align:right">raw patches</th>
<th style="text-align:right">视觉 token</th>
<th style="text-align:right">单请求可容纳</th>
</tr>
</thead>
<tbody>
<tr>
<td>3840×2160</td>
<td style="text-align:right">8.29 M</td>
<td style="text-align:right">32,400</td>
<td style="text-align:right">8,100</td>
<td style="text-align:right">4 张</td>
</tr>
<tr>
<td>1920×1080</td>
<td style="text-align:right">2.07 M</td>
<td style="text-align:right">8,100</td>
<td style="text-align:right">2,025</td>
<td style="text-align:right"><strong>16 张</strong></td>
</tr>
<tr>
<td>1280×720</td>
<td style="text-align:right">0.92 M</td>
<td style="text-align:right">3,600</td>
<td style="text-align:right">900</td>
<td style="text-align:right">36 张</td>
</tr>
<tr>
<td>640×360</td>
<td style="text-align:right">0.23 M</td>
<td style="text-align:right">900</td>
<td style="text-align:right">225</td>
<td style="text-align:right">145 张</td>
</tr>
</tbody>
</table>
<p dir="auto">一个攒了 20 张 1080p 截图的会话 = <code>20 × 2,025 = 40,500 &gt; 32,768</code>，整条请求被拒。</p>
<blockquote>
<p dir="auto"><strong>一个说明打包意图的细节。</strong> artifact 内嵌的 <code>preprocessor_config.json</code> 里 <code>size.longest_edge = 16,777,216</code> 像素，而 <code>kMaximumVisionItemTokens × 4 × 256 = 16,777,216</code>——<strong>两个数字完全相等</strong>。打包者是刻意把单图上限顶满的：这个 artifact 是为「一张超大图」调的，不是为「很多张图」调的。对照之下，NInfer 引擎结构体里 <code>image_max_pixels</code> 的默认值只有 <code>1024×1024</code>。</p>
</blockquote>
<h3>怎么修：改客户端，别改 artifact</h3>
<p dir="auto"><code>image_max_pixels</code> 读自 artifact 内嵌的 <code>longest_edge</code>，而 <code>smart_resize_image()</code> 对超限图片是<strong>等比缩小而不是拒绝</strong>（<code>processor.cpp:85</code>）。所以把这个值调小确实能让更多图片装进预算——但那会让<em>所有</em>客户端的单图精度一起下降。</p>
<p dir="auto"><strong>更好的做法是在客户端侧限制。</strong> 我们的客户端（DeepSeek Harness）提供了 provider 级参数，逐图缩放 + 超预算时把最旧的图换成文字占位符：</p>
<pre><code class="language-yaml">requestImagePixelBudget: 1048576      # 逐图 ≤1 Mpx，1080p 缩到 ~1365×768，每张 2,025 → 1,024 token
requestImageMaxBytes:    393216       # 逐图编码目标 384 KB
maxRequestImageBytes:    12582912     # 整请求 12 MB，超出替换最旧的图
</code></pre>
<p dir="auto">改完之后同样 20 张图的请求顺利通过。关键是这样做<strong>只影响那一个客户端</strong>——我们另一条直连 API 的截图判读链路仍然拿到未缩放的 1080p 原图（实测 <code>prompt 2,061</code>，正好等于 2,025 个未缩放的视觉 token）。</p>
<blockquote>
<p dir="auto"><strong>单位错配是根本原因。</strong> 客户端按<strong>字节</strong>限额，引擎按<strong>像素/patches</strong>限额。截图压缩率极高，20 MB 的字节额度能装下远超 32,768 视觉 token 的内容，所以字节限额永远不会先触发。要对症就得用像素单位的那个参数。</p>
</blockquote>
<hr />
<h2>九、能力与质量验证</h2>
<p dir="auto">三项在 Ada 上此前无人验证过的能力，加一组质量回归。</p>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>项目</th>
<th>结果</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>视觉（sm_89 首次跑通）</strong></td>
<td><strong>通过</strong>　640×360 图，四行文字逐字正确 + 形状识别正确，0.6–1.4 s。<strong>issue #189 的移植作者只测了文本负载</strong>，ViT 路径在 Ada 上没有任何先例</td>
</tr>
<tr>
<td><strong>tool call</strong></td>
<td><strong>双向通过</strong>　<code>tools</code> + <code>tool_choice:auto</code> → <code>finish_reason: tool_calls</code> 与正确的 JSON 参数；<code>role:tool</code> 结果回传后正确续写</td>
</tr>
<tr>
<td>长上下文</td>
<td><strong>通过</strong>　4090 吃下 249,952 token（上限的 95%）；4080 吃下 200,932 token</td>
</tr>
<tr>
<td>超长保护</td>
<td><strong>正确</strong>　明确报 <code>context_length_exceeded</code>，不静默截断</td>
</tr>
<tr>
<td>智商 12 题</td>
<td><strong>12 / 12</strong>（开 thinking），与原模型历史满分一致</td>
</tr>
<tr>
<td>拒答 8 题</td>
<td><strong>0 / 8</strong>　完全去审</td>
</tr>
<tr>
<td>MTP 接受率</td>
<td>4090 <strong>53.5–68.1%</strong>；4080 <strong>67.3–73.5%</strong>（强内容相关，见下）</td>
</tr>
</tbody>
</table>
<blockquote>
<p dir="auto"><strong>一条容易踩的测量陷阱。</strong> 同一套智商题，<strong>关 thinking 只有 9/12，开 thinking 是 12/12</strong>。错的三题全是数理推理（时钟夹角、鸡兔同笼、星期推算）。测模型智力必须开 thinking，否则会系统性低估。</p>
</blockquote>
<p dir="auto">MTP 接受率与内容类型强相关。引用作者在 5090 上的分类基准（groupwise-int，draft=3）：Structured 89.5%、Code 76.3%、Translation 74.9%，而 <strong>Story 只有 37.9%</strong>。对应的 decode 是 224.4 / 200.3 / 198.1 / 130.4 tok/s——同一套配置，写作类比结构化输出慢了 <strong>42%</strong>。所以单一场景的 tok/s 数字意义有限。</p>
<hr />
<h2>十、性能天花板在哪</h2>
<p dir="auto">拿作者在 RTX 5090 上的官方基准做硬件缩放换算，两条线独立验证。</p>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>指标</th>
<th style="text-align:right">5090（作者）</th>
<th style="text-align:right">4090（实测）</th>
<th style="text-align:right">硬件比</th>
<th style="text-align:right">换算预期</th>
<th style="text-align:right">偏差</th>
</tr>
</thead>
<tbody>
<tr>
<td>decode · Code 类</td>
<td style="text-align:right">200.3</td>
<td style="text-align:right">115.5</td>
<td style="text-align:right">带宽 1.78×</td>
<td style="text-align:right">112.5</td>
<td style="text-align:right">+2.7%</td>
</tr>
<tr>
<td>prefill · ~26 万 token</td>
<td style="text-align:right">1,609.7</td>
<td style="text-align:right">1,068</td>
<td style="text-align:right">算力 ≈1.5×</td>
<td style="text-align:right">~1,073</td>
<td style="text-align:right">−0.5%</td>
</tr>
</tbody>
</table>
<p dir="auto">两条线都精确落在硬件缩放预期上，误差都在 3% 以内。<strong>结论是当前配置已经跑在硬件极限，不存在「参数没调好」的空间</strong>——能动的只有「每 token 读多少字节」和「每轮吐多少 token」这两件事。</p>
<p dir="auto">顺带解释一个反常现象：issue #189 的移植作者在 <em>RTX 4090</em> 上只跑出 85.4 / 88.8 tok/s，反而不如我们在更弱的 4080 上跑出的 88.2。原因有二——他跑在 <strong>Windows/WDDM</strong>（自述有 3.4 GB 桌面合成器基线），且 <strong>MTP 接受率只有 53.5–58.3%</strong>。Linux 上不要拿那个数字当 Ada 的上限。</p>
<h3>还没实施的三条杠杆</h3>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>方向</th>
<th>机制与预期</th>
<th>风险</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>--kv-dtype k8v4</code></td>
<td>K 8bit + V 4bit，每 token KV 字节降 25%。4090 上 <code>786,432 ÷ 0.75 = 1,048,576 = 4 × 262,144</code>——<strong>并发可升到 4 且每路仍吃满 262K</strong>。满上下文时 KV 约占每步流量 1/3，砍 V 一半 → 总流量 −12%</td>
<td>4-bit V cache <strong>无任何公开质量数据</strong>，作者全部基准都用 int8</td>
</tr>
<tr>
<td><code>--draft-tokens 3 → 4/5</code></td>
<td>verify 那一步<strong>无论窗口多长权重只读一次</strong>，窗口越长＝每次权重读取吐越多 token。按 Code 类接受率 76.3% 推，draft=4 可到 <code>1+4×0.76 ≈ 4.04</code> tokens/round，约 +23%。作者从未发布过窗口扫描数据</td>
<td>强内容相关，写作类（接受率 37.9%）<strong>可能反而变慢</strong></td>
</tr>
<tr>
<td>并发 → 8</td>
<td>作者 5090 数据：C=1 185.8 → C=2 247.0（1.33×）→ C=4 309.5（1.67×）→ <strong>C=8 535.0（2.88×）</strong>。我们 C=3 已达 1.80×</td>
<td>每路上下文压到约 9.8 万，与「保满 262K」冲突</td>
</tr>
</tbody>
</table>
<hr />
<h2>十一、踩过的坑</h2>
<ul>
<li><strong>首次启动必定 FAILURE 一次。</strong> 日志是 <code>ERROR cannot bind 0.0.0.0:8001</code>——<code>Conflicts=</code> 停旧服务与新服务抢端口之间有竞态。<code>Restart=always</code> 10 秒后重试即成功，不是故障。开机场景不会出现（旧服务已 disable，没有竞态对象）。</li>
<li><strong><code>pkill -f ninfer-serve</code> 会杀掉执行它的 shell 自己</strong>，因为那条命令的 command line 里也含这个字符串。用 <code>pkill -f "[n]infer-serve"</code>。</li>
<li><strong>通过 ssh 用 <code>nohup … &amp;</code> 起守护进程不可靠</strong>——日志文件都没被创建。直接上 systemd。</li>
<li><strong>三个默认值在长 prompt 场景下都偏小</strong>：<code>--pending-timeout-ms</code> 30 秒（排队必炸）、视觉预算 32,768 merged token（多图会话必炸）、以及客户端侧按字节而非像素限额（永远不先触发）。这三条在单请求基准里全都看不见。</li>
<li><strong>先开 <code>--request-log-jsonl</code> 再排障。</strong> <code>prefix_reuse_path</code> 一个字段就能区分「缓存没命中」和「缓存不工作」——前者是冷启动，后者才是 bug。我们一度以为是后者，日志证明是前者。</li>
<li><strong>不必为了验证格式下载整个 18 GB。</strong> <code>.ninfer</code> 是私有单文件格式，但<strong>开头就是明文 JSON manifest</strong>。用 HTTP Range 取前 1.5 MB 就能读出 <code>identity.weights_id</code>，并数清 <code>vision/</code>（333 个张量）和 <code>mtp/</code>（12 个张量）命名空间是否存在。</li>
<li><strong>在中国大陆需要走 HF 镜像。</strong> 本机 <code>huggingface.co</code> 被 DNS 污染（解析到 Facebook 的 IPv6 段 <code>2a03:2880:…</code>，curl 卡在 SYN-SENT）。配 <code>HF_ENDPOINT=https://hf-mirror.com</code> 后正常。</li>
<li><strong>装 CUDA 前先做 apt 模拟。</strong> <code>apt-get install -s</code> 数 <code>Remv</code> 行确认为 0 再执行。实测 <code>cuda-toolkit-13-1</code> 是 63 新装 + 1 升级（<code>cuda-toolkit-config-common</code> 纯配置包）、<strong>零删除</strong>，不碰驱动、dkms、libnvidia-*。装完要复核 <code>/usr/local/cuda</code> 符号链接没有被改走。</li>
</ul>
<hr />
<h2>十二、值不值得</h2>
<p dir="auto"><strong>值得，但要清楚自己押上了什么。</strong></p>
<p dir="auto">收益是真实的：4090 上 KV 池 +74%、并发从 2 到 3 且每路都能吃满 262K、单路 decode +14%、启动快 85%；4080 上 KV +131%、上下文 +78%。视觉和 tool call 都完整可用。</p>
<p dir="auto">代价同样真实：<strong>两张卡现在跑同一个引擎、同一个 artifact，而这个引擎的 Ada 支持来自一个 0 star、单 commit、上游明确拒收、已落后 93 commit 的 fork。</strong> 引擎与模型的多样性都没了，它出问题两张卡一起受影响。上游每前进一步，这个分支的 rebase 成本就高一分。</p>
<p dir="auto">我们的对冲是保留完整回滚路径——旧的 SGLang launcher 和 unit 一个都没删，只是 <code>disable</code>，<code>Conflicts=</code> 会在启动旧服务时自动停掉 NInfer：</p>
<pre><code class="language-bash">sudo systemctl start sglang-4090-twolven      # 4090 回退
sudo systemctl start sglang-4080-awq-int4     # 4080 回退
</code></pre>
<p dir="auto"><strong>如果只带走一条</strong>：先确认前缀缓存在你的客户端上真的命中（开 <code>--request-log-jsonl</code> 看 <code>prefix_reuse_path</code>）。命中与不命中之间是 226 秒和 0.4 秒的差距，比任何参数调优都重要一个数量级。</p>
<p dir="auto">如果你手上是 5090，这些移植工作一样都不用做，直接用上游；如果是 Ada 消费卡，上面这条路是通的，但请把它当成一个需要你自己维护的分支，而不是一个能长期依赖的上游产品。</p>
<hr />
<p dir="auto"><em>全部数值为 2026-09-15 单机实测。5090 数字引自 NInfer 仓库 <code>docs/performance.md</code>。</em><br />
<em>硬件 RTX 4090 48 GB + RTX 4080 32 GB · Ubuntu 24.04.4 · CUDA 13.1.115 · 驱动 580.159.04</em></p>
]]></description><link>https://lcz.me/topic/1726</link><generator>RSS for Node</generator><lastBuildDate>Mon, 12 Oct 2026 13:29:05 GMT</lastBuildDate><atom:link href="https://lcz.me/topic/1726.rss" rel="self" type="application/rss+xml"/><pubDate>Tue, 15 Sep 2026 11:00:32 GMT</pubDate><ttl>60</ttl><item><title><![CDATA[Reply to 把只支持 5090 的推理引擎跑在 4090/4080 上，性能全面超过 SGLang on Tue, 15 Sep 2026 19:03:11 GMT]]></title><description><![CDATA[<p dir="auto"><a class="plugin-mentions-user plugin-mentions-a" href="/user/michael-zhou" aria-label="Profile: michael-zhou">@<bdi>michael-zhou</bdi></a> 移植到 sm_89 后有个关键点值得写清楚：5090 原生的 FP4 / tcgen05 路径在 Ada 上没了。你们是把 sm_120a 的 kernel 换成 INT8/FP8 反量化，还是重写成了 groupwise-int kernel？这决定了它能快多久——如果只是把 5090 的 kernel 用兼容层糊过去、实际跑 FP16/INT8，那「全面超过 SGLang」更多是单路 decode 的 kernel 调优，不代表算力效率真追上了。</p>
<p dir="auto">想核对几点：</p>
<ul>
<li>4090 48G 和 4080 32G 的 KV 池数字（786,432 / 350,208）对应的 block size 和 KV 量化是什么？能吃满 262K 的通道数差距主要来自显存，不是引擎。</li>
<li>4080 是 16G 原生魔改成 32G，长时间满载的显存稳定性怎么样，有没有 memtest / 长时间压测过。</li>
<li>prefill 1068 / 701 tok/s 是 batch=1 还是并发下测的？agent 场景更看多路 prefill 总吞吐，和 SGLang 的 chunked prefill 对比才有意义。</li>
</ul>
<p dir="auto">这套 kernel 要是稳定，单卡聊天确实很香；双卡、多模型、agent 还是 SGLang 的主场。</p>
]]></description><link>https://lcz.me/post/18439</link><guid isPermaLink="true">https://lcz.me/post/18439</guid><dc:creator><![CDATA[Xiaote]]></dc:creator><pubDate>Tue, 15 Sep 2026 19:03:11 GMT</pubDate></item><item><title><![CDATA[Reply to 把只支持 5090 的推理引擎跑在 4090/4080 上，性能全面超过 SGLang on Tue, 15 Sep 2026 18:53:32 GMT]]></title><description><![CDATA[<p dir="auto"><a class="plugin-mentions-user plugin-mentions-a" href="/user/applejuice" aria-label="Profile: applejuice">@<bdi>applejuice</bdi></a> 你想多了，3090*2现在也要2万了，跑comfyui被吊起来锤，而且还不说发热噪音功耗之类的。</p>
]]></description><link>https://lcz.me/post/18435</link><guid isPermaLink="true">https://lcz.me/post/18435</guid><dc:creator><![CDATA[terry]]></dc:creator><pubDate>Tue, 15 Sep 2026 18:53:32 GMT</pubDate></item><item><title><![CDATA[Reply to 把只支持 5090 的推理引擎跑在 4090/4080 上，性能全面超过 SGLang on Tue, 15 Sep 2026 11:18:58 GMT]]></title><description><![CDATA[<p dir="auto">奇怪了<br />
4090 48gb 居然跟 3090 x2 差不多？<br />
这样看来 3090 还真是性比价 之王</p>
]]></description><link>https://lcz.me/post/18358</link><guid isPermaLink="true">https://lcz.me/post/18358</guid><dc:creator><![CDATA[applejuice]]></dc:creator><pubDate>Tue, 15 Sep 2026 11:18:58 GMT</pubDate></item></channel></rss>