从 C# 使用 TensorSharp

TensorSharp 是一个真正的 .NET 库,而不只是可执行文件。从源码 checkout 引用其项目,即可在不经过 HTTP 的情况下从自己的代码驱动推理。

项目与包边界

仓库按可发布包边界拆分。下方名称是项目/包 ID,但当前 Runtime/Models/Backends/CLI/Server 包尚未发布到 NuGet.org

命名空间职责
TensorSharp.CoreTensorSharp张量原语、运算、分配器、存储、设备抽象。
TensorSharp.RuntimeTensorSharp.RuntimeGGUF 解析、分词器、提示词渲染、采样、分页 KV 缓存、连续批处理调度器。
TensorSharp.ModelsTensorSharp.ModelsModelBase、架构实现、多模态编码器、批量/分页前向。
TensorSharp.Backends.GGMLTensorSharp.GGMLGGML 支撑的执行与原生互操作。
TensorSharp.Backends.CudaTensorSharp.Cuda直接 CUDA 分配器、存储、cuBLAS GEMM、PTX 内核、量化 CUDA 运算。
TensorSharp.Backends.MLXTensorSharp.MLXApple Silicon MLX 后端(mlx-c / Metal)。
TensorSharp.ServerTensorSharp.ServerASP.NET Core 服务器、OpenAI/Ollama 适配器、推理引擎宿主、Web UI。
TensorSharp.CliTensorSharp.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 早停、SeedMaxBlocksmodel.CanvasLength 给出每块的 canvas 大小。

🌫️

在 GPU 后端上,prompt 的 K/V 每块缓存一次并在各去噪步间复用;GGML 后端默认使用融合的整模型解码加融合 lm-head 尾。调优开关(DIFFUSION_STEPSDIFFUSION_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 暴露 WidthHeight 与平面/交错的 Pixels 缓冲;ImageIO 提供 LoadDecode(byte[])EncodePngSavePng 及缩放辅助。

🖼️

图像编辑计算量大:每个去噪步都要跑完整的 60 块 MMDiT(开启 CFG 时跑两遍)。请使用 CUDA 或 Metal 的 GGML 后端做实用的全质量编辑;除非你固定 Width/Height,否则流水线会按设备 VRAM 预算自动钳制目标面积。伴随 GGUF 布局见模型卡

SamplingConfig

采样旋钮与 CLI 参数及 API 选项一一对应。默认值与 Ollama 一致。

属性类型默认含义
Temperaturefloat0.8随机性;0 = 贪心/确定性。
TopKint40限制为概率最高的 K 个 token;0 = 禁用。
TopPfloat0.9核采样;1.0 = 禁用。
MinPfloat0相对最大值的最小概率阈值。
RepetitionPenaltyfloat1.1乘性惩罚;>1 抑制重复。
PresencePenaltyfloat0对已出现 token 的加性惩罚。
FrequencyPenaltyfloat0与 token 频率成比例的加性惩罚。
Seedint-1可复现采样;-1 = 基于时间。
StopSequencesList<string>null产生其中任一字符串时停止。
MaxTokensint0最大生成 token 数;0 = 使用调用方默认。

关键类型与接口

类型角色
ModelBase每个架构的抽象基类。Create(path, backend)Forward(int[])Sample(...)SampleGreedy(...),外加 ConfigTokenizer
BackendType枚举:CpuGgmlCpuGgmlMetalGgmlCudaGgmlVulkanCudaMlx
SamplingConfig采样配置(见上表)。
ITokenizerEncode(text, addSpecial)Decode(ids)IsEos(id)EosTokenIds(BPE 与 SentencePiece 实现)。
ModelConfig架构元数据:VocabSize、上下文长度等。
IBatchedPagedModel可选的批量/分页前向(ForwardBatch),多数架构为连续批处理而实现。
DiffusionGemmaModel + DiffusionGemmaSampler文本扩散模型及其 EntropyBound 去噪采样器(Generate(promptTokens, DiffusionEbParams, …))。Forward() 不受支持。
QwenImageModel + QwenImageParamsQwen-Image-Edit 图像编辑器。EditImage(prompt, RgbImage, QwenImageParams) 返回修改后的 RgbImageImageIO 负责 PNG 加载/保存。
InferenceEngine支撑服务器连续批处理的 worker 线程调度器 + 分页块池(在 TensorSharp.Runtime.Scheduling 中)。

其他值得了解的运行时契约:IModelArchitectureIPromptRendererIOutputProtocolParserIMultimodalInjectorIKvBlockCodec(带内置 TurboQuantKvCodec)以及 IKVCachePolicy

💡

对多数应用而言,最简单的集成是运行 TensorSharp.Server 并通过兼容 OpenAI 的 API 调用它 —— 你的应用进程保持干净,并免费获得连续批处理。当你需要进程内控制或自定义解码时,再使用库 API。