~/satyajit

TensorFold and Strata: two one-person inference engines, checked against the memory bus

mdjsonmcp

2026-09-26 · 20 min · explainer · inference-optimization · speculative-decoding · mixture-of-experts · on-device · llama-cpp · qwen · systems

Two posts crossed my feed on 26 September, each about an inference engine one person wrote for a handful of models.

@ashxhart announced TensorFold: "I spent six months making one weight read count for more than one token on Apple Silicon. Draft tokens run through parallel lanes; the model verifies them together and keeps only what passes." The numbers: Qwen3.8-27B at 120-124 tok/s, Nemotron Lightning at 188-206, Qwen3.8 Flash Next at 88-92, all MLX 4-bit.

@TeksEdge amplified a r/LocalLLaMA post about Strata: someone running Qwen3.8-Flash-Next on a 12 GB RTX 5070 went from "llama.cpp → ~15 tps" to "Strata → up to 65.1 tps".

Both engines have code. ashhart/TensorFold is MIT, with a recipe page per model recording what worked, what was thrown away and the measurement behind each. Niko1221/Strata is CUDA and C++, with a nine-page paper and raw benchmark JSON. So both can be checked against the one thing no engine negotiates with: bytes per token over bandwidth.

The arithmetic both engines are fighting

Decoding one token means reading every weight that token touches, and on a single stream that read sets the speed. Speculative decoding drafts a few tokens cheaply and verifies them all in one forward pass, so one weight read commits several tokens. That is the post's "one weight read counts for more than one token".

For a dense model in fast memory the ceiling is a straight line:

tok/s  ≤  BD×L\text{tok/s} \;\le\; \frac{B}{D} \times L

where BB is the memory bandwidth, DD the bytes read per forward pass and LL the tokens committed per pass (accepted drafts plus the one the model always adds). Nothing in a kernel can put a point above that line.

A mixture-of-experts model that does not fit in VRAM has two rooflines in series. The always-used weights (attention, DeltaNet, routers, shared experts, the head) are read once per pass from VRAM. But each token in the pass routes to its own experts, and the ones not cached on the GPU come down a slow path: the CPU reading system RAM, or PCIe. With EE the expert bytes one token reads and hh the share already in VRAM:

tpass=DBfast+max⁡ ⁣(h E LBfast, (1−h) E LBslow)+tfixed,tok/s=Ltpasst_\text{pass} = \frac{D}{B_\text{fast}} + \max\!\Big(\frac{h\,E\,L}{B_\text{fast}},\ \frac{(1-h)\,E\,L}{B_\text{slow}}\Big) + t_\text{fixed}, \qquad \text{tok/s} = \frac{L}{t_\text{pass}}

The first term is paid once per pass; the expert term grows with LL. That one difference decides what drafting can buy each engine, and it is the thread through everything below.

TensorFold: what the lanes are

ashhart/TensorFold@d7470ed · snapshot 2026-09-26
tracked files
177
license
MIT
branch
HEAD
tests
45 files
source
1.6 MB
commit date
2026-09-26
source by language
Python1.6 MB(143)CUDA33.0 kB(3)C++12.9 kB(3)

by size of tracked source at this commit, file counts in brackets; docs, data and vendored trees excluded

local clone, 2026-09-26 at d7470ed — branch, commit, commitDate, fileCount, hasTests, languages, license, licenseFile, testFileCount

"Parallel lanes" are the rows of one verify pass. The docstring of engine/lane_engine.py says it directly: "The rows of one pass can hold the near future of one stream (drafts), alternative futures (a draft tree), or rows of other streams." Where the drafts come from depends on the model.

Qwen3.8-27B drafts with a separate model, z-lab's DFlash2 block drafter: five layers, trained on blocks of 8, reading the target's hidden states at layers 5, 19, 33, 47 and 61. TensorFold builds a best-first tree over the drafter's candidates, 4 children a node and up to 15 nodes, and verifies it in one forward. It is not the MTP head. The recipe tried the checkpoint's MTP head as a chain: more tokens a pass (5.33 against 4.55) but a step cost 1.5-2.9 ms, too slow to pay. It also tried lanes with no drafter at all, Jacobi decoding: 1.07 tokens a pass. On top of the tree sit copy windows (continue an earlier span once 8 or more tokens match) and a session n-gram prior on the tree scores. The fast path needs an M5-generation GPU for its tensor units; elsewhere the 27B runs on MLX's kernels, and drafted rows are checked at width rather than bit-identical to one-row decoding.

Qwen3.8 Flash Next drafts with its own MTP head, as a chain of up to 3. The depth follows a running average of acceptance: 1 draft below 80%, 2 below 90%, otherwise 3. Acceptance runs about 87% on code and 73% on prose.

Nemotron 3.5 Lightning is the surprise. The README says its speed rows "predate its MTP drafts". The 188-206 tok/s in the post is mostly kernel fusion (about 900 kernels a token down to 370), tokens drawn on the GPU and one step decoded ahead, against 138 for mlx_lm. Flash Next is similar: fusion took it from 30 tok/s at first port to 79 serial, and drafting adds about a third. Only the 27B's number, 27 serial to 120-124, is mostly about drafting.

Why sixteen rows cost what one does

A lane pays only if a pass over many rows costs little more than a pass over one. For a dense model that is a kernel question, and the recipe measured it: the whole-model forward of the 27B at 1, 2, 4, 8 and 16 rows.

One forward pass, 1 to 16 rows · measured, TensorFold recipe pages
020406080100120124816rows verified in one passforward ms27B · lane matmuldense, M5 Max27B · MLX matmuldense, M5 MaxFlash Next · fusedMoE, M3 Ultra, 2k
rows in the pass
pathpass msms / tokenvs one row
27B · lane matmul53.710.74.36×
27B · MLX matmul112.622.51.46×
Flash Next · fusednot measured at 16 rows

“vs one row” is the speed of the drafted pass against that same path decoding one token per pass. Committed tokens are an input here, not a measurement.

TensorFold's lane matmul does 4-bit weights times bf16 activations on the M5's tensor units with one arithmetic for every row count: 46.8 ms at one row, 53.7 at sixteen. MLX's own quantized matmul starts cheaper, 32.9 ms, and climbs to 112.6. The recipe puts one lane row at about 1.35x MLX's vector kernel, and states the trade plainly: pay more for one row so that sixteen cost what one does.

The MoE line is different in kind. Flash Next on the M3 Ultra goes from 11.0 ms at one row to 36.7 at eight, because each row brings experts of its own: 8 consecutive tokens pick about 40 distinct experts across their 80 slots. That is the dense term against the expert term of the equation above, measured inside one project.

Exact means byte-identical

Most speculative decoders accept a draft with a random test (Leviathan et al.), so the text depends on how drafting went. TensorFold replaces the test with a coupling. The token at position pp is

arg⁡max⁡i  (logitiT+g(seed,p,i))\arg\max_i \;\Big(\frac{\text{logit}_i}{T} + g(\text{seed}, p, i)\Big)

over the top-k/top-p candidates, with gg Gumbel noise hashed from the seed, the position and the token id (engine/exact_sampling.py). That is an exact draw from the model's distribution, and it depends only on the row's logits and its position. The kernels give a row of a multi-row pass the same bits as a one-row step. So a draft is accepted exactly when it equals the token serial decoding would sample there, and drafted output is byte-identical to serial output. Send "draft": false to check.

This is what Daliri, Musco and Suresh call drafter-invariant speculative decoding. TensorFold does not cite it, but the construction is the same, and so is the price. A shared-noise coupling agrees with probability at least (1−DTV)/(1+DTV)(1-D_\text{TV})/(1+D_\text{TV}), where the optimal coupling that rejection sampling uses reaches 1−DTV1-D_\text{TV}. At a total-variation distance of 0.2 that is a floor of 0.67 against 0.80 per draft. Byte-exactness costs some acceptance in the worst case. The recipe also names a limit: for Flash Next and Nemotron a reply can depend on which prompt prefix was already cached, down to the last bits.

TensorFold: what 120 tok/s implies

The byte count is checkable. The site's Splash piece counted this checkpoint at 26,935,320,064 parameters in 15.155 GB at 4.5 bits per weight (4-bit values plus a bf16 scale and bias per 64). Decode reads one row of the embedding table, not all of it; drop its 1,271,398,400 parameters and 14.44 GB remains per forward. The recipe says "about 14.4 GB". Reasoned, and it agrees. The download is consistent too: TensorFold's README lists the checkpoint at 16.1 GB, and 15.155 GB plus the 0.93 GB bf16 vision tower the Splash piece found is 16.09.

The recipe measured the M5 Max reading about 569 GB/s. That rules out the 32-core M5 Max, whose rated peak is 460 GB/s, so this is the 40-core part (614 GB/s). Then:

At five tokens a round, each committed token costs 14.44 / 5 = 2.9 GB of weight traffic instead of 14.44. That is the whole trick.

TensorFold's banner: a folded mesh on dark blue with the text 'Qwen3.8 27B 120-124 tok/s M5 Max, Nemotron 3.5 188-206 tok/s M5 Max, Qwen3.8 Flash Next 88-92 tok/s M3 Ultra, 4-bit, short answers with thinking, server decode'.
The numbers in the post, from the banner at the top of the README. Its Flash Next figure is older than the README's own table, which reads 105-107; its Nemotron row predates MTP drafts (TensorFold README, assets/tensorfold-hero-speeds.png).

The git history dates the numbers. The banner was committed at 01:24 (UTC+1) on 26 September, the post went out at 01:31, and "perf: Flash Next decode 15-17% faster, byte-exact" landed at 02:45. So the README table says 105-107 for Flash Next (79 without drafts) while the banner above it still says 88-92.

What the baselines are. "27 without drafts" is TensorFold's own serial engine, not stock MLX; mlx_lm is quoted only for Nemotron. No run compares against mlx_lm with its own draft model. The most careful comparison in the repository is on NVIDIA: on one DGX Spark, the 27B does 49.6 tok/s on sampled code against vLLM's 17.7 with MTP=3, the same client, a median of five seeds. vLLM ran an NVFP4 checkpoint rather than TensorFold's 4-bit MLX weights, which is a real difference. With its own DFlash2 at seven drafts, vLLM reached 28.3 and 22.9 tok/s on one Spark; the recipe does not say which prompt is which. Against that, TensorFold's 49.6 and 45.8 are about 2x, not 3x.

Strata: 66 GB into 12 GB

Niko1221/Strata@6da1f66 · snapshot 2026-09-26
tracked files
220
branch
HEAD
tests
11 files
source
2.5 MB
commit date
2026-09-25
source by language
C++1.6 MB(132)CUDA603.5 kB(44)Python265.2 kB(18)HTML8.5 kB(1)Shell1.4 kB(1)

by size of tracked source at this commit, file counts in brackets; docs, data and vendored trees excluded

local clone, 2026-09-26 at 6da1f66 — branch, commit, commitDate, fileCount, hasTests, languages, testFileCount

Qwen3.8-Flash-Next is 125B parameters plus 51B of n-gram embeddings, with 6B active per token (see the model piece). Its 48 layers each carry 512 routed experts of width 640, of which the router picks 10. Strata's paper opens with a byte census.

Three stacked tiers. GPU VRAM, 12 GB at 672 GB/s: dense weights about 3.3 GB, an expert cache of about 4,500 of 24,576 experts, the MTP layer at 0.8 GB, KV and state. PCIe 4.0 x16 at about 26 GB/s carries experts by DMA. System RAM, 64 GB DDR5 at 41-52 GB/s with 6 AVX-512 cores: a pinned arena of all 24,576 experts, 34 GB for Q2_0, where the CPU computes misses in place. SSD: the GGUF shard read once at start and the 28.8 GB n-gram table, memory-mapped.
Where every byte lives. The design is organised around the 0.66 GB of routed experts a token reads, not the 34 GB they occupy (Strata paper, Figure 1).

I recomputed the number the design is built around. One token routes to 10 experts in each of 48 layers; each expert is three matrices of 2,560 × 640. That is 2,359,296,000 weights, and at the Q2_0 file's 2.25 bits per weight it is 0.66 GB per token, 2% of the 34.0 GB arena. The same share of the other arenas gives 0.69 GB for IQ2_XS and 0.84 GB for IQ3_XXS. The always-used weights are about 3.5 GB per pass (1.8 GB of mixers and shared experts, 1.3 GB of gated residual, a 0.44 GB head). One user's log in the thread even prints the head: "experimental native Q5_K head, 437043200 bytes". 2,560 × 248,320 weights at 5.5 bits is exactly 437,043,200 bytes.

The n-gram table is kept, not dropped. It lives on the SSD as a 28.8 GB memory-mapped file, and a token reads 16 rows of it, at most 23 KB. That is the opposite of the --no-ngram fork the model piece traced.

One layer, three workers

A timeline of one layer. The GPU runs gated read, mixer, gated write and router, then writes 10 expert ids to a doorbell. The GPU then runs the shared expert and cached experts while PCIe copies 20% of the misses by DMA and the CPU computes the missed experts in place in RAM on 6 threads. The GPU combines the results and starts the next layer. One layer is about 0.6 ms at 4K.
The GPU never waits on the driver; the CPU spins on a doorbell in pinned memory, and the next layer waits for the slower half (Strata paper, Figure 2).

The CPU splits each layer's ten experts: cached ones go to the GPU, a share of the misses is copied over PCIe by the copy engine (20% for Q2_0, 55% for the i-quants, whose CPU is arithmetic-bound and leaves RAM bandwidth free), and six AVX-512 cores compute the rest where they sit in RAM. The 48-layer pass is one captured CUDA graph per window size. The PCIe-or-CPU split is the trade the site worked through for FreeToken.

Three more pieces carry the speed:

A curve of hit rate against experts held in VRAM, rising steeply from 0 to about 0.5 at 4,500 experts, 0.67 at about 7,500, and flattening toward 1.0 at 24,576. Points mark 12 GB at 4K, 16 GB and 24 GB cards.
The profile-filled cache alone. The adaptive swapping lifts the 12 GB card from the curve's 0.50 to a measured 0.72 at 4K (Strata paper, Figure 3).

Strata: what 65 tok/s implies

First, which 65. The 65.1 is the Q2_0 file at a 128K context: a 128,478-token code-review prompt, 256 generated tokens, greedy decoding, one run from a cold start, 2.65 tokens per verify window, 3,079 experts in VRAM. The Reddit title's "65" is also the IQ3_XXS file at 4K (65.6). A short chat on Q2_0 runs 95.

Now the bytes. Every token needs 0.66 GB of experts from somewhere.

So the VRAM cache is doing real work. The paper's Table 5 gives the 4K round for Q2_0: 34.3 ms, of which the GPU takes 14.6, the CPU's experts 13.4, drafting 2.0 and the rest 4.3, at 3.23 tokens per window and a 0.72 hit rate. Laying the bytes out (reasoned, assuming no two tokens share an expert):

Per 4K window, Q2_0GBwhere
dense weights3.54VRAM
expert hits (0.72 × 0.66 × 3.23)1.53VRAM
expert misses, CPU share0.48RAM
expert misses, DMA share0.12PCIe

About nine bytes in ten come from VRAM and one in fifty crosses PCIe. The time splits the other way. At 672 GB/s the GPU's 5.1 GB is 7.6 ms of reading against 14.6 measured, about half its roofline. The CPU's 0.48 GB at 41 GB/s is 11.6 ms against 13.4 measured, close to its. The CPU reads a twelfth of the bytes and takes almost as long as the GPU does for everything else.

Decode ceiling · dense read once per pass, experts per token · reasoned
06012018024012345678tokens committed per verify passceiling tok/slimit 222measured 94.6
one pass at these settings26.1 ms · ceiling 124 tok/s
dense read 5.3 msexpert misses (slow path) 14.6 msdrafting + other 6.3 ms

A bandwidth model, not a strict bound. It leaves out two things that would raise the line (tokens of a pass sharing an expert, and Strata sending a share of its misses over PCIe in parallel) and everything that lowers the real number (attention, kernel launches, the host). The dashed “limit” is the line as tokens per pass grows without bound.

With the paper's own inputs the model puts the Q2_0 4K ceiling at about 124 tok/s against 94.6 measured, and IQ3_XXS at about 72 against 65.6. The shape is the lesson. On the dense 27B preset the curve is a straight line. On the Strata presets it bends toward a limit set by the miss path (about 222 tok/s for Q2_0 at 4K), because drafting multiplies expert traffic as fast as it multiplies tokens. A higher hit rate is worth more than a deeper draft, which is why the paper says of the GPU: "its size matters more than its speed".

At 128K the same engine gives 65.1: the KV cache takes VRAM, the cache shrinks from 4,464 to 3,079 experts, and windows fall from 3.23 to 2.65 tokens. The paper does not publish the 128K hit rate, so I stop there.

The 15 it is measured against

The "~15 tps" comes from the author's own post on 16 September, same PC, the IQ3_XXS file: 13.8 tok/s at 1K, 14.6 at 8K, 14.0 at 20K and 11.3 at 90-100K. Asked, he said speculative decoding was on and that it "ran over llamacpp". Flags were requested in both threads and never posted; in the Strata thread the answer was "There's no flags since its a custom engine."

The same thread shows the baseline moving. On 17 September: 19-20 tok/s, then 21 at 128K. On 18 September: 29. On 22 September: "around 32tps + 550 ppts". So the post compares the first, untuned llama.cpp run on IQ3_XXS with Strata's best file, Q2_0, at a different context.

Like for like on IQ3_XXS: Strata's 44.8 at 128K against llama.cpp's 11.3 at 90-100K is 3.96x; Strata's 65.6 at 4K against the author's best reported 32 (context not stated) is 2.05x. A 2x to 4x speedup is a real result. It is not the 4.3x the post implies by pairing 65.1 with 15. One more check: if the 32 was still IQ3_XXS, it is above what an all-CPU expert path gives on this box, Strata's measured 24 GB/s over my 0.84 GB per token, or 28.6 tok/s even with unlimited drafting, unless drafted tokens shared experts. Keeping some experts on the GPU, the usual --n-cpu-moe setup, is the likelier explanation.

Other users' numbers fit a 2x reading. One got 38.0-51.6 tok/s from ik_llama.cpp with the MTP head on a 12 GB RTX 3060, though beside an 8-channel DDR4 server CPU. Another, on a 12 GB laptop GPU, got 50 tok/s from Strata against 23 from a llama.cpp fork on the same quant.

Is the output the same?

TensorFold is exact by construction and says how to check it. For Flash Next on DGX Spark the recipe compares drafted replies against "draft": false for 3 prompts, 2 seeds and greedy: 9 of 9 equal on one and on two Sparks.

Strata claims its speculative output is "identical, token for token, to plain greedy decoding", tested with forced wrong drafts, and a mean KL of 0.022 against llama.cpp, below the 0.058 between llama.cpp's own CPU and GPU builds. Both reported, both plausible.

But Strata's default launch passes --expert-cache auto, and with the cache on, src/program/generate.cpp prints this every time:

strata generate: *** WARNING: --expert-cache is enabled and the GPU hit path is NOT
                 CORRECT. The generated tokens diverge from a cache-off run (measured:
                 first difference at token 40 at 2.97% hits, token 0 at 54.4%). Any
                 timing from this run is real; any OUTPUT from it is not. ***

One user's log in the thread shows it, verbatim. The comment above it dates it to "round 328". A later change marked "R4.2h, round 331" names a cause: GPU hits quantized their activations with an fp16 scale while CPU misses used fp32, "80 of 80 chunks differing by up to 4.761e-04 relative", and fixes it so a hit reproduces a miss. The warning was never removed, and nothing in the repository re-measures cache-on against cache-off. It matters because the cache adapts to the conversation: if hits and misses disagree, the same prompt can decode differently depending on what came before it. I cannot tell from the tree which is true. The author can, with one diff.

Two limits are stated plainly in the docs and worth knowing before pointing an agent at it. Decoding is greedy only; temperature is ignored. And every request re-reads its whole prompt, about a minute per 30,000 tokens, because nothing is kept between requests.

Openness

TensorFoldStrata
LicenceMIT, with third-party noticesno licence file; the README says "Free and open source"
History14 commits, 25-26 Sep4 commits, 24-25 Sep
Tests45 test files; lane kernels need an M5 GPU, CUDA tests an NVIDIA one11 test files; in-tree parity checks run with --selftest
Benchmark clienttools/bench_openai.py, used for every numberraw JSON in bench/results/
Missingnothing I neededref/*.py references (only ref/load.py ships), the llama.cpp-oracle tests in bench/micro/, tools/logits_identical.py

Without a licence file, Strata is all rights reserved by default, whatever the README says; it does carry ggml's MIT notice for the code it transcribes. Its llama.cpp-oracle parity tests are wired into CMake, but their sources are not in the tree, so the llama.cpp comparisons the paper leans on cannot be rerun from what was published.

What I would take from both

Both multipliers come from bytes per committed token. TensorFold's is a kernel result: make a row's arithmetic independent of how many rows share the pass, put the rows on tensor units, and a dense model verifies a 16-node tree for about what one token costs. Strata's is a placement result: the 3.5 GB every token needs in VRAM, as many hot experts beside it as fit, and six CPU cores computing the rest where they sit.

The equation says which lever each engine should pull next. For a dense model in fast memory, more tokens per pass. For an offloaded MoE, a higher hit rate first, because a deeper draft drags its experts down the slow path with it.

What would change my mind

3 claims above, and what would falsify each

  1. Strata's expert cache changes the generated tokens.

    Run one prompt greedy with --expert-cache auto and with the cache off. If the token ids match for a few hundred tokens at a realistic hit rate, the R4.2h fix worked and the warning is stale.

  2. TensorFold's 120 tok/s on the 27B needs at least three committed tokens per pass.

    If a forward reads materially less than 14.44 GB, or the chip sustains more than 569 GB/s, the floor drops. A count of tokens per round during a 120 tok/s run settles it.

  3. Against a tuned llama.cpp on the same PC and file, Strata is about 2x, not 4x.

    Rerun the llama.cpp command behind the 32 tok/s run on IQ3_XXS at 4K and 128K beside Strata. If it lands near 15, the 4x holds and my reading of the thread is wrong.

Cite this article

For attribution, please use the following reference or BibTeX:

Satyajit Ghana, "TensorFold and Strata: two one-person inference engines, checked against the memory bus", ai.thesatyajit.com, September 2026.

bibtex
@misc{ghana2026tensorfoldstrataengines,
  author = {Satyajit Ghana},
  title  = {TensorFold and Strata: two one-person inference engines, checked against the memory bus},
  url    = {https://ai.thesatyajit.com/articles/tensorfold-strata-engines},
  year   = {2026}
}
share