高级功能
让 TensorSharp 既快又可扩展的系统:带分页 KV 缓存的连续批处理、推测解码、文本扩散,以及底层的内核级与内存级优化。
连续批处理与分页 KV 缓存
服务器的 InferenceEngine 是一个 vLLM 式连续批处理引擎,默认开启。它不再一次只运行一个请求,而是以单个 decode 步的粒度将多个请求交错运行。
- 分页 KV 池 —— KV 缓存被划分为取自共享池的定长块,因此内存按块分配,而非按最坏情况的序列长度分配。
- 块哈希前缀共享 —— 每个完整块都做内容哈希;相同前缀(系统提示、共享上下文)在并发与顺序请求间共享,而非重新计算。
- 迭代级调度器 —— 在批中接纳与抢占序列,并在实现
IBatchedPagedModel的模型上把它们打包进一次前向。 - 可选 SSD 层 —— 对于很大的 KV 工作集,冷块可溢出到 SSD 层。
尚未实现批量路径的模型仍运行在引擎隔离的逐序列 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_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 里的草稿头在服务器上用 --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
两种草稿头形态
- Qwen 3.6(内嵌 NextN) —— GGUF 携带一个额外解码块以及 NextN 投影/归一化张量。无需独立文件;
--draft-model会被忽略。只有 unsloth/Qwen3.6-35B-A3B-MTP-GGUF 一类保留 NextN 张量的导出才具备该块;基础仓库导出会剥离它并静默回退到标准 decode。GatedDeltaNet 主干状态会被快照,以便部分被拒绝的验证批可回滚。 - Gemma 4(独立
gemma4-assistantGGUF) —— 一个 EAGLE 式循环草稿器,用--draft-model加载(写出该文件本身即启用投机)。它自身不持有 K/V:每个草稿层都查询目标模型已有的逐层 KV 缓存。草稿的隐藏维度必须与目标一致(把 12B 目标与其 12B 草稿配对)。不匹配或不完整的草稿会在启动时立即失败并给出修复提示。
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.6 | Gemma 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_vulkan 与 cpu 对该架构没有投机路径,配置了草稿器时会打印警告。在 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 + DSpark | ggml_cuda 基线 | ggml_cuda + DSpark |
|---|---|---|---|---|
| Decode(生成 200 token) | 26.0 tok/s | 34.0 tok/s(1.31×) | 26.4 tok/s | 37.1 tok/s(1.41×) |
| Prefill(15K 提示词) | 962 tok/s | 955 tok/s | 952 tok/s | 954 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_DFlashInject 与 TSGgml_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 去噪 —— 整个答案是被迭代精炼,而非从左到右写出。
- CLI ——
--diffusion-steps(每块去噪步数)、--diffusion-seed与--diffusion-blocks。 - Web UI —— 流式发送整条消息的
replace事件,让你实时观看答案去噪,并通过DiffusionBatchScheduler在块边界批处理并发扩散请求。 - 优化 —— 在 GPU 后端上,
[prompt | canvas]的提示侧每块预取一次并跨步复用;GGML 后端使用融合的整模型扩散 decode 加上融合的 lm-head 尾部。
性能优化
这是跨架构的概要;docs/models/ 中每个模型卡都会以确切分派的 GGML 图逐一讲解相同内核。
已验证的 Gemma 4 E4B Q8_0 快路径:仓库基准已在 GPU 后端上验证原生 GGML 的 E4B Q8_0 家族与执行路径。推荐从公开的 ggml-org/gemma-4-E4B-it-GGUF 下载。
- 单图 GPU decode(Gemma 4 dense,包括 E4B)—— 所有 transformer 层在一次 GGML 图分派中完成,把每 token 的 CPU↔GPU 往返从数百次降到一次(相对逐 op 分派约 2.6×)。
- 整模型融合 decode 计算图(Gemma 4 dense + MoE、Qwen 3.5/3.6、GPT OSS)—— 一个 decode token 的全部计算(含 MoE 路由与专家、最终 norm 与 LM head)作为一次 GGML 计算图提交。在 CUDA/Vulkan 上该图只构建一次、张量地址保持稳定后反复重放,这正是 ggml-cuda 能把它捕获成 CUDA 图的前提。GPT OSS decode 在 A40 上从 24 → 154 tok/s,且随上下文长度基本持平(16K 时 133 tok/s,而逐层路径已跌到 2.3)。按模型的关闭开关:
TS_GPTOSS_MODEL_DECODE=0、TS_GEMMA4_FD_PERSIST=0、TS_QWEN35_FD_PERSIST=0。 - DeepSeek V4 整模型执行器 ——
deepseek4完全绕开通用的逐算子前向。每套执行器自行加载分片 GGUF,把权重按层切分到所有可见 GPU,在设备上持有全部 DSV4 KV 状态(原始 SWA 环、CSA/HCA 压缩 K 缓存、lightning indexer 缓存),并把每个 prefill/decode 微批作为一张计算图执行,配合按形状签名的缓存,使稳态 decode 直接重放已捕获的 CUDA 图。decode 注意力通过融合的 index-gather 取出紧凑的[ring | top-512]K,而不是扫描整个上下文。 - GLM 5.x 整模型执行器 ——
glm-dsa的做法相同,而且同一套架构描述符同时服务 GLM-5.2、GLM-5.3 与 GLM-5.3-Flash:原生执行器自行加载分片 GGUF(GLM-5.2 UD-IQ2_XXS 为 6 个分片,GLM-5.3 UD-Q2_K_XL 的 236.4 GiB 为 7 个分片),把权重按层切分到所有可见 GPU,在设备上持有 MLA 与 lightning indexer 的缓存,并通过按形状键控的 LRU 缓存把每个 prefill/decode 微批作为一张计算图提交,使稳态 decode 重放同一张已分配(在 CUDA 上还已捕获)的图。提示长度不到索引器 top-k 时会跳过稀疏选择直接走稠密注意力,因此短提示不为 DSA 付出任何代价;显存允许时TS_GLM_UBATCH=2048可以调大 prefill 微批(3× RTX PRO 6000 上 pp2048 从 918.9 提到 1145.8 tok/s,该数字出自 GLM-5.2 UD-IQ2_XXS;GLM-5.3 自己的实测数字来自另一台机器 —— 8× A40)。 - Nemotron-H 设备侧 decode 注意力 —— 在 GGML 后端上,注意力层直接用 flash-attention 内核对常驻 KV 缓存做 decode(
TS_NEMOTRON_FLASH_DECODE=0恢复主机路径),decode 速度不再随上下文长度衰减。 - 保留 E4B 语义的融合 prefill / verify —— 原生整模型路径会在 E4B prefill/verify 中携带逐层嵌入(PLE)与共享 KV donor 映射,而不会回退到数百次逐 op 提交。
- 调度器自动 N=1 快路径 —— 只有一个序列被调度时,服务器会自动使用 Gemma 4 的融合逐序列 forward,避免完全批处理路径的固定开销。
- 分块 prefill(Gemma 4)—— 长提示被切成有界块,避免滑动窗口层产生 O(n²) 的注意力分数张量。独占请求使用
TS_SCHED_SOLO_PREFILL_CHUNK(默认 8192);混合 prefill+decode 步使用TS_SCHED_PREFILL_CHUNK(默认 256),仅 prefill 时则公平用满设备 token 预算。 - 融合 Qwen 3.5/3.6 注意力与 FFN —— 单图融合注意力层 decode、融合 prefill 注意力、融合 out-proj + FFN,以及融合视觉编码器块(约 15 op → 2)。CUDA 与 Metal 的并发 decode 会共享一张槽位稳定的 token 批处理图,不再为每个请求分别扫描一次权重。
- 融合 QK-Norm + RoPE(Qwen 3.5/3.6,direct CUDA)——
cuda后端在纯文本 prefill 路径用单个 CUDA kernel 完成 QK-Norm 与 NeoX RoPE。默认开启;用TS_FUSED_QKNORM_ROPE=0禁用。 - 原生量化计算 —— Q4_K_M、Q6_K、Q8_0、IQ2_XXS、MXFP4 直接用于 matmul 而不展开为 FP32;批量
AddmmQuantBatch在一次分派中处理多个子权重 matmul。 - 批量 GPU MoE —— 所有被选专家(加上可选的共享专家与残差相加)在每个 MoE 层折叠为一次 GGML 图分派。
- KV 缓存前缀复用 —— 多轮对话复用最长匹配 token 前缀;滑动窗口模型按窗口大小回退截断。
- 内核预热 —— CLI 与服务器都在启动时运行一次很小的前向,以预编译 GPU 内核并预热内存池,避免冷启动延迟。
内存优化
- 零拷贝文件映射权重 —— GGUF 被内存映射,量化张量直接绑定进原生运算,去掉了大致使常驻内存翻倍的逐张量拷贝。例如
Qwen3.5-35B-A3B-IQ2_XXS(约 10 GB GGUF)在 Metal 下峰值约 7 GB,而非约 17 GB。 - 最佳匹配内存池,带有界保留(块上限 64 MB,池上限 32 块),让长时间运行中工作集保持紧凑。
- 带可选 SSD 溢出的分页 KV 块池 —— RAM 上限、LRU 淘汰,并跨会话做内容哈希前缀复用。
- KV 块编解码器 —— 通过
--paged-kv-quant-bits用TurboQuantKvCodec(Q2 / Q4 / Q8)做可选的原地压缩,以很小的精度代价换取每块占用减半 / 减至四分之一。 - 上下文按实际空闲的显存来定 —— 自报的上下文是上限而不是请求。GLM-5.2 的 GGUF 宣称 1,048,576 token,那是约 93 GiB 的 KV,所以权重落盘之后,加载器会去问每张卡还剩多少,按「缓存加一整个 prefill 计算图」能装下的大小定上下文,并打印它的选择:3 卡按层切分 342,272 token,
--n-cpu-moe 30646,400,--tp 391,136。设MAX_CONTEXT则把某个长度变成硬性要求——放得下就照办,放不下会带着数字直接报错。