Using TensorSharp from C#

TensorSharp is a real .NET library, not just an executable. Reference its projects from a source checkout and drive inference directly from your own code when you do not want an HTTP hop.

Project and package boundaries

The repository is split along publishable package boundaries. The names below are project/package IDs, but the current Runtime/Models/Backends/CLI/Server packages are not available on NuGet.org.

PackageNamespaceResponsibility
TensorSharp.CoreTensorSharpTensor primitives, ops, allocators, storage, device abstraction.
TensorSharp.RuntimeTensorSharp.RuntimeGGUF parsing, tokenizers, prompt rendering, sampling, paged KV cache, continuous-batching scheduler.
TensorSharp.ModelsTensorSharp.ModelsModelBase, architecture implementations, multimodal encoders, batched/paged forward passes.
TensorSharp.Backends.GGMLTensorSharp.GGMLGGML-backed execution and native interop.
TensorSharp.Backends.CudaTensorSharp.CudaDirect CUDA allocator, storage, cuBLAS GEMM, PTX kernels, quantized CUDA ops.
TensorSharp.Backends.MLXTensorSharp.MLXApple-Silicon MLX backend (mlx-c / Metal).
TensorSharp.ServerTensorSharp.ServerASP.NET Core server, OpenAI/Ollama adapters, inference engine host, web UI.
TensorSharp.CliTensorSharp.CliConsole host and debugging / batch tooling.

For a typical embedding scenario, reference TensorSharp.Models.csproj; it already references Core, Runtime, and the backend projects. From a sibling application beside the TensorSharp checkout:

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

Do not use dotnet add package TensorSharp.Models yet: no current package with that ID is published on NuGet.org. Source builds compile native GGML/MLX by default; for managed-CPU development pass -p:TensorSharpSkipGgmlNative=true -p:TensorSharpSkipMlxNative=true, or build the native libraries described under Backends.

Minimal example — load, generate, decode

Every model is loaded the same way: ModelBase.Create() reads the GGUF metadata and instantiates the right architecture. From there you tokenize, run forward passes, sample, and decode.

using System;
using System.Collections.Generic;
using System.Linq;
using TensorSharp.Models;
using TensorSharp.Runtime;

// 1. Load any supported GGUF — the architecture is detected from metadata.
//    Pick the BackendType your build supports: GgmlCuda, GgmlMetal, GgmlVulkan, or GgmlCpu.
using var model = ModelBase.Create("gemma-4-E4B-it-Q8_0.gguf", BackendType.GgmlCuda);

// 2. Configure sampling (defaults match Ollama: temp 0.8, top_k 40, top_p 0.9).
var sampling = new SamplingConfig { Temperature = 0.7f, TopP = 0.9f, TopK = 40 };

// 3. Tokenize the prompt.
var tokens = model.Tokenizer
    .Encode("Explain mixture-of-experts in one sentence.", addSpecial: true)
    .ToList();
var generated = new List<int>();

// 4. Prefill the complete prompt once.
float[] logits = model.Forward(tokens.ToArray());

// 5. Decode one new token at a time. Forward() owns the KV-cache position,
// so after prefill pass only the newly sampled token, not the full history.
for (int step = 0; step < 200; step++)
{
    int next = model.Sample(logits, sampling, generated); // applies penalties + sampling
    if (model.Tokenizer.IsEos(next)) break;
    generated.Add(next);
    logits = model.Forward(new[] { next });
}

// 6. Detokenize the result.
Console.WriteLine(model.Tokenizer.Decode(generated));

For greedy/deterministic decoding, call model.SampleGreedy(logits) instead of Sample.

Smoke-test variant

A one-shot sanity check that loads a model, runs a single forward pass, and prints the 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 — text diffusion

DiffusionGemma is a block text-diffusion model, not an autoregressive one. ModelBase.Create() still loads it (the GGUF architecture is diffusion-gemma / diffusion_gemma) and returns a DiffusionGemmaModel, but its Forward(int[]) intentionally throws — generation runs through DiffusionGemmaSampler, which iteratively denoises fixed-length canvas blocks over a [prompt | canvas] sequence. See the DiffusionGemma model card for the architecture.

using TensorSharp.Models;
using TensorSharp.Models.DiffusionGemma;
using TensorSharp.Runtime;

// Load a diffusion-gemma GGUF — the architecture is auto-detected.
using var model = (DiffusionGemmaModel)ModelBase.Create("diffusion-gemma.gguf", BackendType.GgmlCuda);

// Render the prompt with the model's chat template, then tokenize.
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();

// Configure the EntropyBound denoising sampler.
var p = new DiffusionEbParams
{
    MaxDenoisingSteps = 48,   // refinement steps per canvas block
    Seed = 0,                 // deterministic
    MaxBlocks = 1,            // block-autoregressive canvas blocks
};

var sampler = new DiffusionGemmaSampler(model);

// Generate. The optional callback fires after every denoising step with
// (blockIndex, step, totalSteps, previewTokens) — useful for a live 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));

Key parameters on DiffusionEbParams: MaxDenoisingSteps (48), TMin/TMax temperature schedule (0.4 / 0.8), EntropyBound (0.1), StabilityThreshold / ConfidenceThreshold early-stop, Seed, and MaxBlocks. model.CanvasLength reports the per-block canvas size.

🌫️

On GPU backends the prompt K/V is cached once per block and reused across denoising steps; GGML backends default to a fused whole-model decode plus a fused lm-head tail. Tunables (DIFFUSION_STEPS, DIFFUSION_NO_SC, …) are in the Advanced page.

Qwen-Image-Edit — image editing

Qwen-Image-Edit takes a prompt + an input image and returns an edited image. The loaded qwen_image GGUF is only the MMDiT diffusion transformer; the model also pulls in two companion GGUFs resolved next to the DiT file (or via the TS_QWEN_IMAGE_VAE / TS_QWEN_IMAGE_TE / TS_QWEN_IMAGE_MMPROJ environment variables): the Qwen-Image VAE and the Qwen2.5-VL-7B text encoder. Like DiffusionGemma it is not an autoregressive text model — the autoregressive entry points throw and editing is driven through EditImage().

using TensorSharp.Models;
using TensorSharp.Models.QwenImage;
using TensorSharp.Runtime;

// Load the MMDiT GGUF (architecture = qwen_image). The VAE + Qwen2.5-VL
// companions are resolved from the same directory (or the TS_QWEN_IMAGE_* env vars).
using var model = (QwenImageModel)ModelBase.Create("qwen-image-edit-DiT-Q4_K_M.gguf", BackendType.GgmlCuda);

// Load the input image (PNG/JPEG decoded to an RgbImage).
RgbImage input = ImageIO.Load("input.png");

var p = new QwenImageParams
{
    Steps = 30,               // FlowMatch-Euler denoising steps
    CfgScale = 4.0f,          // true-CFG guidance; <= 1 disables the negative pass
    NegativePrompt = " ",     // used only when CfgScale > 1
    Seed = 0,
    TargetArea = 1024 * 1024, // ~1 MP; aspect follows the input (dims snapped to /16)
    // Width = 0, Height = 0, // pin an explicit output size to bypass the VRAM clamp
};

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.");

For a live UI, set p.OnStep = (step, total, preview) => { … } and p.PreviewCount to receive decoded RGB snapshots of the partially denoised latent on throttled steps. RgbImage exposes Width, Height, and a planar/interleaved Pixels buffer; ImageIO provides Load, Decode(byte[]), EncodePng, SavePng, and resize helpers.

🖼️

Image editing is compute-heavy: each denoise step runs the full 60-block MMDiT (twice when CFG is on). Use a CUDA or Metal GGML backend for practical full-quality edits; the pipeline auto-clamps the target area to the device VRAM budget unless you pin Width/Height. See the model card for the companion-GGUF layout.

SamplingConfig

The sampling knobs map one-to-one to the CLI flags and API options. Defaults match Ollama.

PropertyTypeDefaultMeaning
Temperaturefloat0.8Randomness; 0 = greedy/deterministic.
TopKint40Limit to the top-K most probable tokens; 0 = disabled.
TopPfloat0.9Nucleus sampling; 1.0 = disabled.
MinPfloat0Minimum probability threshold relative to the max.
RepetitionPenaltyfloat1.1Multiplicative penalty; >1 discourages repetition.
PresencePenaltyfloat0Additive penalty for tokens already present.
FrequencyPenaltyfloat0Additive penalty proportional to token frequency.
Seedint-1Reproducible sampling; -1 = time-based.
StopSequencesList<string>nullStop when any of these strings is produced.
MaxTokensint0Maximum tokens to generate; 0 = use the caller's default.

Key types & interfaces

TypeRole
ModelBaseAbstract base for every architecture. Create(path, backend), Forward(int[]), Sample(...), SampleGreedy(...), plus Config and Tokenizer.
BackendTypeEnum: Cpu, GgmlCpu, GgmlMetal, GgmlCuda, GgmlVulkan, Cuda, Mlx.
SamplingConfigSampling configuration (table above).
ITokenizerEncode(text, addSpecial), Decode(ids), IsEos(id), EosTokenIds (BPE & SentencePiece implementations).
ModelConfigArchitecture metadata: VocabSize, context length, and more.
IBatchedPagedModelOptional batched/paged forward (ForwardBatch) implemented by most architectures for continuous batching.
DiffusionGemmaModel + DiffusionGemmaSamplerText-diffusion model and its EntropyBound denoising sampler (Generate(promptTokens, DiffusionEbParams, …)). Forward() is unsupported.
QwenImageModel + QwenImageParamsQwen-Image-Edit image editor. EditImage(prompt, RgbImage, QwenImageParams) returns the modified RgbImage; ImageIO loads/saves PNG.
InferenceEngineWorker-thread scheduler + paged block pool that powers the server's continuous batching (in TensorSharp.Runtime.Scheduling).

Other runtime contracts worth knowing: IModelArchitecture, IPromptRenderer, IOutputProtocolParser, IMultimodalInjector, IKvBlockCodec (with the built-in TurboQuantKvCodec), and IKVCachePolicy.

💡

For most applications the easiest integration is to run TensorSharp.Server and call it over the OpenAI-compatible API — you keep your app process clean and get continuous batching for free. Reach for the library API when you need in-process control or custom decoding.