Skip to content
←返回开源项目

OPEN SOURCE DEEP DIVE

LLM InferenceCUDASpeculative Decoding量化

NInfer:单张 5090 上从零写的 C++/CUDA 推理引擎

从零实现的 C++/CUDA 推理引擎,只服务五个注册 Qwen 权重、只跑一张 RTX 5090:MTP/DFlash 投机解码、五种 KV 存储、24 万 token 上下文与 Device/Host 前缀检查点复用,C=8 聚合解码 1,146.9 tok/s。

Neroued/ninfer1.4kC++Apache-2.05 min read

从零写起:只为一张 5090 服务的推理引擎

NInfer 是一套完全从零实现的 C++/CUDA 推理引擎,服务对象是五个显式注册的 Qwen 权重,运行硬件限定为单张 NVIDIA GeForce RTX 5090(32 GiB)。它通过本地 CLI 或兼容 OpenAI / Anthropic 的 HTTP 接口处理文本、图像与视频提示。这里没有 PyTorch 运行时,也不是把上游引擎裁剪一遍:算子、KV 分配器、调度器、tokenizer 与对话模板前端、HTTP 协议层,全部是同一棵 C++20 构建树里的第一方代码。

这种特化是明写在 README 里的:一张 GPU、一个常驻模型、启动时固定的 1 到 8 路并发容量。构建阶段同样不留退路——除 sm_120a 以外的 CUDA 架构直接被拒,仓库也没有 install 目标和打包发行版,只能从源码构建树里直接跑。引擎拒绝做的每一件事,都是拿单卡吞吐换来的。

五个 artifact 身份,权重与模型一次锁定

一个 artifact 身份同时锁定具体模型和权重方案。每个 .ninfer 文件还内嵌了目标模型所需的 tokenizer、对话模板与媒体前端资源,运行时不需要再回头找 Hugging Face。快速上手示例用的是 Qwen3.8-27B NVFP4。

模型权重方案artifact
Qwen3.6-27Bgroupwise-intqwen3_6_27b.ninfer
Qwen3.6-27Bnvfp4qwen3_6_27b_nvfp4.ninfer
Qwen3.8-27Bgroupwise-intqwen3_8_27b.ninfer
Qwen3.8-27Bnvfp4qwen3_8_27b_nvfp4.ninfer
Qwen3.6-35B-A3Bgroupwise-intqwen3_6_35b_a3b.ninfer

已发布的 artifact 派生自 Qwen/Qwen3.6-27B、Qwen/Qwen3.8-27B 与 Qwen/Qwen3.6-35B-A3B(均为 Apache-2.0);两个 NVFP4 变体还分别使用了 rdtand/Qwen3.6-27B-PrismaSCOUT-Blackwell-NVFP4-BF16-vllm 的定点打包权重与 unsloth/Qwen3.8-27B-NVFP4 的 FP8/NVFP4 混合权重。

并发解码:单卡 1,146.9 tok/s

饱和解码的测量条件是 INT8 group-64 KV、CUDA Graph、MTP3,每路请求生成 8,192 token。表中数值是"聚合已提交解码吞吐 + MTP 接受率",只取实际解码批大小等于配置并发度的完整区间。

模型方案C=1 tok/s / 接受率C=2C=4C=8C8 / C1
Qwen3.6-27B groupwise-int185.8 / 68.2%247.0 / 69.0%309.5 / 68.4%535.0 / 68.3%2.88×
Qwen3.6-27B nvfp4202.4 / 69.3%399.7 / 71.4%699.7 / 69.3%1,146.9 / 68.6%5.67×
Qwen3.6-35B-A3B groupwise-int593.0 / 67.2%877.7 / 68.2%1,166.0 / 69.8%1,313.8 / 67.3%2.22×
Qwen3.8-27B nvfp4143.8 / 48.9%267.6 / 48.1%461.1 / 45.8%766.6 / 46.0%5.33×

MoE 那一路是吞吐异常值:Qwen3.6-35B-A3B 每 token 只激活约 3B 参数,C=1 就有 593.0 tok/s,C=8 达到 1,313.8 tok/s。接受率的落差同样值得注意——Qwen3.6 系列稳定在 67-71%,Qwen3.8 只有 46-49%,这正是新模型绝对解码速度反而更低的原因,尽管它才是快速上手示例的主角。

串行单请求服务用同一套 INT8 group-64 KV 与 CUDA Graph,1,024 token 的 prefill 分块,预热后跑五个固定随机种子:

模型方案7,680 token prefill260,096 token prefill结构化输出 MTP3 解码
Qwen3.6-35B-A3B groupwise-int15,544.3 tok/s5,157.1 tok/s770.9 tok/s
Qwen3.6-27B groupwise-int3,218.1 tok/s1,614.8 tok/s193.0 tok/s
Qwen3.6-27B nvfp411,191.5 tok/s2,510.6 tok/s252.2 tok/s
Qwen3.8-27B groupwise-int3,274.7 tok/s1,609.7 tok/s224.4 tok/s
Qwen3.8-27B nvfp48,340.4 tok/s2,203.1 tok/s219.8 tok/s

反直觉的结论:C=8 时"更小的权重"打赢"更快的权重"

另有一组 makespan(整批耗时)实验:完整投机解码语料(3 个长推理 fixture + 12 个代码/故事/翻译/结构化 fixture,每个 5 个固定种子,共 75 个请求),每个并发度都用同一份以种子 20260811 打乱过的发送顺序,从客户端全部放行计时到最后一个 HTTP 响应读完为止。结果推翻了"NVFP4 一定更快"的直觉:

  • groupwise-int:C=1 到 C=8 的 makespan 从 4,622.59 s 降到 2,211.20 s,加速 2.09×,平均批大小升到 4.76,C=8 是最优点。
  • nvfp4:C=4 时 1,647.74 s(2.83×)为最优,C=8 反而回退到 2,164.90 s,平均批大小只有 2.36。

原因是显存而不是算子。groupwise-int 的权重占用是 16.672 GiB,NVFP4 是 19.729 GiB;到 C=8 时更轻的方案能留下 313,984 token 的 Device KV,而不是 187,712 token——缓存几乎翻倍,可达批大小也就几乎翻倍。groupwise-int 的 --kv-capacity auto 在 C=1/2/4/8 分别解析为 131,072 / 262,144 / 341,952 / 313,984 token。两组实验各 300 个请求全部完成,没有请求失败、CUDA 错误或显存溢出;C=8 最多出现 4 个排队请求,未发生一次 spill、owner 降级/驱逐或搜索穷尽事件。

24 万 token 上下文,以及扛得住显存压力的前缀检查点

一个可复用的前缀检查点保存的是某条精确提示词边界上的 KV 加上完整的续写状态。驻留在 Device 上的检查点可以直接续跑;显存吃紧时,规划器会按"立即恢复的代价 + 之后复用的收益"权衡 Device 保留、pinned Host State/KV 与驱逐,而在跑的请求始终保留自己的完成额度。官方推荐的长上下文服务配置把这些层级写得很明白:

./build/apps/ninfer-serve models/qwen3_8_27b_nvfp4.ninfer \
  --max-context 240000 \
  --kv-capacity 240000 \
  --max-concurrency 2 \
  --kv-dtype fp8 \
  --device-state-slots 2 \
  --host-state-slots 8 \
  --host-kv-mib 8192 \
  --spec mtp --draft-tokens 3 \
  --lm-head-draft \
  --preserve-thinking

每个请求的逻辑上限是 240,000 token;共享的 240,000 token Device KV 池服务所有已准入请求,单独跑时一个请求可以吃满整池。除两个活跃 StateImage 之外,进程还持有 2 个 Device 检查点槽、8 个 pinned Host State 槽与 8 GiB pinned Host KV。--max-context 是单序列逻辑上限,--kv-capacity 决定共享 Main Text KV 池的大小(活跃请求与保留前缀共用),auto 则在启动时用扣掉权重后的剩余显存解出最大合法容量,并留 1 GiB 的分配余量;显式容量在进程生命周期内不变。tools/bench/ttft 压测工具覆盖热复用、Host 恢复、驱逐、共享前缀、调度边界与多模态负载,走的都是对外 HTTP 路径。

投机解码的常驻形态是启动期决策

GPU 常驻在 Engine 启动瞬间冻结,相关开关不是懒加载:没开 Vision 的 Engine 会拒绝媒体输入,之后也无法再打开。

  • --spec mtp:草稿窗口 1 到 5 个位置;已发布成绩用的是 MTP3(--draft-tokens 3 --lm-head-draft)。
  • --spec dflash:仅 35B-A3B 目标支持,草稿窗口 1 到 15;实测点是块长 8(--draft-tokens 7)与原生满块 15。MTP 与 DFlash 不能同时开启。
  • --lm-head-draft:加载优化后的 proposal head;若用完整 proposal head 则不加载它。
  • --vision:加载 Vision 权重并把唯一的 Program workspace 扩到能跑 encode/handoff;默认关闭,同时省掉 Vision 专属的 unified-workspace 空间。

DFlash 可以和 Vision 一起开,但它加速的是多模态 prefill 之后的文本解码,不加速 Vision encode 本身。不带 --spec 时,MTP/DFlash 权重、状态与优化 proposal head 一概不加载,这就是最小常驻配置的来源。单请求 CLI 还额外跑在 root-only context 模式下,因此不会预留一个后续请求根本用不上的 Device 检查点 StateImage。

五种 KV 存储格式,一条分块 prefill 路径

所有注册模型 ID 都支持 BF16、INT8、FP8、NVFP4、K8V4 五种 KV 存储;prefill 分块可配(128 的整数倍,默认 1,024);解码走精确批大小的 CUDA Graph 且批规模在启动时封顶;另有离线因果困惑度打分、私有与共享的精确前缀复用(Device/Host 双层 State 与 KV 保留)、以及模型感知的采样默认值和显式采样器覆盖。所有已发布测量都用 INT8 group-64 KV。

接口面不大,但契约写得很死

方法与路径行为
GET /healthEngine 就绪状态;全局故障后返回 503;无鉴权;队列饱和不算不可用
GET /v1/models、/v1/models/{id}配置的 OpenAI 别名与生效的 max_model_len
POST /v1/chat/completionsOpenAI 风格对话生成,支持流式与非流式
POST /v1/responsesOpenAI Responses Core 生成、本地状态、类型化 Items、SSE
POST /v1/responses/input_tokens只数提示词 token,不生成
GET/DELETE /v1/responses/{id}、/input_items读取、删除本地存储的 Response,或列出其归一化输入 Items
POST /v1/messagesAnthropic 风格消息生成
POST /v1/messages/count_tokens按 checkpoint 原生展开的输入 token 计数

作为一个个人项目,传输层细节规定得相当到位:每个 OpenAI 兼容响应(含流式与错误响应)都带唯一 x-request-id;三个生成型 SSE 端点在 5 秒没有协议事件时发一条 : keep-alive 注释;Linux 上已接受的连接启用 TCP keepalive 与 15 秒 TCP_USER_TIMEOUT,因此对端死掉或不再确认时通常在约 20 秒内被取消,请求还在排队或 prefill 阶段也算。

能力处理走严格路线而不是宽松兼容。凡引擎无法提供其可观测行为的选项,一旦被真正请求就会被拒绝:JSON 约束输出、非零 logit_bias、要求 log probabilities、音频/文件输入或音频输出、strict:true、required 或指名 tool choice、启用工具时的 parallel_tool_calls:false、显式 low/high 图像 detail、web search、moderation、low/high verbosity、存储式 Chat Completions、非空的 legacy functions。已知的约束解码别名(grammar、structured_outputs、guided_json、guided_regex、guided_choice、guided_grammar)同样明确拒绝,而不是当成未知提示悄悄忽略。语义中性的字段则照收不改行为:全零 logit_bias、logprobs:false、top_logprobs:0、verbosity:"medium"、prediction,以及 metadata、用户/安全标识、service tier 与 prompt cache 提示;未知顶层字段直接忽略。每条拒绝都会点明受影响的字段和 NInfer 给不出的那条保证。

工具调用会被解析并返回给客户端,NInfer 自己不执行工具。思考模式默认开启,--reasoning-effort low|medium|xhigh 只在内嵌对话模板暴露该维度时生效;--thinking-budget N 给模型自产思考 token 设正上限:若在该边界上模型还没吐出 </think>,引擎会不采样地追加 Qwen 官方的提前收尾提示与闭合标记,经 reasoning 流对外发布,然后从更新后的上下文继续正常生成;边界上如果先遇到自然闭合、停止条件、取消或总量上限,则以它们为准,不插入。

能力分数是走服务路径测出来的

能力评测全程走 NInfer 自己的 OpenAI 兼容服务路径,开启思考、用 MTP3,工具是 EvalScope 1.9.0,0-shot 规则打分、每题一个样本——也就是说这些数字描述的是实际交付路径,不是另一套评测脚本。

模型方案AIME 2025AIME 2026GPQA-DiamondERQARealWorldQA
Qwen3.6-27B groupwise-int86.67%93.33%86.87%--
Qwen3.6-27B NVFP493.33%93.33%84.34%--
Qwen3.6-35B-A3B groupwise-int90.00%90.00%85.35%--
Qwen3.8-27B groupwise-int96.67%96.67%87.37%66.25%82.22%
Qwen3.8-27B NVFP496.67%96.67%90.40%66.25%83.53%

采样参数按代际区分:Qwen3.6 各行用 temperature 0.6 + presence penalty 1.0,Qwen3.8 各行用 temperature 1.0 + presence penalty 0.0。多模态评测开 --vision 并把上下文限制在 81,920 token;文本评测用 262,144 token,只有 Qwen3.8-27B NVFP4 因为要在这张 5090 上装下权重而降到 252,928 token。每题一个样本,具体对/总题数与评测说明写在各自的 model card 里。

它明确不做的事

  • 每个 Engine 只用一张 RTX 5090、一个常驻模型:没有多卡,没有分布式服务,没有权重 offload。
  • 并发容量启动时固定在 1 到 8 路,入口是有界 FIFO;没有请求抢占,没有优先级/QoS,没有活跃请求换入换出。
  • 活跃请求与保留前缀共用一个启动时定容的 KV 池。
  • 不做运行时模型发现,未注册的 checkpoint 没有回落路径。
  • 解析出的工具调用交回客户端,进程内不执行工具。
  • 树内的 C++ 头文件不作为已安装 SDK 分发。

构建、运行与容器化

依赖清单:64 位 Linux、RTX 5090、CUDA Toolkit 13.1 及以上、CMake 3.28 及以上、C++20 宿主编译器、Ninja、pkg-config、FFmpeg 开发库(libavformat ≥ 60、libavcodec ≥ 60、libavutil ≥ 58、libswscale ≥ 7)与 libcurl ≥ 7.85。测试、基准与维护者工具默认不参与构建。

git clone https://github.com/Neroued/ninfer.git && cd ninfer
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build -j

hf download neroued/Qwen3.8-27B-nvfp4-NInfer \
  qwen3_8_27b_nvfp4.ninfer --local-dir models

./build/apps/ninfer models/qwen3_8_27b_nvfp4.ninfer \
  --prompt "解释 prefill 与 decode 的区别,然后给出简短结论。" \
  --max-context 32768 --max-new 8192 \
  --kv-dtype fp8 --spec mtp --draft-tokens 3 --lm-head-draft

答案正文写 stdout;启动诊断以及 CLI 自带的推理内容、耗时、吞吐、显存与投机解码报告写 stderr,且都是不带前缀的产品输出,所以 > answer.txt 2> run.log 能把两者干净分开。终端下权重物化只有一行瞬态进度加一段紧凑的 Engine-ready 摘要,重定向到文件时则是不含回车与 ANSI 转义的可读进度。仓库提供 Dockerfile,装好 NVIDIA Container Toolkit 后 docker build --tag ninfer:local . 即可,容器里跑的是同一条服务命令,模型目录只读挂载。文档覆盖 CLI、HTTP 服务、性能方法论、困惑度评测与资源调度/上下文缓存算法,另有提交进仓库的文本、图像、视频、混合媒体、思考、长解码与长上下文示例。NInfer 采用 Apache-2.0,是维护者的个人兴趣项目,可通过 Ko-fi 自愿支持,不附带任何服务、功能承诺或路线决策权。

相关开源项目

作为亚马逊联盟会员,我们可能从符合条件的购买中获得佣金。