高级功能

让 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)上,它比逐序列 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、Hunyuan Dense、带 GatedDeltaNet 循环状态池的 Qwen 3.5/3.6,以及带 Mamba2 循环状态池的 Nemotron-H)把 N 个序列打包进一次 ForwardBatch 调用,每层一次批量线性投影 matmul,并配以分页 K/V 散写。Gemma 4 达到约 1.5–1.6× 单序列吞吐;Nemotron-H Mamba2 批量在 Apple M4 Pro 上 batch=3 时达到约 3.95×。

共享前缀 checkpoint

对普通注意力模型而言,复用已缓存的 KV 页就够了。但对状态存放在递归状态或融合 holder 里的模型,在每一个会话都要以之开头的那段提示词——系统提示词、工具声明、选中的技能——的末尾,仅靠分页并不够,每个新会话原本都要把它从头 prefill 一遍。在 GGML 后端上的 Gemma 4 与 Qwen 3.5/3.6/3.8 家族,以及 Qwen 3.8 Flash Next(包括其按层切分)上,引擎会在这段共享前缀的末尾对完整模型状态做 checkpoint,并让每个新会话从它的副本开始,于是新会话只需重新 prefill 自己的那条消息;两个 Qwen 家族在张量并行下都不做 checkpoint。Muse Spark 的原生执行器用一个 checkpoint 槽位做同样的事:共享前缀边界之前的 token 只复制进该槽位一次,再克隆进每个新会话;由于它的模板让每个会话都以相同的系统头部开场,即使没有系统消息,这段头部也是共享前缀。宿主声明的共享前缀至少为 64 个 token。TS_PREFIX_CHECKPOINTS_MAX(默认 4)限制同时保留 checkpoint 的不同前缀数量;设为 0 可关闭。

宿主可以给引擎挂上 IPrefixCheckpointStore,让 checkpoint 活得比进程更久——执行器会在准入阶段读取已保存的 checkpoint,并在取 checkpoint 时写回,字节格式由模型家族自己拥有并校验(Gemma 4 与 Qwen 3.5 家族支持导出)。TensorAgent 正是这样让每次启动的第一条消息只付出恢复的代价而不是完整 prefill:在 iPhone 17 Pro Max 上以 Qwen3.5 9B 实测,原本 54 秒的冷启动首条消息,变成 1.2 秒预热加约 0.6 秒的首条消息。服务器默认也这样做:它在打开端口之前为启动模型前向一遍共享提示词,并把 checkpoint 存在二进制旁的 prefix-cache/ 中,每个模型一个目录(TENSORSHARP_PREFIX_CACHE_DIR 可改位置)——在一份智能体配置上实测首条消息从 21.8 s 降到 0.7 s。--no-prefix-cache 会同时关闭复用、启动预热与持久化。CLI 的缓存只在进程生命周期内保存在内存里。PrefixCheckpointExactnessTests 证明:从恢复的副本开始的会话,与冷 prefill 产出相同的 token。

与之配套的还有终态保留(默认开启;TS_RETAINED_FUSED_CACHE_MAX 限制保留的数量,默认取运行中请求上限,使每个并行运行的请求都保有自己的状态,设为 0 关闭),它保留已结束请求的融合 holder,使前缀完全一致的续写不必重新 prefill —— 适用于 Gemma 4、Qwen 3.5 家族与 Qwen 3.8 Flash Next。DeepSeek V4.1 与 GLM 5.x 会为每个已结束会话的下一轮保留它的原生 slot(V4.1 预算 TS_DSV41_RETAINED_CACHE_MB,默认 2048;加载了 DSpark 草稿器时不保留),Muse Spark 的原生执行器也是如此,并且还能回退该 slot 以重新生成回答。此外还有三个 K/V 尺寸旋钮——TS_KV_INITIAL_TOKENS、TS_KV_GENERATION_RESERVE_MAX、TS_KV_HOLDER_POOL_MAX——让内存受限的宿主把缓存从小起步、按需增长,而不是每个请求都预留整个上下文窗口。

📖

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

MTP / NextN 推测解码

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

🎯

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

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

# 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-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

草稿头形态

推测解码与 radix 前缀缓存。--spec 不再关闭前缀复用。Qwen 3.5/3.6/3.8 的 NextN 头在复用前缀(radix 命中、启动预热或从磁盘恢复的共享前缀、保留的对话轮次)结束处只重启它私有的注意力缓存,主干保留完整的缓存状态,因此 prefill 仍走正常的快路径;在草稿头重建历史之前,初始接受率可能偏低。这适用于单序列请求的默认融合验证路径;关闭融合验证而选中的分页推测路径仍需从位置 0 prefill。Gemma 4 的 assistant 头不持有状态,可从任意位置起草;块级草稿器(DFlash、DSpark)与 n-gram 推测器同样可以在复用前缀之后启用;Qwen 3.8 Flash Next 的头则不行。

GLM-5.3(非 Flash 版,GGUF 架构 glm-dsa)同样自带内嵌形态:在主干 GGUF 的 blk.78 上完整携带一个 NextN 块,位置与 GLM-5.2 自己那一个相同。--spec 必须在加载之前就出现在命令行上,因为加载器是在读取主干的同时决定要不要把该块一并读进来(TS_SPEC)。该块不含 nextn.shared_head_head.weight,只能借用主干的 LM head,因此投机在单设备或显式按层切分模式(--layer-split N,不启用张量并行)时生效:--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 验证/草稿
GGML CPU / GGML Vulkan✅ 会推测 —— 在 Qwen3.6-35B-A3B ggml_cpu 上以 8 token 窗口实测 1.21×(接受率 86%),该测量早于该家族默认窗口收窄为 3 token;当前默认值下没有已提交的实测数字✅ 会推测 —— Gemma 4 的门限是任意 GGML 后端
CPU / MLX不按后端关闭 —— 只要草稿头能加载就遵从 --spec;未实测加速标准解码

调参:--spec-type auto|draft-head|block|ngram 选择算法 —— auto(默认)使用检查点自带的草稿器,ngram 是在序列自身 token 上做后缀匹配、无需权重的推测器,不需要草稿文件。但 n-gram 仍需要模型是推测目标,因此并非处处可用:它可运行在 Qwen 3.5/3.6/3.8 家族、Gemma 4(GGML 后端或 cuda)、GLM 5.x(GLM-5.3-Flash 仅在其回滚可用时)、Qwen 3.8 Flash Next(GGML)与 Muse Spark(它唯一的推测方式;其原生 --tp N 下不推测,只做普通 decode)上,而不能运行在 GPT OSS、Mistral 3 或 Hunyuan Dense 上;DeepSeek V4/V4.1 与 Muse-Glimmer 只有挂上各自的块级草稿器才会推测,Nemotron-H 则拒绝一切推测器。--spec-draft(默认 8)限定每步草拟的 token 数;--spec-pmin 是保留某 token 所需的最低草稿置信度,其默认值取决于草稿器类型(逐 token 草稿头为 0.15,块级草稿器为 0.35 —— 后者的门限是累积前缀概率,因此同一个数字要严格得多)。在 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 --layer-split 4 --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 --layer-split 4 --draft-model DSpark-drafter-Q2K-Q8-0731.gguf

它可运行在两个 GPU 引擎上 —— --backend cuda(Direct CUDA)与 --backend ggml_cuda(原生 ggml);ggml_vulkan、ggml_metal、ggml_cpu 与 cpu 对该架构没有投机路径,配置了草稿器时会打印警告。在 ggml 上草稿器就是计算图里额外的三层,其 key ring 由主干图自己提交,因此投机不产生任何主机往返。CLI 与服务端都通过共享引擎运行它,每一行验证都用该请求自己的采样器,因此可与任意采样设置组合。投机只服务单序列 —— 一旦有第二个请求在途,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,它的 DSpark 支持是实验性的。加载器只在 ggml_cuda 与 ggml_cpu 上接受通过 --draft-model 指定的 deepseek41-dspark 草稿器,并拒绝 V4 草稿器或其他任何草稿架构。在其他所有后端上 —— 包括 --backend cpu 这个 100% 纯 C# 的 V4.1 执行器,以及 Direct CUDA 引擎 —— 配置了草稿器都会被硬拒绝("DeepSeek V4.1 DSpark requires the TensorSharp ggml executor (--backend ggml_cuda or ggml_cpu) and a matching deepseek41-dspark drafter. Other executors do not implement the V4.1 draft graph."),而不是 V4 的「告警后继续」。训练模型已在 ggml_cuda 双 GPU 按层切分下通过初步文本/图像 HTTP 检查;尚不构成通用质量或吞吐验证。

DFlash / DFlash2 块级投机解码(Muse-Glimmer、Qwen 3.5 家族)

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_DFlashInject 与 TSGgml_DFlashDraftBlock,后者以设备端 argmax 收尾,因此 202048 宽的概率块无需经 PCIe 回传主机。每一行验证都用该请求自己的采样器抽取,因此贪心运行输出的就是普通贪心流。运行期的成本调控器会把投机与普通解码逐 token 计时对比,在投机确实更慢时暂停起草——这保证投机只会更快,但也意味着它需要生成数百个 token 才能稳定,因此很短的回答可能看不到收益。在单张 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 挂载——文件本身会说明它是哪一代。两者也都可以以 Qwen 3.5 / 3.8 主干为目标;Qwen 3.8 检查点还可能同时内嵌 NextN 块,挂上 DFlash 草稿器时以后者为准。

Nemotron 3.5 Lightning 的 DSpark 模块不会被挂载。NVIDIA 为它发布了一个 6 层 DSpark 草稿器(SWA-1024、每头的 attention-sink 偏置、在第 2/6/20/30/42/52 层读取主干残差的编码器,以及 rank-512 的 Markov 头),eng/nemotron-dspark-to-gguf.py 也仍可把它导出为 dflash 架构的 GGUF。但 Nemotron-H 拒绝一切推测器,n-gram 也不例外:它的多 token 验证与单 token decode 走的是不同的注意力与 MoE 内核,logits 并不相同,推测流因此无法与普通贪心 decode 一致,而逐位一致的验证代价与逐行 decode 相当。用 --draft-model 指定的草稿器不会被挂载:CLI 会报告并按普通方式 decode,服务器则拒绝启动,直到去掉该参数。

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 下载。

内存优化