基准测试
TensorSharp 在相同模型与硬件上与其他推理引擎的对比 —— 文本对比 llama.cpp,视频生成对比 stable-diffusion.cpp —— 以及守护正确性的测试框架。
同台对比:TensorSharp vs llama.cpp
一个纯 .NET 引擎与手工优化的 C++ llama.cpp 正面较量 —— 相同 GGUF、相同 NVIDIA RTX 3080 Laptop GPU(16 GB)、统一的 OpenAI /v1/chat/completions 接口,且两个引擎均分别在 GGML CUDA 与 GGML Vulkan 构建上测量,覆盖短提示、长上下文与多轮对话场景。下方数据来自当前签入的 benchmarks/engine_comparison harness 运行(docs/engine_comparison_report.md),可在自己的硬件上复现。
CUDA prefill 与 TTFT
Gemma 4 E4B 的 prefill 几何平均快 1.28×(短提示最高 1.39×);2-bit 量化的 Qwen 3.6 35B-A3B MoE 同样达到 1.28× prefill / 1.27× TTFT。
多轮 prefill 全面获胜
在 CUDA 上,后续轮次的 prefill 在每个模型上都更快 —— 1.27×(E4B)、1.39×(12B)、1.49×(35B-A3B)、1.11×(27B)—— 首 token 最高早 1.47× 到达。
CUDA decode:持平或更快
几何平均 E4B 1.02×、12B 1.04×、27B 1.07× —— 长上下文 decode 最高 1.12× —— 35B-A3B MoE 差距在 2% 以内(0.98×)。
Vulkan dense decode 1.21×
Gemma 4 12B 在厂商中立的 Vulkan 路径上 decode 快 1.21×(长上下文最高 1.32×),prefill / TTFT 不低于持平。
下表为 TensorSharp 相对 llama.cpp 在相同后端上的几何平均加速比(单流、贪心采样、关闭 MTP)。> 1.0× 表示 TensorSharp 更快(decode / prefill)或延迟更低(TTFT):
| 模型 | 后端 | decode | prefill | TTFT |
|---|---|---|---|---|
| Gemma 4 E4B it (Q8_0, dense 多模态) | CUDA | 1.02× | 1.28× | 1.27× |
| Gemma 4 E4B it (Q8_0, dense 多模态) | Vulkan | 1.00× | 1.05× | 1.03× |
| Gemma 4 12B it (UD-Q4_K_XL, dense) | CUDA | 1.04× | 1.17× | 1.16× |
| Gemma 4 12B it (UD-Q4_K_XL, dense) | Vulkan | 1.21× | 1.04× | 1.03× |
| Qwen 3.6 35B-A3B (UD-IQ2_XXS, MoE) | CUDA | 0.98× | 1.28× | 1.27× |
| Qwen 3.6 35B-A3B (UD-IQ2_XXS, MoE) | Vulkan | 0.87× | 1.04× | 1.03× |
| Qwen 3.6 27B (UD-IQ2_XXS, dense) | CUDA | 1.07× | 0.96× | 0.95× |
| Qwen 3.6 27B (UD-IQ2_XXS, dense) | Vulkan | 1.02× | 0.85× | 0.84× |
这些领先来自 TensorSharp 的架构专属快路径:融合的整模型 prefill/verify 图(配以融合的 FFN / attention 内核)、融合的整模型 decode,以及 vLLM 式带跨请求前缀共享的分页 KV 缓存。即便在极限量化下引擎也不落下风:在 2-bit IQ2_XXS 权重上,dense Qwen 3.6 27B 的 decode 快 1.07×(CUDA)/ 1.02×(Vulkan),35B-A3B MoE 的 prefill 快 1.28×——纯 .NET 引擎与手工优化的 C++ 并驾齐驱。剩余低于 1.0× 的项——主要是 Qwen 3.6 35B-A3B 在 Vulkan 上的 decode(0.87×)、Qwen 3.6 27B dense 的 prefill(CUDA 0.96× / Vulkan 0.85×)以及 E4B 在 Vulkan 上的长提示 prefill(0.71×)——仍是正在优化的目标,而非最终结果。
文本场景之外 —— harness 还支持工具调用、结构化输出(JSON)、图像 / 音频 / 视频、MTP 开/关与并发扩展等场景,以及对比 stable-diffusion.cpp 的 image_edit 场景;该场景较早的一次 CUDA 运行(Qwen-Image-Edit 2511 Q2_K DiT + Lightning 4-step LoRA,544×1184,4 步,相同输入与 seed)测得 TensorSharp 预热后的编辑耗时 40.44 s vs 48.16 s,约快 1.19×。这些 cell 可通过 benchmarks/engine_comparison 中记录的 harness 参数启用。
诚实的对比会同时展示胜势与差距。这些数字来自单台机器(RTX 3080 Laptop、单流);你的硬件、模型与量化都会改变它们。请在你自己的机器上运行内置基准与引擎对比框架,得到对你而言真正有意义的数字。
Apple Silicon:ggml_metal 对比 llama.cpp
ggml_metal 是 macOS 与 iOS 上的生产后端,因此同样要与 llama.cpp 正面对比。在 Apple M5 Pro(20 核 GPU、48 GB、macOS 26.6)上,两个引擎在空闲机器上以 caffeinate 前后交替跑同一个 GGUF——TensorSharp.Cli --backend ggml_metal --benchmark 对 llama-bench,各三次重复。
| 模型(未标注即 Q8_0) | pp2048 | pp8192 | tg128 | tg128@4096 |
|---|---|---|---|---|
| Qwen3.5-9B | 0.98× | 0.91× | 1.04× | 1.05× |
| gemma-4-E4B | 0.96× | 0.94× | 0.95× | 0.96× |
| gemma-4-E2B | 1.06× | 1.00× | 0.93× | 0.95× |
| Qwen3.6-35B-A3B IQ2_XXS | 1.10× | 0.98× | 1.10× | 1.08× |
| gpt-oss-20b | 1.13× | 1.11× | 0.93× | 0.93× |
比值为 TensorSharp 相对 llama.cpp,> 1.0× 表示 TensorSharp 更快。这轮测量在 TensorSharp 一侧找出四处计算图构建上的差距——残差 add 的写法让 ggml 的 norm+add 融合无法生效、融合 QKV 用拷贝而不是带步长的视图来切分、以及饱和后的滑动窗口被旋转读取并逐 token 重建——单个单元最高收益 +12.6%(gemma-4-E4B tg128@4096),且没有任何单元回退。文中还记录了两项没有收益的改动,以免重复尝试。完整方法、逐单元数据与过程中发现的一处正确性 bug:docs/perf/metal-vs-llama-cpp.md。
此处两个引擎带的 ggml checkout 并不相同(TensorSharp 的新约两周)。这个混淆因素对 TensorSharp 有利,因此该文档的结论只针对 TensorSharp 自身的计算图构建,而不是针对 ggml。
Muse-Glimmer 30B,带与不带 DFlash
这是在另一套硬件上单独手工执行的对比:单张 RTX PRO 6000 Blackwell(96 GB),Muse-Glimmer-30B-Q8_0,两侧都是贪心,生成 128 token,两次重复且两个引擎交替执行;关键在于两侧 prefill 的是同一串 token(把 TensorSharp 渲染后的聊天提示交给 llama-cli -no-cnv,再用 llama-tokenize 确认 token 数一致)。llama.cpp 为 master 8e7f22b,与 TensorSharp 内置的 ggml 相差一天以内。
| 提示 token 数 | prefill(llama.cpp → TS) | decode(llama.cpp → TS) | DFlash decode(llama.cpp → TS) |
|---|---|---|---|
| 60 | 362 → 459(1.27×) | 34.7 → 35.0(1.01×) | 45.5 → 50.9(1.12×) |
| 501 | 927 → 1135(1.23×) | 36.2 → 34.3(0.95×) | 117.5 → 164.6 |
| 2050 | 1132 → 1317(1.16×) | 35.0 → 33.5(0.96×) | 24.9 → 43.5 |
| 16126 | 1325 → 1249(0.94×) | 32.2 → 30.9(0.96×) | 80.2 → 55.8 |
| 123931 | 1166 → 1073(0.92×) | 30.7 → 26.6(0.86×) | 69.0 → 42.3 |
规律很清楚:TensorSharp 的融合整模型图在约 2K 以下的 prefill 上领先 1.16-1.27×,两个引擎在 2K 到 16K 之间交叉,再往上 llama.cpp 保持 6-8% 的 prefill 优势;decode 在短上下文时打平,到 128K 降到 0.86×。启用 DFlash 投机解码后,TensorSharp 在短与中等提示上领先,从 16K 起落后——但这个差距主要来自运行期的成本调控器:一次误判的探测会让它暂停起草;在保持启用的那些重复里,TensorSharp 在 16K 达到 llama.cpp 的 94%、64K 达到 96%。DFlash 的绝对数字还高度依赖语料(501 token 那一点在两个引擎上都是 100% 接受率)。两个引擎都能在单卡上跑满 128K。两行长上下文有一个说明:它们是在换行符归一化修复落地之前测的,因此在这两个点上 TensorSharp prefill 的是同一文档的 CRLF 形式(多 1.2% 的 token);吞吐是速率,影响很小,但那里两侧生成的续写并不严格可比。方法、逐次重复的离散度、显存与调控器分析见 Muse-Glimmer 架构卡片(docs/models/muse-glimmer_zh-cn.md)。
GLM 5.x:规模的另一端
第二组手工对比,这次跑的是单卡装不下的模型:3× RTX PRO 6000 Blackwell(各 97 GiB,PCIe,无 NVLink),GLM-5.2-UD-IQ2_XXS(744B-A40B MoE,MLA + DeepSeek 稀疏注意力,218 GiB 权重按层切分到三张卡上),两个引擎在同一次会话里背靠背测量——llama.cpp 用 llama-bench,TensorSharp 用对拍工具的 --bench,同样取两次重复中的较好值。这些数字的重复间波动约 4%。
| 测试项 | llama.cpp | TensorSharp(默认 n_ubatch 1024) | TensorSharp(TS_GLM_UBATCH=2048) |
|---|---|---|---|
| pp128 | 276.5 | 254.8 | 264.4 |
| pp512 | 695.4 | 666.9 | 659.6 |
| pp2048 | 763.1 | 918.9 | 1145.8 |
| pp4096 | 715.8 | 864.7 | 1048.7 |
| tg64 | 42.2 | 43.7 | 43.9 |
交叉点大约落在一千个提示 token 上,原因就是微批:256 个专家 top-8 的情况下,一个 512 token 的分块平均只给每个专家路由约 16 行,于是每个专家 GEMM 的 tile 大部分都是填充,把分块调大比图上任何其他改动都更管用。再往下,一整次 prefill 就是一张小图,每次调用的固定开销——托管层的一跳、输入上传、154,880 宽的 logits 回拷——占比明显,llama.cpp 那几个百分点就来自这里。Decode 受访存约束,两边都是各领先几个百分点。除了计时也校验输出:在同一后端上,TensorSharp 与 llama.cpp 逐 token 一致——ggml_cuda 上 6/6 条录制提示,ggml_cpu 上 3/3,100% 托管的 cpu 后端上 1/1。
有两件事在这台机器上不会让它变快。--tp 3 把 pp2048 / tg64 从 915.9 / 43.9 拉到 505.6 / 17.6——78 层里每层都要两次 [6144, n_tokens] 隐状态的 all-reduce,没有 NVLink 的 PCIe 付不起这笔账。--n-cpu-moe 30 则拉到 94.7 / 16.4。它们存在的意义是让原本装不下的权重能跑起来:卸载把加载器能定下的上下文从 342,272 抬到 646,400 token。参见 GLM 5.x。
GLM-5.3 在 8× A40 上:decode 打平,加载快 2.9×
GLM-5.3(非 Flash)与 GLM-5.2 是同一套 glm-dsa 块结构 —— 79 个块(78 层主干加一个 NextN)、256 个路由专家 top-8 外加一个共享专家、带 lightning indexer 的 MLA、rope base 8e6 —— 所以它就走上面这条路径加载,不需要新代码,也不需要新开关;又因为 llama.cpp 注册了 glm-dsa,这个模型确实存在可比的参考列(GLM-5.3-Flash 则不行:它的 glm5next 需要尚未合入主线的 llama.cpp 构建 2e0e57f / PR #27754 才能对比)。测量环境是 8× A40 46 GB(无 NVLink,CUDA 12.8)、GLM-5.3 UD-Q2_K_XL、10,531 token 的提示与 300 个 decode token,三次重复取中位数、每次换一份新的提示正文,两个引擎都按整层切分放置:
| 指标 | llama.cpp | TensorSharp |
|---|---|---|
| Prefill(10,531 个提示 token) | 未记录 | 251.6 tok/s |
| Decode(300 token) | 20.28 tok/s | 20.48 tok/s |
| 首 token 时延 | 29.0 s | 41.9 s |
| 加载(236.4 GiB,7 个分片) | 753 s | 264 s |
Decode 在 1% 以内打平,而 TensorSharp 加载这份 236.4 GiB 的权重快 2.9×。诚实的差距在首 token 时延:41.9 s 对 29.0 s,约慢 1.4×,这也是那轮对比中最值得动手的一处。llama.cpp 的 prefill 单元是故意留空的:那次运行早于客户端在流里索取 usage,因此它没有自己的提示 token 计数,也就没有可记录的 tok/s 数字。两个引擎跑的都是整层切分放置(TensorSharp 用 --tp 1 选它);--tp 8 在这个模型上意味着真正的张量并行、每个 rank 都跑每一层,需要每个 rank 41.7 GiB,装不进 46 GB 的卡 —— 它是一个被接受的模式,加载时会把这笔算术连同办法一起打印出来,但并不是一份已实测的配置。方法、模型来源与该矩阵的其余部分:docs/validation/cross-engine-2026-09/README.md。
MiniMax-H3:视频与音频一次生成
MiniMax-H3 在同一个打包潜变量里对画面和原生 32 kHz 立体声音轨一起去噪,因此下面每个数字都是同时产出两者的一次完整运行。同一条 22 帧、8 步的请求在两台机器上对 stable-diffusion.cpp 做过测量,而两者的结论并不一致。两组数据都列在下面,各自标明硬件;它们互不取代,把两者平均出来的数字不对应任何一台真实存在的机器。
Apple M5 Pro、ggml_metal
22 帧、8 步、同一随机种子,对手是处在其最佳配置下的 stable-diffusion.cpp:
| 分辨率 | stable-diffusion.cpp | TensorSharp | 提速 |
|---|---|---|---|
| 256×256 | 49.3 s | 20.9 s | 2.4× |
| 640×384 | 108.5 s | 63.1 s | 1.7× |
应当以第二行为准:人脸需要像素,所以 640×384 才是起点,256×256 只是让倍数好看的那个角落,而不是真正拿来出片的尺寸。两行都是同一条 22 帧、8 步的请求;由于权重是 CFG 蒸馏的,这 8 步就是 8 次 DiT 前向,而不是 16 次。→ MiniMax-H3 下载与模式
RTX 3080 Laptop 16 GB、ggml_cuda
同一条请求,换到一台刻意被内存卡死的机器上 —— 16,384 MiB 显存、31.7 GB 内存,面对 33.5 GB 的模型集合(DiT 10.64 GiB、Qwen3-VL-32B 16.97 GiB、视频 VAE 4.85 GiB、音频 VAE 0.56 GiB,两个引擎用的是同一批权重文件)。22 帧、8 步、同一随机种子、三次取最好,发布构建;对手是 stable-diffusion.cpp 97d2990(ggml 8e800ce),按 SD_CUDA=ON、arch 86 重新编译。在这台机器上,整体耗时是 stable-diffusion.cpp 更快:
| 分辨率 | stable-diffusion.cpp | TensorSharp | 端到端 |
|---|---|---|---|
| 256×256 | 37.8 s | 43.6 s | sd.cpp 快 1.15× |
| 640×384 | 59.8 s | 63.7 s | sd.cpp 快 1.07× |
这块 16,384 MiB 显卡上的显存峰值:TensorSharp 15,780 MiB,stable-diffusion.cpp 12,035 MiB。sd.cpp 用的是 --auto-fit --stream-layers --diffusion-fa --rng cpu;它默认的 --offload-to-cpu 路径在这台机器上根本跑不起这个模型 —— 它要把 17.7 GB 锁进只有 12.3 GB 空闲的内存里。
按单个去噪步算,TensorSharp 在 CUDA 上同样领先 —— 用 8 步与 16 步的斜率把计算和一次性开销拆开,得到 3.325 s 对 3.338 s,直接逐步测量为 3.00–3.11 s。它让出去的完全是固定的启动开销,而剩下 3.9 s 的差距里大约 3 s 根本不是推理:一是 H.264 编码 —— sd.cpp 这边只是把 MJPEG + PCM 写进 AVI;二是 .NET 进程启动,对手是原生可执行文件。
两个硬件点给出两种答案,两者都保留。在 M5 Pro 上,TensorSharp 整体快 1.7–2.4×;在这台 16 GB 的 CUDA 笔记本上,它按步更快,端到端慢 1.07–1.15× —— 因为权重和页缓存都装不下,这么短的一次运行里一次性开销占了大头。如果你的显卡是 16 GB,请按 CUDA 这两行来预期。每步的领先只有 13 ms,所以运行拉长也只能非常缓慢地改变这个比例。
上一轮优化改变了什么
同一台 RTX 3080 Laptop、同样的 22 帧 8 步负载,优化前后:
| 分辨率 | 之前 | 之后 | 提速 |
|---|---|---|---|
| 640×384 | 89.0 s | 63.7 s | 1.40× |
| 256×256 | 67.2 s | 43.6 s | 1.54× |
两处改动都是冲着同一个 16 GB 天花板去的。其一,去噪器跑完之后会先交还设备驻留,再去加载视频 VAE,解码期间的显存峰值因此从 16,041 MiB 降到约 5,600 MiB,在 640×384 上值 22 秒 —— 跑完的去噪器加上视频 VAE 一共 15.8 GB,一块 16 GB 的卡装不下,而 WDDM 并不会让这次分配失败,它会悄悄用主机内存兜住溢出的部分,于是整个解码都按 PCIe 的速度在跑。其二,去噪器权重文件会被预读,并且与它自己的上传流水线重叠,把第一个去噪步从 14.87 s 降到约 10.2 s:权重是以指针形式绑定在 mmap 的 GGUF 上的,没有预读时,第一次主机到设备的拷贝要在拷贝过程中把每一页都从磁盘缺页调入,速率只有 0.91 GB/s,而从已驻留的可分页内存拷贝是 5.97 GB/s。开与不开预读,输出逐字节一致。
该动哪个杠杆,以及先后顺序
先动步数。默认 20 步,4–8 步是快速工作点 —— 4–8 步时运动主体周围会出现一些色边,到约 20 步就消失了。除此之外没有引导分支可以缓存或跳过:这个权重出厂就是 CFG 蒸馏的,必须 --cfg 1.0,更高的值会被拒绝,这也让 --negative-prompt 与 --cfg-cache-stride 在这里完全失效 —— 根本没有无条件分支供它们作用。其次是分辨率:--width / --height 向上取整到 32 的倍数,默认 640×384。再次是帧数:--video-frames 向上对齐到 17k+5 网格(5、22、39、56、73、90、107……),fps 固定为 24。最后才是参考的数量 —— 超过大约四个之后,它的分量就完全盖过步数。
一个参考要花多少
Ref2VA 权重最多接受九个参考 —— 静图、片段、独立音轨 —— 在去噪器这一侧它们既便宜又完全可预估。在 RTX 3080 Laptop 上,640×384 × 22 帧,单个去噪步从没有参考时的 4.37 s 变成八个参考时的 9.38 s:大约每个参考每步 626 ms,从一个到八个基本恒定。但时间并不花在去噪器上。
| Ref2VA 参考数 | 进入 Qwen3-VL 的提示 token | 文本条件 | 去噪,秒 / 步 |
|---|---|---|---|
| 无 | — | — | 4.37 s |
| 两个 | 548 | ≈65 s | — |
| 八个 | 2,086 | ≈447 s | 9.38 s |
每个参考会给提示词加上约 250 个视觉占位 token,而整条提示词要过完 Qwen3-VL-32B 文本编码器的全部 50 层。超过大约四个参考之后,真正占住整次运行的就是这一趟,而不是去噪器:八个参考时,整个 8 步去噪只要约 75 s,文本条件却要约 447 s。
与其减少步数,不如先减少参考数量、提高参考质量。在八个参考的情况下,把步数一路砍到零省下的时间,都不如把参考减到两个。
与参考实现的数值一致性
四个网络分别在相同输入上核对:文本编码器 cos 0.999999,单个 DiT 步 cos 0.998,视频 VAE 编码与解码 cos 1.000000,音频 VAE 解码 cos 0.999995。
这里的 MiniMax-H3 数据与下面的 Wan 数据,各自都是对 stable-diffusion.cpp 测得的,模型不同、负载不同、硬件也不同。它们不是 MiniMax-H3 与 Wan 之间的同台对比 —— 本仓库并没有这样的对比数据。
Wan 视频(仅视频):两个彼此独立的提速杠杆
视频是唯一一种选错会浪费数小时而非数秒的工作负载,因此值得把两个杠杆分开来看。下表三行是同一个请求 —— Apple M5 Pro(20 核 GPU、48 GB 统一内存)、ggml_metal、Wan2.2-TI2V-5B Q8_0,图生视频,1088×832 × 121 帧 = 27,404 个 DiT token —— 各行之间只有引擎版本与 --model 指向哪个权重的区别。
| 配置 | DiT 前向次数 | 秒 / 次 | 去噪 | VAE 解码 | 端到端 |
|---|---|---|---|---|---|
| 基础权重,本轮优化之前 | 100 | 206.2 s | 20,615 s | 863 s | ≈5 h 58 m |
| 基础权重,当前引擎 | 100 | 120.2 s | 12,020 s | 563 s | ≈3 h 30 m |
| 步数蒸馏(Turbo,4 步)权重 | 4 | 120.2 s | 481 s | 563 s | 17 m 30 s |
总收益可以拆成两个相乘的因子:引擎侧带来的每次前向约 1.7×(F16 注意力 K/V 加上 VAE 卷积路径),以及步数蒸馏带来的前向次数少 25×。Wan2.2-TI2V-5B 的官方配方是 50 步 × 2 次无分类器引导(CFG)分支 = 100 次 DiT 前向;步数蒸馏权重在训练时就不使用引导,只需 4 次。这不是一个开关,而是换一个 --model 文件。TensorSharp 会从 DiT 文件名识别它(turbo、distill、lightning、lightx2v、fastwan、-dmd,或显式的 …-4steps-…),加载时打印 step-distilled checkpoint detected -> 4 steps, guidance off 并自动关闭引导;--diffusion-steps / --cfg 仍可覆盖。→ Wan 下载
一旦换成蒸馏权重,VAE 解码就成了瓶颈 —— 1,050 s 的总耗时里占 563 s,约 55%。再往下优化 DiT 的收益,远不如减少帧数或帧面积。
分辨率与帧数对耗时的影响
同一台 M5 Pro、同一份 ggml_metal 构建、同一个 Turbo 权重与同一张源图。DiT 自注意力的开销是 O(token²),而 token 数为 latent_frames × (h/2) × (w/2),因此帧面积与帧数的影响远超其他因素:
| 请求 | DiT token 数 | 去噪 | VAE 解码 | 总计 |
|---|---|---|---|---|
| 736×544 × 81 帧(3.4 s,480p 档) | 8,211 | 84 s | 159 s | 4 m 09 s |
| 736×544 × 121 帧(5 s,480p 档) | 12,121 | 137 s | 237 s | 6 m 19 s |
| 1088×832 × 121 帧(5 s,720p 档) | 27,404 | 481 s | 563 s | 17 m 30 s |
480p(约 0.4 MP)是 Wan 训练过的分辨率,因此 736×544 这两行是分布内的正常输出,而不是降级模式 —— 当你只想等几分钟时,就应该选它。低于约 0.3 MP 后画质确实会下滑,宽 × 高小于 300,000 px 时管线会给出告警。
每次前向的 1.7× 来自哪里
| 改动 | 测量对象 | 前 → 后 | 数值一致性 |
|---|---|---|---|
| DiT flash 路径中的 F16 注意力 K/V | 单次自注意力,seq 27,404 / 24 头 / head dim 128,M5 Pro | 4,993 → 2,467 ms(2.02×) | 对 diffusers 参考实现,两种 dtype 的余弦相似度均为 0.999964 |
| Wan VAE 卷积走 MPSGraph(Metal) | VAE 解码,736×544 × 81 帧 | 159 → 80 s(1.99×) | 93.9 dB PSNR,最大 Δ 为 255 中的 0.128 |
Wan VAE 解码的性能剖析显示:MUL_MAT 占整图 44.4%,IM2COL 再占 30.2% —— 合计 74.6% 都在卷积里,这正是把这些卷积交给 MPSGraph 会有回报的原因:按形状分别达到 6.2×(512→512 k3,320×240 t9)、8.6×(256→256 k3,640×480 t9)、10.4×(160→160 k3)与 13.9×(512→512 k1),因为 MPS 能跑到约 30 TFLOP/s,而 ggml 的 Metal GEMM 只有约 4.9。两处都留了 A/B 用的开关:TS_WAN_DIT_KV_F16=0 与 TS_WAN_VAE_MPS_CONV=0。实测 Q8_0 的注意力 K/V 反而比 F16 慢(2,652 ms),把 DiT 从 Q8_0 反量化到 F16 也只让矩阵乘变化不到 5%,因此更高精度的 Wan 量化基本换不来速度。
Wan 的后端选择与 sd.cpp 对比
RTX 2000 Ada(16 GB)上的参考耗时,Wan2.1-1.3B F16、832×480、33 帧、30 步 UniPC —— 即官方 480p 配方:
| 后端 | 秒 / 步 | 总计 |
|---|---|---|
ggml_cuda | 12.0 s | 445 s |
ggml_vulkan | 17.2 s | 625 s |
cuda(直连驱动 API) | 19.3 s | 700 s |
在 NVIDIA 上跑 Wan 应当选 ggml_cuda;cpu / ggml_cpu 能正确出片,但一段 480p 的数秒视频要几十分钟,而 mlx 根本不是 Wan 支持的后端。与 stable-diffusion.cpp(master-769,相同 GGUF 与设置,33 帧 480p)相比,采样阶段接近持平 —— 281 s vs 258 s —— 但 sd.cpp 的 VAE 解码会实体化约 8 GB 的 3D im2col,把 16 GB 显卡压进 WDDM 换页:51 s vs 1,762 s,因此该次运行中 TensorSharp 端到端快 6.0×。
测试
下方构建与测试命令需要按平台安装的 .NET 10 SDK;运行命令前请先验证安装。
单元测试(xUnit)
InferenceWeb.Tests 测试无需运行服务器的进程内行为:托管量化运算、直接 CUDA 与 MLX 后端内核(在硬件可用时)、分页 KV 调度、批量执行器正确性、各模型批量前向与 legacy 路径的一致性、MTP / NextN 推测解码正确性、纯 C# DeepSeek V4.1 执行器对独立 PyTorch 参考实现的数值门控、DiffusionGemma 探针、编解码器往返、提示词渲染,以及服务器 CLI 选项构建器。
其中 V4.1 那一项值得展开说。InferenceWeb.Tests.Dsv41CpuExecutorTests 把 DeepSeek4CpuExecutor(--backend cpu 跑 DeepSeek V4.1 Flash 所用的 100% 纯 C# 执行器,不依赖 ggml、不依赖原生库、不依赖 GPU)对齐到独立的 PyTorch 参考实现 eng/dsv41-reference.py,容差 atol=rtol=2e-5,并要求贪心 argmax 完全一致,覆盖一次性 prefill、分块 prefill(块大小 1/3/5/8,逐位置检查)与 reset。有两条注意事项必须一起交代:这些测试在未设置 TS_DSV41_FIXTURE_DIR 时会静默返回;而 fixture 是刻意做小的 —— 五层、256 隐藏维、16 token 的 F32 合成模型 —— 因此它证明的是与参考实现的架构级一致,而不是真实 246 GiB Q2_K 权重上的 CPU/CUDA 一致性。它是正确性与可移植性路径,而非服务路径,这也是它只出现在这里、不出现在上面任何表格里的原因:--backend cpu 上跑完整 V4.1 权重的吞吐、加载时间与常驻内存都从未测量过。
dotnet test InferenceWeb.Tests/InferenceWeb.Tests.csproj
服务器集成测试
TensorSharp.Server/testdata/ 中的集成测试覆盖全部三种 API 风格(Web UI SSE、Ollama、OpenAI)、多轮对话、思考模式、工具调用、结构化输出、队列状态兼容、并发请求与中止支持。架构相关功能会自动检测,并在当前模型不支持时跳过。
# 先启动 TensorSharp.Server,然后运行:
python3 TensorSharp.Server/testdata/test_multiturn.py
# 或
bash TensorSharp.Server/testdata/test_multiturn.sh
推理矩阵运行器
TensorSharp.TestMatrix 是更广泛的、由 CLI 驱动的长时段 模型 × 后端 × 功能 × 环境变量 覆盖框架。它发现 GGUF 文件、过滤不可用后端与不支持的提示类型、运行基线加环境变量扫描的各个 cell、为每个 cell 写一份 JSON 结果、生成聚合 Markdown 报告,并与各主机基线对比。
dotnet build TensorSharp.TestMatrix/TensorSharp.TestMatrix.csproj -c Release
dotnet run --project TensorSharp.TestMatrix -c Release -- --dry-run
基准数字高度依赖硬件、模型、量化与 KV 缓存 dtype。请把上面的数字当作单台机器上可复现的参考点,而非普适保证 —— 在你自己的硬件上运行内置基准,得到对你真正有意义的数字。