High-performance single-GPU inference for selected model checkpoints and GPUs.
C++
16
1,209 commits
updated Oct 9, 2026
Check out Infernix instead. It is my new, revised engine grown from this fork, with new features including support for the Qwen3.8-Flash-Next-NVIDIA-NVFP4 models (Dense8 and bit-exact conversions), and I recommend it over this fork.
AI disclaimer: Everything added to this fork, including most of this README, was written with AI (mostly Claude Opus 5.5, Qwen3.8-27B running on NInfer, plus a few other AI systems I’ve been testing). It is likely to be neither complete nor entirely accurate. This is hobby development.
This is a personal fork of Neroued/ninfer. It follows upstream
closely and adds changes on top: the history is upstream master (81c8ce09), the Windows port,
then one commit per fork change. The sections below explain what is different, grouped by topic,
with credit given as best my AI agents can where a change came from someone else. The upstream
README follows, copied unchanged, under the "Upstream README" heading. A huge thank you to Neroued
for creating NInfer!
The short version. Compared with upstream, this fork:
--host-context-mib N; --prefix-cache-file PATH
keeps the cache across restarts. It still beats upstream's new context cache (October 2026):
on the agentic benchmark below it serves 90.5 % of prompt tokens from cache against 85.8 %,
prefills a third fewer prompt tokens, cuts the average time to first token by 39 % and finishes
the workload 26 % sooner. Stop the server with Ctrl+C (twice) rather than by
closing the window, because Windows does not always leave enough time after a window closes to
save a large cache--use-original-prefix-caching--use-original-int8-prefill-kernel and
--use-original-nvfp4-prefill-kernel select upstream's INT8 and NVFP4 kernels; NVFP4 and K8V4
P×V runs on 8-bit Tensor Cores by default (5-7 % less end-to-end long-prompt prefill, KL
divergence from BF16 KV within 1.1× the FP16 form's), while INT8 KV keeps FP16 P×V unless
--prefill-8bit-pv asks for its 8-bit form, and --no-prefill-8bit-pv forces FP16 everywhere--kv-dtype vq2 and k4v2 (a quarter and three eighths of INT8's
KV memory, based on HyperQuant and cometkim's implementation),
which keep the most recent keys exact, always run P×V in FP16, and decode faster than INT8 at
long context--vision-offload on), based on the work of
Valeriy Selitskiy (iamwavecut)--rope-yarn-factor F, F from 1 to 4)--vram-headroom-mib N shrink the 1 GiB VRAM headroom left after --kv-capacity auto--default-thinking-budget N with
--thinking-budget-message "...")--tolerant-tool-calls recovers some
broken tool calls--log-colours on), a statistics panel at the bottom
(--log-stats-panel off removes it) and a --help screen organised by category, and can
rotate the request log by size (--request-log-max-mib N)I recommend using this with the NVIDIA NVFP4 artifact I’ve uploaded here, which runs a bit faster than the original artifact based on the Unsloth quant and takes up less VRAM: https://huggingface.co/wallawalla47/Qwen3.8-27B-NVIDIA-NVFP4-NInferV3
Prerequisites: Visual Studio 2026 (MSVC), the CUDA 13 toolkit, and FFmpeg + curl from vcpkg
(x64-windows, at C:\vcpkg); adjust the paths at the top of build_native.bat for your machine.
build_native.bat configure
build_native.bat build
Sources compile as UTF-8 (/utf-8), so the build works under any Windows system code page
(reported by Woesch-Nich; commit 965ad6f).
The server is build-windows\apps\Release\ninfer-serve.exe, with the FFmpeg, curl and zlib DLLs
copied next to it. The launch I use on a single 32 GB RTX 5090 (stop any other resident model
first):
ninfer-serve.exe qwen3_8_27b_nvfp4-nvidia.ninfer --host 127.0.0.1 --port 8080 --max-context 240000 --max-concurrency 2 --spec dflash2 --draft-tokens 7 --lm-head-draft --ngram-draft-tokens 15 --ngram-min-match 12 --kv-dtype int8 --preserve-thinking --host-context-mib 52000 --pending-timeout-ms 900000 --prefill-chunk 4096 --kv-capacity auto --vram-headroom-mib 0 --log-colours on --ngram-archive-mib 2048 --ngram-session-mib 256 --ngram-native-sessions --request-log-jsonl log.json --default-thinking-budget 16384 --thinking-budget-message "Considering the limited time available to the user, I must stop thinking now. Time to act:" --tolerant-tool-calls
Add --prefix-cache-file PATH to keep the prefix cache across restarts. Stop the server by
pressing Ctrl+C twice (the first press shows a prompt at the bottom of the console). The server then
cancels running and queued requests, saves the cache and exits; pressing Ctrl+C once more exits
without saving and deletes the unfinished file. ninfer-serve.exe --help lists every option by
category.
Running on another PC needs an RTX 50-series GPU (the build targets sm_120a) and an NVIDIA driver
of 580 or later (CUDA 13); no CUDA toolkit is needed. Copy the DLLs next to ninfer-serve.exe and
install the Visual C++ redistributable if it is missing.
The fork builds and runs on 64-bit Linux too, including WSL2 (tested on Ubuntu 24.04 under WSL2
with CUDA 13.4 and GCC 13.3). Prerequisites: a CUDA 13 toolkit, CMake 3.28 or newer, a C++20
compiler, Ninja, pkg-config, and the FFmpeg and curl development packages. On Ubuntu 24.04:
sudo apt-get install -y build-essential cmake ninja-build pkg-config libavformat-dev libavcodec-dev libavutil-dev libswscale-dev libcurl4-openssl-dev
cmake --preset release
cmake --build build -j
The server is build/apps/ninfer-serve and takes the same options as on Windows, for example the
launch line above with ./build/apps/ninfer-serve in place of ninfer-serve.exe.
Under WSL2 the GPU driver is the Windows NVIDIA driver (580 or later); do not install a Linux NVIDIA
driver inside WSL. NVIDIA's wsl-ubuntu CUDA repository stops at CUDA 13.3, so for 13.4 add its
ubuntu2404 repository and install only cuda-toolkit-13-4 (not cuda or cuda-drivers, which
pull in a driver). Artifacts on a Windows drive load slowly through /mnt/, so copy the .ninfer
file into the Linux filesystem first.
The agentic benchmark and perplexity below compare this fork with upstream + Windows port:
upstream master at abb7f14f, which replaced upstream's prefix cache with its new context cache
(incremental KV reservation, preemption and replay), plus only the Windows port (build 07f7a944
of the branch upstream-Windows-Port). The measured fork build is 580c91ea: this history before
the rebase onto 68c54356, whose one upstream commit changes only how the chat template keeps
cache boundaries when it trims text. A third arm runs the fork as it was before the rebase onto
abb7f14f (cfe8d4d4, its own cache on upstream's previous engine), to check that the rebase lost
nothing. Everything ran on an RTX 5090 under Windows with the official Qwen3.8-27B NVFP4 artifact
(qwen3_8_27b_nvfp4-official.ninfer), in October 2026.
The fork's hybrid prefix cache still beats upstream's new cache: it serves 90.5 % of prompt tokens from cache against 85.8 %, prefills 33 % fewer prompt tokens, answers 39 % sooner on average (48 % at the median) and finishes the workload 26 % sooner. Upstream's new cache closed much of the gap to its previous one (69 % served from cache in September's run), but it still re-prefilled whole prompts on main-session turns where the fork reused them.
The closed-loop suite in bench/agentic_ab/ replays three
coding-agent sessions plus eleven subagents: 130 requests with fan-outs, a concurrent subagent
pair, compaction, retries, an abort and a solo wrap-up, with prompts of 25K-135K tokens and
thinking on. Each arm's own answers are fed back as an agent client does, and the three main
sessions take their turns in lock-step rounds, so every build meets the same order of session
turns whatever its speed. Every build completed every request on each of three workload seeds
(42, 43, 44), which replay different observations.
Settings:
Fork arm: the Quick start launch flags:
--max-context 170000 --max-concurrency 2 --spec dflash2 --draft-tokens 7 --lm-head-draft
--ngram-draft-tokens 15 --ngram-min-match 12 --kv-dtype int8 --preserve-thinking
--host-context-mib 52000 --pending-timeout-ms 900000 --prefill-chunk 4096 --kv-capacity auto
--vram-headroom-mib 0 --ngram-archive-mib 2048 --ngram-session-mib 256 --ngram-native-sessions
--default-thinking-budget 16384 --thinking-budget-message "Considering the limited time
available to the user, I must stop thinking now. Time to act:" --tolerant-tool-calls
Upstream arm: the same flags minus those upstream does not have; upstream's
--host-context-mib 52000 gives its new cache the same host RAM:
--max-context 170000 --max-concurrency 2 --spec dflash2 --draft-tokens 7 --lm-head-draft
--kv-dtype int8 --preserve-thinking --host-context-mib 52000 --pending-timeout-ms 900000
--prefill-chunk 4096 --kv-capacity auto --default-thinking-budget 16384
Context: 170,000 tokens, the largest context the upstream build starts with under these
flags; at it the fork's device KV holds 223,424 tokens and upstream's 175,424. Sampling:
temperature 1.0, top_p 0.95, top_k 20, max_tokens: 64000 on agent turns.
Each cell is the mean over the three seeds, with the lowest and highest seed in brackets; changes are computed per seed against that seed's upstream run and averaged.
| Metric | Upstream + Windows port | Fork | Fork before the rebase |
|---|---|---|---|
| Average time to first token (s) | 4.2 (3.7-4.8) | 2.5 (2.2-2.7), −39.2 % | 3.2 (3.0-3.2), −23.5 % |
| Median time to first token (s) | 1.21 (1.09-1.31) | 0.63 (0.60-0.69), −47.5 % | 0.75 (0.54-0.87), −38.4 % |
| 90th-percentile time to first token (s) | 11.4 (9.6-13.4) | 7.5 (6.6-8.1), −32.2 % | 7.5 (6.9-8.5), −33.1 % |
| Average TTFT, continuing-session turns (s) | 4.00 (3.37-4.75) | 2.26 (1.86-2.54), −41.4 % | 3.18 (3.05-3.29), −18.8 % |
| Average TTFT, new long prompts (s) | 10.0 (9.9-10.0) | 6.5 (6.4-6.6), −34.5 % | 6.7 (6.6-6.8), −32.8 % |
| Prompt tokens served from cache | 85.8 % (85.1-87.0) | 90.5 % (89.9-91.2) | 90.9 % (90.6-91.3) |
| Prompt tokens prefilled | 817K (748-863K) | 547K (518-587K), −32.6 % | 526K (522-531K), −35.3 % |
| Main-session turns that re-prefilled the whole prompt (of 75) | 1.3 (1-2) | 0 | 0 |
| Subagent turns that re-prefilled the whole prompt (of 37) | 0 | 0 | 0 |
| Prefill tok/s, requests with no cache hit in any arm | 6,234 (6,152-6,306) | 8,482 (8,347-8,558), +36.1 % | 8,465 (8,360-8,589), +35.8 % |
| Prefill tok/s, the same requests from 32K tokens | 6,052 (6,020-6,089) | 8,316 (8,251-8,357), +37.4 % | 8,300 (8,246-8,388), +37.2 % |
| Output tok/s, one request decoding | 212 (208-216) | 219 (210-237), +3.4 % | 220 (198-258), +3.7 % |
| Decode rounds/s, one request decoding (engine speed) | 58.9 (58.3-59.8) | 61.7 (61.4-62.2), +4.7 % | 61.4 (60.9-61.7), +4.2 % |
| Tokens per round, one request decoding (acceptance) | 3.60 (3.53-3.69) | 3.55 (3.39-3.85) | 3.58 (3.22-4.19) |
| Output tok/s, two requests decoding (combined) | 360 (346-370) | 348 (339-359), −3.4 % | 376 (359-395), +4.5 % |
| Decode rounds/s, two requests decoding (engine speed) | 55.2 (54.9-55.5) | 54.9 (52.7-56.4), −0.7 % | 56.3 (54.9-57.7), +1.9 % |
| Decode rounds that ran two requests | 29.2 % (29.1-29.4) | 40.9 % (36.9-43.0) | 38.3 % (31.1-42.0) |
| Output tok/s, all decoding at the run's own batching | 254 (253-256) | 280 (273-283), +10.2 % | 281 (271-301), +10.7 % |
| Workload wall time (min) | 15.4 (13.1-18.1) | 11.1 (9.8-12.0), −26.0 % | 11.5 (10.1-12.3), −24.5 % |
The decode-saturation suite of tools/bench/run_serve_concurrency.py starts a fresh server at
each --max-concurrency C and decodes C requests at once, each up to 8,192 tokens, with DFlash2
K=7 and --lm-head-draft (no ngram drafting), stochastic sampling, --max-context 32768 --kv-capacity auto. The builds alternated point by point in two passes, C=1 to 8 and back. This
benchmark was measured in September 2026 against upstream d44ab584 + Windows port and the fork
rebased on it (build fe869b56), and has not been repeated since the rebase onto abb7f14f.
| C | Upstream tok/s | Fork tok/s | Fork time per decode round vs upstream (pass 1, pass 2) |
|---|---|---|---|
| 1 | 198.3 | 196.6 | −2.3 %, −2.4 % |
| 2 | 365.8 | 380.0 | −2.2 %, −2.3 % |
| 3 | 516.1 | 533.9 | −3.1 %, −3.1 % |
| 4 | 649.6 | 677.4 | −3.2 %, −3.2 % |
| 5 | 748.6 | 789.0 | −3.5 %, −3.5 % |
| 6 | 868.7 | 879.7 | −1.8 %, −1.7 % |
| 7 | 965.7 | 1002.6 | −1.4 %, −1.3 % |
| 8 | 1076.0 | 1092.2 | −1.3 %, −1.3 % |
Tok/s is the mean of both passes. The fork's decode rounds are 1.3-3.5 % faster at every concurrency, the same in both passes; tok/s also moves with speculative acceptance on the sampled text, which is why C=1 is lower despite faster rounds (2.99 against 3.08 tokens per round).
Commits: 2b765da, fba0928,
61de484.
Build this fork, then the upstream control from the branch upstream-Windows-Port
(details); stop any other server on the port first:
build_native.bat configure
build_native.bat build
git worktree add C:\ab\control\src upstream-Windows-Port
set AB_CONTROL_SRC=C:\ab\control\src
set AB_CONTROL_BUILD=C:\ab\control\build
bench\agentic_ab\build_control.bat configure
bench\agentic_ab\build_control.bat build
Agentic workload (about 1.7 hours for the two arms on three seeds). The runner reads
the model path and launch flags from AB_LAUNCH_BAT, calibrates the largest context the control
starts with, and writes report.md under profiles\bench\agentic_ab\. treatment is the fork
and control upstream; an optional alt arm runs another build (AB_ALT_EXE) or the fork with
other flags (AB_ALT_EXTRA_FLAGS, default --use-original-prefix-caching):
set AB_CONTROL_EXE=C:\ab\control\build\apps\Release\ninfer-serve.exe
set AB_LAUNCH_BAT=<a launch .bat with the fork flags above>
py -3.11 bench\agentic_ab\runner.py --arms treatment,control --seeds 42,43,44
Concurrent decode, one call per build (repeat --concurrency to sweep, or alternate single-point
calls between the builds as above):
py -3.11 tools\bench\run_serve_concurrency.py --serve build-windows\apps\Release\ninfer-serve.exe ^
--artifact q38=qwen3_8_27b_nvfp4-official.ninfer --mode dflash2_7 --suite decode-saturation ^
--max-context 32768 --kv-capacity auto --concurrency 1 --concurrency 8 --output profiles\bench\cc-fork
Upstream uses the same command with the control checkout's own copy of the script and its
ninfer-serve.exe, because its server writes an older request-log schema. On Windows that copy
needs the fork's wait_for_final_throughput (commit fba0928),
which reads the final statistics interval that Windows otherwise loses when the server is stopped.
Each topic lists everything that affects it, whether written here or taken from elsewhere.
"Upstream PR" means an open pull request on Neroued/ninfer that this fork merged before upstream
did.
Picking individual changes. This fork's
history is upstream master
(81c8ce09), then the Windows port, then one commit per fork change in dependency order, each
a whole feature with its fixes folded in. Each item below links to its commit, and each commit
message lists the earlier fork commits it builds on, so a change can be cherry-picked into another
fork together with those prerequisites.
Commit: 7487474.
Designed around how Qwen3.5-family models work: most of their layers are linear-attention (GDN) layers, whose recurrent state cannot be rebuilt from the KV cache, so resuming a prompt needs the KV of every earlier token plus a saved state at the exact token where the new prompt continues. The cache keeps the two apart and stores each as cheaply as it can. The design document explains the design and records every decision taken while building and running it, with the measurements behind it and the alternatives that were tried and reverted.
--kv-capacity defaults to
auto); --host-context-mib (default 8192, 0 = GPU only) is one pinned host RAM pool that
blocks and snapshots share, split by how much prefill time each entry saves; and
--prefix-cache-file PATH saves the host tier on shutdown and reloads it at startup (a smaller
host tier keeps the most valuable entries). A file from a different model, KV format or
ninfer-serve build is ignored and replaced.--host-context-mib is derived from --max-concurrency and --prefill-chunk;
--device-snapshot-slots, --cache-taps-per-request, --cache-tap-ladder and
--cache-tap-min-gap are optional overrides.7487474.--use-original-prefix-cachingUpstream's own context cache: private continuations and shared checkpoints with incremental KV
reservation, resource-pressure preemption and snapshot or replay recovery. --device-state-slots
requires --use-original-prefix-caching, and --host-context-mib sizes its shared Host budget.
Details: resource scheduling and context cache.
The fixes this fork carried for upstream's previous checkpoint-catalog cache (answer-sized KV
leases, recency eviction, lineage value, a single host budget, automatic long anchors, prefix
salvage on abort, a longer planning budget and shared-catalog reclaim) were retired with that cache
when upstream replaced it in abb7f14f.
--kv-dtype int8): a FlashAttention-2 style kernel
in which each warp keeps its query rows, scores and output in registers and accumulates P×V in
FP16. The prefill chunk is rounded down to whole GPU waves (4096 runs as 3584). Per attention
layer it takes 0.61-0.66× the time of upstream's INT8 prompt kernel on 3584-token chunks from an
empty context to 128K. On the full ninfer-ppl-1m-v1 corpus it is closer to BF16 KV than the
original INT8 kernel (4K windows: 4.8986 against 4.9027, BF16 KV 4.8948).
--use-original-int8-prefill-kernel keeps upstream's kernel.
Commit: fbe6a0c.ninfer-ppl-1m-v1 corpus moved
from 4.9077081 to 4.9076804 with 4K windows and from 4.9041197 to 4.9041624 with 64K windows, each
within 0.7 standard errors of the per-window differences. The partials took up to 64 MiB of
workspace: 576 fewer KV tokens (0.23 %) at startup with --max-context 140000; their bound is now
--prefill-split-workspace-mib (below).--prefill-8bit-pv
turns it on for INT8 and K4V2 KV, which default to FP16 P×V, and --no-prefill-8bit-pv
forces FP16 everywhere).
NVFP4 and K8V4 KV: the MX-FP8 tiled prompt kernel rounds each probability (as 256 p against its
tile's own row maximum, which the softmax reference follows) and each decoded V value to E4M3 and
multiplies them on block-scaled E4M3 Tensor Cores with FP32 accumulation, four times the rate of
upstream's FP16 P×V with FP32 accumulation; a per-tile power of two keeps V in E4M3's range and
returns through the MMA's block scale. Its error against exact attention over the stored values is
about 15 times the FP16 form's (relative L2 0.026-0.037 against 0.0017), but the divergence it
causes does not follow that: on a Q6 artifact with BF16 activations, where the measurement is not
buried under A4 activation noise, its KL divergence from a BF16 KV reference over the full corpus
at 64K context is 0.0203 against the FP16 form's 0.0205 (NVFP4) and 0.0141 against 0.0134 (K8V4),
within 1.1× of FP16 in every context bucket and within 0.06 pp of top-1 agreement. Per attention
layer, against the FP16 form, 3584-token chunks take 19 % (K8V4) and 18 % (NVFP4) less time over
128K keys and up to 20 % less elsewhere; end to end, one request's prefill (DFlash2, 4096-token
chunks) is 4.7-5.1 % (NVFP4) and 5.0-5.4 % (K8V4) faster at 64K tokens and 6.8-7.1 % and 7.5-7.7 %
at 128K (two runs differing by 0.4-0.7 pp).
Perplexity on the full ninfer-ppl-1m-v1 corpus: 4.89624 → 4.91573 (K8V4) and 4.91651 → 4.91384
(NVFP4) with 4K windows, 4.84164 → 4.97411 and 4.89965 → 4.87335 with 64K windows; the median
stream moved by under 0.002 nats per token, but at 64K single streams moved by up to 0.36 (K8V4) —
the corpus moves that much for any small numeric change, which is why the KL comparison above is
the direct measure.
VQ2 and K4V2 KV: the vector-quantized prompt kernel runs the same integer form over the tile's
INT8 V rows, scaling each group's probabilities by that key's row scale and rounding them to
u8 codes against their row maximum. VQ2 takes it by default: against a BF16 KV reference on a
BF16-activation model its KL divergence moves 0.0367 → 0.0378 (1.03x, every bucket within
1.05x, top-1 within 0.04 pp) while prefill of one request is 5.0 % faster at 64K tokens and
6.8 % at 128K. K4V2 stays opt-in: its divergence moves 0.0212 → 0.0232, 1.09x overall but
1.11x in the 32-64K bucket, against the 1.10x bound the other formats were held to, for
5.3 % and 7.2 % the same way. Per attention layer the two are 3.3-18 % faster at 3584-key
chunks over long context (geomean 0.97 over 256-3584 keys at 0-64K) and up to 5 % slower for
512-1024-key calls over an empty context.
INT8 KV (--prefill-8bit-pv, off by default): the fast kernel multiplies P×V on INT8 Tensor Cores
(4× the FP16 rate with FP32 accumulation on RTX 5090); each row's probabilities, scaled by the V
group scale, are quantized to 8-bit codes per 64-key tile and multiplied against the stored INT8 V
codes, which stay exact. Per attention layer, 3584-token chunks take 3.5 % less time from an empty
context and 8-11 % less from 16K to 128K; end to end, prefill is 1.9-2.6 % faster at a 16K-token
prompt, 2.6-4.2 % at 64K and 4.0-5.1 % at 128K (two runs). Unlike the E4M3 form it is not free: probabilities below
half a code step of their tile's largest round to zero, and on the same Q6 artifact the KL
divergence from BF16 KV about doubles, 0.0051 → 0.0111 at 64K, with all sixteen streams worse and
the growth concentrated in longer context (the 32-64K bucket ×2.28, top-1 98.9 % → 98.6 %). The
fast kernel's FP16 P×V matches the original INT8 prompt kernel there (0.0051 against 0.0050), so
the whole cost is the 8-bit P, and FP16 P×V stays INT8's default.--kv-dtype nvfp4 over more than 768 cached
keys, re-tuned from 2048 with the decode-once kernel: forced at every visible count the fast
kernel is within 0.03 % of the per-shape best above 768 visible keys in both head geometries
(24 query heads x 4 KV heads and 16 x 2), where 2048 costs 2.3-2.7 % overall and up to 64 % in
its worst cell):
QK runs on block-scaled FP4 Tensor Cores (8× the FP16 rate on RTX 5090) straight from the stored K
codes, with Q as two NVFP4 terms (0.9 % RMS error). It now runs on the MX-FP8 tiled kernel (below)
with NVFP4 keys: each 64-key V tile is decoded once per CTA into shared memory instead of in every
warp's registers. Per attention layer it takes 6-26 % less time than the previous fast NVFP4
kernel wherever it runs (3584-token chunks 8 % less over an empty context, 15 % over 8K-32K and
17 % over 128K keys), and 0.32-0.63× the time of upstream's NVFP4 prompt kernel; end to end,
prefill of one request (DFlash2) is 2.1 % faster at 16K tokens, 3.6 % at 32K, 6.0 % at 64K and
9.2 % at 128K than with the previous fast kernel, both with the previous kernel's 64 MiB split
budget; the bound is now --prefill-split-workspace-mib (below). Perplexity:
4.90646 → 4.91651 with 4K windows and 4.90946 → 4.89965 with 64K windows, moves of the size the
corpus shows for any small numeric change (below). --use-original-nvfp4-prefill-kernel keeps
upstream's kernel.
Commit: ed32d76.--prefill-split-workspace-mib (next).--prefill-split-workspace-mib, default
256 MiB). The fast INT8 and NVFP4 prompt kernels and the FP8/K8V4 prompt kernel split a launch's
keys across SMs when its row blocks alone would leave SMs idle, which needs FP32 partial rows in
workspace. INT8 and NVFP4 were capped at 64 MiB and FP8 and K8V4 were unbounded (about 550 MiB with
4096-token chunks; upstream's default 1024-token chunks need 169 MiB). One bound now applies to all
four, and a launch that would need more runs fewer splits. Per attention layer against unbounded
(3 interleaved passes, widths 256-4096 over 0-128K cached keys), 1024-2048-token chunks take
17-25 % longer at 64 MiB over 32K-128K keys (27 % at worst), 2-5 % at 128 MiB and under 1 % from
192 MiB, the same for all four formats; at the 256 MiB default 3-4K-token chunks over 128K keys
still take up to 5 % longer, and 3584-token chunks and short contexts are unaffected. These widths
are follow-up turns over a cached prefix and --prefill-round-robin steps beside another request;
a fresh 64K or 128K prompt prefills in the same time within 0.3 % end to end. The startup
workspace with 4096-token chunks and --max-context 220000 is 515 MiB at 256 MiB against 380 MiB
at 64 MiB: about 4K fewer INT8 KV tokens (7.7K NVFP4); FP8 and K8V4 gain about 300 MiB of KV
cache against unbounded.--prefill-chunk is now rounded down to whole attention waves for them too (4096 runs as
3584), which keeps each full chunk's attention free of a mostly idle last wave. One request,
DFlash2: FP8 prefill 1.3 % faster at 32K tokens, 1.2 % at 64K and 0.8 % at 128K (same-session A/B,
two passes), K8V4 0.7-1.6 %, NVFP4 1.4-2.2 % at 128K and within noise at 32-64K.7df17be.rmsnorm_rope route (#273, Michael Dementii), a
Q6 34,816×5120 shape for the fused MLP gate_up (#284, bingchengcc),
and, adapted from llmq (IST-DASLab, Erik Schultheis) by
DuncanBetts, a fused NVFP4 RMSNorm + quantise for the attention
input projection (#305) and a single-pass target log-probability kernel (#307).
Commits: 32e875a, 426a0b6, e903843,
045655c.RESEARCH_NOTES.md.
Commit: 24a9f4f.339543d, dd17eac.473df20, d5e6c8f, ee2e1d2.qwen3_8_27b_nvfp4_nvidia recipe): the drafter's MLP gate/up
projections are stored as NVFP4 (new nvfp4_mse quantizer) instead of Q8 and run with 16-bit
activations through the fused SwiGLU kernel, which now covers every width (sliced kernels to 32
tokens, a Tensor Core route above). On the decode-saturation suite (DFlash2 K=7, n-gram 15/12,
INT8 KV) rounds are 1.6 % shorter with one request (+1.2 % tokens/s) and 0.2 % shorter with four
(tokens/s within noise). Acceptance moved by -0.35 % on 24 sampled agent prompts and -0.4 %
(tokens per round) on the suite. Existing artifacts must be reconverted to use it.c78888f.--max-concurrency above 1 and builds each prompt's index while the prompt is prepared, off
the engine worker. See ngram copy proposals.
Commits: 5a0b6db, 275ebe3,
55d5ea0.--draft-tree-nodes auto or a per-batch-size list):
instead of one proposal path, a round verifies a small tree of proposals that the GPU builds every
round from the DFlash2 drafter's candidate lattice, spending the extra columns where the drafter
is least sure. Sampling stays exact (recursive rejection sampling over each node's alternatives),
and the accepted path is moved onto the main-chain columns so the KV cache, GDN state and drafter
see an ordinary round. It works with every --kv-dtype on artifacts whose GDN input projections
are single FP8 or NVFP4 matrices. A column buys the same acceptance at any batch size but costs
more of the round as the batch and the context grow, so auto measures both while it runs: the
round time of each width per batch size and context length, and, from every tree round's accepted
path, the tokens each narrower tree and the chain would have emitted on the same text. Each round
then verifies the chain or a tree of K+5 or K+9 columns, whichever gives the most tokens per
second. Each round computes every tree column's GDN recurrence once, in one depth-first walk per
row, so 12- and 16-column tree rounds cost 0.4-0.6 % less than replaying every root-to-leaf path
with one request, 1.7-2.0 % with two and 2.9-4.0 % with four. On the decode-saturation suite
(DFlash2 K=7, n-gram 15/12, INT8 KV) auto decodes an estimated 6.9 % faster with one request and
4.4 % with two (measured before the walk), and about 2.9 % with three and 2.7 % with four (from
2.0 % and 0.3 % before the walk and the LM head change above, in the same runs); at 128K tokens of
context it loses 0.6-2.3 % in a fresh process (a fixed 16-column tree loses 4.2-5.4 %), and on
text the drafter already predicts it keeps chain verification. Below about 64K tokens the fixed
table 16,12,12,0 gains up to 2 points more (7-8.5 % with one request and 4-5 % with two on INT8,
K8V4 and NVFP4 KV) and keeps seeded one-request runs reproducible, which auto does not, since
its choice depends on measured time. See tree
verification.41b90af.--kv-dtype vq2 and k4v2Two vector-quantized KV formats. vq2 stores K and V in 2 bits per value, and k4v2 stores K
in 4 bits and V in 2. Per token and KV head they take 132 and 196 bytes, against 528 for INT8 and
288 for NVFP4. For the 27B models' attention layers, 240K tokens of context need 1.9 GiB of KV as
vq2 and 2.8 GiB as k4v2, against 7.6 GiB as INT8 and 4.1 GiB as NVFP4, so the same VRAM holds
three or four times the context or requests. How it works:
vq2, then store every 8 values as one 16-bit code (one of 512 trained magnitude
patterns plus 7 sign bits, the 8th sign set by parity);k4v2 stores 4-bit Lloyd-Max levels instead;Recent keys stay exact. The attention sinks (the first 64 tokens) and the 768 tokens before each query are read from INT8 copies held in the request's state. Which keys a query reads exactly depends only on positions, so a context reads the same values however it was split into prefill chunks, decode steps or cache reuse points. These copies take about 40 MB per state slot for the 27B models; each running request, each GPU checkpoint slot and each host checkpoint image holds one.
Quality (perplexity at 64K context, 32K stride, ninfer-ppl-1m-v1 quick corpus, NVIDIA NVFP4
27B artifact):
| KV | Perplexity | vs BF16 |
|---|---|---|
| BF16 | 4.1661 | |
| INT8 | 4.1728 | +0.16 % |
| NVFP4 | 4.1764 | +0.25 % |
k4v2 | 4.1709 | +0.12 % |
vq2 | 4.1859 | +0.48 % |
Speed (one request, official NVFP4 27B artifact, RTX 5090, ninfer_bench against INT8 measured
in the same run):
| Run | Format | Prefill | Decode |
|---|---|---|---|
| 32K context | vq2 / k4v2 | -4.5 % | +1.6 % / +1.4 % |
| 128K context | vq2 / k4v2 | -7.3 % / -8.7 % | +7.4 % / +6.7 % |
| DFlash2 K=7, 32K context | vq2 / k4v2 | -4.2 % / -4.4 % | -3.4 % / -3.6 % |
In the DFlash2 rows, draft acceptance is 0.82 for both formats against INT8's 0.85.
Per attention layer, against INT8 measured in the same run (ninfer_causal_softmax_attention_bench,
40 calls, cold, first row / four rows):
| Call | 2K keys | 8K keys | 32K keys | 128K keys |
|---|---|---|---|---|
| one-token decode | 1.54x / 2.23x | 1.19x / 0.96x | 0.80x / 0.62x | 0.62x / 0.59x |
| 4-column verification | 1.61x / 2.21x | 1.23x / 1.07x | 0.89x / 0.75x | 0.76x / 0.73x |
| 8-column verification | 1.54x / 2.48x | 1.23x / 1.42x | 1.09x / 1.10x | 1.04x / 1.03x |
| 16-column verification | 1.71x / 3.29x | 1.28x / 1.64x | 1.30x / 1.31x | 1.22x / 1.33x |
Below 32K keys these formats pay for expanding their codes; from 32K, calls of up to four columns
overtake INT8 at 0.6-0.9x, because the attention reads a quarter of the KV bytes. Wider
verification stays above INT8 at 1.0-1.4x, where its G64 kernel fills the SM better. k4v2 is
within about 0.05x of vq2 except on 16-column verification, where it is 0.1-0.4x slower
(1.4-1.9x at 8K keys and below). Prompt attention is 1.2-1.3x at 32-128K keys
and 1.4x at 8K against INT8's fast prompt kernel (896-3584 columns, 20 calls), and on short
prompts the encoding of each new row costs more than the attention.
They work with the hybrid and original prefix caches (a restored checkpoint reads exactly what the original request read), cache files, MTP, DFlash2 chain and tree verification, n-gram drafting, concurrent requests, CUDA Graphs and vision.
Based on the HyperQuant KV cache (arXiv 2606.23406) and the implementation of it by cometkim (Hyeseong Kim). Measured on this model's K and V rows, a fixed-rate 8-value code beat HyperQuant's E8 lattice with Rice coding at fewer bytes. Keeping recent keys exact mattered more than either, so the fork implements the fixed-rate code with an exact window.
<function name="…">, <invoke>, <function_calls>, short <param> tags), also while
streaming. Upstream PR #300 by pkochubey (upstream issue
#276). Commit: dffd56b.</parameter> stays in the value
(upstream PR #318). Commits: db9a6f6, da6c4d8.</think> no longer ends the reasoning early: it only ends the reasoning when a line
break or the end of the turn follows. Adapted from upstream PR #309 by Fedor Suchkov.
Commit: c65feb9.--tolerant-tool-calls keeps a good call followed by junk, a final call cut off by the
output limit (if a parameter is complete), repairs a missing > after the function name, and
returns calls to undeclared tools. By David Oelfke in the gzenz/ninfer fork, ported onto this
fork's parser. The request log's tool_call_parse.tolerant_recovered shows when it rescued a
call (from giveen's giveen/ninfer-ext). It also returns complete calls a model strands in
thinking it never closed before ending its turn (free tool output, tool_constraints:"auto";
the default constrained output cannot end inside thinking), logged as
tool_call_parse.recovered_from_reasoning. Based on Woesch-Nich's PR #3 and Hundsbuah's review
of it. Commits: 8c3409a, 41a7df0,
813355b.--chat-template gains the
froggeric v22.5 template. Commits: 41b75f9,
f8e2273./v1/models (upstream PR #162 by
Hector Ramon Jimenez (hecrj)) and ignore_eos on chat
completions (upstream PR #197 by Thireus).
Commits: ae93169, c2881ad.custom tools, a custom tool choice as a
named one, tool names up to 256 bytes, --usage-chunk-choice): the
serving commits of upstream PR #316 by paq85 (Damian Sromek).
Commit: 4326af1.reasoning.summary,
include: ["reasoning.encrypted_content"]): upstream PR #295 by
Macasacker, based on an earlier PR by
Sha1rholder. Commit: a755a75.0c0ce70.
Upstream now enforces response_format and tool_choice through constrained decoding and
reports a cut-off tool call as cut off, so the fork's accept-only versions of those were dropped
in the rebase onto 81c8ce09.max_tokens is
accepted, and API routes answer under a doubled /v1 prefix (a base URL ending in /v1).
Forced, named and strict tool choices (upstream issue #223) are now enforced by upstream.
Commits: c5c6098, 5eb36c5.thinking.display: "omitted" is accepted instead of rejected: Thinking blocks
come back with empty text and the reasoning in their signature, which is restored when the
client sends the block back, so retained thinking and prefix-cache reuse are unchanged. Ported
from giveen's change in giveen/ninfer-ext. Commit: a3b2b64.48fc625.07aa6ff, 3fc84ae,
5d01a16, 8162985,
39b7f1a.21ef823, b278ba5, 7a75da4,
713f802.a6b1e48.qwen3_8_27b_nvfp4_nvidia recipe storing the output head as FP8; Quasar NVFP4 conversion
with DFlash2 heads and an indexed proposal head; third-party tokenizer settings rebuilt during
conversion. Commits: 6bd8813, 710aef5,
23cdc94.nvfp4_absmax and nvfp4_mse conversion methods quantize BF16 sources to NVFP4 for
16-bit-activation parents; the NVIDIA recipe uses them for the DFlash2 drafter MLP. nvfp4_mse
picks each 16-value group's scale from all 126 E4M3 values by squared error (based on
giveen's scale sweep in giveen/ninfer-ext): on the drafter's gate projection the relative
error is 0.0812, against 0.0847 for the earlier best of five targets and 0.0952 for absmax.
Commit: deeb2b3.qwen3_8_27b_q6 recipe and a grouped_mse scale-search method for groupwise
quantisation; Q8 MTP and a general BF16 GEMM fallback for shapes without a dedicated
kernel. Commits: 426a0b6, b41b0fb,
6a6fc4c, e93644e.--rope-yarn-factor F for YaRN context extension (F from 1 to 4, up to 1M tokens of
context). Commit: 3b15b3b.--vision-offload on keeps the vision tower in pinned system RAM instead of VRAM and streams
it to the GPU while an image is encoded, and --vision-max-merged N bounds the merged vision
tokens per image or video. Based on the original work by
Valeriy Selitskiy (iamwavecut), rewritten for this engine.
Commit: 8dd1f4a.upstream-Windows-Port. Commits: 608e70b,
74d64cc.--log-colours on colours the console statistics, and a session statistics panel
beneath the log shows session and last-ten averages of TTFT, cache hit rate, prefill and decode
speed, the mean decode batch and drafter acceptance (--log-stats-panel off removes it); its
n-gram columns appear only with n-gram drafting. Engine messages and FFmpeg's log are ordinary
log records that scroll above the panel.
Commits: 73767bc, 1739b2f,
128dc5e, 493babf, 43421e5.--help by category on ninfer-serve and the ninfer CLI, with separate sections
for the two prefix caching systems. Commit: 27bbd2b.--vram-headroom-mib N sets how much GPU memory --kv-capacity auto leaves spare (upstream
always leaves 1 GiB). Commit: eb17df4.c4df956.--thinking-budget-message S sets the message inserted when a request reaches its
--default-thinking-budget N. Commit: fb6b334.--request-log-max-mib N rotates the --request-log-jsonl file once it reaches N MiB,
keeping --request-log-keep K older files (default 4); each new file starts with a copy of the
server's start record. Based on the rotation by
Gideon Zenz (gzenz) in the gzenz/ninfer fork.
Commit: e0b0968.This fork is rebased onto upstream master whenever upstream moves; the latest is 81c8ce09
(October 2026). Where upstream rewrote code this fork had changed, the rebase starts from
upstream's version and carries the fork's change onto it only where an A/B test on the RTX 5090
shows the fork's version is faster. Once upstream adopts a change, it leaves this list.
The rebase onto 81c8ce09 brought in upstream's constrained decoding, which runs each MTP and
DFlash round as a Forward and a Finish CUDA Graph with the grammar masks built on the Host between
them. The fork's n-gram copy families and DFlash2 tree families became Forward/Finish pairs too (see
item 16 of the short version). Decode at one request on the NVIDIA NVFP4 artifact with K8V4 KV
(ninfer_bench -pg 2048,256 -r 5, two alternating passes per arm) was unchanged against the fork
before the rebase: no speculation 82.1 vs 82.0 tok/s, MTP K=3 252.3 vs 252.3, DFlash2 K=7 500.6
vs 500.7, DFlash2 with n-gram 31/12 2042.5 vs 2042.4, and 8192-token prefill 11,595 vs 11,574
tok/s (every difference within 0.2 %, the spread between passes of one build). Several requests
were not compared, because the earlier benchmark tool ran one request only.
Dropped in the rebase onto 81c8ce09, because upstream now does the same thing properly:
response_format json_object/json_schema without enforcing it and accepting
Anthropic forced, named and strict tool choices as advisory: upstream enforces both through
constrained decoding.Dropped in the rebase onto abb7f14f, because upstream now does the same thing:
--prefill-round-robin) and concurrent staged-prefill lanes (by
David Oelfke in the gzenz/ninfer fork): upstream now prefills several lanes and rotates prefill
between them by default. The narrow prefill steps beside other requests were not carried over.Dropped in the rebase onto d44ab584, with the measurement that decided each:
qwen3_8_27b_nvfp4_nvidia recipe
in tools/convert/official_recipes.py. The weights are imported bit-exact except the output
head (NVFP4 → row-scale FP8), with the DFlash2 draft model and an indexed 131,072-row proposal
head for --lm-head-draft.tools/convert/quasar_nvfp4.py, with the same
DFlash2 draft model and proposal head.Both are single-file .ninfer artifacts for an RTX 5090 (sm_120a); each Hugging Face page has
the creation outline and conversion report. The Quick start launch works
for either.
This fork is developed with Claude Code. If you would like to help pay for that subscription, you can sponsor me on GitHub.
Please support Neroued, the creator of the original NInfer, on Ko-fi.
A big thank you to all the contributors to upstream NInfer and to the forks this one draws on — Neroued, Michael Dementii, Minnnn, DuncanBetts, Thireus, remesis, Valeriy Selitskiy (iamwavecut), Hector Ramon Jimenez (hecrj), giveen, bingchengcc, pkochubey, paq85, Macasacker, Sha1rholder, adubkov, Gideon Zenz (gzenz), cometkim (Hyeseong Kim), Woesch-Nich, Hundsbuah, David Oelfke, Fedor Suchkov, Yunado, and everyone else whose pull requests, reviews and commits made this fork possible — and a particular thank you to Neroued for creating NInfer, maintaining upstream so well, and for the work this branch builds on.
Everything below is a copy of the upstream
NInfer README as of the upstream
commit this fork is rebased on (81c8ce09), unchanged except for one added link to
the fork's ngram copy proposals guide.
Selected checkpoints. Maximum single-GPU inference performance.
NInfer is a from-scratch C++/CUDA inference engine for Qwen3.5 Dense and MoE architectures on a single NVIDIA GeForce RTX 5090. It runs text, image, and video prompts through a local CLI or OpenAI-/Anthropic-compatible HTTP APIs. The runtime is deliberately specialized: one GPU, one resident model, and one to eight execution lanes fixed at startup.
Five official artifacts are available. The quick-start commands use Qwen3.8-27B NVFP4.
| Model | Weights | Artifact | Download and model card |
|---|---|---|---|
| Qwen3.6-27B | groupwise-int | qwen3_6_27b.ninfer | Qwen3.6-27B |
| Qwen3.6-27B | nvfp4 | qwen3_6_27b_nvfp4.ninfer | Qwen3.6-27B NVFP4 |
| Qwen3.8-27B | groupwise-int | qwen3_8_27b.ninfer | Qwen3.8-27B |
| Qwen3.8-27B | nvfp4 | qwen3_8_27b_nvfp4.ninfer | Qwen3.8-27B NVFP4 |
| Qwen3.6-35B-A3B | groupwise-int | qwen3_6_35b_a3b.ninfer | Qwen3.6-35B-A3B |
Each v3 .ninfer artifact carries model configuration, encoded weights, logical bindings and
frontend resources. Runtime execution uses those facts with the implemented model and Op
capabilities. You can also convert your own weights, reuse an official
recipe or choose another supported mixture of formats.
The current engine requires v3 artifacts. Existing official v2 downloads can be upgraded locally without downloading the weights again.
NInfer requires 64-bit Linux, an NVIDIA GeForce RTX 5090, a CUDA toolkit supporting sm_120a,
CMake 3.28 or newer, a C++20 host compiler, Ninja, pkg-config, FFmpeg development libraries
(libavformat, libavcodec, libavutil, and libswscale), and libcurl >= 7.85.
CUDA 13.1 is the validated development toolkit; CMake does not impose a CUDA version floor.
The build rejects CUDA architectures other than sm_120a.
Build the product binaries:
git clone https://github.com/Neroued/ninfer.git
cd ninfer
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build -j
Tests and benchmarks are excluded from the default build. cmake --preset release configures
the same product build; cmake --preset dev also enables tests and benchmarks and finds a
Python 3 interpreter. Both presets use build/ and explicitly reset the build options.
Machine-specific compiler and Python paths belong in the ignored CMakeUserPresets.json.
See build organization and configuration for details.
There is no install target or packaged binary distribution; run NInfer from its source build tree. Python tools run independently of CMake; the standalone HBM probe has its own build command.
Download the artifact used by this example with the Hugging Face CLI:
hf download neroued/Qwen3.8-27B-nvfp4-NInfer \
qwen3_8_27b_nvfp4.ninfer \
--local-dir models
Start a long-running text/agent server with two execution lanes and prefix caching:
./build/apps/ninfer-serve models/qwen3_8_27b_nvfp4.ninfer \
--max-context 240000 \
--kv-capacity 240000 \
--max-concurrency 2 \
--kv-dtype fp8 \
--device-state-slots 2 \
--spec mtp --draft-tokens 3 \
--lm-head-draft \
--preserve-thinking
Each request has a 240,000-token logical ceiling. A shared 240,000-token Device KV pool serves resident requests and retained prefixes. Requests acquire KV pages as execution advances; under pressure, the scheduler can pause a request and resume it later. The profile provides two extra Device StateImages and the default shared pinned Host budget: 8 GiB plus eight model StateImages, used for retained state, KV and pause snapshots.
Send an OpenAI-style request:
curl http://127.0.0.1:8080/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{
"model": "qwen3.8-27b",
"messages": [{"role": "user", "content": "Reply with one short sentence."}],
"max_tokens": 64
}'
Run a one-shot CLI request with a 32,768-token allocation:
./build/apps/ninfer models/qwen3_8_27b_nvfp4.ninfer \
--prompt "Explain prefill and decode, then give a concise conclusion." \
--max-context 32768 \
--max-new 8192 \
--kv-dtype fp8 \
--spec mtp --draft-tokens 3 \
--lm-head-draft
Answer content is written to stdout. Human-readable startup/runtime diagnostics and the CLI-owned
reasoning, timing, throughput, memory, and speculative-decoding report are written to stderr;
reasoning and the result report remain unprefixed product output. On a terminal, weight
materialization uses one transient progress line followed by a compact Engine-ready summary.
Redirected stderr receives persistent readable progress without terminal control sequences. Use
--log-level debug for complete startup detail. Option and local input errors remain direct command
diagnostics. Use --messages FILE and --vision for structured image/video input; see the
CLI guide and committed examples.
A reusable checkpoint combines KV with the complete continuation state at an exact token frontier. The engine retains completed conversation endpoints and stable input boundaries for multi-turn and agent reuse. Inactive checkpoints share Device and pinned Host capacity; pressure reclaims retained resources before pausing resident requests. Paused requests resume from a snapshot or rebuild their state by replaying already committed tokens.
See Resource scheduling and context cache for the algorithm and Serve TTFT benchmark for public-HTTP coverage of hot reuse, Host resume, eviction, shared prefixes, scheduling boundaries, and multimodal load.
Published measurements use an RTX 5090. The performance index links to per-model run records and the measurement rules. The tables below are excerpts from those detailed results. Qwen3.8 uses FP8 E4M3 row-256 KV; Qwen3.6 uses INT8 group-64 KV.
Saturated decode used CUDA Graphs, MTP3, and one 8,192-token generation per active request. Throughput uses aggregate committed decode tokens from complete intervals whose actual decode batch equaled the configured concurrency. Acceptance covers the complete request wave; these rates are steady decode (tok/s).
| Model profile | C=1 tok/s / accept | C=2 tok/s / accept | C=4 tok/s / accept | C=8 tok/s / accept |
|---|---|---|---|---|
Qwen3.6-27B groupwise-int | 185.8 / 68.2% | 247.0 / 69.0% | 309.5 / 68.4% | 535.0 / 68.3% |
Qwen3.6-27B nvfp4 | 202.4 / 69.3% | 399.7 / 71.4% | 699.7 / 69.3% | 1,146.9 / 68.6% |
Qwen3.6-35B-A3B groupwise-int | 642.5 / 68.6% | 907.2 / 66.3% | 1,213.5 / 69.6% | 1,380.7 / 68.0% |
Qwen3.8-27B groupwise-int | 136.5 / 44.4% | 253.3 / 45.2% | 398.1 / 46.1% | 582.4 / 46.4% |
Qwen3.8-27B nvfp4 | 147.7 / 46.2% | 291.0 / 48.7% | 522.2 / 45.8% | 922.4 / 46.1% |
The serial serving corpus used CUDA Graphs, a 1,024-token prefill chunk, and five fixed seeds after warm-up. The table keeps one short-prefill, one extreme-prefill, and one structured-output MTP3 point for each published profile; the full context and scenario matrices are linked from each model below.
| Model profile | 7,680-token prefill | 260,096-token prefill | Structured MTP3 decode |
|---|---|---|---|
Qwen3.6-35B-A3B groupwise-int | 17,705.4 tok/s | 5,247.0 tok/s | 779.6 tok/s |
Qwen3.6-27B groupwise-int | 3,218.1 tok/s | 1,614.8 tok/s | 193.0 tok/s |
Qwen3.6-27B nvfp4 | 11,191.5 tok/s | 2,510.6 tok/s | 252.2 tok/s |
Qwen3.8-27B groupwise-int | 3,331.9 tok/s | 2,139.4 tok/s | 214.7 tok/s |
Qwen3.8-27B nvfp4 | 12,819.1 tok/s | 4,016.4 tok/s | 231.7 tok/s |
Capability scores were measured through NInfer's OpenAI-compatible serving route with thinking enabled, MTP3, and EvalScope 1.9.0 (0-shot, rule scoring, one sample per problem):
| Model profile | AIME 2025 | AIME 2026 | GPQA-Diamond | ERQA | RealWorldQA |
|---|---|---|---|---|---|
| Qwen3.6-27B groupwise-int | 86.67% | 93.33% | 86.87% | — | — |
| Qwen3.6-27B NVFP4 | 93.33% | 93.33% | 84.34% | — | — |
| Qwen3.6-35B-A3B groupwise-int | 90.00% | 90.00% | 85.35% | — | — |
| Qwen3.8-27B groupwise-int | 96.67% | 96.67% | 87.37% | 66.25% | 82.22% |
| Qwen3.8-27B NVFP4 | 96.67% | 96.67% | 90.40% | 66.25% | 83.53% |
The Qwen3.6 rows used temperature 0.6 and presence penalty 1.0; the Qwen3.8 rows used temperature
1.0 and presence penalty 0.0. Multimodal evaluation used --vision and an 81,920-token context
limit. Text evaluation used 262,144 tokens except Qwen3.8-27B NVFP4, which used 252,928 tokens to
fit the RTX 5090 after weights. Each score is one sample per problem; model cards contain the
correct/total counts and evaluation notes.
GPU residency is fixed at process startup. --spec selects speculative decoding residency, and
--vision independently selects Vision residency. Qwen3.6-35B-A3B DFlash can be combined with
Vision; it accelerates generated-text decode after multimodal prefill, not Vision encode itself.
Build the runtime image on a host with the NVIDIA Container Toolkit:
docker build --tag ninfer:local .
Mount the downloaded model and run the same example server profile:
docker run --rm \
--gpus '"device=0"' \
--publish 8080:8080 \
--volume "$PWD/models:/models:ro" \
ninfer:local \
ninfer-serve /models/qwen3_8_27b_nvfp4.ninfer \
--host 0.0.0.0 \
--max-context 240000 \
--kv-capacity 240000 \
--max-concurrency 2 \
--kv-dtype fp8 \
--device-state-slots 2 \
--spec mtp --draft-tokens 3 \
--lm-head-draft \
--preserve-thinking
The official artifacts provide the following capabilities, with optional components enabled at startup:
The 35B-A3B target additionally supports DFlash with draft windows from one to fifteen for Text and
image/video Vision prompts. Qwen3.8-27B artifacts with the DFlash2 companion weights support
--spec dflash2 --draft-tokens 7 for the same Text/Vision Engine path, with draft counts 1..15
and either full or optimized proposal heads.
The product boundary remains intentionally small:
--max-context is each sequence's logical limit. --kv-capacity sizes the shared Main Text KV pool
used by active requests and retained prefixes; auto resolves the largest legal capacity at
startup from the memory remaining after weights while keeping 1 GiB of sizing headroom. Explicit
capacities remain fixed for the process lifetime.
Run the relevant --help for the exact current option contract.
NInfer is a personal project that I develop out of interest. If you find it useful and would like to support its continued development, you can support the project on Ko-fi.
Support is entirely voluntary. It is not a purchase or investment and does not come with financial returns, promised services or features, or a role in project decisions. The project's direction, priorities, technical choices, and release schedule remain independently determined by the maintainer.
NInfer is licensed under the Apache License 2.0.
The published artifacts are derived from
Qwen/Qwen3.6-27B,
Qwen/Qwen3.8-27B, and
Qwen/Qwen3.6-35B-A3B. The Qwen3.6-27B NVFP4 artifact
also uses the fixed packed weights from
rdtand/Qwen3.6-27B-PrismaSCOUT-Blackwell-NVFP4-BF16-vllm.
The Qwen3.8-27B NVFP4 artifact also uses the fixed mixed FP8/NVFP4 weights from
unsloth/Qwen3.8-27B-NVFP4. These source
repositories are distributed under Apache-2.0. Vendored dependencies retain their own license files
under third_party/.
High-performance single-GPU inference for selected model checkpoints and GPUs.
C++
16
1,209 commits
updated Oct 9, 2026
Check out Infernix instead. It is my new, revised engine grown from this fork, with new features including support for the Qwen3.8-Flash-Next-NVIDIA-NVFP4 models (Dense8 and bit-exact conversions), and I recommend it over this fork.
AI disclaimer: Everything added to this fork, including most of this README, was written with AI (mostly Claude Opus 5.5, Qwen3.8-27B running on NInfer, plus a few other AI systems I’ve been testing). It is likely to be neither complete nor entirely accurate. This is hobby development.
This is a personal fork of Neroued/ninfer. It follows upstream
closely and adds changes on top: the history is upstream master (81c8ce09), the Windows port,
then one commit per fork change. The sections below explain what is different, grouped by topic,
with credit given as best my AI agents can where a change came from someone else. The upstream
README follows, copied unchanged, under the "Upstream README" heading. A huge thank you to Neroued
for creating NInfer!
The short version. Compared with upstream, this fork:
--host-context-mib N; --prefix-cache-file PATH
keeps the cache across restarts. It still beats upstream's new context cache (October 2026):
on the agentic benchmark below it serves 90.5 % of prompt tokens from cache against 85.8 %,
prefills a third fewer prompt tokens, cuts the average time to first token by 39 % and finishes
the workload 26 % sooner. Stop the server with Ctrl+C (twice) rather than by
closing the window, because Windows does not always leave enough time after a window closes to
save a large cache--use-original-prefix-caching--use-original-int8-prefill-kernel and
--use-original-nvfp4-prefill-kernel select upstream's INT8 and NVFP4 kernels; NVFP4 and K8V4
P×V runs on 8-bit Tensor Cores by default (5-7 % less end-to-end long-prompt prefill, KL
divergence from BF16 KV within 1.1× the FP16 form's), while INT8 KV keeps FP16 P×V unless
--prefill-8bit-pv asks for its 8-bit form, and --no-prefill-8bit-pv forces FP16 everywhere--kv-dtype vq2 and k4v2 (a quarter and three eighths of INT8's
KV memory, based on HyperQuant and cometkim's implementation),
which keep the most recent keys exact, always run P×V in FP16, and decode faster than INT8 at
long context--vision-offload on), based on the work of
Valeriy Selitskiy (iamwavecut)--rope-yarn-factor F, F from 1 to 4)--vram-headroom-mib N shrink the 1 GiB VRAM headroom left after --kv-capacity auto--default-thinking-budget N with
--thinking-budget-message "...")--tolerant-tool-calls recovers some
broken tool calls--log-colours on), a statistics panel at the bottom
(--log-stats-panel off removes it) and a --help screen organised by category, and can
rotate the request log by size (--request-log-max-mib N)I recommend using this with the NVIDIA NVFP4 artifact I’ve uploaded here, which runs a bit faster than the original artifact based on the Unsloth quant and takes up less VRAM: https://huggingface.co/wallawalla47/Qwen3.8-27B-NVIDIA-NVFP4-NInferV3
Prerequisites: Visual Studio 2026 (MSVC), the CUDA 13 toolkit, and FFmpeg + curl from vcpkg
(x64-windows, at C:\vcpkg); adjust the paths at the top of build_native.bat for your machine.
build_native.bat configure
build_native.bat build
Sources compile as UTF-8 (/utf-8), so the build works under any Windows system code page
(reported by Woesch-Nich; commit 965ad6f).
The server is build-windows\apps\Release\ninfer-serve.exe, with the FFmpeg, curl and zlib DLLs
copied next to it. The launch I use on a single 32 GB RTX 5090 (stop any other resident model
first):
ninfer-serve.exe qwen3_8_27b_nvfp4-nvidia.ninfer --host 127.0.0.1 --port 8080 --max-context 240000 --max-concurrency 2 --spec dflash2 --draft-tokens 7 --lm-head-draft --ngram-draft-tokens 15 --ngram-min-match 12 --kv-dtype int8 --preserve-thinking --host-context-mib 52000 --pending-timeout-ms 900000 --prefill-chunk 4096 --kv-capacity auto --vram-headroom-mib 0 --log-colours on --ngram-archive-mib 2048 --ngram-session-mib 256 --ngram-native-sessions --request-log-jsonl log.json --default-thinking-budget 16384 --thinking-budget-message "Considering the limited time available to the user, I must stop thinking now. Time to act:" --tolerant-tool-calls
Add --prefix-cache-file PATH to keep the prefix cache across restarts. Stop the server by
pressing Ctrl+C twice (the first press shows a prompt at the bottom of the console). The server then
cancels running and queued requests, saves the cache and exits; pressing Ctrl+C once more exits
without saving and deletes the unfinished file. ninfer-serve.exe --help lists every option by
category.
Running on another PC needs an RTX 50-series GPU (the build targets sm_120a) and an NVIDIA driver
of 580 or later (CUDA 13); no CUDA toolkit is needed. Copy the DLLs next to ninfer-serve.exe and
install the Visual C++ redistributable if it is missing.
The fork builds and runs on 64-bit Linux too, including WSL2 (tested on Ubuntu 24.04 under WSL2
with CUDA 13.4 and GCC 13.3). Prerequisites: a CUDA 13 toolkit, CMake 3.28 or newer, a C++20
compiler, Ninja, pkg-config, and the FFmpeg and curl development packages. On Ubuntu 24.04:
sudo apt-get install -y build-essential cmake ninja-build pkg-config libavformat-dev libavcodec-dev libavutil-dev libswscale-dev libcurl4-openssl-dev
cmake --preset release
cmake --build build -j
The server is build/apps/ninfer-serve and takes the same options as on Windows, for example the
launch line above with ./build/apps/ninfer-serve in place of ninfer-serve.exe.
Under WSL2 the GPU driver is the Windows NVIDIA driver (580 or later); do not install a Linux NVIDIA
driver inside WSL. NVIDIA's wsl-ubuntu CUDA repository stops at CUDA 13.3, so for 13.4 add its
ubuntu2404 repository and install only cuda-toolkit-13-4 (not cuda or cuda-drivers, which
pull in a driver). Artifacts on a Windows drive load slowly through /mnt/, so copy the .ninfer
file into the Linux filesystem first.
The agentic benchmark and perplexity below compare this fork with upstream + Windows port:
upstream master at abb7f14f, which replaced upstream's prefix cache with its new context cache
(incremental KV reservation, preemption and replay), plus only the Windows port (build 07f7a944
of the branch upstream-Windows-Port). The measured fork build is 580c91ea: this history before
the rebase onto 68c54356, whose one upstream commit changes only how the chat template keeps
cache boundaries when it trims text. A third arm runs the fork as it was before the rebase onto
abb7f14f (cfe8d4d4, its own cache on upstream's previous engine), to check that the rebase lost
nothing. Everything ran on an RTX 5090 under Windows with the official Qwen3.8-27B NVFP4 artifact
(qwen3_8_27b_nvfp4-official.ninfer), in October 2026.
The fork's hybrid prefix cache still beats upstream's new cache: it serves 90.5 % of prompt tokens from cache against 85.8 %, prefills 33 % fewer prompt tokens, answers 39 % sooner on average (48 % at the median) and finishes the workload 26 % sooner. Upstream's new cache closed much of the gap to its previous one (69 % served from cache in September's run), but it still re-prefilled whole prompts on main-session turns where the fork reused them.
The closed-loop suite in bench/agentic_ab/ replays three
coding-agent sessions plus eleven subagents: 130 requests with fan-outs, a concurrent subagent
pair, compaction, retries, an abort and a solo wrap-up, with prompts of 25K-135K tokens and
thinking on. Each arm's own answers are fed back as an agent client does, and the three main
sessions take their turns in lock-step rounds, so every build meets the same order of session
turns whatever its speed. Every build completed every request on each of three workload seeds
(42, 43, 44), which replay different observations.
Settings:
Fork arm: the Quick start launch flags:
--max-context 170000 --max-concurrency 2 --spec dflash2 --draft-tokens 7 --lm-head-draft
--ngram-draft-tokens 15 --ngram-min-match 12 --kv-dtype int8 --preserve-thinking
--host-context-mib 52000 --pending-timeout-ms 900000 --prefill-chunk 4096 --kv-capacity auto
--vram-headroom-mib 0 --ngram-archive-mib 2048 --ngram-session-mib 256 --ngram-native-sessions
--default-thinking-budget 16384 --thinking-budget-message "Considering the limited time
available to the user, I must stop thinking now. Time to act:" --tolerant-tool-calls
Upstream arm: the same flags minus those upstream does not have; upstream's
--host-context-mib 52000 gives its new cache the same host RAM:
--max-context 170000 --max-concurrency 2 --spec dflash2 --draft-tokens 7 --lm-head-draft
--kv-dtype int8 --preserve-thinking --host-context-mib 52000 --pending-timeout-ms 900000
--prefill-chunk 4096 --kv-capacity auto --default-thinking-budget 16384
Context: 170,000 tokens, the largest context the upstream build starts with under these
flags; at it the fork's device KV holds 223,424 tokens and upstream's 175,424. Sampling:
temperature 1.0, top_p 0.95, top_k 20, max_tokens: 64000 on agent turns.
Each cell is the mean over the three seeds, with the lowest and highest seed in brackets; changes are computed per seed against that seed's upstream run and averaged.
| Metric | Upstream + Windows port | Fork | Fork before the rebase |
|---|---|---|---|
| Average time to first token (s) | 4.2 (3.7-4.8) | 2.5 (2.2-2.7), −39.2 % | 3.2 (3.0-3.2), −23.5 % |
| Median time to first token (s) | 1.21 (1.09-1.31) | 0.63 (0.60-0.69), −47.5 % | 0.75 (0.54-0.87), −38.4 % |
| 90th-percentile time to first token (s) | 11.4 (9.6-13.4) | 7.5 (6.6-8.1), −32.2 % | 7.5 (6.9-8.5), −33.1 % |
| Average TTFT, continuing-session turns (s) | 4.00 (3.37-4.75) | 2.26 (1.86-2.54), −41.4 % | 3.18 (3.05-3.29), −18.8 % |
| Average TTFT, new long prompts (s) | 10.0 (9.9-10.0) | 6.5 (6.4-6.6), −34.5 % | 6.7 (6.6-6.8), −32.8 % |
| Prompt tokens served from cache | 85.8 % (85.1-87.0) | 90.5 % (89.9-91.2) | 90.9 % (90.6-91.3) |
| Prompt tokens prefilled | 817K (748-863K) | 547K (518-587K), −32.6 % | 526K (522-531K), −35.3 % |
| Main-session turns that re-prefilled the whole prompt (of 75) | 1.3 (1-2) | 0 | 0 |
| Subagent turns that re-prefilled the whole prompt (of 37) | 0 | 0 | 0 |
| Prefill tok/s, requests with no cache hit in any arm | 6,234 (6,152-6,306) | 8,482 (8,347-8,558), +36.1 % | 8,465 (8,360-8,589), +35.8 % |
| Prefill tok/s, the same requests from 32K tokens | 6,052 (6,020-6,089) | 8,316 (8,251-8,357), +37.4 % | 8,300 (8,246-8,388), +37.2 % |
| Output tok/s, one request decoding | 212 (208-216) | 219 (210-237), +3.4 % | 220 (198-258), +3.7 % |
| Decode rounds/s, one request decoding (engine speed) | 58.9 (58.3-59.8) | 61.7 (61.4-62.2), +4.7 % | 61.4 (60.9-61.7), +4.2 % |
| Tokens per round, one request decoding (acceptance) | 3.60 (3.53-3.69) | 3.55 (3.39-3.85) | 3.58 (3.22-4.19) |
| Output tok/s, two requests decoding (combined) | 360 (346-370) | 348 (339-359), −3.4 % | 376 (359-395), +4.5 % |
| Decode rounds/s, two requests decoding (engine speed) | 55.2 (54.9-55.5) | 54.9 (52.7-56.4), −0.7 % | 56.3 (54.9-57.7), +1.9 % |
| Decode rounds that ran two requests | 29.2 % (29.1-29.4) | 40.9 % (36.9-43.0) | 38.3 % (31.1-42.0) |
| Output tok/s, all decoding at the run's own batching | 254 (253-256) | 280 (273-283), +10.2 % | 281 (271-301), +10.7 % |
| Workload wall time (min) | 15.4 (13.1-18.1) | 11.1 (9.8-12.0), −26.0 % | 11.5 (10.1-12.3), −24.5 % |
The decode-saturation suite of tools/bench/run_serve_concurrency.py starts a fresh server at
each --max-concurrency C and decodes C requests at once, each up to 8,192 tokens, with DFlash2
K=7 and --lm-head-draft (no ngram drafting), stochastic sampling, --max-context 32768 --kv-capacity auto. The builds alternated point by point in two passes, C=1 to 8 and back. This
benchmark was measured in September 2026 against upstream d44ab584 + Windows port and the fork
rebased on it (build fe869b56), and has not been repeated since the rebase onto abb7f14f.
| C | Upstream tok/s | Fork tok/s | Fork time per decode round vs upstream (pass 1, pass 2) |
|---|---|---|---|
| 1 | 198.3 | 196.6 | −2.3 %, −2.4 % |
| 2 | 365.8 | 380.0 | −2.2 %, −2.3 % |
| 3 | 516.1 | 533.9 | −3.1 %, −3.1 % |
| 4 | 649.6 | 677.4 | −3.2 %, −3.2 % |
| 5 | 748.6 | 789.0 | −3.5 %, −3.5 % |
| 6 | 868.7 | 879.7 | −1.8 %, −1.7 % |
| 7 | 965.7 | 1002.6 | −1.4 %, −1.3 % |
| 8 | 1076.0 | 1092.2 | −1.3 %, −1.3 % |
Tok/s is the mean of both passes. The fork's decode rounds are 1.3-3.5 % faster at every concurrency, the same in both passes; tok/s also moves with speculative acceptance on the sampled text, which is why C=1 is lower despite faster rounds (2.99 against 3.08 tokens per round).
Commits: 2b765da, fba0928,
61de484.
Build this fork, then the upstream control from the branch upstream-Windows-Port
(details); stop any other server on the port first:
build_native.bat configure
build_native.bat build
git worktree add C:\ab\control\src upstream-Windows-Port
set AB_CONTROL_SRC=C:\ab\control\src
set AB_CONTROL_BUILD=C:\ab\control\build
bench\agentic_ab\build_control.bat configure
bench\agentic_ab\build_control.bat build
Agentic workload (about 1.7 hours for the two arms on three seeds). The runner reads
the model path and launch flags from AB_LAUNCH_BAT, calibrates the largest context the control
starts with, and writes report.md under profiles\bench\agentic_ab\. treatment is the fork
and control upstream; an optional alt arm runs another build (AB_ALT_EXE) or the fork with
other flags (AB_ALT_EXTRA_FLAGS, default --use-original-prefix-caching):
set AB_CONTROL_EXE=C:\ab\control\build\apps\Release\ninfer-serve.exe
set AB_LAUNCH_BAT=<a launch .bat with the fork flags above>
py -3.11 bench\agentic_ab\runner.py --arms treatment,control --seeds 42,43,44
Concurrent decode, one call per build (repeat --concurrency to sweep, or alternate single-point
calls between the builds as above):
py -3.11 tools\bench\run_serve_concurrency.py --serve build-windows\apps\Release\ninfer-serve.exe ^
--artifact q38=qwen3_8_27b_nvfp4-official.ninfer --mode dflash2_7 --suite decode-saturation ^
--max-context 32768 --kv-capacity auto --concurrency 1 --concurrency 8 --output profiles\bench\cc-fork
Upstream uses the same command with the control checkout's own copy of the script and its
ninfer-serve.exe, because its server writes an older request-log schema. On Windows that copy
needs the fork's wait_for_final_throughput (commit fba0928),
which reads the final statistics interval that Windows otherwise loses when the server is stopped.
Each topic lists everything that affects it, whether written here or taken from elsewhere.
"Upstream PR" means an open pull request on Neroued/ninfer that this fork merged before upstream
did.
Picking individual changes. This fork's
history is upstream master
(81c8ce09), then the Windows port, then one commit per fork change in dependency order, each
a whole feature with its fixes folded in. Each item below links to its commit, and each commit
message lists the earlier fork commits it builds on, so a change can be cherry-picked into another
fork together with those prerequisites.
Commit: 7487474.
Designed around how Qwen3.5-family models work: most of their layers are linear-attention (GDN) layers, whose recurrent state cannot be rebuilt from the KV cache, so resuming a prompt needs the KV of every earlier token plus a saved state at the exact token where the new prompt continues. The cache keeps the two apart and stores each as cheaply as it can. The design document explains the design and records every decision taken while building and running it, with the measurements behind it and the alternatives that were tried and reverted.
--kv-capacity defaults to
auto); --host-context-mib (default 8192, 0 = GPU only) is one pinned host RAM pool that
blocks and snapshots share, split by how much prefill time each entry saves; and
--prefix-cache-file PATH saves the host tier on shutdown and reloads it at startup (a smaller
host tier keeps the most valuable entries). A file from a different model, KV format or
ninfer-serve build is ignored and replaced.--host-context-mib is derived from --max-concurrency and --prefill-chunk;
--device-snapshot-slots, --cache-taps-per-request, --cache-tap-ladder and
--cache-tap-min-gap are optional overrides.7487474.--use-original-prefix-cachingUpstream's own context cache: private continuations and shared checkpoints with incremental KV
reservation, resource-pressure preemption and snapshot or replay recovery. --device-state-slots
requires --use-original-prefix-caching, and --host-context-mib sizes its shared Host budget.
Details: resource scheduling and context cache.
The fixes this fork carried for upstream's previous checkpoint-catalog cache (answer-sized KV
leases, recency eviction, lineage value, a single host budget, automatic long anchors, prefix
salvage on abort, a longer planning budget and shared-catalog reclaim) were retired with that cache
when upstream replaced it in abb7f14f.
--kv-dtype int8): a FlashAttention-2 style kernel
in which each warp keeps its query rows, scores and output in registers and accumulates P×V in
FP16. The prefill chunk is rounded down to whole GPU waves (4096 runs as 3584). Per attention
layer it takes 0.61-0.66× the time of upstream's INT8 prompt kernel on 3584-token chunks from an
empty context to 128K. On the full ninfer-ppl-1m-v1 corpus it is closer to BF16 KV than the
original INT8 kernel (4K windows: 4.8986 against 4.9027, BF16 KV 4.8948).
--use-original-int8-prefill-kernel keeps upstream's kernel.
Commit: fbe6a0c.ninfer-ppl-1m-v1 corpus moved
from 4.9077081 to 4.9076804 with 4K windows and from 4.9041197 to 4.9041624 with 64K windows, each
within 0.7 standard errors of the per-window differences. The partials took up to 64 MiB of
workspace: 576 fewer KV tokens (0.23 %) at startup with --max-context 140000; their bound is now
--prefill-split-workspace-mib (below).--prefill-8bit-pv
turns it on for INT8 and K4V2 KV, which default to FP16 P×V, and --no-prefill-8bit-pv
forces FP16 everywhere).
NVFP4 and K8V4 KV: the MX-FP8 tiled prompt kernel rounds each probability (as 256 p against its
tile's own row maximum, which the softmax reference follows) and each decoded V value to E4M3 and
multiplies them on block-scaled E4M3 Tensor Cores with FP32 accumulation, four times the rate of
upstream's FP16 P×V with FP32 accumulation; a per-tile power of two keeps V in E4M3's range and
returns through the MMA's block scale. Its error against exact attention over the stored values is
about 15 times the FP16 form's (relative L2 0.026-0.037 against 0.0017), but the divergence it
causes does not follow that: on a Q6 artifact with BF16 activations, where the measurement is not
buried under A4 activation noise, its KL divergence from a BF16 KV reference over the full corpus
at 64K context is 0.0203 against the FP16 form's 0.0205 (NVFP4) and 0.0141 against 0.0134 (K8V4),
within 1.1× of FP16 in every context bucket and within 0.06 pp of top-1 agreement. Per attention
layer, against the FP16 form, 3584-token chunks take 19 % (K8V4) and 18 % (NVFP4) less time over
128K keys and up to 20 % less elsewhere; end to end, one request's prefill (DFlash2, 4096-token
chunks) is 4.7-5.1 % (NVFP4) and 5.0-5.4 % (K8V4) faster at 64K tokens and 6.8-7.1 % and 7.5-7.7 %
at 128K (two runs differing by 0.4-0.7 pp).
Perplexity on the full ninfer-ppl-1m-v1 corpus: 4.89624 → 4.91573 (K8V4) and 4.91651 → 4.91384
(NVFP4) with 4K windows, 4.84164 → 4.97411 and 4.89965 → 4.87335 with 64K windows; the median
stream moved by under 0.002 nats per token, but at 64K single streams moved by up to 0.36 (K8V4) —
the corpus moves that much for any small numeric change, which is why the KL comparison above is
the direct measure.
VQ2 and K4V2 KV: the vector-quantized prompt kernel runs the same integer form over the tile's
INT8 V rows, scaling each group's probabilities by that key's row scale and rounding them to
u8 codes against their row maximum. VQ2 takes it by default: against a BF16 KV reference on a
BF16-activation model its KL divergence moves 0.0367 → 0.0378 (1.03x, every bucket within
1.05x, top-1 within 0.04 pp) while prefill of one request is 5.0 % faster at 64K tokens and
6.8 % at 128K. K4V2 stays opt-in: its divergence moves 0.0212 → 0.0232, 1.09x overall but
1.11x in the 32-64K bucket, against the 1.10x bound the other formats were held to, for
5.3 % and 7.2 % the same way. Per attention layer the two are 3.3-18 % faster at 3584-key
chunks over long context (geomean 0.97 over 256-3584 keys at 0-64K) and up to 5 % slower for
512-1024-key calls over an empty context.
INT8 KV (--prefill-8bit-pv, off by default): the fast kernel multiplies P×V on INT8 Tensor Cores
(4× the FP16 rate with FP32 accumulation on RTX 5090); each row's probabilities, scaled by the V
group scale, are quantized to 8-bit codes per 64-key tile and multiplied against the stored INT8 V
codes, which stay exact. Per attention layer, 3584-token chunks take 3.5 % less time from an empty
context and 8-11 % less from 16K to 128K; end to end, prefill is 1.9-2.6 % faster at a 16K-token
prompt, 2.6-4.2 % at 64K and 4.0-5.1 % at 128K (two runs). Unlike the E4M3 form it is not free: probabilities below
half a code step of their tile's largest round to zero, and on the same Q6 artifact the KL
divergence from BF16 KV about doubles, 0.0051 → 0.0111 at 64K, with all sixteen streams worse and
the growth concentrated in longer context (the 32-64K bucket ×2.28, top-1 98.9 % → 98.6 %). The
fast kernel's FP16 P×V matches the original INT8 prompt kernel there (0.0051 against 0.0050), so
the whole cost is the 8-bit P, and FP16 P×V stays INT8's default.--kv-dtype nvfp4 over more than 768 cached
keys, re-tuned from 2048 with the decode-once kernel: forced at every visible count the fast
kernel is within 0.03 % of the per-shape best above 768 visible keys in both head geometries
(24 query heads x 4 KV heads and 16 x 2), where 2048 costs 2.3-2.7 % overall and up to 64 % in
its worst cell):
QK runs on block-scaled FP4 Tensor Cores (8× the FP16 rate on RTX 5090) straight from the stored K
codes, with Q as two NVFP4 terms (0.9 % RMS error). It now runs on the MX-FP8 tiled kernel (below)
with NVFP4 keys: each 64-key V tile is decoded once per CTA into shared memory instead of in every
warp's registers. Per attention layer it takes 6-26 % less time than the previous fast NVFP4
kernel wherever it runs (3584-token chunks 8 % less over an empty context, 15 % over 8K-32K and
17 % over 128K keys), and 0.32-0.63× the time of upstream's NVFP4 prompt kernel; end to end,
prefill of one request (DFlash2) is 2.1 % faster at 16K tokens, 3.6 % at 32K, 6.0 % at 64K and
9.2 % at 128K than with the previous fast kernel, both with the previous kernel's 64 MiB split
budget; the bound is now --prefill-split-workspace-mib (below). Perplexity:
4.90646 → 4.91651 with 4K windows and 4.90946 → 4.89965 with 64K windows, moves of the size the
corpus shows for any small numeric change (below). --use-original-nvfp4-prefill-kernel keeps
upstream's kernel.
Commit: ed32d76.--prefill-split-workspace-mib (next).--prefill-split-workspace-mib, default
256 MiB). The fast INT8 and NVFP4 prompt kernels and the FP8/K8V4 prompt kernel split a launch's
keys across SMs when its row blocks alone would leave SMs idle, which needs FP32 partial rows in
workspace. INT8 and NVFP4 were capped at 64 MiB and FP8 and K8V4 were unbounded (about 550 MiB with
4096-token chunks; upstream's default 1024-token chunks need 169 MiB). One bound now applies to all
four, and a launch that would need more runs fewer splits. Per attention layer against unbounded
(3 interleaved passes, widths 256-4096 over 0-128K cached keys), 1024-2048-token chunks take
17-25 % longer at 64 MiB over 32K-128K keys (27 % at worst), 2-5 % at 128 MiB and under 1 % from
192 MiB, the same for all four formats; at the 256 MiB default 3-4K-token chunks over 128K keys
still take up to 5 % longer, and 3584-token chunks and short contexts are unaffected. These widths
are follow-up turns over a cached prefix and --prefill-round-robin steps beside another request;
a fresh 64K or 128K prompt prefills in the same time within 0.3 % end to end. The startup
workspace with 4096-token chunks and --max-context 220000 is 515 MiB at 256 MiB against 380 MiB
at 64 MiB: about 4K fewer INT8 KV tokens (7.7K NVFP4); FP8 and K8V4 gain about 300 MiB of KV
cache against unbounded.--prefill-chunk is now rounded down to whole attention waves for them too (4096 runs as
3584), which keeps each full chunk's attention free of a mostly idle last wave. One request,
DFlash2: FP8 prefill 1.3 % faster at 32K tokens, 1.2 % at 64K and 0.8 % at 128K (same-session A/B,
two passes), K8V4 0.7-1.6 %, NVFP4 1.4-2.2 % at 128K and within noise at 32-64K.7df17be.rmsnorm_rope route (#273, Michael Dementii), a
Q6 34,816×5120 shape for the fused MLP gate_up (#284, bingchengcc),
and, adapted from llmq (IST-DASLab, Erik Schultheis) by
DuncanBetts, a fused NVFP4 RMSNorm + quantise for the attention
input projection (#305) and a single-pass target log-probability kernel (#307).
Commits: 32e875a, 426a0b6, e903843,
045655c.RESEARCH_NOTES.md.
Commit: 24a9f4f.339543d, dd17eac.473df20, d5e6c8f, ee2e1d2.qwen3_8_27b_nvfp4_nvidia recipe): the drafter's MLP gate/up
projections are stored as NVFP4 (new nvfp4_mse quantizer) instead of Q8 and run with 16-bit
activations through the fused SwiGLU kernel, which now covers every width (sliced kernels to 32
tokens, a Tensor Core route above). On the decode-saturation suite (DFlash2 K=7, n-gram 15/12,
INT8 KV) rounds are 1.6 % shorter with one request (+1.2 % tokens/s) and 0.2 % shorter with four
(tokens/s within noise). Acceptance moved by -0.35 % on 24 sampled agent prompts and -0.4 %
(tokens per round) on the suite. Existing artifacts must be reconverted to use it.c78888f.--max-concurrency above 1 and builds each prompt's index while the prompt is prepared, off
the engine worker. See ngram copy proposals.
Commits: 5a0b6db, 275ebe3,
55d5ea0.--draft-tree-nodes auto or a per-batch-size list):
instead of one proposal path, a round verifies a small tree of proposals that the GPU builds every
round from the DFlash2 drafter's candidate lattice, spending the extra columns where the drafter
is least sure. Sampling stays exact (recursive rejection sampling over each node's alternatives),
and the accepted path is moved onto the main-chain columns so the KV cache, GDN state and drafter
see an ordinary round. It works with every --kv-dtype on artifacts whose GDN input projections
are single FP8 or NVFP4 matrices. A column buys the same acceptance at any batch size but costs
more of the round as the batch and the context grow, so auto measures both while it runs: the
round time of each width per batch size and context length, and, from every tree round's accepted
path, the tokens each narrower tree and the chain would have emitted on the same text. Each round
then verifies the chain or a tree of K+5 or K+9 columns, whichever gives the most tokens per
second. Each round computes every tree column's GDN recurrence once, in one depth-first walk per
row, so 12- and 16-column tree rounds cost 0.4-0.6 % less than replaying every root-to-leaf path
with one request, 1.7-2.0 % with two and 2.9-4.0 % with four. On the decode-saturation suite
(DFlash2 K=7, n-gram 15/12, INT8 KV) auto decodes an estimated 6.9 % faster with one request and
4.4 % with two (measured before the walk), and about 2.9 % with three and 2.7 % with four (from
2.0 % and 0.3 % before the walk and the LM head change above, in the same runs); at 128K tokens of
context it loses 0.6-2.3 % in a fresh process (a fixed 16-column tree loses 4.2-5.4 %), and on
text the drafter already predicts it keeps chain verification. Below about 64K tokens the fixed
table 16,12,12,0 gains up to 2 points more (7-8.5 % with one request and 4-5 % with two on INT8,
K8V4 and NVFP4 KV) and keeps seeded one-request runs reproducible, which auto does not, since
its choice depends on measured time. See tree
verification.41b90af.--kv-dtype vq2 and k4v2Two vector-quantized KV formats. vq2 stores K and V in 2 bits per value, and k4v2 stores K
in 4 bits and V in 2. Per token and KV head they take 132 and 196 bytes, against 528 for INT8 and
288 for NVFP4. For the 27B models' attention layers, 240K tokens of context need 1.9 GiB of KV as
vq2 and 2.8 GiB as k4v2, against 7.6 GiB as INT8 and 4.1 GiB as NVFP4, so the same VRAM holds
three or four times the context or requests. How it works:
vq2, then store every 8 values as one 16-bit code (one of 512 trained magnitude
patterns plus 7 sign bits, the 8th sign set by parity);k4v2 stores 4-bit Lloyd-Max levels instead;Recent keys stay exact. The attention sinks (the first 64 tokens) and the 768 tokens before each query are read from INT8 copies held in the request's state. Which keys a query reads exactly depends only on positions, so a context reads the same values however it was split into prefill chunks, decode steps or cache reuse points. These copies take about 40 MB per state slot for the 27B models; each running request, each GPU checkpoint slot and each host checkpoint image holds one.
Quality (perplexity at 64K context, 32K stride, ninfer-ppl-1m-v1 quick corpus, NVIDIA NVFP4
27B artifact):
| KV | Perplexity | vs BF16 |
|---|---|---|
| BF16 | 4.1661 | |
| INT8 | 4.1728 | +0.16 % |
| NVFP4 | 4.1764 | +0.25 % |
k4v2 | 4.1709 | +0.12 % |
vq2 | 4.1859 | +0.48 % |
Speed (one request, official NVFP4 27B artifact, RTX 5090, ninfer_bench against INT8 measured
in the same run):
| Run | Format | Prefill | Decode |
|---|---|---|---|
| 32K context | vq2 / k4v2 | -4.5 % | +1.6 % / +1.4 % |
| 128K context | vq2 / k4v2 | -7.3 % / -8.7 % | +7.4 % / +6.7 % |
| DFlash2 K=7, 32K context | vq2 / k4v2 | -4.2 % / -4.4 % | -3.4 % / -3.6 % |
In the DFlash2 rows, draft acceptance is 0.82 for both formats against INT8's 0.85.
Per attention layer, against INT8 measured in the same run (ninfer_causal_softmax_attention_bench,
40 calls, cold, first row / four rows):
| Call | 2K keys | 8K keys | 32K keys | 128K keys |
|---|---|---|---|---|
| one-token decode | 1.54x / 2.23x | 1.19x / 0.96x | 0.80x / 0.62x | 0.62x / 0.59x |
| 4-column verification | 1.61x / 2.21x | 1.23x / 1.07x | 0.89x / 0.75x | 0.76x / 0.73x |
| 8-column verification | 1.54x / 2.48x | 1.23x / 1.42x | 1.09x / 1.10x | 1.04x / 1.03x |
| 16-column verification | 1.71x / 3.29x | 1.28x / 1.64x | 1.30x / 1.31x | 1.22x / 1.33x |
Below 32K keys these formats pay for expanding their codes; from 32K, calls of up to four columns
overtake INT8 at 0.6-0.9x, because the attention reads a quarter of the KV bytes. Wider
verification stays above INT8 at 1.0-1.4x, where its G64 kernel fills the SM better. k4v2 is
within about 0.05x of vq2 except on 16-column verification, where it is 0.1-0.4x slower
(1.4-1.9x at 8K keys and below). Prompt attention is 1.2-1.3x at 32-128K keys
and 1.4x at 8K against INT8's fast prompt kernel (896-3584 columns, 20 calls), and on short
prompts the encoding of each new row costs more than the attention.
They work with the hybrid and original prefix caches (a restored checkpoint reads exactly what the original request read), cache files, MTP, DFlash2 chain and tree verification, n-gram drafting, concurrent requests, CUDA Graphs and vision.
Based on the HyperQuant KV cache (arXiv 2606.23406) and the implementation of it by cometkim (Hyeseong Kim). Measured on this model's K and V rows, a fixed-rate 8-value code beat HyperQuant's E8 lattice with Rice coding at fewer bytes. Keeping recent keys exact mattered more than either, so the fork implements the fixed-rate code with an exact window.
<function name="…">, <invoke>, <function_calls>, short <param> tags), also while
streaming. Upstream PR #300 by pkochubey (upstream issue
#276). Commit: dffd56b.</parameter> stays in the value
(upstream PR #318). Commits: db9a6f6, da6c4d8.</think> no longer ends the reasoning early: it only ends the reasoning when a line
break or the end of the turn follows. Adapted from upstream PR #309 by Fedor Suchkov.
Commit: c65feb9.--tolerant-tool-calls keeps a good call followed by junk, a final call cut off by the
output limit (if a parameter is complete), repairs a missing > after the function name, and
returns calls to undeclared tools. By David Oelfke in the gzenz/ninfer fork, ported onto this
fork's parser. The request log's tool_call_parse.tolerant_recovered shows when it rescued a
call (from giveen's giveen/ninfer-ext). It also returns complete calls a model strands in
thinking it never closed before ending its turn (free tool output, tool_constraints:"auto";
the default constrained output cannot end inside thinking), logged as
tool_call_parse.recovered_from_reasoning. Based on Woesch-Nich's PR #3 and Hundsbuah's review
of it. Commits: 8c3409a, 41a7df0,
813355b.--chat-template gains the
froggeric v22.5 template. Commits: 41b75f9,
f8e2273./v1/models (upstream PR #162 by
Hector Ramon Jimenez (hecrj)) and ignore_eos on chat
completions (upstream PR #197 by Thireus).
Commits: ae93169, c2881ad.custom tools, a custom tool choice as a
named one, tool names up to 256 bytes, --usage-chunk-choice): the
serving commits of upstream PR #316 by paq85 (Damian Sromek).
Commit: 4326af1.reasoning.summary,
include: ["reasoning.encrypted_content"]): upstream PR #295 by
Macasacker, based on an earlier PR by
Sha1rholder. Commit: a755a75.0c0ce70.
Upstream now enforces response_format and tool_choice through constrained decoding and
reports a cut-off tool call as cut off, so the fork's accept-only versions of those were dropped
in the rebase onto 81c8ce09.max_tokens is
accepted, and API routes answer under a doubled /v1 prefix (a base URL ending in /v1).
Forced, named and strict tool choices (upstream issue #223) are now enforced by upstream.
Commits: c5c6098, 5eb36c5.thinking.display: "omitted" is accepted instead of rejected: Thinking blocks
come back with empty text and the reasoning in their signature, which is restored when the
client sends the block back, so retained thinking and prefix-cache reuse are unchanged. Ported
from giveen's change in giveen/ninfer-ext. Commit: a3b2b64.48fc625.07aa6ff, 3fc84ae,
5d01a16, 8162985,
39b7f1a.21ef823, b278ba5, 7a75da4,
713f802.a6b1e48.qwen3_8_27b_nvfp4_nvidia recipe storing the output head as FP8; Quasar NVFP4 conversion
with DFlash2 heads and an indexed proposal head; third-party tokenizer settings rebuilt during
conversion. Commits: 6bd8813, 710aef5,
23cdc94.nvfp4_absmax and nvfp4_mse conversion methods quantize BF16 sources to NVFP4 for
16-bit-activation parents; the NVIDIA recipe uses them for the DFlash2 drafter MLP. nvfp4_mse
picks each 16-value group's scale from all 126 E4M3 values by squared error (based on
giveen's scale sweep in giveen/ninfer-ext): on the drafter's gate projection the relative
error is 0.0812, against 0.0847 for the earlier best of five targets and 0.0952 for absmax.
Commit: deeb2b3.qwen3_8_27b_q6 recipe and a grouped_mse scale-search method for groupwise
quantisation; Q8 MTP and a general BF16 GEMM fallback for shapes without a dedicated
kernel. Commits: 426a0b6, b41b0fb,
6a6fc4c, e93644e.--rope-yarn-factor F for YaRN context extension (F from 1 to 4, up to 1M tokens of
context). Commit: 3b15b3b.--vision-offload on keeps the vision tower in pinned system RAM instead of VRAM and streams
it to the GPU while an image is encoded, and --vision-max-merged N bounds the merged vision
tokens per image or video. Based on the original work by
Valeriy Selitskiy (iamwavecut), rewritten for this engine.
Commit: 8dd1f4a.upstream-Windows-Port. Commits: 608e70b,
74d64cc.--log-colours on colours the console statistics, and a session statistics panel
beneath the log shows session and last-ten averages of TTFT, cache hit rate, prefill and decode
speed, the mean decode batch and drafter acceptance (--log-stats-panel off removes it); its
n-gram columns appear only with n-gram drafting. Engine messages and FFmpeg's log are ordinary
log records that scroll above the panel.
Commits: 73767bc, 1739b2f,
128dc5e, 493babf, 43421e5.--help by category on ninfer-serve and the ninfer CLI, with separate sections
for the two prefix caching systems. Commit: 27bbd2b.--vram-headroom-mib N sets how much GPU memory --kv-capacity auto leaves spare (upstream
always leaves 1 GiB). Commit: eb17df4.c4df956.--thinking-budget-message S sets the message inserted when a request reaches its
--default-thinking-budget N. Commit: fb6b334.--request-log-max-mib N rotates the --request-log-jsonl file once it reaches N MiB,
keeping --request-log-keep K older files (default 4); each new file starts with a copy of the
server's start record. Based on the rotation by
Gideon Zenz (gzenz) in the gzenz/ninfer fork.
Commit: e0b0968.This fork is rebased onto upstream master whenever upstream moves; the latest is 81c8ce09
(October 2026). Where upstream rewrote code this fork had changed, the rebase starts from
upstream's version and carries the fork's change onto it only where an A/B test on the RTX 5090
shows the fork's version is faster. Once upstream adopts a change, it leaves this list.
The rebase onto 81c8ce09 brought in upstream's constrained decoding, which runs each MTP and
DFlash round as a Forward and a Finish CUDA Graph with the grammar masks built on the Host between
them. The fork's n-gram copy families and DFlash2 tree families became Forward/Finish pairs too (see
item 16 of the short version). Decode at one request on the NVIDIA NVFP4 artifact with K8V4 KV
(ninfer_bench -pg 2048,256 -r 5, two alternating passes per arm) was unchanged against the fork
before the rebase: no speculation 82.1 vs 82.0 tok/s, MTP K=3 252.3 vs 252.3, DFlash2 K=7 500.6
vs 500.7, DFlash2 with n-gram 31/12 2042.5 vs 2042.4, and 8192-token prefill 11,595 vs 11,574
tok/s (every difference within 0.2 %, the spread between passes of one build). Several requests
were not compared, because the earlier benchmark tool ran one request only.
Dropped in the rebase onto 81c8ce09, because upstream now does the same thing properly:
response_format json_object/json_schema without enforcing it and accepting
Anthropic forced, named and strict tool choices as advisory: upstream enforces both through
constrained decoding.Dropped in the rebase onto abb7f14f, because upstream now does the same thing:
--prefill-round-robin) and concurrent staged-prefill lanes (by
David Oelfke in the gzenz/ninfer fork): upstream now prefills several lanes and rotates prefill
between them by default. The narrow prefill steps beside other requests were not carried over.Dropped in the rebase onto d44ab584, with the measurement that decided each:
qwen3_8_27b_nvfp4_nvidia recipe
in tools/convert/official_recipes.py. The weights are imported bit-exact except the output
head (NVFP4 → row-scale FP8), with the DFlash2 draft model and an indexed 131,072-row proposal
head for --lm-head-draft.tools/convert/quasar_nvfp4.py, with the same
DFlash2 draft model and proposal head.Both are single-file .ninfer artifacts for an RTX 5090 (sm_120a); each Hugging Face page has
the creation outline and conversion report. The Quick start launch works
for either.
This fork is developed with Claude Code. If you would like to help pay for that subscription, you can sponsor me on GitHub.
Please support Neroued, the creator of the original NInfer, on Ko-fi.
A big thank you to all the contributors to upstream NInfer and to the forks this one draws on — Neroued, Michael Dementii, Minnnn, DuncanBetts, Thireus, remesis, Valeriy Selitskiy (iamwavecut), Hector Ramon Jimenez (hecrj), giveen, bingchengcc, pkochubey, paq85, Macasacker, Sha1rholder, adubkov, Gideon Zenz (gzenz), cometkim (Hyeseong Kim), Woesch-Nich, Hundsbuah, David Oelfke, Fedor Suchkov, Yunado, and everyone else whose pull requests, reviews and commits made this fork possible — and a particular thank you to Neroued for creating NInfer, maintaining upstream so well, and for the work this branch builds on.
Everything below is a copy of the upstream
NInfer README as of the upstream
commit this fork is rebased on (81c8ce09), unchanged except for one added link to
the fork's ngram copy proposals guide.
Selected checkpoints. Maximum single-GPU inference performance.
NInfer is a from-scratch C++/CUDA inference engine for Qwen3.5 Dense and MoE architectures on a single NVIDIA GeForce RTX 5090. It runs text, image, and video prompts through a local CLI or OpenAI-/Anthropic-compatible HTTP APIs. The runtime is deliberately specialized: one GPU, one resident model, and one to eight execution lanes fixed at startup.
Five official artifacts are available. The quick-start commands use Qwen3.8-27B NVFP4.
| Model | Weights | Artifact | Download and model card |
|---|---|---|---|
| Qwen3.6-27B | groupwise-int | qwen3_6_27b.ninfer | Qwen3.6-27B |
| Qwen3.6-27B | nvfp4 | qwen3_6_27b_nvfp4.ninfer | Qwen3.6-27B NVFP4 |
| Qwen3.8-27B | groupwise-int | qwen3_8_27b.ninfer | Qwen3.8-27B |
| Qwen3.8-27B | nvfp4 | qwen3_8_27b_nvfp4.ninfer | Qwen3.8-27B NVFP4 |
| Qwen3.6-35B-A3B | groupwise-int | qwen3_6_35b_a3b.ninfer | Qwen3.6-35B-A3B |
Each v3 .ninfer artifact carries model configuration, encoded weights, logical bindings and
frontend resources. Runtime execution uses those facts with the implemented model and Op
capabilities. You can also convert your own weights, reuse an official
recipe or choose another supported mixture of formats.
The current engine requires v3 artifacts. Existing official v2 downloads can be upgraded locally without downloading the weights again.
NInfer requires 64-bit Linux, an NVIDIA GeForce RTX 5090, a CUDA toolkit supporting sm_120a,
CMake 3.28 or newer, a C++20 host compiler, Ninja, pkg-config, FFmpeg development libraries
(libavformat, libavcodec, libavutil, and libswscale), and libcurl >= 7.85.
CUDA 13.1 is the validated development toolkit; CMake does not impose a CUDA version floor.
The build rejects CUDA architectures other than sm_120a.
Build the product binaries:
git clone https://github.com/Neroued/ninfer.git
cd ninfer
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build -j
Tests and benchmarks are excluded from the default build. cmake --preset release configures
the same product build; cmake --preset dev also enables tests and benchmarks and finds a
Python 3 interpreter. Both presets use build/ and explicitly reset the build options.
Machine-specific compiler and Python paths belong in the ignored CMakeUserPresets.json.
See build organization and configuration for details.
There is no install target or packaged binary distribution; run NInfer from its source build tree. Python tools run independently of CMake; the standalone HBM probe has its own build command.
Download the artifact used by this example with the Hugging Face CLI:
hf download neroued/Qwen3.8-27B-nvfp4-NInfer \
qwen3_8_27b_nvfp4.ninfer \
--local-dir models
Start a long-running text/agent server with two execution lanes and prefix caching:
./build/apps/ninfer-serve models/qwen3_8_27b_nvfp4.ninfer \
--max-context 240000 \
--kv-capacity 240000 \
--max-concurrency 2 \
--kv-dtype fp8 \
--device-state-slots 2 \
--spec mtp --draft-tokens 3 \
--lm-head-draft \
--preserve-thinking
Each request has a 240,000-token logical ceiling. A shared 240,000-token Device KV pool serves resident requests and retained prefixes. Requests acquire KV pages as execution advances; under pressure, the scheduler can pause a request and resume it later. The profile provides two extra Device StateImages and the default shared pinned Host budget: 8 GiB plus eight model StateImages, used for retained state, KV and pause snapshots.
Send an OpenAI-style request:
curl http://127.0.0.1:8080/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{
"model": "qwen3.8-27b",
"messages": [{"role": "user", "content": "Reply with one short sentence."}],
"max_tokens": 64
}'
Run a one-shot CLI request with a 32,768-token allocation:
./build/apps/ninfer models/qwen3_8_27b_nvfp4.ninfer \
--prompt "Explain prefill and decode, then give a concise conclusion." \
--max-context 32768 \
--max-new 8192 \
--kv-dtype fp8 \
--spec mtp --draft-tokens 3 \
--lm-head-draft
Answer content is written to stdout. Human-readable startup/runtime diagnostics and the CLI-owned
reasoning, timing, throughput, memory, and speculative-decoding report are written to stderr;
reasoning and the result report remain unprefixed product output. On a terminal, weight
materialization uses one transient progress line followed by a compact Engine-ready summary.
Redirected stderr receives persistent readable progress without terminal control sequences. Use
--log-level debug for complete startup detail. Option and local input errors remain direct command
diagnostics. Use --messages FILE and --vision for structured image/video input; see the
CLI guide and committed examples.
A reusable checkpoint combines KV with the complete continuation state at an exact token frontier. The engine retains completed conversation endpoints and stable input boundaries for multi-turn and agent reuse. Inactive checkpoints share Device and pinned Host capacity; pressure reclaims retained resources before pausing resident requests. Paused requests resume from a snapshot or rebuild their state by replaying already committed tokens.
See Resource scheduling and context cache for the algorithm and Serve TTFT benchmark for public-HTTP coverage of hot reuse, Host resume, eviction, shared prefixes, scheduling boundaries, and multimodal load.
Published measurements use an RTX 5090. The performance index links to per-model run records and the measurement rules. The tables below are excerpts from those detailed results. Qwen3.8 uses FP8 E4M3 row-256 KV; Qwen3.6 uses INT8 group-64 KV.
Saturated decode used CUDA Graphs, MTP3, and one 8,192-token generation per active request. Throughput uses aggregate committed decode tokens from complete intervals whose actual decode batch equaled the configured concurrency. Acceptance covers the complete request wave; these rates are steady decode (tok/s).
| Model profile | C=1 tok/s / accept | C=2 tok/s / accept | C=4 tok/s / accept | C=8 tok/s / accept |
|---|---|---|---|---|
Qwen3.6-27B groupwise-int | 185.8 / 68.2% | 247.0 / 69.0% | 309.5 / 68.4% | 535.0 / 68.3% |
Qwen3.6-27B nvfp4 | 202.4 / 69.3% | 399.7 / 71.4% | 699.7 / 69.3% | 1,146.9 / 68.6% |
Qwen3.6-35B-A3B groupwise-int | 642.5 / 68.6% | 907.2 / 66.3% | 1,213.5 / 69.6% | 1,380.7 / 68.0% |
Qwen3.8-27B groupwise-int | 136.5 / 44.4% | 253.3 / 45.2% | 398.1 / 46.1% | 582.4 / 46.4% |
Qwen3.8-27B nvfp4 | 147.7 / 46.2% | 291.0 / 48.7% | 522.2 / 45.8% | 922.4 / 46.1% |
The serial serving corpus used CUDA Graphs, a 1,024-token prefill chunk, and five fixed seeds after warm-up. The table keeps one short-prefill, one extreme-prefill, and one structured-output MTP3 point for each published profile; the full context and scenario matrices are linked from each model below.
| Model profile | 7,680-token prefill | 260,096-token prefill | Structured MTP3 decode |
|---|---|---|---|
Qwen3.6-35B-A3B groupwise-int | 17,705.4 tok/s | 5,247.0 tok/s | 779.6 tok/s |
Qwen3.6-27B groupwise-int | 3,218.1 tok/s | 1,614.8 tok/s | 193.0 tok/s |
Qwen3.6-27B nvfp4 | 11,191.5 tok/s | 2,510.6 tok/s | 252.2 tok/s |
Qwen3.8-27B groupwise-int | 3,331.9 tok/s | 2,139.4 tok/s | 214.7 tok/s |
Qwen3.8-27B nvfp4 | 12,819.1 tok/s | 4,016.4 tok/s | 231.7 tok/s |
Capability scores were measured through NInfer's OpenAI-compatible serving route with thinking enabled, MTP3, and EvalScope 1.9.0 (0-shot, rule scoring, one sample per problem):
| Model profile | AIME 2025 | AIME 2026 | GPQA-Diamond | ERQA | RealWorldQA |
|---|---|---|---|---|---|
| Qwen3.6-27B groupwise-int | 86.67% | 93.33% | 86.87% | — | — |
| Qwen3.6-27B NVFP4 | 93.33% | 93.33% | 84.34% | — | — |
| Qwen3.6-35B-A3B groupwise-int | 90.00% | 90.00% | 85.35% | — | — |
| Qwen3.8-27B groupwise-int | 96.67% | 96.67% | 87.37% | 66.25% | 82.22% |
| Qwen3.8-27B NVFP4 | 96.67% | 96.67% | 90.40% | 66.25% | 83.53% |
The Qwen3.6 rows used temperature 0.6 and presence penalty 1.0; the Qwen3.8 rows used temperature
1.0 and presence penalty 0.0. Multimodal evaluation used --vision and an 81,920-token context
limit. Text evaluation used 262,144 tokens except Qwen3.8-27B NVFP4, which used 252,928 tokens to
fit the RTX 5090 after weights. Each score is one sample per problem; model cards contain the
correct/total counts and evaluation notes.
GPU residency is fixed at process startup. --spec selects speculative decoding residency, and
--vision independently selects Vision residency. Qwen3.6-35B-A3B DFlash can be combined with
Vision; it accelerates generated-text decode after multimodal prefill, not Vision encode itself.
Build the runtime image on a host with the NVIDIA Container Toolkit:
docker build --tag ninfer:local .
Mount the downloaded model and run the same example server profile:
docker run --rm \
--gpus '"device=0"' \
--publish 8080:8080 \
--volume "$PWD/models:/models:ro" \
ninfer:local \
ninfer-serve /models/qwen3_8_27b_nvfp4.ninfer \
--host 0.0.0.0 \
--max-context 240000 \
--kv-capacity 240000 \
--max-concurrency 2 \
--kv-dtype fp8 \
--device-state-slots 2 \
--spec mtp --draft-tokens 3 \
--lm-head-draft \
--preserve-thinking
The official artifacts provide the following capabilities, with optional components enabled at startup:
The 35B-A3B target additionally supports DFlash with draft windows from one to fifteen for Text and
image/video Vision prompts. Qwen3.8-27B artifacts with the DFlash2 companion weights support
--spec dflash2 --draft-tokens 7 for the same Text/Vision Engine path, with draft counts 1..15
and either full or optimized proposal heads.
The product boundary remains intentionally small:
--max-context is each sequence's logical limit. --kv-capacity sizes the shared Main Text KV pool
used by active requests and retained prefixes; auto resolves the largest legal capacity at
startup from the memory remaining after weights while keeping 1 GiB of sizing headroom. Explicit
capacities remain fixed for the process lifetime.
Run the relevant --help for the exact current option contract.
NInfer is a personal project that I develop out of interest. If you find it useful and would like to support its continued development, you can support the project on Ko-fi.
Support is entirely voluntary. It is not a purchase or investment and does not come with financial returns, promised services or features, or a role in project decisions. The project's direction, priorities, technical choices, and release schedule remain independently determined by the maintainer.
NInfer is licensed under the Apache License 2.0.
The published artifacts are derived from
Qwen/Qwen3.6-27B,
Qwen/Qwen3.8-27B, and
Qwen/Qwen3.6-35B-A3B. The Qwen3.6-27B NVFP4 artifact
also uses the fixed packed weights from
rdtand/Qwen3.6-27B-PrismaSCOUT-Blackwell-NVFP4-BF16-vllm.
The Qwen3.8-27B NVFP4 artifact also uses the fixed mixed FP8/NVFP4 weights from
unsloth/Qwen3.8-27B-NVFP4. These source
repositories are distributed under Apache-2.0. Vendored dependencies retain their own license files
under third_party/.