Server & Web UI
TensorSharp.Server is an ASP.NET Core app that hosts a single GGUF model and exposes a browser chat UI plus Ollama- and OpenAI-compatible REST APIs on the same port. The continuous-batching engine handles concurrency.
Start the server
Quick start in ~30 seconds (Gemma 4 E4B)
Install the .NET 10 SDK for your platform, Git, CMake, and curl, then paste this from a terminal. Copying and running the commands takes about 30 seconds; the 7.48 GiB model download and the first restore/build take longer and depend on your connection and machine. It hosts the repository's benchmark-verified Gemma 4 E4B Q8_0 from the recommended public ggml-org artifact on the native GGML bridge. This block is for Linux + NVIDIA (the CUDA build also needs the CUDA Toolkit):
git clone https://github.com/zhongkaifu/TensorSharp.git
cd TensorSharp
TENSORSHARP_GGML_NATIVE_ENABLE_CUDA=ON dotnet build TensorSharp.slnx -c Release -p:TensorSharpSkipMlxNative=true
curl --create-dirs --fail -L "https://huggingface.co/ggml-org/gemma-4-E4B-it-GGUF/resolve/main/gemma-4-E4B-it-Q8_0.gguf?download=true" -o models/gemma-4-E4B-it-Q8_0.gguf
dotnet run --project TensorSharp.Server -c Release --no-build -- --model models/gemma-4-E4B-it-Q8_0.gguf --backend ggml_cuda
On Apple Silicon, omit the CUDA environment assignment and use ggml_metal; on a supported Windows/Linux Vulkan GPU, request TENSORSHARP_GGML_NATIVE_ENABLE_VULKAN=ON instead and use ggml_vulkan; with no supported GPU, drop the assignment and use ggml_cpu. The lower-memory gemma-4-E4B-it-Q4_K_M.gguf is in the same repository. Text needs no projector; image, video, or audio also requires the matching mmproj-gemma-4-E4B-it-Q8_0.gguf passed with --mmproj. See Getting Started for Windows PowerShell and full platform syntax.
In a second terminal, verify the OpenAI-compatible endpoint:
curl -s http://localhost:5000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"gemma-4-E4B-it-Q8_0.gguf","messages":[{"role":"user","content":"Reply with one short hello."}],"max_tokens":32}'
The API base is http://localhost:5000. Open the bundled chat UI at http://localhost:5000/index.html; GET / is the liveness endpoint and returns "TensorSharp.Server is running".
--model is required for inference. The server hosts exactly the startup GGUF and optional, explicitly supplied --mmproj; it does not scan a model directory or auto-detect a projector. /api/models/load can only re-load that same startup pair, optionally on another supported backend. A model-less process cannot choose a GGUF at runtime. The listen address is fixed at http://0.0.0.0:5000; there is no port flag.
The server has no built-in API-key authentication or TLS and binds every interface. Keep it behind a host firewall for local use, or put an authenticated HTTPS reverse proxy in front of it; do not expose port 5000 directly to an untrusted network.
Already-built source tree
Run these commands from the repository root after building. They invoke TensorSharp.Server/bin/TensorSharp.Server.dll; that output directory also contains the copied native libraries and wwwroot/. The current v3.0.5.0 GitHub release has no attached binary archives.
# Apple Silicon / Metal
dotnet TensorSharp.Server/bin/TensorSharp.Server.dll --model ./models/model.gguf --backend ggml_metal
# NVIDIA / GGML CUDA
dotnet TensorSharp.Server/bin/TensorSharp.Server.dll --model ./models/model.gguf --backend ggml_cuda
# AMD, Intel, or NVIDIA / Vulkan; inspect device indices first
dotnet TensorSharp.Server/bin/TensorSharp.Server.dll --list-gpus
dotnet TensorSharp.Server/bin/TensorSharp.Server.dll --model ./models/model.gguf --backend ggml_vulkan --gpu-device 1
# Multimodal: the projector is always explicit
dotnet TensorSharp.Server/bin/TensorSharp.Server.dll --model ./models/model.gguf --mmproj ./models/mmproj.gguf --backend ggml_cuda
Server-wide default sampling
Defaults fill in any field a request omits. Out of the box they match Ollama: temperature 0.8, top-k 40, top-p 0.9, min-p 0, repeat penalty 1.1, presence/frequency 0, seed -1. A parameter you set explicitly also overrides the value a client sends for it β many chat clients (VS Code Copilot Chat among them) hardcode temperature/top_p into every request. Add --sampling-precedence request to hand that control back to clients.
dotnet TensorSharp.Server/bin/TensorSharp.Server.dll --model ./models/model.gguf --backend ggml_metal \
--temperature 0.7 --top-p 0.9 --top-k 40 --repeat-penalty 1.1 \
--presence-penalty 0.0 --frequency-penalty 0.0 --seed 42 \
--stop "</s>" --stop "<|endoftext|>"
Web UI features
Open http://localhost:5000/index.html. The browser interface supports:
- Multi-turn chat conversations with streaming token generation (SSE).
- Per-tab chat sessions β each tab owns tracked conversation history; request KV blocks and prefix reuse are owned by the inference engine.
- Image, video, audio, PDF, and text/code uploads for multimodal inference (up to 500 MB).
- PDF documents: born-digital PDFs have their complete text layer extracted and inlined into the prompt; scanned PDFs fall back to page images for vision-capable models (
TS_PDF_MAX_PAGEScaps the pages read). The final rendered prompt is checked against the model's actual context window. - Thinking / reasoning mode toggle and tool calling with function definitions.
- Message editing and deletion with regeneration from any point in the conversation.
- DiffusionGemma denoising previews when a
diffusion-gemmaGGUF is hosted (the whole assistant message is replaced on each step, then finalized). - Qwen-Image-Edit flow when a
qwen_imageDiT is hosted: attach an image, type the edit instruction, and temporary edited images (live denoising previews, up to 8 frames) refresh in place until the final PNG appears with a download link. - Free scrolling β read earlier replies while new tokens stream; auto-scroll resumes at the bottom.
Configuration file (--config)
Instead of a long command line, pass a JSON file with --config. The CLI reads the same format. Command-line options always winβfile values are applied first, then anything you also pass on the command line overrides them, so one file can be reused across hosts while you override just what differs. Repeat --config to layer files (later files win). Comments and trailing commas are allowed.
# Read all options from a file; override just the backend for this host
dotnet TensorSharp.Server/bin/TensorSharp.Server.dll --config config/server-basic.json
dotnet TensorSharp.Server/bin/TensorSharp.Server.dll --config config/server-basic.json --backend ggml_cpu
Keys are the same long option names below (with or without the leading --). A string/number becomes --key value, true becomes the bare switch --key, and an array becomes a repeated flag (e.g. "stop": ["</s>", "<|eot|>"]).
Variables. Define shared values once under "variables" and reference them with ${name} in any string value (an undefined name falls back to an environment variable of the same name). Declare as many roots as you need β models in different folders each get their own.
Auto-download. Any file option can be an object with a local path and one or more urls. If path is missing it downloads from the first working URL (mirrors tried in order), saves it there, and reuses it next time; progress prints to stderr, and an optional sha256 verifies the file.
{
"variables": { "modelRoot": "C:/models", "repo": "https://huggingface.co/unsloth/gemma-4-E4B-it-GGUF/resolve/main" },
"backend": "ggml_cuda",
"max-tokens": 4096,
"continuous-batching": true,
"stop": ["</s>", "<|eot|>"],
"model": { "path": "${modelRoot}/gemma-4-E4B-it-Q8_0.gguf", "urls": [ "${repo}/gemma-4-E4B-it-Q8_0.gguf" ] },
"mmproj": { "path": "${modelRoot}/gemma-4-E4B-mmproj-F16.gguf", "urls": [ "${repo}/mmproj-F16.gguf" ] }
}
Ready-to-use examples live in the repository's config/ folder (cli-basic.json, server-basic.json, variables.json, auto-download.json, qwen-image-edit.json) β each uses real, public, ungated URLs, so it works on a fresh machine. See config/README.md for the full reference.
Server options
| Option | Description |
|---|---|
--model <path> | GGUF file to host (required for inference). |
--mmproj <path> | Explicit multimodal projector GGUF; a bare filename resolves next to the model (pass none to disable). Requires --model; there is no automatic projector scan. |
--backend <type> | Default backend: cpu, cuda, mlx, ggml_cpu, ggml_metal, ggml_cuda, ggml_vulkan. |
--tp <N> | Tensor parallelism degree β split the hosted model across N local GPUs (default: 1; env TENSORSHARP_TP_DEGREE). Requires --backend cuda, ggml_cuda, or ggml_vulkan. β Multi-GPU & Multi-Node |
--tp-node-id <N> / --tp-peers <list> | Join a multi-node TP cluster: this node's 0-based ID and the shared, identically-ordered host:port list of every node. The server can only be node 0 β the driver that owns sampling and serves HTTP; other nodes run TensorSharp.Cli workers. |
--gpu-device <N> | Vulkan device index for the ggml_vulkan backend on multi-GPU hosts (default: 0; env TS_GGML_VULKAN_DEVICE). |
--list-gpus | List the Vulkan devices ggml-vulkan can see (index + adapter name) and exit. |
--help | Print the full parameter reference and exit; it is also shown when no arguments are supplied. Inference always requires a startup --model. |
--config <path> | Read options from a JSON config file (command-line options override it). Supports ${variables} and auto-downloading models via { "path": ..., "urls": [...] }. Repeatable. |
--max-tokens <N> | Generation limit for every endpoint (Web UI, Ollama and OpenAI alike): fills in when a request omits it, and caps a request that asks for more (default: 20000, which only fills in). |
--temperature / --top-k / --top-p / --min-p | Sampling values (defaults: 0.8 / 40 / 0.9 / 0). |
--repeat-penalty / --presence-penalty / --frequency-penalty / --seed | Penalties and seed (defaults: 1.1 / 0 / 0 / -1). |
--stop <string> | Stop sequence (repeatable). Merged with a per-request stop list under config precedence; replaced by it under request. |
--sampling-precedence <config|request> | Who wins when a request also carries a sampling parameter you configured above: config (default) keeps your value, request lets the client's win. Parameters you did not configure always come from the request. Env: TENSORSHARP_SAMPLING_PRECEDENCE. |
--continuous-batching / --no-continuous-batching | Enable (default) or disable iteration-level paged batching. Alias: --paged-batching. |
--mtp-spec / --no-mtp-spec | Enable / disable NextN / MTP speculative decoding (default off). Only engages on models with an MTP/NextN draft head β for Qwen 3.6 that means a GGUF that retains the NextN block (e.g. the -MTP- repos); base-repo GGUFs strip it and silently fall back to standard decode. β MTP |
--mtp-draft <N> | Max tokens drafted per speculative step (default 8). |
--mtp-pmin <f> | Minimum draft confidence to keep a token. The default depends on the drafter kind: 0.75 for a per-token draft head, 0.35 for a block drafter (where the gate is the cumulative prefix probability, so the same number is far stricter). |
--mtp-draft-model <path> | Separate MTP draft GGUF (Gemma 4's gemma4-assistant). Qwen 3.6 embeds its NextN draft in the trunk GGUF and needs no draft file. If an explicitly requested draft cannot be activated on the startup model, startup fails fast. |
--draft-model <path> | Block drafter GGUF for architectures whose drafter must be resident before the layer split β DeepSeek V4's DSpark support module. Needs --mtp-spec; engages for solo sequences on the cuda and ggml_cuda backends. Verification draws every row with the request's own sampler, so it composes with any sampling settings. Env: TS_DSV4_DSPARK. β DSpark |
--spec-draft-n-max <N> / --spec-draft-conf-min <p> | Tokens drafted per speculative block (default: the drafter's block size) and the minimum cumulative acceptance probability to keep a drafted position (default 0.35). |
--prefill-chunk-size <N> | Maximum prefill tokens per scheduler step (sets TS_SCHED_PREFILL_CHUNK). |
--kv-cache-dtype <type> | KV cache precision: f32, f16, q8_0, or q4_0 (default: auto β the backend/model pick; env KV_CACHE_DTYPE; q4_0 targets very long 128Kβ256K contexts). |
--paged-kv / --no-paged-kv | Legacy compatibility switch for the standalone PagedKvCacheManager. It is not on the current server request path; active request KV is engine-owned. |
--paged-kv-block-size / --paged-kv-ram-mb / --paged-kv-ssd-dir / --paged-kv-ssd-mb | Legacy standalone paged-KV tuning. Use the TS_SCHED_* engine settings below for current server requests. |
--paged-kv-quant-bits <b> | Legacy standalone TurboQuant setting; this server flag accepts 0, 4, or 8 (the runtime env var and CLI additionally accept 2). |
--qwen-image-vae / --qwen-image-vl / --qwen-image-mmproj <path> | Override the resolved Qwen-Image-Edit companion GGUFs (VAE / Qwen2.5-VL text encoder / mmproj). |
--qwen-image-lora <path> | Qwen-Image-Edit Lightning distillation LoRA (.safetensors) merged into the DiT; auto-derives the denoise step count and switches CFG to 1.0. |
Per-request fields (temperature, top_p, seed, stop, β¦) fill in every parameter you did not configure here. For one you did configure, your value wins unless the server runs with --sampling-precedence request. The chat.start log line prints the sampler each request actually ran with.
Environment variables
| Variable | Description |
|---|---|
BACKEND | Default backend when --backend is not passed (default: ggml_metal on macOS, ggml_cpu elsewhere). |
MAX_TOKENS | Default max generation length (default: 20000). |
TS_PDF_MAX_PAGES | Cap on PDF pages read during upload β text extraction and page-image rendering (default: 0 = all pages; also honored by the CLI's --pdf). |
VIDEO_SAMPLE_FPS / VIDEO_MAX_FRAMES | Frames sampled per second of video / optional upper bound on extracted frames. |
TS_FUSED_QKNORM_ROPE | Fused QK-Norm + RoPE CUDA kernel for Qwen 3.5/3.6 text prefill on the direct cuda backend (default on; 0 disables). |
TENSORSHARP_TEMPERATURE, β¦_TOP_K, β¦_TOP_P, β¦_MIN_P | Default sampling values when neither the flag nor the request body sets one. |
TENSORSHARP_REPEAT_PENALTY, β¦_PRESENCE_PENALTY, β¦_FREQUENCY_PENALTY, β¦_SEED | Default penalties and seed. |
TENSORSHARP_LOG_LEVEL / β¦_LOG_DIR / β¦_LOG_FILE | Logger level, directory, and file toggle (also honored by the CLI). |
DIFFUSION_STEPS / DIFFUSION_MAX_BATCH | DiffusionGemma denoising steps per block / max concurrent diffusion requests batched. |
TENSORSHARP_TP_DEGREE | Number of local GPUs to split the hosted model across (default: 1); the server also takes it as --tp N. Requires --backend cuda, ggml_cuda, or ggml_vulkan. β Multi-GPU & Multi-Node |
TENSORSHARP_TP_DEVICES | GPU ordinals the TP ranks map to, e.g. 0,2 (default 0..tp-1). GGML backends. |
TENSORSHARP_TP_NODE_ID / TENSORSHARP_TP_PEERS | Extend tensor parallelism across machines: this node's 0-based ID and the shared comma-separated host:port list of every node (also --tp-node-id / --tp-peers). Set both, or neither. The server can only be node 0 β the driver that serves HTTP; other nodes run TensorSharp.Cli workers. |
TS_KV_CACHE_REDIS_URL / TS_KV_CACHE_REDIS_TTL_MINUTES | Persist paged KV blocks to Redis for cross-session (and cross-process) reuse, and the entry TTL in minutes (default 1440; 0 = no TTL). CLI: --redis-url / --paged-kv-redis-url / --paged-kv-redis-ttl. |
TS_RESPONSES_STORE_REDIS_URL | Back the OpenAI Responses API store with Redis instead of process memory. CLI: --redis-url. |
The current binary listens on a fixed http://0.0.0.0:5000; the Docker Space images patch that constant to 7860 at build time.
Continuous-batching tunables
The scheduler / engine knobs are read at process start. Set them via the environment (or the --continuous-batching flags, which translate to them).
| Variable | Description |
|---|---|
TS_SCHED_DISABLE_BATCHED | 1 forces per-sequence KV-swap even when a model supports batching (= --no-continuous-batching). |
TS_SCHED_MAX_BATCHED_TOKENS | Per-step token budget (default 4096). |
TS_SCHED_MAX_RUNNING_SEQS | Maximum in-flight sequences (default 16). |
TS_SCHED_PREFILL_CHUNK | Maximum prefill tokens per step (default 1024). |
TS_SCHED_SOLO_PREFILL_CHUNK | Prefill chunk size when at most one sequence is in the system β caps all solo prefill chunks (default 8192). |
TS_SCHED_DECODE_QUANTUM | Decode tokens before a sequence switch (default 256 = block size). |
TS_SCHED_NUM_BLOCKS / TS_SCHED_BLOCK_SIZE | Physical blocks in the engine pool (default 256) / tokens per block (default 256). |
TS_SCHED_PREFIX_CACHE | 0 disables block-hash prefix sharing across requests. |
TS_<FAMILY>_BATCHED=0 | Per-model escape hatch (e.g. TS_GEMMA4_BATCHED=0) to fall back to per-sequence KV-swap. |
The full environment-variable surface (MLX tunables, MTP knobs, diffusion) is on the API Reference page and the Advanced page.