榨干 RTX5090 算力!Qwen3 专用单卡推理引擎,手写C++/CUDA 算子实现 MTP 推测解码

一、项目释义

当下主流LLM推理引擎,例如vLLM、TensorRT-LLM,设计目标是通用性优先。一套代码要兼容几十款显卡、多种模型结构、多卡分布式、五花八门量化格式。为了适配各种场景,代码里大量if分支、抽象封装层、动态shape逻辑,会带来额外开销,很难把单卡硬件性能压榨到极限。

这个项目选择完全相反的路线:高性能单GPU推理,只适配选定的GPU型号与指定模型权重文件。不再做全硬件、全模型兼容,砍掉所有为通用性准备的冗余逻辑,聚焦在单张GPU上,为指定模型做深度定制优化。

整套引擎从零使用C++20 + CUDA开发,原生支持Dense、MoE两类Transformer架构,支持文本、图片、多图、视频混合多模态输入。对外提供两套交互入口:本地命令行一次性推理;兼容OpenAI、Anthropic协议的HTTP服务接口,支持流式输出、工具调用、token用量统计。

引擎设计约束:进程启动时仅加载一个模型,权重全程驻留显存;最大并发请求数量在启动阶段固定,范围1~8路,请求队列采用FIFO。

项目使用自定义模型工件文件,工件内部打包模型配置、编码量化权重、算子绑定参数、多模态前端资源。支持的工件版本为V3;旧版V2工件可以本地原地升级,不需要重新下载权重。同时支持用户自行编写转换脚本,把原始模型权重导出为本引擎的工件格式。

支持的模型工件清单:

模型 权重类型 工件文件
Qwen3.6-27B groupwise-int qwen3_6_27b.ninfer
Qwen3.6-27B nvfp4 qwen3_6_27b_nvfp4.ninfer
Qwen3.8-27B groupwise-int qwen3_8_27b.ninfer
Qwen3.8-27B nvfp4 qwen3_8_27b_nvfp4.ninfer
Qwen3.6-35B-A3B groupwise-int qwen3_6_35b_a3b.ninfer

二、行业技术知识点

1. Blackwell架构NVFP4量化

目标GPU搭载Blackwell架构,硬件原生支持NVFP4 4bit浮点计算。和传统INT4整数量化对比,NVFP4保留浮点指数,大模型权重压缩时精度损失更小。引擎手写CUDA算子,原生支持NVFP4权重加载、反量化与矩阵计算,无需上层框架格式转换。

引擎同时支持BF16、INT8、FP8、K8V4多种KV缓存存储格式。

2. MTP推测解码 + DFlash增强推测解码

MTP(多候选推测解码),原理是先用轻量Draft头并行生成多个候选token,批量送入主模型一次性验证,匹配成功直接输出,失败则截断回退重生成。普通MTP支持1~5个draft token窗口;35B-A3B模型额外支持DFlash增强推测解码,draft窗口上限提升至15。

DFlash可以加速多模态Prefill之后的文本解码阶段,不参与图像编码加速。

3. 分层KV缓存 + 精确前缀缓存复用

KV缓存分为两层存储:GPU显存设备缓存、主机锁页内存缓存。

前缀检查点(prefix checkpoint)保存完整prompt对应的KV缓存+模型状态快照。新请求到来时,检索前缀哈希表,命中则直接复用已计算好的KV,跳过重复prefill,长文档RAG场景收益巨大。

当显存资源紧张,调度器评估恢复开销,自动把冷门前缀缓存从GPU显存迁移到主机锁页内存;再次访问时拷贝回显存。

4. Chunked Prefill 分块预填充 + CUDA Graph 捕获Decode图

超长prompt不会一次性全部送入GPU计算,拆分为固定大小分块逐块prefill,防止单次计算显存溢出。

Decode阶段使用CUDA Graph捕获完整算子序列,推理时直接提交Graph,消除反复kernel launch带来CPU-GPU调度开销,是高并发吞吐的核心优化手段。引擎使用精确批次CUDA Graph解码。

5. 工件打包格式设计

自定义V3工件文件,把模型配置、量化权重、前端资源打包在单个二进制文件,方便分发与版本管理。权重提前按照CUDA算子最优内存排布存储,减少推理时的内存转置拷贝。

三、架构设计思路

整体架构划分为4大模块:工件解析与权重加载模块、CUDA算子内核模块、KV缓存与资源调度模块、前端服务模块

  1. 工件解析与权重加载模块
    读取自定义二进制工件,解析模型结构、量化参数、权重数据。进程启动阶段一次性将权重反量化、重排内存布局,拷贝到GPU显存,运行阶段权重常驻显存。支持本地升级旧版本工件。
  2. CUDA算子内核模块
    手写CUDA Kernel,专门针对sm_120a架构优化。编译阶段强制校验GPU架构,非sm_120a直接编译失败。算子包含多头因果注意力、LayerNorm、FFN、NVFP4/FP8/groupwise-int量化矩阵乘、MTP验证算子、图像视频编解码算子。不依赖cuBLAS等通用矩阵库,针对模型固定shape特化,减少通用库额外开销。
  3. KV缓存与资源调度模块
    全局共享KV内存池,所有并发请求共用这块显存空间。调度器负责:
  • 追踪每个请求KV占用大小;
  • 设备KV与主机锁页KV之间的数据迁移;
  • 前缀缓存哈希查找、LRU淘汰、快照管理;
  • 请求准入控制,最大并发1~8路。
    KV池容量在进程启动时确定,运行期间无法动态扩容。
  1. 前端服务模块
    两套入口:本地CLI一次性推理;HTTP服务,实现OpenAI Chat Completions、Anthropic消息协议,支持SSE流式输出、token计数、工具调用。引擎只返回解析后的工具调用结构,不会执行工具代码。图片、视频输入依赖FFmpeg库做视频帧解码。

四、核心代码实现原理(源码详细解读)

4.1 CMake构建脚本,GPU架构强校验

项目使用CMake+Ninja构建,强制C++20标准。编译脚本增加CUDA架构硬校验,只允许sm_120a:

cpp 复制代码
# CMake片段:限制CUDA架构
if(NOT CMAKE_CUDA_ARCHITECTURES STREQUAL "120a")
  message(FATAL_ERROR "Engine only support sm_120a GPU architecture. Other arch not allowed.")
endif()

# 构建预设定义
include(CMakePresets)
# release预设:关闭测试与benchmark,只编译推理主程序
# dev预设:开启单元测试、性能基准,自动查找本地Python环境

构建两个预设:

  • release:生产版本,关闭测试与benchmark;
  • dev:开发调试版本,开启测试与性能基准,自动检索Python解释器。

重要特性:项目没有install安装目标,编译产物只能在build目录直接运行,不做系统级安装部署。Python权重转换脚本独立于C++编译流程;HBM探测工具拥有独立编译命令。

4.2 V3工件文件解析核心C++代码

工件是二进制文件,使用mmap内存映射方式加载,避免一次性大文件读入内存。文件头部保存元信息,包含配置偏移、权重偏移、文件版本标识:

cpp 复制代码
// 工件头部结构体定义
struct ArtifactHeader {
    uint32_t magic;
    uint32_t version;
    uint64_t config_offset;
    uint64_t weight_offset;
    uint64_t weight_size;
    uint64_t resource_offset;
    uint64_t resource_size;
};

// 加载工件完整函数
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <cstdint>
#include <string>

struct ModelArtifact {
    void* config_ptr;
    uint8_t* weights_ptr;
    size_t weight_bytes;
};

ModelArtifact load_artifact(const std::string& artifact_path) {
    int fd = open(artifact_path.c_str(), O_RDONLY);
    if(fd < 0) {
        throw std::runtime_error("open artifact file failed");
    }
    off_t file_sz = lseek(fd, 0, SEEK_END);
    lseek(fd, 0, SEEK_SET);

    void* mapped = mmap(nullptr, file_sz, PROT_READ, MAP_SHARED, fd, 0);
    if(mapped == MAP_FAILED) {
        close(fd);
        throw std::runtime_error("mmap artifact failed");
    }

    ArtifactHeader* hdr = reinterpret_cast<ArtifactHeader*>(mapped);
    if(hdr->version != 3) {
        munmap(mapped, file_sz);
        close(fd);
        throw std::runtime_error("only support v3 artifact");
    }

    ModelArtifact art;
    art.config_ptr = static_cast<uint8_t*>(mapped) + hdr->config_offset;
    art.weights_ptr = static_cast<uint8_t*>(mapped) + hdr->weight_offset;
    art.weight_bytes = hdr->weight_size;
    return art;
}

加载后,将art.weights_ptr指向的量化权重,通过cudaMemcpy一次性拷贝到GPU显存,拷贝完成后根据量化类型,在CUDA kernel内部实时做反量化计算。

4.3 KV缓存分层存储结构体 C++源码

区分设备显存Buffer与主机锁页Buffer,每个slot保存token长度与标记,标记区分普通KV与可复用前缀检查点:

cpp 复制代码
#include <cuda_runtime.h>

struct DeviceBuffer {
    void* ptr;
    size_t bytes;
    ~DeviceBuffer() {
        if(ptr) cudaFree(ptr);
    }
};

struct HostPinnedBuffer {
    void* ptr;
    size_t bytes;
    ~HostPinnedBuffer() {
        if(ptr) cudaFreeHost(ptr);
    }
};

struct KVCacheSlot {
    DeviceBuffer k_dev;
    DeviceBuffer v_dev;
    HostPinnedBuffer k_host;
    HostPinnedBuffer v_host;
    size_t token_len;
    bool is_prefix_checkpoint;
};

调度器维护全局std::unordered_map<uint64_t, KVCacheSlot> prefix_pool,key为prompt前缀哈希值。收到新请求,计算prompt哈希,查找prefix_pool,命中直接复用slot,跳过prefill。显存不足时,调度器调用cudaMemcpyAsync将slot数据在GPU显存与主机锁页内存之间迁移。

4.4 MTP推测解码执行控制逻辑 C++

cpp 复制代码
// MTP推测解码主循环伪实现(项目原生C++逻辑,非伪代码)
// draft_tokens:配置的draft token数量,如3
// accept_threshold:验证通过逻辑
struct MTPResult {
    std::vector<int> accept_tokens;
    int truncate_pos;
};

MTPResult mtp_verify(ModelContext* ctx, const std::vector<int>& draft_tokens) {
    // 将draft序列打包送入主模型一次性批量验证
    ctx->submit_draft_batch(draft_tokens);
    ctx->wait_cuda_graph();

    std::vector<int> accept;
    int cut_pos = 0;
    for(; cut_pos < draft_tokens.size(); cut_pos++) {
        if(ctx->get_verify_token(cut_pos) == draft_tokens[cut_pos]) {
            accept.push_back(draft_tokens[cut_pos]);
        } else {
            break;
        }
    }
    return {accept, cut_pos};
}

MTP流程:

  1. 主模型+MTP Draft头并行生成N个候选draft token;
  2. 打包draft序列送入主模型,使用CUDA Graph批量验证;
  3. 逐个比对token,收集连续匹配token作为输出;
  4. 在第一个不匹配位置截断,丢弃后面draft token,主模型重新生成。

4.5 HTTP服务请求处理逻辑(OpenAI接口)

内置HTTP服务,接收JSON请求,解析messages,送入调度器,生成token后SSE流式返回。

cpp 复制代码
// HTTP请求处理核心逻辑
void handle_chat_completion(HttpRequest& req, HttpResponse& resp, Engine* engine) {
    auto chat_req = parse_json_chat(req.body);
    InferenceTask task;
    task.messages = chat_req.messages;
    task.max_new_tokens = chat_req.max_tokens;
    task.stream = chat_req.stream;

    // 提交任务到调度器FIFO队列
    TaskHandle handle = engine->submit_task(task);
    resp.set_header("Content-Type", "text/event-stream");

    // 流式循环输出SSE
    while(true) {
        auto chunk = engine->get_next_chunk(handle);
        if(chunk.is_finish) break;
        resp.write_chunk(build_sse_json(chunk));
    }
    resp.write_chunk(build_finish_event());
}

视频输入部分,引擎调用FFmpeg libavformat/libavcodec库,读取视频文件、解码视频帧,送入视觉编码算子。

五、环境配置与运行测试教程

硬件硬性约束

  • GPU:目标NVIDIA显卡(sm_120a架构)
  • OS:64位Linux,Windows平台不支持

完整软件依赖清单

  1. CMake ≥ 3.28
  2. Ninja构建工具
  3. C++20兼容编译器(GCC / Clang)
  4. CUDA Toolkit 13.1(官方验证版本,CMake不强制下限,但推荐使用13.1)
  5. FFmpeg开发库:libavformat、libavcodec、libavutil、libswscale
  6. libcurl >= 7.85>

可选依赖:Python环境,仅用于权重转换脚本,推理运行不需要Python

编译步骤

cpp 复制代码
cd source_dir

# Release正式编译
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build -j

编译完成,可执行文件位于build/apps/,两个核心程序:

  1. cli-infer:本地一次性CLI推理程序
  2. infer-serve:HTTP推理服务程序

模型工件下载

使用HuggingFace CLI下载预打包工件,放置models目录:

复制代码
hf download 模型仓库标识 qwen3_8_27b_nvfp4.ninfer --local-dir models

测试1:本地CLI一次性推理

cpp 复制代码
./build/apps/cli-infer models/qwen3_8_27b_nvfp4.ninfer \
--prompt "Explain prefill and decode, then give a concise conclusion." \
--max-context 32768 \
--max-new 8192 \
--kv-dtype fp8 \
--spec mtp --draft-tokens 3 \
--lm-head-draft

输出规则:

  • 模型回答内容输出stdout;
  • 启动日志、吞吐、时序、MTP验证统计信息输出stderr;
  • --log-level debug:打印完整启动调试日志;
  • --messages FILE加载结构化对话文件;--vision开启图片/视频多模态输入。

测试2:启动HTTP推理服务,2并发

复制代码
./build/apps/infer-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

参数说明:

  • max-context:单条请求逻辑最大token上限;
  • kv-capacity:全局KV池总token容量;
  • max-concurrency:最大并发请求数量;
  • device-state-slots:GPU显存保存状态快照数量;
  • host-state-slots:主机内存保存状态快照数量;
  • host-kv-mib:主机锁页KV缓存容量,单位MiB;
  • --preserve-thinking:保留模型内部思考推理内容。

服务启动后监听127.0.0.1:8080。

测试3:调用OpenAI兼容接口

cpp 复制代码
curl [http://127.0.0.1:8080/v1/chat/completions](http://127.0.0.1:8080/v1/chat/completions) \
-H 'Content-Type: application/json' \
-d '{
    "model": "qwen3.8-27b",
    "messages": [{"role": "user", "content": "Reply with one short sentence."}],
    "max_tokens": 64
}'

Docker容器部署

主机提前安装NVIDIA Container Toolkit

cpp 复制代码
# 构建镜像
docker build --tag llm-single-infer:local .

# 启动容器,挂载模型目录,端口映射
docker run --rm \
--gpus '"device=0"' \
--publish 8080:8080 \
--volume "$PWD/models:/models:ro" \
llm-single-infer:local \
infer-serve /models/qwen3_8_27b_nvfp4.ninfer \
--host 0.0.0.0 \
--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

六、落地用途

  1. 单卡私有化本地部署
    小团队、个人本地私有化部署大模型,单卡即可承载27B/35B级别模型,原生支持图文视频多模态,对外提供OpenAI兼容API,业务侧代码几乎不用修改。
  2. 长文档RAG后端服务
    依托前缀缓存复用机制,大量请求共用相同知识库Prompt,大幅降低重复Prefill开销,适合企业知识库、长文档问答场景。
  3. Blackwell架构底层学习研究
    全套CUDA算子、KV缓存调度、MTP推测解码手写实现,没有厚重通用框架封装,适合研究NVFP4量化、CUDA Graph、推测解码、分层KV缓存底层原理。
  4. 多模态Agent原型快速验证
    原生支持图片、多图、视频输入,支持保留模型thinking推理内容,适合本地多模态Agent原型开发。
  5. 离线Perplexity评估
    内置离线因果困惑度打分能力,用于模型效果评估。

七、实测性能数据

全部测试基于目标GPU,测试环境使用INT8 group-64 KV、CUDA Graph、MTP3,每次请求生成8192 token

并发MTP3解码吞吐(tok/s / 候选token接受率)

模型配置 C=1 C=2 C=4 C=8 C8/C1加速比
Qwen3.6-27B groupwise-int 185.8 / 68.2% 247.0 / 69.0% 309.5 / 68.4% 535.0 / 68.3% 2.88×
Qwen3.6-27B nvfp4 202.4 / 69.3% 399.7 / 71.4% 699.7 / 69.3% 1146.9 / 68.6% 5.67×
Qwen3.6-35B-A3B groupwise-int 642.5 / 68.6% 907.2 / 66.3% 1213.5 / 69.6% 1380.7 / 68.0% 2.15×
Qwen3.8-27B nvfp4 143.8 / 48.9% 267.6 / 48.1% 461.1 / 45.8% 766.6 / 46.0% 5.33×

单请求Prefill性能(tok/s)

模型配置 7680 token短Prefill 260096 token超长Prefill 结构化MTP3解码
Qwen3.6-35B-A3B groupwise-int 17705.4 5247.0 779.6
Qwen3.6-27B groupwise-int 3218.1 1614.8 193.0
Qwen3.6-27B nvfp4 11191.5 2510.6 252.2
Qwen3.8-27B groupwise-int 3274.7 1609.7 224.4
Qwen3.8-27B nvfp4 8340.4 2203.1 219.8

模型评测结果(0-shot,单样本评测)

模型配置 AIME 2025 AIME 2026 GPQA-Diamond ERQA RealWorldQA
Qwen3.6-27B groupwise-int 86.67% 93.33% 86.87% - -
Qwen3.6-27B NVFP4 93.33% 93.33% 84.34% - -
Qwen3.6-35B-A3B groupwise-int 90.00% 90.00% 85.35% - -
Qwen3.8-27B groupwise-int 96.67% 96.67% 87.37% 66.25% 82.22%
Qwen3.8-27B NVFP4 96.67% 96.67% 90.40% 66.25% 83.53%

If you need the complete source code, please add the WeChat number (c17865354792)

八、项目边界与局限

  1. 单引擎进程只能绑定单张目标GPU,不支持其他显卡型号,无多卡分布式推理;
  2. 进程启动加载模型,权重常驻显存,运行期间不能动态切换模型;
  3. 不支持请求抢占、任务优先级QoS调度;
  4. 没有权重CPU/GPU动态换页能力;
  5. 引擎只返回解析后的工具调用信息,不会执行任何工具;
  6. 源码内C++头文件不作为对外SDK,仅能在源码编译目录运行;
  7. KV池容量、最大并发数量,进程启动后固定,运行阶段不可动态调整;
  8. 仅对显式实现的模型架构、shape、量化路径提供支持。

Welcome to follow WeChat official account【程序猿编码】

相关推荐
西飘客2 小时前
vs 怎么根据dll 生成lib文件
c++·qt
bkspiderx3 小时前
Qt 插件机制:动态扩展应用功能的核心框架
开发语言·qt·元数据·qt 插件·qpluginloader
程序员老陆3 小时前
深入理解 C++ thread_local:线程私有存储的正确打开方式
开发语言·c++·程序设计
沐晓时光3 小时前
C语言入门,深入理解指针(3)
c语言·开发语言
码云数智-园园3 小时前
unique_ptr 还是 shared_ptr?C++ 智能指针选型与内存泄漏实战分析
java·开发语言
禾小西4 小时前
07丨Redis 哨兵机制:主库故障后,如何恢复服务?
java·开发语言·redis
我不是阵雨4 小时前
JDK 21虚拟线程Pinning陷阱:一文拔钉解困
java·开发语言
徐小黑ACG5 小时前
Golang 基础05 结构体struct
开发语言·算法·golang
优橙教育5 小时前
零基础学AI应用开发要多久?3个月能到什么水平
服务器·开发语言·网络·php