从 C# 使用 TensorSharp

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

项目与包边界

仓库按可发布包边界拆分。源码树的发布集合是下表的全部 13 个包,由 eng/verify-packages.ps1 校验,并作为发布流水线的门禁。

核验于 2026-09-08:NuGet.org 上目前只有其中 8 个,版本 3.1.2(2026 年 7 月):Tensors、Runtime、Models、三个后端、Server 与 Cli。它们落后于当前源码树与服务器版本——已发布的 TensorSharp.Server 早于日志层与聊天层拆分。其余 5 个(TensorSharp.Runtime.LoggingTensorSharp.AgentHostTensorSharp.ChatTensorSharp.Server.HostTensorSharp.Distributed)已可打包并通过校验,但尚未推送。

项目 / 包命名空间职责
TensorSharp.Core / TensorSharp.TensorsTensorSharp张量原语、运算、分配器、存储、设备抽象;NuGet ID 与项目名不同。
TensorSharp.Runtime.LoggingTensorSharp.Runtime.Logging各宿主共用的日志抽象与输出目标,不再压在引擎各层上。尚未发布到 NuGet.org。
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.ChatTensorSharp.Chat与宿主无关的聊天流水线——ModelService、会话、生成、技能循环、Web UI 请求/流式契约。不依赖 ASP.NET Core;由服务器、CLI 与 iOS 应用共用。尚未发布到 NuGet.org。
TensorSharp.ServerTensorSharp.ServerASP.NET Core 库:OpenAI/Ollama 适配器、基于 TensorSharp.Chat 的 HTTP 传输、Web UI 资源。它是库——单独构建不产生可执行文件。
TensorSharp.Server.HostTensorSharp.Server.Host基于该库的可运行 Web 应用:Program.cs、宿主装配、wwwroot/ 与命令行。实际运行的是它。尚未发布到 NuGet.org。
TensorSharp.CliTensorSharp.Cli控制台宿主与调试 / 批处理工具。
TensorSharp.AgentHostTensorSharp.AgentHost.*技能、有界智能体循环、五个代码工具、沙箱、工作区与产物。尚未发布到 NuGet.org。
TensorSharp.DistributedTensorSharp.Distributed点对点 TCP 分布式张量并行。尚未发布到 NuGet.org。

典型嵌入场景只需引用 TensorSharp.Models.csproj;它已引用 Core、Runtime 与各后端项目。若你的应用与 TensorSharp checkout 同级:

dotnet add reference ../TensorSharp/TensorSharp.Models/TensorSharp.Models.csproj
📦

NuGet 3.1.2 是较早的快照;需要当前实现时请使用项目引用。Runtime.Logging、AgentHost、Chat、Server.Host 与 Distributed 还没有已发布的包,必须从源码引用。源码构建默认编译原生 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 = 使用调用方默认。

Agent Skills —— SkillsChatClient

Agent Skill(智能体技能)是一个面向模型的说明文件夹——一份 SKILL.md,外加它引用的脚本、参考文档与素材——模型只在任务需要时才加载它。SkillsChatClient(命名空间 TensorSharp.AgentHost.Skills,位于 TensorSharp.AgentHost 项目)就是 .NET 应用取用技能的入口。它通过 HTTP 调用兼容 OpenAI 的聊天端点,而不是在进程内加载 GGUF,因此与本页其余内容不同:它既不需要后端,也不需要引用 TensorSharp.Models

在 TensorSharp 支持工具的聊天格式上,已选择和发现到的技能最初都只披露元数据;模型按需读取说明与随附文件。若模型没有可用的工具解析器,TensorSharp 会改为内联已选择技能的正文并隐藏内置技能/代码工具,使请求仍然有用,而不会假装工具往返能工作。

它覆盖实际会遇到的两种情形,由 SkillsChatClientOptions.Delivery 选择。

面向 TensorSharp.Server —— SkillDelivery.Server

只需点名技能,其余全部由服务端在模型旁边完成,渐进式披露的循环也在那里跑。每次请求不上传任何东西,技能文件也从不离开服务端。

using System;
using System.Threading.Tasks;
using TensorSharp.AgentHost.Skills;

using var client = new SkillsChatClient(new SkillsChatClientOptions
{
    Endpoint = "http://localhost:5000",           // 带不带结尾的 /v1 都可以
    DefaultModel = "gemma-4-E4B-it-Q8_0.gguf",
    Delivery = SkillDelivery.Server,
});

// 单轮场景的便捷写法:提示词 + 要使用的技能。
SkillsChatResponse reply = await client.CompleteAsync(
    SkillsChatRequest.User("Extract the tables from report.pdf", "pdf"));

Console.WriteLine(reply.Content);
Console.WriteLine($"{reply.Rounds} 轮,prompt {reply.PromptTokens} + completion {reply.CompletionTokens} token");

服务端交付模式下,渐进式披露的循环跑在服务端内部,因此 reply.SkillInvocations 返回为空,reply.Rounds1 —— 本客户端恰好只发出了一次请求。

面向任意其他兼容 OpenAI 的端点 —— SkillDelivery.Local

把客户端指向一个本地 SkillRegistry,它就会在本进程内构造提示词块、声明技能工具并运行整个循环,于是一个从没听说过技能的端点表现得就像原生支持一样。代价是模型每读一个文件多一次网络往返。

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using TensorSharp.Runtime;          // ChatMessage
using TensorSharp.AgentHost.Skills;

var registry = new SkillRegistry(new SkillRegistryOptions
{
    Roots = new[] { "/srv/skills" },   // 最深扫描 MaxDepth(3)层
});

using var client = new SkillsChatClient(new SkillsChatClientOptions
{
    Endpoint = "https://api.example.com/v1",
    ApiKey = Environment.GetEnvironmentVariable("EXAMPLE_API_KEY"),
    DefaultModel = "some-hosted-model",
    Delivery = SkillDelivery.Local,
    Registry = registry,
    Discovery = true,                  // 同时展示本次请求没有点名的技能
});

SkillsChatResponse reply = await client.CompleteAsync(new SkillsChatRequest
{
    Messages = { new ChatMessage { Role = "user", Content = "Fill in this AcroForm and tell me what you set." } },
    Skills = { "pdf" },
    MaxTokens = 800,
});

Console.WriteLine(reply.Content);

// 模型读过的每一个技能文件,按顺序排列。
foreach (SkillToolInvocation call in reply.SkillInvocations)
    Console.WriteLine($"round {call.Round}: {call.Tool} {call.SkillId}/{call.ResourcePath} ok={call.Ok}");

默认值 SkillDelivery.Auto 会探测一次 /v1/skills,并把结果缓存到该客户端的整个生命周期:端点有响应就走服务端交付,没有响应且本客户端带了注册表就走本地交付。await client.ResolveDeliveryAsync() 会告诉你最终选了哪一种;await client.ListServerSkillsAsync() 则向端点询问它有哪些技能,端点没有实现技能 API 时返回空列表而不是抛异常。

🔁

SkillsChatResponse.ToolCalls 装的是对调用方自己的工具(即传入 SkillsChatRequest.Tools 的那些)的调用,客户端从不执行它们——只有调用方知道这些工具做什么。列表非空就意味着你需要自己执行它们,并在后续请求中把结果送回;技能工具此时已经应答完毕,reply.Messages 就是可以直接续写的完整对话记录。请求失败会抛出 SkillsChatException

SkillsChatClientOptions

属性类型默认含义
Endpointstring必填API 根地址,带不带结尾的 /v1 都可以。
ApiKeystring?null端点需要时使用的 Bearer token。TensorSharp.Server 不需要。
DefaultModelstring?null未按请求单独指定时,随每次请求发送的模型名。
DeliverySkillDeliveryAuto由谁解析技能:AutoServerLocal
RegistrySkillRegistry?null本地交付时使用的技能集合。服务端交付时忽略——那时技能归端点所有。
PromptOptionsSkillPromptOptions.Default本地交付的提示词预算。
LoopOptionsSkillAgentLoopOptions.Default本地交付的循环上限 —— MaxRounds 8、MaxCallsPerRound 8,以及每次工具调用执行后触发的 OnInvocation 回调。
Discoverybooltrue把本次请求未点名的技能也展示给模型,好让它自己发现。SkillsChatRequest.Discovery 可按请求覆盖。
TimeoutTimeSpan10 分钟单次请求超时。仅在客户端自行创建 HttpClient 时生效——若从 IHttpClientFactory 取一个传进构造函数,则不会改动它。

SkillsChatRequestSkillsChatResponse

成员类型含义
SkillsChatRequest.MessagesList<ChatMessage>对话内容。首条 system 消息会与技能文本块合并,而不是被顶掉。
SkillsChatRequest.SkillsList<string>要使用的技能名,取自注册表中的名称。
SkillsChatRequest.ToolsList<ToolFunction>?调用方自己的工具。客户端从不执行,会原样回传给调用方处理。
SkillsChatRequest.Model · MaxTokens · Temperature · TopP · Think · Discoverystring? · int? · double? · double? · bool · bool?按请求覆盖的设置;为 null 时交由端点决定(Discovery 则回落到客户端自身的选项)。
SkillsChatRequest.User(prompt, params skills)静态方法单轮常见场景的便捷写法。
SkillsChatResponse.Content · Thinkingstring · string?助手的回答,以及模型给出推理内容时的推理文本。
SkillsChatResponse.ToolCallsIReadOnlyList<ToolCall>对调用方自己的工具的调用,客户端从不执行。
SkillsChatResponse.MessagesList<ChatMessage>完整对话记录,含披露循环追加的全部内容。
SkillsChatResponse.SkillInvocationsIReadOnlyList<SkillToolInvocation>模型读过的每一个技能文件,按顺序排列 —— RoundToolSkillIdResourcePathOkResultBytes。服务端交付时为空。
SkillsChatResponse.FinishReason · PromptTokens · CompletionTokens · Roundsstring? · int · int · int生成停止的原因、逐轮累加的 token 数,以及实际跑了几次生成。Rounds 为 1 表示模型没读任何东西就作答了。

SkillRegistrySkillRegistryOptions

注册表负责发现(在配置的目录中查找 SKILL.md)、安装与查找;它是唯一接触技能存储的组件,因此边界约束规则集中在一处。CLI 与服务端也正是用 --skills-dir 构造出它。

成员类型 / 默认含义
SkillRegistryOptions.RootsIReadOnlyList<string>,空要扫描的目录,按优先级顺序排列。一个根目录可以是单个技能(直接含 SKILL.md),也可以是装着若干技能的目录。
SkillRegistryOptions.InstallDirectorystring?,null运行时安装的技能写入的位置;同样会被扫描,并且优先级永远排在最前。为 null 时注册表只读。
SkillRegistryOptions.MaxDepthint,3在一个根目录下查找 SKILL.md 的最大深度。
SkillRegistryOptions.MaxSkills · MaxManifestBytes · MaxSkillBytes · MaxSkillFiles512 · 4 MB · 256 MB · 4096各项上限,好让指错目录的根目录显式失败,而不是耗尽内存。
Skills · Errors · RootsIReadOnlyList<Skill> · IReadOnlyList<SkillLoadError> · IReadOnlyList<string>成功加载的技能、加载失败的项(含 PathMessage),以及实际扫描过的根目录。
CanInstall · InstallDirectorybool · string?是否配置了运行时安装,以及安装到哪里。
TryGet(id, out Skill) · Resolve(ids, out unknown)bool · IReadOnlyList<Skill>按名称查找单个技能,或解析一组选择并得知哪些名称没找到。
Refresh()SkillScanResult重新扫描全部根目录 —— POST /api/skills/rescan 调用的就是它。
InstallFromDirectory(path, overwrite) · InstallFromZip(stream, overwrite, limits) · Remove(id)Skill · Skill · bool从文件夹或上传的压缩包安装技能,或删除一个已安装的技能。压缩包的每个条目都要经过与模型读取相同的那道路径关卡;被拒绝的压缩包会抛出 SkillInstallException

完整参考(frontmatter 字段、提示词预算与安全模型)见 docs/agent_skills.md。可直接取用的开源技能:github.com/anthropics/skills

AgentHost 运行时层

TensorSharp.AgentHost 是构建在 TensorSharp.Runtime 之上的可选智能体层;Runtime 不反向依赖它。CodeExecOptionsShellRunnerCodeRunnerAdapter 实现宿主拥有的五个工具:read_fileedit_filewrite_fileshellapply_patch。只有宿主提供持久工作区时才声明文件工具;没有工作区的直接调用方只获得 shell

SkillAgentLoop 运行有界的单助手循环,只执行 TensorSharp 自有的技能/代码调用,并把应用自身工具的调用放入 SkillLoopResult.PendingClientToolCalls 返回。宿主默认最多生成 8 轮;提供代码工具时提升为 24,除非运维方显式设定。SessionWorkspaceManager 为每个服务器会话提供私有工作区;仅请求调用方则在一次请求内共享。工作文件、允许导出的环境状态与已安装包会在该作用域内保留,而 PATH 每次 shell 调用都会重建。

🔒

代码工具默认关闭,沙箱模式默认 required。macOS Seatbelt 无法保证清理刻意脱离的子进程;Linux 需要 bubblewrap 0.12+;Windows 作业对象无法约束文件或套接字,因此执行必须显式选择无隔离模式。这些是启动权限,不是逐命令审批,循环也不会生成子智能体。结构化输出请求会内联已选择技能并隐藏内置工具。完整边界见智能体工作

关键类型与接口

类型角色
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 加载/保存。
SkillsChatClient + SkillsChatClientOptions面向兼容 OpenAI 的端点使用 Agent SkillsCompleteAsync(SkillsChatRequest) 返回 SkillsChatResponseSkillDelivery 决定披露循环由服务端还是本进程运行(位于 TensorSharp.AgentHost.Skills)。
SkillRegistry + SkillRegistryOptions宿主已知的技能集合:在配置的根目录中扫描 SKILL.md、从文件夹或 .zip 安装,以及查找。它是唯一接触技能存储的组件,因此边界约束集中在一处生效。
InferenceEngine支撑服务器连续批处理的 worker 线程调度器 + 分页块池(在 TensorSharp.Runtime.Scheduling 中)。

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

💡

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