市面上大多数推理引擎是 Python 写的,开发门槛低,但调度和内核都隔着解释器一层。今天拆解的这套引擎走的是另一条路——核心运行时全部用 C++ 实现,第三方依赖极少、几乎全部静态链接,目标是把同一份代码部署到 NVIDIA GPU、x86 服务器、ARMv9(倚天 710)这三类完全不同的硬件上,并且精度对齐 PyTorch 与主流 GPU 引擎。下面从"为什么这么设计"讲到"每个核心算子到底怎么实现",再给一份能直接照着跑的部署与测试教程。

一、项目释义:为什么要做一套 C++ 原生的异构推理引擎

大模型推理落地时有三个绕不过去的矛盾:

  1. 性能 vs 易用:Python 框架上手快,但请求调度、显存管理、内核启动都要穿过 Python 解释器和 PyTorch 调度栈,在 batch 很大、请求很短的高并发场景下,这部分"运行时税"会直接吃掉吞吐。
  2. 性能 vs 硬件多样性:GPU 有 Tensor Core,x86 有 AVX2 / AMX,ARMv9 有 SVE。想用一份代码同时吃到这三类指令集的红利,调度层必须足够"薄",否则每加一种后端都要重写一套上层逻辑。
  3. 部署 vs 体积/依赖:在生产环境(尤其是边缘、信创服务器)里,动辄拖一整套 Python + CUDA + Torch 的镜像往往不允许,需要尽量精简依赖、静态链接。

这套引擎的定位就是把上面三点一次解决:用 C++ 写出一个轻量、可静态链接、跨 GPU/x86/ARMv9 的推理运行时,对外同时暴露 C++ 接口和 Python 接口,再通过标准跨语言接口进一步扩展到 Java、Rust 等语言。它的设计取舍可以概括成一句话——把"薄"留给上层调度,把"厚"压进底层内核。

引擎支持的能力清单:

能力维度具体内容
架构原生 C++ 运行时,极少第三方依赖,依赖库几乎全部静态链接
接口同时提供 C++ / Python 接口;C++ 接口可扩展到 Java、Rust
精度推理结果对齐 PyTorch 与主流 GPU 引擎
批处理连续批处理(Continuous Batching),新请求即时插入、支持流式输出
注意力自研分页注意力(Paged Attention),结合 int8 / uint4 KV Cache 量化
缓存高效 Prefix Cache,GPU 与 CPU 都能用,可加速标准 LLM 与多模态 MLLM
量化免微调动态权重量化 + GPTQ,支持 int8/int4 权重、int8 激活
异步基于请求的异步接口,逐请求独立控制生成参数与请求状态
服务可对接 API 服务层,直接暴露 OpenAI 兼容接口

二、行业技术知识点:先把地基讲透

在拆代码之前,必须把这套引擎用到的关键技术讲清楚,否则后面看到的内核优化会很突兀。

2.1 自回归串行瓶颈:问题的根

Transformer 解码是"生成一个 token → 拼回输入 → 再生成一个"的串行过程。每一步理论上要重算所有历史 token 的注意力,复杂度爆炸。于是有了两个标准解法:KV Cache 和 ** batching**。

  • KV Cache:把每一层每个 token 的 Key/Value 缓存下来,下一步只算新 token 的 Q、然后和缓存的 K/V 做注意力。代价是显存随序列长度线性增长:

    每 token KV 占用 ≈ 2 × L层 × num_kv_heads × head_dim × sizeof(dtype) × 2(K和V)
    

    以 7B / BF16 / 4096 上下文为例,单请求 KV Cache 轻松吃到 GB 级。

2.2 Continuous Batching(连续批处理)

传统静态批处理要求一批请求长度相近、同时进同时出,短请求要等长请求跑完才能释放槽位,利用率很低。连续批处理允许任何时刻把已完成序列的请求移出、把等待队列里的新请求"插"进当前 batch 的空位,配合流式输出,GPU 几乎不空转。

关键点:插进来的请求处于不同阶段——有的是 prefill(首次处理整个 prompt,算力密集),有的是 decode(每次只算 1 个 token,带宽/显存密集)。调度器要能把这两类活混在同一次 kernel launch 里,或者用 chunked prefill 把长 prompt 切块、和 decode 错峰。

2.3 Paged Attention:把操作系统分页搬到 KV Cache 上

KV Cache 最朴素的分配方式是为每个请求预留"最大可能长度"的连续显存,结果产生大量内部碎片(预留了但没用到)和外部碎片(释放出来的大小不一,凑不出连续块)。表现就是明明显存还剩一半,服务却 OOM。

分页注意力的思路:把 KV Cache 切成固定大小的物理 block(页),逻辑上一个序列的 KV 是连续的,物理上分散存放,用一张页表做间接寻址。这彻底消除了外部碎片、大幅压缩内部碎片,显存利用率从约 40% 拉到 90% 以上。注意力 kernel 计算时,不再是顺序扫描一段连续 K/V,而是遍历页表、按物理 block 地址去读。

2.4 KV Cache 量化:int8 / uint4

光分页还不够,长上下文时 KV Cache 本身也想压缩。把 K/V 从 BF16 量化到 int8 甚至 uint4,显存占用直接砍半甚至砍到 1/4。难点在于注意力是逐 token 累加 softmax,量化误差会随序列累积,所以要配合按 channel / 按 block 的 scale 管理以及高效的反量化 + 计算融合内核。

2.5 Prefix Cache:公共前缀只算一次

很多场景(多轮对话、系统提示词、few-shot 模板)请求开头一大段是公共的。Prefix Cache 让命中前缀的请求直接复用已经算好的 KV Cache block,省掉重复 prefill,显著降低 TTFT(首 token 延迟)。命中率高时收益极大——这套引擎把 Prefix Cache 同时做到了 GPU 显存和 CPU 内存两层,CPU 那一层相当于一个可换入换出的二级缓存。

2.6 量化:两个正交维度

引擎里的量化方案可以沿两个维度拆开看:

维度一:量化了什么

类别存储计算收益精度
Weight-Only 权重量化权重存 int8/int4仍用 BF16/FP16 算只省显存带宽几乎无损
Activation 激活量化权重存 int8激活也走 int8 算省带宽 + 提速略降,需业务验证

为什么激活量化能提速?因为 NVIDIA GPU 只有 int8 Tensor Core(没有 int4 Tensor Core),int8 矩阵乘既有带宽红利又有算力红利,所以是更"理想"的方案——但精度会略微下降,必须用业务数据回归。

维度二:量化粒度

粒度含义精度复杂度
Per-Channel(=Per-Token)每个输出通道一个 scale一般够用了实现简单、性能最好
Sub-Channel一个通道再分 N 组各算 scale更高复杂、略慢,且激活量化受公式约束基本做不了(激活只能 Per-Channel)

量化公式(对称 / 非对称按通道):

scale_c     = (max_c - min_c) / 255          # int8 共 255 档
zero_point_c = round(-min_c / scale_c)
Q_c = clamp(round(W / scale_c) + zero_point_c, 0, 255)
# 反量化:W ≈ (Q_c - zero_point_c) * scale_c

引擎里给了两条路线:免微调动态量化(拿到 FP16 权重直接现场量化,无需校准、无需微调,部署极快,精度几乎不掉)和 GPTQ(需要离线微调/校准,精度更高但要额外一步)。

2.7 异构指令集:同一份上层、三套底层内核

硬件指令集关键加速点
CUDA GPUSM70–SM90aTensor Core(int8/BF16/FP8)、FlashAttention、2:4 稀疏 Tensor Core
x86 CPUAVX2 / AMXAMX 用于第四/五代 Xeon(Sapphire/Emerald Rapids)矩阵加速
ARMv9 CPUSVE倚天 710 等,SVE 可变长向量

这正是"用 C++ 做运行时"的价值:内核可以是 hand-tuned 的 CUDA / AVX-512 / SVE 代码,而调度、显存管理、请求生命周期这套上层逻辑只写一遍。

2.8 MoE 与 Top-K 路由

混合专家模型每层用门控网络对每个 token 选出 Top-K 个专家。GPU 上做 Top-K,传统"归并/选择"实现依赖片上有限的 shared memory,k 一大就不可扩展,batch 一上来吞吐更差。这套引擎在 MoE 算子里用了一个基于基数排序的 GPU 并行 Top-K 选择——它不靠片上排序,而是按位分桶,天然支持更大的 k、对输入长度和 batch size 都不敏感,配合一套面向高带宽/高资源利用率的优化框架;再叠加不规则负载的静态批处理调度,把 MoE 推理的效率拉起来。


三、架构设计思路:分层拆解

整套引擎是自底向上的四层结构:

┌──────────────────────────────────────────────────────────┐
│  接口层   C++ SDK  |  Python 绑定  |  跨语言扩展(Java/Rust) │
│           |  OpenAI 兼容 API Server(对接服务层)          │
├──────────────────────────────────────────────────────────┤
│  调度/引擎层  连续批处理调度器 · 请求生命周期 · 异步结果队列 │
│             Prefix Cache(GPU/CPU) · Chunked Prefill · LoRA │
├──────────────────────────────────────────────────────────┤
│  算子/内核层  分页注意力 Span 内核 · GEMM/GEMV · 反量化融合 │
│             并行基数 Top-K · Softmax · MoE Batch GEMM      │
├──────────────────────────────────────────────────────────┤
│  后端层   CUDA(SM70-90a) | x86(AVX2/AMX) | ARMv9(SVE)      │
│           KV Cache int8/uint4 · 权重 int8/int4 · 激活 int8 │
└──────────────────────────────────────────────────────────┘

三条设计主线:

  1. 薄上层、厚底层:上层只负责"请求怎么排、显存怎么管、状态怎么回",所有跟硬件性能相关的都沉到内核层按后端各写一份。
  2. 配置驱动 + 接口稳定:模型结构、量化方式、调度参数全部通过引擎配置注入,对外 API 面窄而稳定,方便集成进已有系统。
  3. 静态链接 + 极少依赖:依赖库尽量编进二进制,降低部署体积和版本地狱,这是面向生产/边缘的取舍。

数据流(一次请求的旅程):

新请求到达
   │
   ├─► 异步接口登记 (request_id, 生成参数)
   │
   ├─► 调度器:插入当前 batch 的空槽
   │        │
   │        ├─ prefill 阶段:命中 Prefix Cache? ─ 是 → 复用 KV block(跳过重算)
   │        │                                   └ 否 → Chunked Prefill 切块计算
   │        └─ decode  阶段:每步 1 token,GEMV/分页注意力
   │
   ├─► 内核层:按后端选 CUDA/AMX/SVE 实现;量化路径现场反量化+融合
   │
   ├─► MoE 层? → 并行基数 Top-K 选专家 → 不规则负载静态批处理 → 专家 Batch GEMM
   │
   ├─► 采样 → 写 KV block(分页,必要时 swap 到 CPU 二级缓存)
   │
   └─► 结果进入异步队列 → 客户端 GetTimeout / 流式取 token

四、全套核心代码实现原理

这一节给实现级别的代码骨架,配合真实机制逐行讲解,而不是空喊概念的伪代码。

4.1 引擎骨架与异步请求队列

引擎对外是一个 handle + 一组请求级异步方法。核心状态是请求队列与结果队列:

// 引擎句柄与一次生成请求的抽象(实现级骨架)
struct GenerateReq {
    std::string request_id;
    std::vector<int> token_ids;   // 输入 prompt 的 token
    SamplingParams sampling;      // 逐请求独立的生成参数
    int max_new_tokens;
    bool stream;
};

class LlmEngine {
public:
    // 异步提交:立即返回 request_id,不等结果
    std::string async_generate(const GenerateReq& req);

    // 非阻塞取结果;超时则返回,支持阻塞/非阻塞两种用法
    Status get(const std::string& request_id,
               std::string& out_text, int64_t timeout_us);

    // 查询某个请求的实时状态:等待/运行/完成/中止
    ReqStatus get_status(const std::string& request_id);
    void cancel(const std::string& request_id);

private:
    WaitingQueue waiting_;        // 还没进 batch 的请求
    RunningQueue running_;        // 正在解码的请求,每个绑一组 KV block
    ResultQueue  results_;        // 已生成 token 的出口队列
    Scheduler    scheduler_;
    BlockManager block_mgr_;      // 分页 KV 的物理 block 分配器
    PrefixCache  prefix_cache_;   // GPU + CPU 两级前缀缓存
};

实现要点:

  • 逐请求异步:提交即返回,调用方各自用 get(request_id, timeout) 控制,结果队列早期在并发场景下是瓶颈,后来专门优化了 ResultQueue 并加了带超时的接口(GetTimeout),既支持阻塞等待也支持超时返回。
  • logits kernel 优化:在大 batch(400~500)下,引擎运行时开销压到 < 1%——意味着 Python/C++ 框架这一层几乎不再拖后腿,这正是 C++ 原生的核心价值。

4.2 连续批处理调度器

调度器的职责是每一"步"决定 GPU 上跑哪些请求、哪些进入哪些退出,并处理 prefill/decode 混排:

class Scheduler:
    def __init__(self, max_running_seqs, gpu_blocks, prefix_cache):
        self.running = []          # 正在生成的请求
        self.waiting = []          # 排队请求
        self.block_mgr = GPUBlockAllocator(gpu_blocks)
        self.prefix = prefix_cache

    def schedule_step(self):
        # 1) 先让出已结束的请求,释放其 KV block(可能触发 swap 到 CPU)
        self.running = [r for r in self.running if not r.is_finished()]
        self._free_finished_blocks()

        # 2) 给当前 running 里还没分配下一步 block 的请求按需补 block
        self._ensure_decode_blocks(self.running)

        # 3) 用剩余空槽 + 剩余 block 接纳新的 waiting 请求
        room = self.max_running_seqs - len(self.running)
        admitted = []
        for req in list(self.waiting):
            hit_blocks = self.prefix.match_and_lock(req.token_ids)  # 前缀命中
            need = ceil((len(req.token_ids) - len(hit_blocks)) / BLOCK)
            if room <= 0 or not self.block_mgr.can_alloc(need):
                break
            req.prefill_skip = hit_blocks           # 命中部分不重算
            self.block_mgr.alloc(req, need)
            admitted.append(req)
            self.waiting.remove(req)
            room -= 1

        return PrefillChunkPlan(self.running, admitted,
                                chunked=self.chunked_prefill)

要点:

  • 即时插入:每步都重新决定 batch,新请求只要有空槽和 block 立刻进;
  • chunked prefill:把超长 prompt 切成块,和 decode 错峰,避免一个长 prompt 把整步 GPU 时间占满导致其它 decode 请求卡顿;
  • context 优先级调度:对不同上下文长度组合做优先级排布,配合大 batch 短序列专用的 Softmax 内核提升吞吐。

4.3 分页 KV Cache 与注意力内核的间接寻址

BlockManager 把 KV Cache 切成固定大小 block(典型每 block 16 个 token),用页表映射"逻辑第 i 段 → 物理 block 号":

// 分页 KV:物理 block 池 + 每请求页表
struct KVBlock { void* k_ptr; void* v_ptr; uint32_t ref_count; int8_t  dtype; };

class BlockManager {
    std::vector<KVBlock> pool_;       // 物理池
    std::vector<uint32_t> free_list_; // 空闲 block 索引
public:
    // 给请求分配/扩展一页表,返回逻辑→物理映射
    PageTable acquire(const std::vector<uint32_t>& old_table, int need_tokens);
    void release(const PageTable& t) { /* ref_count--,归零则回收 */ }
    void swap_out(const PageTable& t);  // GPU block → CPU 二级缓存
    void swap_in (PageTable& t);        // CPU → GPU
};

注意力内核计算时,不再假设 K/V 连续,而是顺着页表逐 block 间接寻址(这就是"分页注意力"的内核形态):

__global__ void paged_attention_kernel(
    const half* q,          // [head, head_dim] 当前 query
    const int8_t* k_pool,   // 量化后的 KV 物理池
    const int8_t* v_pool,
    const uint32_t* page_table,   // 逻辑 block -> 物理 block
    int num_blocks, int block_size,
    const float* scales,          // per-channel / per-block scale
    half* out)
{
    // 每个 warp 处理一个注意力头
    float acc_o[HEAD_DIM] = {0}; float m_i = -1e30, l_i = 0;

    for (int b = 0; b < num_blocks; ++b) {
        uint32_t phys = page_table[b];              // ① 间接寻址
        const int8_t* kb = k_pool + phys*block_size*HEAD_DIM;
        // ② 从共享内存 tile 化读取并反量化 + 算 score
        //    score = dot(q, dequant(kb, scales[b])) / sqrt(head_dim)
        // ③ 在线 softmax(Flash 风格),边算边 rescale,避免物化整段概率
        // ④ 对 value 同样按页表读 kb 对应 v,累加进 acc_o
    }
    // ⑤ 输出归一化
    write_out(out, acc_o, l_i);
}

实现红利:

  • 这套分页注意力后来进一步用 GEMM 化的批量注意力内核重写——当 batch/上下文足够大时,把注意力转成 GEMM 形式吃满 Tensor Core,相比朴素实现拿到 GQA 场景 3~8×、端到端 1.6~2×(A100) 级别的加速,并且和 int8/uint4 的 KV 量化叠加使用。
  • GEMM 与 GEMV 两套矩阵乘内核是地基:prefill 用 GEMM,decode 阶段单 token 用 GEMV。

4.4 免微调动态权重量化:反量化与计算融合

权重量化的性能命门不是"压缩",而是别让反量化单独占一遍带宽。实现是把反量化直接内联进 GEMV 内核,从全局内存读 int8 权重,在寄存器里用 scale 还原后立刻乘加:

// Weight-Only int8 GEMV:读 int8 权重,寄存器内反量化,融合进 dot
__global__ void iq_gemv_int8(const float* x,   // 激活 (BF16 提升为 float 累加)
                             const int8_t* wq, // int8 权重 [N, K]
                             const float* w_scale,   // per-channel(N 方向) scale
                             const int8_t* w_zeropoint,
                             float* out, int N, int K) {
    int n = blockIdx.x * blockDim.x + threadIdx.x;
    if (n >= N) return;
    float s = w_scale[n]; int zp = w_zeropoint[n];   // ① 当前通道量化参数
    float acc = 0.f;
    for (int k = 0; k < K; ++k) {
        // ② 关键:int8 -> 浮点 的还原就发生在寄存器,不落显存
        float w = (float(wq[n*K + k]) - zp) * s;
        acc += x[k] * w;
    }
    out[n] = acc;
}

要点:

  • Per-Channel(= Per-Token)粒度:N 方向每个输出通道一组 (scale, zero_point),绝大多数精度需求这就够了;只有不够时才升级到 Sub-Channel(把一个通道再分 N 组,精度更高但内核更复杂、性能略降)。
  • 免微调动态量化:拿到 FP16 权重现场算 scale/zp,无需校准集、无需微调,部署路径最短,评测显示对精度几乎无影响。
  • int4 则是把两个 4-bit 权重打包进一个字节,内核里再做 unpack + 反量化,显存再省一半;ARMv9 上 int8 走 SVE,x86 上 int8/BF16 走 AVX2、矩阵部分走 AMX。

4.5 激活量化:直接吃 int8 Tensor Core

与权重量化不同,激活量化把 x 和 w 都存 int8,矩阵乘直接用 Tensor Core 的 int8 路径,带宽与算力双省:

// int8 GEMM:调用 Tensor Core 的 s8s8s32 MMA,epilogue 做反量化
// 注意 NVIDIA 只有 int8 Tensor Core(无 int4 TC),故 int8 激活最"理想"
void a8w8_int8_gemm(const int8_t* A, const int8_t* B, int32_t* C_accum,
                    const float* a_scale, const float* b_scale,
                    float* out, int M, int N, int K) {
    // ① mma.sync.s8s8s32:A(int8)×B(int8) -> 累加进 int32(保精度)
    launch_int8_tensorcore_mma(A, B, C_accum, M, N, K);
    // ② epilogue:out = (C_accum * a_scale[m] * b_scale[n]) 还原为浮点
    dequant_epilogue(C_accum, a_scale, b_scale, out, M, N);
}

代价是精度略低于 Weight-Only,所以必须用业务数据回归——这也是文档反复强调"业务测精度"的原因。FP8 同样支持(H20/H100 等 Ada/Hopper 架构)。

4.6 MoE 的并行基数 Top-K 路由

MoE 门控要先为每个 token 选出权重最大的 Top-K 个专家。归并式选择受片上 shared memory 限制、k 大了不可扩展。基数式做法按比特位分桶:

// GPU 并行基数 Top-K:按位分桶逐步缩小候选,支持超大 k / 大 batch
template<int BITS>
__global__ void radix_topk(const float* logits,   // [rows, num_experts]
                           int k, int num_experts,
                           int* out_idx, int* hist) {
    // ① 高位先统计:对每个 bit 位做一次 histogram(global memory,可大 k)
    for (int bit = BITS - 1; bit >= 0; --bit) {
        count_bit(logits, bit, hist);          // 该 bit=1 的元素数
        // ② 据累计计数判断阈值落在桶内/桶外,记录"边界前缀"
        int kept = update_threshold_prefix(hist, k);
        // ③ 把落在目标桶的元素写回候选,bit-- 继续细化
        partition_by_bit(logits, bit, kept, out_idx);
    }
    // ④ adaptive scaling:在对抗性分布(大量并列值)上动态调整分桶宽度,
    //    避免最坏情况退化,进一步提速。
}
  • 相比归并式实现,非 batch 查询提速可达 ~2.5×、batch 查询 ~4.8×,对抗分布上再多 ~2.7×;
  • 选完专家后,专家负载天然不均匀(不规则负载),用静态批处理框架把每个专家收到的 token 打包成尽量规整的 GEMM,配 底层算子库的 MoE Batch GEMM 跑批。

4.7 Prefix Cache + CPU Swap

前缀命中即复用,未命中部分才 prefill;GPU 显存放不下的历史前缀换出到 CPU 内存二级缓存:

def match_and_lock(prompt_tokens):
    # 按 block 边界对齐地做最长公共前缀匹配
    locked = []
    for i in range(0, len(prompt_tokens) - len(prompt_tokens) % BLOCK, BLOCK):
        key = hash(tuple(prompt_tokens[i:i+BLOCK]))
        blk = gpu_pool.get(key) or cpu_pool.get(key)
        if blk is None:
            break                       # 前缀断了,后面全部重算
        if blk.where == "cpu":
            blk = swap_in(blk)          # CPU -> GPU
        blk.ref_count += 1
        locked.append(blk)
    return locked                       # 这些 block 直接当 prefill 的初始 KV

多模态请求(图文、视频)同样能命中前缀缓存,所以标准 LLM 和 MLLM 一起受益。

4.8 多语言接口与服务层

  • C++ 接口是主体;Python 接口通过对 C++ 内核的薄封装暴露,便于研究/快速接入;
  • 通过标准跨语言接口(C ABI)可把 C++ 接口再延伸到 Java、Rust;
  • 服务层把它包成 OpenAI 兼容 API Server(对接一个 chat 服务组件即可),文本/图像/视频输入统一走 Chat Completion 协议,结构化 JSON 模式输出也内建(Guided Decoding)。
  • 多模态 toolkit 单独放在 multimodal/ 目录,作为基于同一引擎的视觉语言推理工具包。

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

5.1 环境要求

项目要求
Python(走 Python 接口时)3.8+
CUDA GPU计算能力 SM70–SM90a:T4 / V100 / A10 / A100 / 3090 / 4090 / L20 / H20 / H100;驱动需支持 CUDA 11.4–12.4
x86 CPU需 AVX2;Sapphire Rapids / Emerald Rapids 等可启用 AMX
ARMv9 CPU需 SVE,如倚天 710
模型格式支持 HuggingFace 格式权重

5.2 安装

# 建议放在 conda 环境里
conda create -n llminfer python=3.10 -y
conda activate llminfer

# Python 轮子安装(按目标后端选 GPU/CPU 版本)
pip install <engine-package>

bash build.sh          # 产出静态链接的库与示例二进制

从某个较新版本起,PyPI 上的预编译轮子默认针对 CUDA 12.x 构建,普通 A100/H100/4090 直接 pip install 即可。

5.3 离线批量推理(Python)

from <engine>.pipeline import pipeline

pipe = pipeline(model_path="/data/qwen2-7b-instruct",
                quant="iq",        # 免微调动态权重量化;也可 "gptq" / "none"
                device="cuda")     # 或 "cpu" / "x86-amx" / "arm-sve"

responses = pipe([
    "用一句话解释 KV Cache 的作用",
    "把下面的中文翻译成英文:床前明月光",
])
print(responses)

pipeline 内部即连续批处理:上面两条不同长度的请求会被调度进同一批,prefill/decode 混排,逐条流式出结果。

5.4 C++ 接口最小可用示例

#include "engine.h"

int main() {
    EngineConfig cfg = EngineConfig::load("configs/qwen2_7b_iq.json");
    auto engine = LlmEngine::create(cfg);

    std::vector<GenerateReq> reqs = {
        MakeReq("r1", "你好,介绍一下上海", /*stream=*/true),
        MakeReq("r2", "解释一下分页注意力", /*stream=*/false),
    };
    for (auto& r : reqs) engine->async_generate(r);   // 立即返回 request_id

    for (auto& r : reqs) {
        std::string out;
        while (engine->get_status(r.request_id) != ReqStatus::DONE) {
            if (engine->get(r.request_id, out, /*timeout_us=*/50000)
                    == Status::HAS_TOKEN) {
                std::cout << out; std::cout.flush();      // 流式打印
            }
        }
    }
    engine->shutdown();
}

异步接口让每个请求独立控制生成参数与状态,超时用 get(..., timeout) 返回而不是阻塞。

5.5 启动 OpenAI 兼容服务

# 加载引擎并以 OpenAI Chat Completion 协议对外服务
<engine> server \
    --model /data/qwen2-7b-instruct \
    --quant iq \
    --enable-prefix-cache \
    --enable-guided-decoding \
    --port 8000
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8000/v1", api_key="EMPTY")
resp = client.chat.completions.create(
    model="qwen2-7b-instruct",
    messages=[{"role": "user", "content": "写一个 JSON:姓名/年龄/城市"}],
    response_format={"type": "json_object"},   # Guided Decoding:强制合法 JSON
)
print(resp.choices[0].message.content)

5.6 量化模型加载与对比测试

# 路线 A:免微调动态量化(最快部署,无需校准)
<engine> bench --model /data/qwen2-7b-instruct --quant iq --dtype int8

# 路线 B:GPTQ 已微调权重(精度更高,需先用校准集导出)
<engine> bench --model /data/qwen2-7b-gptq-4bit --quant gptq --dtype int4

# 路线 C:激活量化(A8W8,int8 Tensor Core,需业务精度回归)
<engine> bench --model /data/qwen2-7b-instruct --quant a8w8 --granularity per-channel

精度对齐验证:拿同一组 prompt 同时跑 PyTorch/HF 基线和本引擎,比对 perplexity 与生成一致性;权重量化应"几乎不掉点",激活量化掉点要落在业务可接受区间。

5.7 多模态(VLM)推理

from <engine>.vlm import VLMPipeline
vlm = VLMPipeline(model_path="/data/qwen2-vl-7b", quant="iq")
out = vlm.chat([
    {"type": "image_url", "image_url": {"url": "file:///data/cat.jpg"}},
    {"type": "text",      "text": "描述这张图,并按 JSON 输出主体/动作/场景"}
])
print(out)

走 Chat Completion 协议、文本+图像/视频统一输入,并可享受 Prefix Cache(图像特征 + 公共文本前缀一起命中)。

5.8 调优参数速查

参数作用建议
block_sizeKV 分页大小(token/block)16 起步,长上下文可调大减少页表开销
max_running_seqs连续批处理最大并发序列受显存 block 上限约束,吞吐/延迟权衡
enable_prefix_cache开前缀缓存多轮/共享系统提示场景务必开
chunked_prefillprefill 切块长 prompt 多时缓解 decode 卡顿
enable_cpu_prefix_swapCPU 二级前缀缓存显存紧张时用容量换 TTFT
quantnone/iq/gptq/a8w8部署快用 iq,追求精度用 gptq,要算力用 a8w8
quant_granularityper-channel/sub-channel默认 per-channel,不够再 sub

5.9 常见问题排查表

现象原因解决
CPU 后端跑不起来缺 AVX2换支持 AVX2 的机器,或启用 AMX(Xeon 四代/五代)
ARM 后端报错缺 SVE确认倚天 710 等 ARMv9
显存够用却 OOMKV 分配碎片开分页 + 调 block_size / max_running_seqs
激活量化精度掉得多int8 激活误差累积退回权重量化,或改 per-channel / 试 gptq
首 token 很慢长 prompt 未命中前缀开 prefix cache / chunked prefill
并发高时延迟抖动单请求长 prompt 独占整步开 chunked prefill + context 优先级调度

六、性能与关键结论

  • 异构统一:同一份上层逻辑跑遍 GPU / x86 / ARMv9,SM70–SM90a 与 int8/uint4 KV 量化、int8/int4 权重、int8 激活组合覆盖从 T4 到 H100 的跨度。
  • 大 batch 不掉速:logits 内核优化后,batch 400~500 时运行时开销 < 1%,这是 C++ 原生最直接的回报。
  • 注意力提速:分页注意力 GEMM 化重写,GQA 场景 3~8×、端到端 1.6~2×(A100)。
  • 前缀缓存:命中越高 TTFT 下降越明显,2:4 稀疏借助 CUDA Sparse Tensor Core、MoE 走 Batch GEMM。
  • 精度对齐:免微调动态权重量化精度几乎无损、部署路径最短;GPTQ 精度更高但需一步微调。

七、落地用途

  1. 高并发在线服务:连续批处理 + 异步接口 + OpenAI 兼容层,直接顶生产流量,尤其适合请求长度参差、需要流式输出的对话/Agent 后端。
  2. 多模态产品后端:VLM toolkit + 图文视频统一 Chat 协议 + 前缀缓存命中视觉/文本公共前缀,做图搜、视觉问答、多模态 Agent 的推理底座。
  3. 量化降本 / 离线评测:免微调动态量化把"部署效率"拉满;同一套评测流程横向比较 FP16 / int8 / int4 / GPTQ / A8W8 的精度-性能曲线。
  4. 异构与信创/边缘部署:静态链接 + 极少依赖 + 三套后端,可落在 x86 服务器、国产 ARMv9 服务器乃至 GPU 边缘盒子上,统一上层、各取硬件红利。
  5. 推理引擎研究与二开:C++ 内核分层清晰,做内核/调度研究的平台,加新模型或新量化方案时只需补一个后端实现。

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

八、总结

这套引擎的核心思路可以浓缩成一句话:把"轻"留给集成、把"重"压进内核、把"对"留给硬件——上层用极简依赖和静态链接换来快速落地,底层用 C++ 手调内核分别吃透 CUDA Tensor Core、x86 AMX、ARMv9 SVE,中间靠连续批处理、分页注意力、前缀缓存、动态权重量化、并行基数 Top-K 这些机制把吞吐与延迟拉到工业可用水位。

按公开的规划,能力边界还在持续外扩:注意力上进一步融合 FlashAttention 路线、把上下文长度推到 32K 以上、补齐 4-bit 与 GPTQ 量化、A8W8 激活量化在 CUDA 上继续打磨、LoRA 在连续批处理下做更优调度、prefill 与 generation 在同一引擎内并行,并把后端从 NVIDIA 扩展到 AMD(ROCm) 平台。对一个原生 C++、跨三类指令集的引擎来说,这条路线的价值正在于此:一次设计,多端落地。

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

Logo

更多推荐