高级功能

让 TensorSharp 既快又可扩展的系统:带分页 KV 缓存的连续批处理、推测解码、文本扩散,以及底层的内核级与内存级优化。

连续批处理与分页 KV 缓存

服务器的 InferenceEngine 是一个 vLLM 式连续批处理引擎,默认开启。它不再一次只运行一个请求,而是以单个 decode 步的粒度将多个请求交错运行。

尚未实现批量路径的模型仍运行在引擎隔离的逐序列 KV 交换回退之上。用 TS_SCHED_* 变量调参,或用 --no-continuous-batching 完全禁用。

原生分页注意力

原生内核 TSGgml_PagedAttentionForward(以及面向 GPT OSS 的 WithSinks 变体)在 C++ 中从分页缓冲收集 K/V,为每个序列构建一个小的 GGML 图,并分派 ggml_flash_attn_ext —— 与单序列路径所用相同的融合 Metal/CUDA 闪存注意力内核。在长上下文 Ministral-3-14B 负载(4×约 800 token)上,它比 legacy 逐序列 GGML 路径快约 21%。

MLA / 稀疏注意力架构(DeepSeek V4、GLM 5.x)走的是第三条路。每个 token 只有一行压缩缓存,没有可分页的布局,因此原生执行器给每个请求分配一个槽位——一整套逐层的 MLA 与索引器缓存,外加它自己的 n_past——绑定请求只是切换活跃槽位,不搬运任何 KV 字节。GLM 的批处理融合解码默认启用:一张图让 N 个序列各出一个 token,权重在它们之间只读一次——4 路并发下总解码吞吐 1.81 倍。设置 TS_BATCHED_FUSED_DECODE=0 可切回串行融合 decode,以便严格比较路径。批处理会改变每一个 GEMM 的形状,而 78 层里有 75 层是 top-8-of-256 路由,可能把最低位的差异放大成 2-bit 权重上不同的专家选择。

批量前向(Mistral 3、Gemma 4、GPT OSS、带 GatedDeltaNet 循环状态池的 Qwen 3.5/3.6,以及带 Mamba2 循环状态池的 Nemotron-H)把 N 个序列打包进一次 ForwardBatch 调用,每层一次批量线性投影 matmul,并配以分页 K/V 散写。Gemma 4 达到约 1.5–1.6× legacy 吞吐;Nemotron-H Mamba2 批量在 Apple M4 Pro 上 batch=3 时达到约 3.95×。

共享前缀 checkpoint

基于块哈希的前缀共享,帮得上在前缀尚未失效时到达的请求;但它对每一个会话都要以之开头的那段提示词——系统提示词、工具声明、选中的技能——毫无帮助,每个新会话原本都要把它从头 prefill 一遍。在 GGML 后端上的 Gemma 4 与 Qwen 3.5/3.6 上,引擎现在会在这段共享前缀的末尾对完整模型状态做 checkpoint,并让每个新会话从它的副本开始,于是新会话只需重新 prefill 自己的那条消息。TS_PREFIX_CHECKPOINTS_MAX(默认 2)限制同时保留 checkpoint 的不同前缀数量;TS_PREFIX_CHECKPOINTS=0 可关闭。

宿主可以给引擎挂上 IPrefixCheckpointStore,让 checkpoint 活得比进程更久——执行器会在准入阶段读取已保存的 checkpoint,并在取 checkpoint 时写回,字节格式由模型家族自己拥有并校验。TensorAgent 正是这样让每次启动的第一条消息只付出恢复的代价而不是完整 prefill:在 iPhone 17 Pro Max 上以 Qwen3.5 9B 实测,原本 54 秒的冷启动首条消息,变成 1.2 秒预热加约 0.6 秒的首条消息。PrefixCheckpointExactnessTests 证明:从恢复的副本开始的会话,与冷 prefill 产出相同的 token。

与之配套的还有 TS_RETAINED_FUSED_CACHE(默认开启,LRU 预算由 TS_RETAINED_FUSED_CACHE_MAX 控制),它保留已结束请求的融合 holder,使前缀完全一致的续写不必重新 prefill;以及三个 K/V 尺寸旋钮——TS_KV_INITIAL_TOKENSTS_KV_GENERATION_RESERVE_MAXTS_KV_HOLDER_POOL_MAX——让内存受限的宿主把缓存从小起步、按需增长,而不是每个请求都预留整个上下文窗口。

📖

完整深入解析:仓库中的 docs/PAGED_ATTENTION_AND_CONTINUOUS_BATCHING.md

MTP / NextN 推测解码

部分架构自带一个多 token 预测(MTP / NextN)草稿头,让服务器可为单序列(非并发)运行无损推测解码。草稿廉价地提出若干未来 token,主干在一次批量前向中验证全部,被接受的 token 一步提交。

🎯

由于请求自身的采样器 —— 温度、top-k/p 与全部惩罚 —— 同时驱动草稿与验证,输出与标准解码完全一致。推测只改变产生相同 token 所需的前向次数。

它默认关闭。内嵌在主干 GGUF 里的草稿头在服务器上用 --spec(环境变量 TS_SPEC=1,旧名 TS_MTP_SPEC=1)启用;以独立 GGUF 发布的草稿头则只需用 --draft-model 写出文件即自动启用:

# Qwen 3.6 —— 必须使用保留 NextN 块的 -MTP- 仓库 GGUF;基础仓库导出会剥离该块
dotnet run --project TensorSharp.Server.Host -c Release --no-build -- --model Qwen3.6-35B-A3B-UD-IQ2_XXS.gguf --backend ggml_cuda \
    --spec --spec-draft 8 --spec-pmin 0.75

# Gemma 4 —— 加载与目标匹配的独立 gemma4-assistant 草稿 GGUF
dotnet run --project TensorSharp.Server.Host -c Release --no-build -- --model gemma-4-12B-it-qat-UD-Q4_K_XL.gguf --backend ggml_cuda \
    --draft-model mtp-gemma-4-12B-it.gguf

两种草稿头形态

GLM-5.3(非 Flash 版,GGUF 架构 glm-dsa)同样自带内嵌形态:在主干 GGUF 的 blk.78 上完整携带一个 NextN 块,位置与 GLM-5.2 自己那一个相同。--spec 必须在加载之前就出现在命令行上,因为加载器是在读取主干的同时决定要不要把该块一并读进来(TS_GLM_MTP 优先,其次 TS_SPEC,再次是旧名 TS_MTP_SPEC)。该块不含 nextn.shared_head_head.weight,只能借用主干的 LM head,因此投机只在默认按层切分(不传 --tp)时生效:--tp N>1 时这个 head 是按列切分的,加载器拒绝用某个 rank 手上的那一条词表切片来起草。这条约束是从原生与托管两侧的加载器代码读出来的,并没有测试覆盖;它也不是 GLM-5.3-Flash 的限制 —— glm5next 的 NextN/MTP 根本没有实现,与「某种模式下不可用」是两回事。下表只给出 Qwen 3.6 与 Gemma 4;GLM-5.3 的内嵌 NextN 目前没有属于它自己的接受率或加速数字。完整机制见仓库中的 docs/models/glm.md 卡片。

何处有收益

后端Qwen 3.6Gemma 4
GGML CUDA / GGML Metal✅ 融合验证 + 草稿内核✅ 融合验证 + 草稿内核
直接 CUDA(cuda,纯 C#)✅ GPU 常驻逐 op 验证/草稿✅ GPU 常驻逐 op 验证/草稿
CPU / GGML CPU / MLX标准解码标准解码

调参:--spec-draft(默认 8)限定每步草拟的 token 数;--spec-pmin 是保留某 token 所需的最低草稿置信度,其默认值取决于草稿器类型(逐 token 草稿头为 0.15,块级草稿器为 0.35 —— 后者的门限是累积前缀概率,因此同一个数字要严格得多)。Gemma 4 的 A/B 开关是 TS_GMTP_* 环境变量。在 GGML 后端上对 Qwen 3.5 显式传入的 --spec-draft 请保持 7 或更小:验证批达到九行时会偏离普通贪心解码,这是正确性缺陷而不是速度问题。默认值不受影响,因为该模型自己要求 3 token 的窗口。

DSpark 块级投机解码(DeepSeek V4)

DeepSeek V4 的 checkpoint 中随模型附带一个 DSpark 支持模块("Confidence-Scheduled Speculative Decoding with Semi-Autoregressive Generation"):三个 DSV4 块读取主干的 hidden states,每步提议一整 token 而不是一个;一个 Markov 头让块内每个位置以其前一个 token 为条件;一个置信度头预测每个位置被接受的概率。主干随后用一次批量前向验证整块,只保留它自己的采样器本来也会产生的前缀。

草稿器是一个独立的 GGUF,用 --draft-model 加载 —— 主干的所有 GGUF 转换都会丢掉 mtp.* 张量。预构建的草稿器列在仓库的 MODEL_DOWNLOADS_zh-cn.md 中,也可以用 eng/dsv4-dspark-to-gguf.py 从上游 safetensors checkpoint 自行转换(只需下载含 mtp.* 的三个分片)。

# CLI —— 所有单序列路径(--input、--multi-turn-jsonl、--interactive)都会用到
dotnet TensorSharp.Cli/bin/TensorSharp.Cli.dll --model DeepSeek-V4-Flash-...-00001-of-00005.gguf \
    --backend ggml_cuda --draft-model DSpark-drafter-Q2K-Q8-0731.gguf \
    --input prompt.txt --max-tokens 200 --temperature 0

# 服务端 —— 同一个草稿器参数;写出文件即启用投机
dotnet TensorSharp.Server.Host/bin/TensorSharp.Server.Host.dll --model DeepSeek-V4-Flash-...-00001-of-00005.gguf \
    --backend ggml_cuda --tp 4 --draft-model DSpark-drafter-Q2K-Q8-0731.gguf

它可运行在两个 GPU 引擎上 —— --backend cuda(Direct CUDA)与 --backend ggml_cuda(原生 ggml);ggml_vulkancpu 对该架构没有投机路径,配置了草稿器时会打印警告。在 ggml 上草稿器就是计算图里额外的三层,其 key ring 由主干图自己提交,因此投机不产生任何主机往返。CLI 上它需要纯 argmax 采样(任何 temperature、top-k/p 或重复惩罚都会将其关闭);服务端每一行验证都用该请求自己的采样器,因此可与任意采样设置组合。投机只服务单序列 —— 一旦有第二个请求在途,DSV4 的 per-sequence slot 会以正常 decode 速度服务这一批。

实测(4×A40,DeepSeek-V4-Flash-0731 UD-Q8_K_XL,贪心)

指标cuda 基线cuda + DSparkggml_cuda 基线ggml_cuda + DSpark
Decode(生成 200 token)26.0 tok/s34.0 tok/s(1.31×)26.4 tok/s37.1 tok/s(1.41×)
Prefill(15K 提示词)962 tok/s955 tok/s952 tok/s954 tok/s
接受率69%69%

多轮对话获益最大 —— 延续既有上下文的轮次,正是草稿器最有把握的地方。在同一台机器上的 5 轮交互式会话中,每轮实测 1.50× 到 2.02×(接受率 66–87%),prefill 保持持平。在 200 token 与 15K 上下文两次运行中,贪心输出都与非投机基线逐字节一致。

为什么是 1.3× 而不是更多:主干是 6 选 256 的稀疏结构,验证批中每多一个 token 就要把它自己那套路由专家拉过显存 —— 无论起草多便宜,一行验证都要花掉约四分之一个完整 decode 步。--spec-pmin(块级草稿器默认 0.35,即保留某个起草位置所需的最小累积接受概率)正是让这笔交易保持正收益的旋钮;--spec-draft 则限制每块的 token 数。

以上都不适用于 DeepSeek V4.1,而且区别是性质上的、不是程度上的:在 V4.1 上,非空的草稿路径或 TS_DSV4_DSPARK 会在所有后端上被硬拒绝 —— 包括 --backend cpu,也就是不依赖 ggml、不依赖 GPU 的 100% 纯 C# V4.1 执行器(正确性与可移植性路径,而非服务路径) —— 给出的是 "DeepSeek V4.1 DSpark speculative decoding is not implemented; omit the draft model.",而不是 V4 的「告警后继续」。V4.1 上根本不存在可加载的 DSpark 模块。

DFlash / DFlash2 / DSpark 块级投机解码(Muse-Glimmer、Qwen 3.8、Nemotron 3.5 Lightning)

Muse-Glimmer 有自己的块级草稿模型 DFlash:一个独立的 5 层 GGUF(general.architecture = dflash),一次前向即提出整个投机窗口。它复用主干的 token embedding 与 LM head,维护自己的滑动窗口 KV 环,并按三步驱动——把主干在 dflash.target_layers 处的逐层输入残差编码成一行宽向量,将该行注入为每个草稿层的 K/V,然后把 [anchor, MASK × (block−1)] 送过 5 个块起草,并用主干的 LM head 打分。随后主干用一次批量前向完成验证。

dotnet TensorSharp.Cli/bin/TensorSharp.Cli.dll --model Muse-Glimmer-30B-UD-IQ2_XXS.gguf \
    --backend ggml_cuda --draft-model dflash-kquant.gguf --spec-draft 15 \
    --input prompt.txt --max-tokens 256

两部分都是可被 CUDA 图捕获并重放的融合原生图:TSGgml_DFlashInjectTSGgml_DFlashDraftBlock,后者以设备端 argmax 收尾,因此 202048 宽的概率块无需经 PCIe 回传主机。验证以贪心方式对齐主干,因此输出与普通贪心 decode 一致。运行期的成本调控器会把投机与普通解码逐 token 计时对比,在投机确实更慢时暂停起草——这保证投机只会更快,但也意味着它需要生成数百个 token 才能稳定,因此很短的回答可能看不到收益。CLI 上不要传任何采样参数:--top-k 1 不满足 SamplingConfig.IsGreedy,会静默关闭整条投机路径。在单张 RTX PRO 6000 Blackwell 上与 llama.cpp 自带的 DFlash 实现对比(Q8_0、贪心、60 token 提示):50.9 tok/s,对比 llama.cpp 的 45.5 与普通解码的 35.0。完整的上下文阶梯(包括 llama.cpp 领先的区间)见仓库中的 docs/models/muse-glimmer.md 卡片。

DFlash2 是同一套主干加上两处新增,两者都由 GGUF 自身标明,因此同一条代码路径可服务两代草稿器:一是在每个注意力与 FFN 子层周围加入分组动态深度卷积,让块扩散式起草无需第二次前向就获得局部的从左到右信号;二是一个候选选择器,它通过两个低秩 [vocab, r] 码本对相邻位置的 top-K 候选做两两打分,并把整块读作在该格状图上的一次游走,于是位置 i+1 不再是在不知道 i 选了什么的情况下被选出来的。两者都用 --draft-model 挂载——文件本身会说明它是哪一代。

Nemotron 3.5 Lightning 上的 DSpark 是同一套块级机制,配 NVIDIA 为它发布的模块:一个 6 层草稿器(SWA-1024、每头的 attention-sink 偏置、在第 2/6/20/30/42/52 层读取主干残差的编码器,以及把每个草稿位置都以其前一个位置为条件的 rank-512 Markov 头)。它导出为 dflash 架构的 GGUF——eng/nemotron-dspark-to-gguf.py 可从 safetensors 重建,并与 llama.cpp 的导出逐位核对——草稿器文件本身会声明带 Markov 头与 sinks。Nemotron 主干是第一个作为块级起草目标的递归(Mamba-2)主干:执行器在部分接受时对 conv/SSM 状态做快照与回滚,DSpark 窗口默认为 3 个草稿。

DiffusionGemma 文本扩散

DiffusionGemma 与自回归模型本质不同:它不逐 token 调用 Forward()。相反,它在 Gemma-4 衍生的 MoE 主干上,对定长画布做块式 EntropyBound 去噪 —— 整个答案是被迭代精炼,而非从左到右写出。

性能优化

这是跨架构的概要;docs/models/ 中每个模型卡都会以确切分派的 GGML 图逐一讲解相同内核。

已验证的 Gemma 4 E4B Q8_0 快路径:仓库基准已在 GPU 后端上验证原生 GGML 的 E4B Q8_0 家族与执行路径。推荐从公开的 ggml-org/gemma-4-E4B-it-GGUF 下载。

内存优化