从 C# 使用 TensorSharp
TensorSharp 是一个真正的 .NET 库,而不只是可执行文件。从源码 checkout 引用其项目,即可在不经过 HTTP 的情况下从自己的代码驱动推理。
项目与包边界
仓库按可发布包边界拆分。下方名称是项目/包 ID,但当前 Runtime/Models/Backends/CLI/Server 包尚未发布到 NuGet.org。
| 包 | 命名空间 | 职责 |
|---|---|---|
TensorSharp.Core | TensorSharp | 张量原语、运算、分配器、存储、设备抽象。 |
TensorSharp.Runtime | TensorSharp.Runtime | GGUF 解析、分词器、提示词渲染、采样、分页 KV 缓存、连续批处理调度器。 |
TensorSharp.Models | TensorSharp.Models | ModelBase、架构实现、多模态编码器、批量/分页前向。 |
TensorSharp.Backends.GGML | TensorSharp.GGML | GGML 支撑的执行与原生互操作。 |
TensorSharp.Backends.Cuda | TensorSharp.Cuda | 直接 CUDA 分配器、存储、cuBLAS GEMM、PTX 内核、量化 CUDA 运算。 |
TensorSharp.Backends.MLX | TensorSharp.MLX | Apple Silicon MLX 后端(mlx-c / Metal)。 |
TensorSharp.Server | TensorSharp.Server | ASP.NET Core 服务器、OpenAI/Ollama 适配器、推理引擎宿主、Web UI。 |
TensorSharp.Cli | TensorSharp.Cli | 控制台宿主与调试 / 批处理工具。 |
典型嵌入场景只需引用 TensorSharp.Models.csproj;它已引用 Core、Runtime 与各后端项目。若你的应用与 TensorSharp checkout 同级:
dotnet add reference ../TensorSharp/TensorSharp.Models/TensorSharp.Models.csproj
目前不要运行 dotnet add package TensorSharp.Models:NuGet.org 上没有该 ID 的当前包。源码构建默认编译原生 GGML/MLX;开发托管 CPU 路径时传入 -p:TensorSharpSkipGgmlNative=true -p:TensorSharpSkipMlxNative=true,或按后端页面构建原生库。
最小示例 —— 加载、生成、解码
每个模型都以相同方式加载:ModelBase.Create() 读取 GGUF 元数据并实例化正确的架构。从这里开始你做分词、运行前向、采样并解码。
using System;
using System.Collections.Generic;
using System.Linq;
using TensorSharp.Models;
using TensorSharp.Runtime;
// 1. 加载任意受支持的 GGUF —— 架构从元数据自动识别。
// 按你的构建选择 BackendType:GgmlCuda、GgmlMetal、GgmlVulkan 或 GgmlCpu。
using var model = ModelBase.Create("gemma-4-E4B-it-Q8_0.gguf", BackendType.GgmlCuda);
// 2. 配置采样(默认值与 Ollama 一致:temp 0.8、top_k 40、top_p 0.9)。
var sampling = new SamplingConfig { Temperature = 0.7f, TopP = 0.9f, TopK = 40 };
// 3. 对提示词分词。
var tokens = model.Tokenizer
.Encode("Explain mixture-of-experts in one sentence.", addSpecial: true)
.ToList();
var generated = new List<int>();
// 4. 完整提示词只 prefill 一次。
float[] logits = model.Forward(tokens.ToArray());
// 5. 每次只 decode 一个新 token。Forward() 自己维护 KV-cache 位置,
// prefill 后只传刚采样出的 token,不要重复传完整历史。
for (int step = 0; step < 200; step++)
{
int next = model.Sample(logits, sampling, generated); // 应用惩罚 + 采样
if (model.Tokenizer.IsEos(next)) break;
generated.Add(next);
logits = model.Forward(new[] { next });
}
// 6. 反分词得到结果。
Console.WriteLine(model.Tokenizer.Decode(generated));
要做贪心/确定性解码,请调用 model.SampleGreedy(logits) 而非 Sample。
冒烟测试变体
一次性健全性检查:加载模型、运行一次前向,并打印 top token:
using var model = ModelBase.Create(modelPath, backend);
var tokenIds = model.Tokenizer.Encode("Hello", addSpecial: true);
float[] logits = model.Forward(tokenIds.ToArray());
int topToken = model.SampleGreedy(logits);
Console.WriteLine($"vocab={model.Config.VocabSize}, tokens={tokenIds.Count}, topToken={topToken}");
DiffusionGemma — 文本扩散
DiffusionGemma 是块文本扩散模型,而非自回归模型。ModelBase.Create() 仍可加载它(GGUF 架构为 diffusion-gemma / diffusion_gemma)并返回 DiffusionGemmaModel,但其 Forward(int[]) 会故意抛出异常——生成需通过 DiffusionGemmaSampler,它在 [prompt | canvas] 序列上对定长的 canvas 块迭代去噪。架构详见 DiffusionGemma 模型卡。
using TensorSharp.Models;
using TensorSharp.Models.DiffusionGemma;
using TensorSharp.Runtime;
// 加载 diffusion-gemma GGUF——架构会自动检测。
using var model = (DiffusionGemmaModel)ModelBase.Create("diffusion-gemma.gguf", BackendType.GgmlCuda);
// 用模型的聊天模板渲染 prompt,然后分词。
var messages = new List<ChatMessage> { new() { Role = "user", Content = "Write a haiku about winter." } };
string rendered = PromptRenderer.Render(model.Config.ChatTemplate, messages,
addGenerationPrompt: true, architecture: model.Config.Architecture);
int[] promptTokens = model.Tokenizer.Encode(rendered, addSpecial: true).ToArray();
// 配置 EntropyBound 去噪采样器。
var p = new DiffusionEbParams
{
MaxDenoisingSteps = 48, // 每个 canvas 块的精化步数
Seed = 0, // 确定性
MaxBlocks = 1, // 块自回归的 canvas 块数
};
var sampler = new DiffusionGemmaSampler(model);
// 生成。可选回调在每个去噪步后触发,参数为
// (blockIndex, step, totalSteps, previewTokens)——适合做实时 UI。
List<int> generated = sampler.Generate(promptTokens, p,
(block, step, total, preview) => Console.Write($"\rblock {block + 1} step {step + 1}/{total} "));
Console.WriteLine();
Console.WriteLine(model.Tokenizer.Decode(generated));
DiffusionEbParams 的关键参数:MaxDenoisingSteps(48)、TMin/TMax 温度调度(0.4 / 0.8)、EntropyBound(0.1)、StabilityThreshold / ConfidenceThreshold 早停、Seed 与 MaxBlocks。model.CanvasLength 给出每块的 canvas 大小。
在 GPU 后端上,prompt 的 K/V 每块缓存一次并在各去噪步间复用;GGML 后端默认使用融合的整模型解码加融合 lm-head 尾。调优开关(DIFFUSION_STEPS、DIFFUSION_NO_SC 等)见高级页。
Qwen-Image-Edit — 图像编辑
Qwen-Image-Edit 接收提示词 + 一张输入图像并返回编辑后的图像。所加载的 qwen_image GGUF 仅是 MMDiT 扩散 Transformer;模型还会拉入两个伴随 GGUF(在 DiT 文件同目录解析,或通过 TS_QWEN_IMAGE_VAE / TS_QWEN_IMAGE_TE / TS_QWEN_IMAGE_MMPROJ 环境变量指定):Qwen-Image VAE 与 Qwen2.5-VL-7B 文本编码器。与 DiffusionGemma 一样,它不是自回归文本模型——自回归入口会抛异常,编辑通过 EditImage() 驱动。
using TensorSharp.Models;
using TensorSharp.Models.QwenImage;
using TensorSharp.Runtime;
// 加载 MMDiT GGUF(架构 = qwen_image)。VAE + Qwen2.5-VL
// 伴随文件从同一目录解析(或用 TS_QWEN_IMAGE_* 环境变量)。
using var model = (QwenImageModel)ModelBase.Create("qwen-image-edit-DiT-Q4_K_M.gguf", BackendType.GgmlCuda);
// 加载输入图像(PNG/JPEG 解码为 RgbImage)。
RgbImage input = ImageIO.Load("input.png");
var p = new QwenImageParams
{
Steps = 30, // FlowMatch-Euler 去噪步数
CfgScale = 4.0f, // true-CFG 引导;<= 1 关闭负向分支
NegativePrompt = " ", // 仅当 CfgScale > 1 时使用
Seed = 0,
TargetArea = 1024 * 1024, // ~1 MP;纵横比随输入(尺寸对齐到 /16)
// Width = 0, Height = 0, // 指定显式输出尺寸可绕过 VRAM 钳制
};
RgbImage output = model.EditImage("Make the sky a dramatic sunset.", input, p);
ImageIO.SavePng("edited.png", output);
Console.WriteLine($"Saved {output.Width}x{output.Height} edited image.");
若要做实时 UI,设置 p.OnStep = (step, total, preview) => { … } 与 p.PreviewCount,即可在节流的步上收到部分去噪潜变量解码出的 RGB 预览。RgbImage 暴露 Width、Height 与平面/交错的 Pixels 缓冲;ImageIO 提供 Load、Decode(byte[])、EncodePng、SavePng 及缩放辅助。
图像编辑计算量大:每个去噪步都要跑完整的 60 块 MMDiT(开启 CFG 时跑两遍)。请使用 CUDA 或 Metal 的 GGML 后端做实用的全质量编辑;除非你固定 Width/Height,否则流水线会按设备 VRAM 预算自动钳制目标面积。伴随 GGUF 布局见模型卡。
SamplingConfig
采样旋钮与 CLI 参数及 API 选项一一对应。默认值与 Ollama 一致。
| 属性 | 类型 | 默认 | 含义 |
|---|---|---|---|
Temperature | float | 0.8 | 随机性;0 = 贪心/确定性。 |
TopK | int | 40 | 限制为概率最高的 K 个 token;0 = 禁用。 |
TopP | float | 0.9 | 核采样;1.0 = 禁用。 |
MinP | float | 0 | 相对最大值的最小概率阈值。 |
RepetitionPenalty | float | 1.1 | 乘性惩罚;>1 抑制重复。 |
PresencePenalty | float | 0 | 对已出现 token 的加性惩罚。 |
FrequencyPenalty | float | 0 | 与 token 频率成比例的加性惩罚。 |
Seed | int | -1 | 可复现采样;-1 = 基于时间。 |
StopSequences | List<string> | null | 产生其中任一字符串时停止。 |
MaxTokens | int | 0 | 最大生成 token 数;0 = 使用调用方默认。 |
关键类型与接口
| 类型 | 角色 |
|---|---|
ModelBase | 每个架构的抽象基类。Create(path, backend)、Forward(int[])、Sample(...)、SampleGreedy(...),外加 Config 与 Tokenizer。 |
BackendType | 枚举:Cpu、GgmlCpu、GgmlMetal、GgmlCuda、GgmlVulkan、Cuda、Mlx。 |
SamplingConfig | 采样配置(见上表)。 |
ITokenizer | Encode(text, addSpecial)、Decode(ids)、IsEos(id)、EosTokenIds(BPE 与 SentencePiece 实现)。 |
ModelConfig | 架构元数据:VocabSize、上下文长度等。 |
IBatchedPagedModel | 可选的批量/分页前向(ForwardBatch),多数架构为连续批处理而实现。 |
DiffusionGemmaModel + DiffusionGemmaSampler | 文本扩散模型及其 EntropyBound 去噪采样器(Generate(promptTokens, DiffusionEbParams, …))。Forward() 不受支持。 |
QwenImageModel + QwenImageParams | Qwen-Image-Edit 图像编辑器。EditImage(prompt, RgbImage, QwenImageParams) 返回修改后的 RgbImage;ImageIO 负责 PNG 加载/保存。 |
InferenceEngine | 支撑服务器连续批处理的 worker 线程调度器 + 分页块池(在 TensorSharp.Runtime.Scheduling 中)。 |
其他值得了解的运行时契约:IModelArchitecture、IPromptRenderer、IOutputProtocolParser、IMultimodalInjector、IKvBlockCodec(带内置 TurboQuantKvCodec)以及 IKVCachePolicy。
对多数应用而言,最简单的集成是运行 TensorSharp.Server 并通过兼容 OpenAI 的 API 调用它 —— 你的应用进程保持干净,并免费获得连续批处理。当你需要进程内控制或自定义解码时,再使用库 API。