~/satyajit

Cinference's 450 tokens a second: what the number counts

mdjsonmcp

2026-09-22 · 17 min · inference-optimization · speculative-decoding · long-context · kv-cache · linear-attention · llm · explainer

satellitedown/fast-long-context-cinference is a twelve-file repository that installs a C++/CUDA inference engine, downloads a 21.5 GB model and starts an OpenAI-compatible server, from one bash setup.sh. Its results table is four rows:

Prompt tokensTokens/s
8,192450.78
32,768432.60
131,072364.84
260,000299.64

Throughput that survives a 32× increase in context is the interesting claim. Decode is memory-bound, attention cost grows with the sequence, and the KV cache stops fitting — so the normal shape of that table is a cliff. This one falls 33%.

The first question is always the same and it is the only one that matters: 450 tokens a second for whom? Four hundred and fifty aggregated across sixty-four concurrent users is an ordinary result. Four hundred and fifty for one user is not.

It is one user. That is the strong reading, and it is stated three times in the repository.

So the headline is honest, and it is the more impressive of the two claims it could have been. What follows is everything else the number depends on, because a per-user 450 tok/s from a 27B model on a consumer card is not a free lunch and the repository does not pretend it is.

The full denominator

Every figure in that table comes from results/rtx5090-archive-recall.json, which is 60 lines of JSON and is the most useful file in either repository. Unpacked:

Hardwareone NVIDIA GeForce RTX 5090, 32 GiB
ModelHuihui Qwen3.8-27B abliterated, NVFP4 weights, 21,492,938,224 bytes
EngineCinference 44e9603, a fork of NInfer 9e163ee
Concurrency1
SpeculationMTP, 10 draft tokens, --lm-head-draft
KV cachek8v4 — FP8 keys, 4-bit values
Context ceiling262,144 · prefill chunk 1,024
Samplingtemperature 0, thinking off, max_output_tokens 256
Workload"deterministic archive recall with six markers at 1%, 10%, 25%, 50%, 75% and 95% depth"
Samplesone cold-prefix request per context checkpoint
The metricthe server's predicted_per_second: (generated - 1) / decode seconds, excluding prompt processing

Two things in that list do most of the work, and neither is in the headline: MTP-10 and the workload.

A dark terminal screenshot of the installer menu. The header reads 'Fast Long Context - Cinference' and 'Huihui Qwen3.8-27B / RTX 5090, K8V4 KV, MTP-10'. Four options follow: 1) Install everything (build + download), 2) Download / resume model, 3) Start the server (Ctrl-C to stop), 0) Exit. A footer reads 'Space: ~21.5 GB model files, plus software. Choose an option:' with a block cursor.
The installer states the configuration in its own header: one card, K8V4 keys and values, MTP-10. Every number in this article is a number about that line. (satellitedown/fast-long-context-cinference, assets/setup-menu.png, commit 104b236, Apache-2.0. The repository's licence and NOTICE are committed beside the image at /articles/cinference/APACHE-2.0-LICENSE.txt and /articles/cinference/NOTICE.txt.)

The 450 is a speculation number

The draft acceptance rate is recorded next to every measurement, and the repository's own MTP tables let you pin down what it means. At three draft tokens and 76.3% acceptance the same tables report 3.29 tokens per round; at 89.5% they report 3.68. Both satisfy

tokens per round=1+ak\text{tokens per round} = 1 + a \cdot k

for acceptance aa and draft window kk. At k=10k = 10 and a=0.931a = 0.931 that is 10.31 tokens per verification round; at a=0.985a = 0.985, 10.85.

Divide the measured rate by that and you get the wall clock of one round. Compare it against the same model, the same card and the same NVFP4 weights with speculation switched off — run N0 in docs/performance/qwen3.8-27b.md, which that page is careful to label a retained upstream NInfer result rather than a Cinference one.

one RTX 5090 · Huihui Qwen3.8-27B NVFP4 · K8V4 KV · max_concurrency 1 · bars are wall-clock per decode round
010203040milliseconds of wall clock8K context8,192-token promptround costs 1.63×a step, returns 10.3×no speculation · 14.04 ms1 token71.2 tok/s · measured at 7,680MTP-10 round · 22.87 ms10.31 tokens450.78 tok/s · 93.1% accepted260K context260,000-token promptround costs 1.92×a step, returns 10.8×no speculation · 18.90 ms1 token52.9 tok/s · measured at 260,096MTP-10 round · 36.21 ms10.85 tokens299.64 tok/s · 98.5% accepted
The 450 is not a faster forward pass. A decode round that drafts ten tokens and verifies them costs 1.63× an ordinary step at 8K and 1.92× at 260K, and returns about ten tokens instead of one. Take the speculation away and the same engine on the same card does 71.2 tok/s at 8K and 52.9 at 260K. Everything above 71 is the draft head being right.

Without speculation this engine does 71.2 tok/s at 8K and 52.9 at 260K. The headline is 6.33× and 5.66× those numbers. Everything above 71 is the draft head being right about the next ten tokens.

That is not a criticism — that is what speculative decoding is for, and getting 6× out of it is a good result. But it relocates the claim. This is not a faster forward pass and not a cheaper attention kernel. It is one forward pass amortised over ten tokens, and it is worth exactly as much as the draft head's hit rate.

A sanity check on the no-speculation line, since it is now load-bearing: 19.729 GiB of weights in 14.04 ms is 1.51 TB/s of weight traffic, about 84% of the RTX 5090's 1,792 GB/s. The plain decode step is doing what a plain decode step does — streaming the whole model through the memory bus — and it is doing it efficiently. The engine is not leaving performance on the table; it is buying tokens per byte read.

Why the long-context figure holds up

Here is the part the table does not show, and it is the article's actual finding.

one request in flight · synthetic archive recall · the rate falls, the acceptance rises
0100200300400500tok/s8K32K128K256Kprompt tokens (log scale)the “above 300” claim93.1%92.3%97.7%98.5%90%95%100%draft accepted451433365300275.73 — a prior repeat of the same pointsame engine, speculation off: 71.2 → 52.9MTP-10MTP0 baselinedraft acceptance (right axis)
Long-context decode does degrade here: the underlying step time grows 35% from 8K to 260K, and the no-speculation line falls with it. What keeps the headline above 300 is the dotted line going the other way — on a recall workload the draft head gets more accurate as the haystack grows, from 93.1% to 98.5% accepted. That is a property of the workload, not of the engine. The hollow marker is the repeat run the same results file records at 260,000 tokens: 275.73 tok/s, under the claim.

Two things happen between 8K and 260K and they point in opposite directions.

The step gets slower. 14.04 ms to 18.90 ms, a 35% increase, exactly as the physics says it should: the K8V4 cache at 260,096 tokens is 6.69 GB, which at 1,792 GB/s is 3.73 ms of extra reading per step. The observed increase is 4.86 ms. The cache accounts for about three quarters of it and the gated-DeltaNet state and lower tail efficiency account for the rest.

The draft head gets better. Acceptance rises from 93.1% to 98.5%. Of course it does: the workload is archive recall, where the model is asked to find six planted markers in a haystack and repeat them. Longer haystack, more verbatim text to copy, easier next-token prediction. Tokens per round go from 10.31 to 10.85.

The second effect cancels most of the first. The engine did not defeat long-context decay; the workload paid for part of it. On a task where the model composes rather than copies, both curves move the same way and the fall is much steeper.

The repository says so itself, in the limitations array of the same results file:

275.73 is under 300. The "stays above 300 TPS even at 256K" claim rests on one of the two recorded observations of that point. It is measured rather than projected — but n = 1 per checkpoint, the results file says so in its sampling field, and the file records the other draw.

How much slower is "much slower"

The repository answers this too, for a different draft window. The MTP3 cross-scenario tables in docs/performance/qwen3.8-27b.md run the NVFP4 weights over three fixtures × five seeds per category, at one request in flight:

Workload, MTP3, NVFP4, C=1AcceptanceTokens/roundDecode
Structured output90.8%3.72219.8 tok/s
Code76.4%3.29194.3 tok/s
Translation75.0%3.25192.3 tok/s
Story37.4%2.12126.1 tok/s

Synthetic recall at 93–98% sits above every one of these. Story writing sits at 37%, which is a 1.7× multiplier on a plain step rather than a 6× one. Widening the window from 3 to 10 does not rescue a low acceptance rate — a chain that breaks at the second draft never reaches the tenth — so the honest expectation for free-form prose on this setup is somewhere between the 52.9–71.2 floor and the 126 that MTP3 gets, not 450.

The 107 seconds nobody mentions

The project is called Fast Long Context. The decode rate at 260,000 tokens is 299.64 tok/s. The prefill for that same request takes 106.72 seconds.

Prompt tokensPrefill (s)Prefill rateDecode
8,1920.909,093 tok/s450.78 tok/s
32,7684.397,453 tok/s432.60 tok/s
131,07232.404,044 tok/s364.84 tok/s
260,000106.722,436 tok/s299.64 tok/s

Prefill throughput falls 3.7× over the same range that decode throughput falls 1.5×. Each 1,024-token chunk has to attend over everything before it, so the 16 full-attention layers' prefill work grows with the square of the sequence, and there is no draft head to amortise any of it. The upstream N0 run measures the same thing as a server-side TTFT: 118,354.8 ms ± 717.2 at 260,096 tokens.

So a 256K session on this box is: wait two minutes, then read at 300 tok/s. That is a perfectly reasonable shape for a document-analysis tool and a bad one for a chat UI, and it is the number I would want in the README next to the other one. Prefix caching helps on the second turn and there is nothing here that measures it.

The cache codec is rotate-then-quantize

k8v4 is not a standard name, and the kernel comment explains it in two lines:

// src/ops/kv_cache/append/k8v4_kernel.cuh:3
// Asymmetric K8V4 append kernel. K uses the existing row-256 E4M3 cache codec; V uses the
// group-16 packed E2M1 cache codec. Both operands receive the fixed FP32 D256 Hadamard rotation.

Three things worth naming there.

Keys get 8 bits, values get 4. That asymmetry is the standard finding — key outliers move attention scores, which then go through a softmax that amplifies the error, while value outliers move the output linearly. Spending the bits on K is the right call and it is what TurboQuant concluded from a different direction.

Both operands are Hadamard-rotated before quantization. This is TurboQuant's central claim shipping in a production engine: rotate the vector by an orthogonal matrix first and its energy spreads evenly across coordinates, so there are no outliers left to clip and one data-free quantizer fits every vector. A fixed D256 Hadamard is the cheap instantiation — no randomness, no calibration set, and it is its own inverse up to a scale.

The V codec is NVFP4 in all but name. Group-16 packed E2M1 with a one-byte scale per group is exactly the format NVIDIA published, applied to the value cache rather than to weights. Which makes this the same month's second shipped 4-bit KV cache — SGLang's is the other — and the two differ on precisely the question TurboQuant raised. Cinference rotates. SGLang does not, and relies on the 16-element block to localise outliers instead.

What is actually holding the context

Quantizing the cache is the visible move. The structural one is in the model.

Qwen3.8-27B is not a transformer with 64 attention layers. Its published geometry, in the engine's own docs/maintainer/qwen3_5-model.md, is 64 text layers of which 16 are full attention and 48 are gated DeltaNet, with the full-attention layers at indices 3, 7, 11 and so on. The 48 linear layers hold a fixed-size recurrent state — 48 layers × 48 value heads × 128×128 in FP32, which the engine's own ReplaySSM note computes as exactly 144 MiB, regardless of context length.

So three quarters of this model does not have a KV cache to quantize, and three quarters of its per-token attention work does not grow with the sequence.

RTX 5090, 32 GiB · Huihui Qwen3.8-27B NVFP4 weight arena 19.729 GiB · KV for a full 262,144-token context
08162432GiB32 GiB cardbf16the default --kv-dtype64.00 KiB/tokenweights 19.73KV 16.0035.73 GiBfp8row-256 E4M3, K and V32.25 KiB/tokenweights 19.73KV 8.0627.79 GiBk8v4FP8 keys, 4-bit values — what this recipe ships25.13 KiB/tokenweights 19.73KV 6.2826.01 GiB
Quantizing the cache is not what makes the 256K context possible — FP8 already fits, with 4.2 GiB to spare. What K8V4 buys is a further 1.8 GiB, and it buys it by halving only the values. The reason any of this fits on a consumer card is upstream of the codec: 48 of this model’s 64 layers are gated DeltaNet and keep a fixed 144 MiB recurrent state instead of a cache that grows. Only 16 layers are drawn here, because only 16 layers have a KV cache at all.

The honest version of "256K on a 32 GB card" is therefore: the architecture removes 75% of the cache, FP8 removes half of what is left, and K8V4 removes a further 1.8 GiB that you do not strictly need. BF16 is the only setting that does not fit.

This also reframes the headline table. A 33% decode falloff from 8K to 256K would be remarkable on a dense-attention model. On a 3:1 linear-to-full hybrid where only 16 layers read a growing cache, it is roughly what you would predict — and the same architectural choice is what makes vLLM's 2.4T numbers legible, where the linear layers' per-request state is 37% of the KV budget and sets the block size for everybody else.

What the fork changed

upstream-provenance.json is unusually candid: fifteen modified paths against NInfer 9e163ee, and a three-line statement of what they do.

"summary": [
  "Raise the product MTP draft-token cap from five to ten and extend related round buffers, contracts, and coverage.",
  "Derive MTP CUDA Graph topology classes from captured node-type and kernel-function signatures while separating batch sizes.",
  "Preserve native binaries, API symbols, .ninfer format, and build organization."
]

That is the whole delta. The paths bear it out — include/ninfer/ops/mtp_round.h, src/ops/kernel/mtp_round.cuh, src/core/decode_graph.cpp, src/models/qwen3_5/program/round_buffers.h, and the two tests that cover them. There is no new attention kernel, no new cache codec, no new scheduler. k8v4, NVFP4 weights, paged KV, the 262,144 context ceiling and the Hadamard rotation are all upstream NInfer.

Which means the fork's contribution to the headline is: the draft window went from 5 to 10. On a workload that accepts 93–98% of drafts, doubling the window is close to doubling the tokens per round, because the acceptance chain rarely breaks. On story writing at 37% acceptance it would buy almost nothing — the expected chain length at a=0.374a = 0.374 is 1+a+a2+1 + a + a^2 + \ldots, which converges to about 1.6 whether the window is 5 or 10 or 50.

The second change is the more generally useful one and it is not in the headline at all. Matching CUDA Graph profiles by their captured node types and kernel function signatures, rather than by the planned context range, lets configurations that compile to the same graph share one executable. On a single-request server with a 262,144-token ceiling, the number of distinct decode shapes is large and the capture cost is paid at startup; sharing executables across them is the kind of change that shows up as a shorter warm-up rather than a bigger number.

What I would take away

Related reading: what a prefill and a decode actually cost; rotating the KV cache before you quantize it; the NVFP4 format itself; SGLang's 4-bit KV cache, which makes the opposite choice about rotation; and continuous batching on a small box, which is what the aggregate-throughput version of this question looks like.

What would change my mind

7 claims above, and what would falsify each

  1. The 450 tok/s figure is single-request, not aggregate.

    runtime-manifest.json sets "max_concurrency": 1, scripts/serve.sh passes it through as --max-concurrency 1, and the results file records "max_concurrency": 1 in its profile block. If a later revision raises that default, or if the native server treats the flag as a soft hint and admits more, this is wrong — and the whole framing of the article flips, because 450 aggregate over even four streams would be an unremarkable number.

  2. Without speculation the same setup does 71.2 tok/s at 8K and 52.9 at 260K, so the headline is 5.7-6.3x a plain step.

    Run --spec none (MTP0) on the shipped profile at 8,192 and 260,000 tokens. My numbers are run N0 from docs/performance/qwen3.8-27b.md, which is a different campaign — 2026-08-17, CUDA driver API 13.3, stochastic sampling, five samples, contexts 7,680 and 260,096 rather than 8,192 and 260,000, and the page explicitly labels it a retained upstream NInfer result rather than a Cinference one. A fresh MTP0 baseline at the recall workload's exact settings could move these by enough to change the multiplier, though it would have to move by a factor of four to change the argument.

  3. Long-context throughput holds up because draft acceptance rises on this workload, not because the engine avoids the usual decay.

    The step-time series is the test. Take the MTP-10 rate and the acceptance at each checkpoint, compute (1 + 10a) / rate for the round time, and check whether it grows the way the MTP0 step time grows. I get 22.87 → 36.21 ms against 14.04 → 18.90 ms, so the round degrades faster than the step and only the rising acceptance keeps the tokens-per-second up. If someone runs the same context sweep on a composition workload and the rate still lands above 300 at 256K, the decay really is being handled in the engine and I have the mechanism wrong.

  4. Ordinary prose on this setup is far below 450 tok/s.

    The repository's own MTP3 story category measures 37.4% acceptance and 126.1 tok/s on NVFP4 at C=1. What I do not have is MTP-10 on prose — nobody has published it. If the wider window recovers substantially more than the geometric-series estimate suggests (about 1.6 accepted tokens per round at that acceptance rate, window-independent), then MTP-10 does something for low-acceptance workloads that MTP3 does not, and my "between 71 and 126" expectation is too pessimistic.

  5. The K8V4 cache costs 25.125 KiB per token, so a full 262,144-token context is 6.28 GiB.

    Computed from the published geometry (16 full-attention layers, 4 KV heads, head dimension 256) and the two cache codecs in src/ops/kv_cache/: K as 256 E4M3 bytes plus one FP16 row scale, V as 128 bytes of packed E2M1 plus 16 one-byte group scales. Start the server with --max-context 262144 --kv-capacity 262144 --kv-dtype k8v4 and read the arena size it reports. If the pool carries per-page metadata I have not counted, the real figure is higher; if V shares a scale across a wider group, lower.

  6. Only 16 of the model's 64 layers keep a KV cache; the other 48 are gated DeltaNet with a fixed 144 MiB state.

    docs/maintainer/qwen3_5-model.md gives the geometry table (64 text layers, 16 full attention / 48 GDN, full-attention layers at indices 3, 7, ...) and docs/maintainer/replayssm-gdn.md computes the recurrent state image at 144 MiB for the 27B. Both are the engine's docs, not the model's card; if the shipped Huihui-Qwen3.8-27B-abliterated-NVFP4-NInfer-v3 artifact carries a different layer_types array, every memory figure here is wrong in proportion. Dump it with the container's own inspector and count the full_attention entries.

  7. The fork's contribution to the headline is the draft window going from five to ten.

    upstream-provenance.json lists fifteen modified paths and a three-line change summary, all of it MTP window, round buffers and CUDA Graph matching. If a diff against Neroued/ninfer@9e163ee shows changes to the attention kernels, the cache codecs or the scheduler that the manifest does not mention, the attribution is wrong. I read the manifest and the file list; I did not diff the C++ line by line.


Read at satellitedown/fast-long-context-cinference commit 104b236 and satellitedown/cinference commit 2205806 (results file rtx5090-archive-recall.json, recorded 2026-09-21, engine revision 44e9603, upstream Neroued/ninfer@9e163ee). Both repositories are Apache-2.0. No model was run: every number here is read out of committed JSON and Markdown, or computed from the published geometry. I have no RTX 5090.

Cite this article

For attribution, please use the following reference or BibTeX:

Satyajit Ghana, "Cinference's 450 tokens a second: what the number counts", ai.thesatyajit.com, September 2026.

bibtex
@misc{ghana2026cinference,
  author = {Satyajit Ghana},
  title  = {Cinference's 450 tokens a second: what the number counts},
  url    = {https://ai.thesatyajit.com/articles/cinference},
  year   = {2026}
}
share