DeepInfer

The work behind
the next token.

A language model writes one piece of text, or token, at a time. How do you keep a whole machine busy?

Thecatsat?

Each choice depends on the text before it. DeepInfer coordinates many such requests on GPUs: processors built to do many numerical operations together.

The experiments below model DeepSeek’s offline engine at commit 774ce24. They do not run the engine. Code walkthroughs.

Two jobs hiding inside one answer

You ask a model: “Why is the sky blue?” Before it can write anything, it must process that prompt. Then it must generate its answer. These are different workloads.

A token can be a word, part of a word, or punctuation. The model assigns probabilities to the possible next tokens. Together, those probabilities form a distribution and sum to 1, or 100%. Sampling means choosing one token according to those probabilities.

The prompt’s tokens are already known, so the model can process many prompt positions together. Each position can use the text up to that position, including itself. This phase is prefill. It prepares reusable state and the probabilities for the first answer token.

After that, decode extends the answer. The model processes the token just chosen to calculate probabilities for the next one. Ordinary decoding cannot process a hundred future answer positions together: their input tokens are not known yet.

Another person’s request has a separate chain of dependencies. An engine groups positions from separate requests into a batch. The GPU can process them together because those requests are independent. An engine step runs a batch through the model, then samples tokens.

The model applies a stack of calculation stages, called layers. Each uses weights: numbers learned during training. Reading those weights takes time. Using the same loaded weights for several requests can produce more tokens per memory read. Eventually, the extra arithmetic and communication make a larger batch slower.

Could three independent requests each advance by one token in the same decode step? Could one request advance by three ordinary decode tokens?

Step through a small engine

This model processes three prompts in chunks. When a prompt finishes, the model samples its first answer token. Once all are ready, each decode step advances every unfinished answer by one token. Prompt work and decode work never share a step.

The budget limits prompt positions, while a decode step advances several separate answers. A longer prompt needs more chunks. Its answer tokens still depend on one another.

DeepInfer also keeps each batch entirely in one phase. Its GPUs coordinate the choice through periodic votes, rather than finishing the whole prompt queue first as this toy does. See the prefill vote and csrc/engine/prepare_sample.cpp:61.

Why not put every request in one enormous batch?

More requests need more cache memory. They also increase work per step, communication and time between tokens. Throughput counts total useful tokens per second. Latency is how long one reader waits. A choice that improves one may hurt the other. Step counts in this toy are not timings.

Remember the past without keeping everything

The prompt and the tokens generated so far form the context. To use that context, attention scores positions in the context for relevance and combines information from them.

The model computes arrays of numbers for this: keys for matching positions, and values for the information to retrieve. A KV cache saves them so each new token can reuse earlier work. The cache holds numerical state, rather than stored sentences.

DeepInfer keeps both long-range history and a recent sliding window. History grows with context. The window keeps only the latest positions, overwriting older entries in a ring.

Not every layer needs to create its own long-range history. Some reuse an earlier layer’s history. The layers that create it are its owners. In this toy, eight layers share two owners instead of keeping eight separate histories. Each layer still has an eight-position window. The engine allocates history in blocks of four records.

Grow the context from 8 to 32 tokens. Which memory keeps growing? Turn sharing off. What changes?

Two memory shapes, one context

Toy: eight layers, an eight-position window per layer and four-position history blocks. Sharing uses two long-range owners instead of eight. The window capacity stays at 64 records. Reserved history grows in blocks.

Blocks make allocation manageable. At a block boundary, one extra token can require a whole new block. Most of that block starts empty. Sharing reduces copies of history. A window bounds recent state. These solve different problems.

DeepInfer’s model config has 40 main layers and four long-range owners. Its recent window is 128 positions, with six extra ring slots so rejected guesses can be undone. Compression, number formats and other state also affect memory use. The toy’s equal-sized records do not estimate GPU memory. See deepinfer/worker/cache_engine.py:88-93, deepinfer/hugging_face/config.py:62-75 and the memory walkthrough.

Does caching mean work is never repeated?

No. If memory runs short, the engine may remove a running request and release its state. This is preemption. It may later recompute that state. The released engine manages local KV. The shared cache store described in the Q&A is outside this release.

The slowest chip sets the pace

Each layer also has experts: separate calculation blocks specialised during training. A router chooses a few to run for each token. This is a mixture of experts, or MoE. All expert weights must be stored, but a token uses only the selected experts and a shared expert that always runs.

DeepInfer spreads experts across a group of GPUs. A GPU handles attention for its own requests, then sends the resulting numbers to the GPUs holding the chosen experts. Results return before computation continues. Even a GPU with no local requests must join the group’s exchange.

Here, one job means one expert’s work for one token. Every job takes the same time. E0–E3 are routed experts. This example omits shared-expert work, communication and moving weights. Suppose E2 gets unusually busy. A copy can split its jobs, but the destination already has work.

At six jobs for E2, will copying E2 to GPU 0 make the layer finish sooner?

Move the bottleneck

Each job takes one equal unit of work. Original loads are 5 and 7. Copying E2 and sending half its jobs to GPU 0 makes the loads 8 and 4. Moving E1 as well makes them 6 and 6. The slowest GPU sets the idealised step duration.

With six jobs for E2, copying it alone makes the peak load worse. Moving E1 as well relieves the new bottleneck. Change E2’s demand to see why the benefit depends on the whole placement, not just the hottest expert.

DeepInfer’s expert load balancing can replicate hot experts and rearrange their placement. The real system also pays for weight movement, dispatch and communication, and cannot know future demand perfectly. These bars show equal-cost work, not a measured speedup. See deepinfer/model_executor/eplb/policy.py:73 and expert placement and its limits.

Guess ahead, then earn the tokens

A smaller draft model proposes future tokens. Those guesses give the main model known inputs to check together in one pass through its layers. This is speculative decoding. An accepted guess becomes a committed token: part of the answer the model keeps.

The catch is correctness. Keeping every draft token would make you sample from the draft model. Keeping only guesses that are the main model’s favourite would also distort its probabilities.

Suppose the draft proposes “on” in 50% of rounds, but the main model wants it in only 20%. Keeping 40% of those proposals contributes 50% × 40% = 20% of all rounds. Always rejecting “on” would lose a valid outcome. Always accepting it would keep too much.

For a proposed token, call its draft probability q and its main-model probability p. Accept with probability min(1, p/q): use p divided by q, capped at 1. A random draw between 0 and 1 decides whether it passes. With a 40% acceptance chance, a draw below 0.4 accepts it.

At the first rejection, the model needs a replacement. For each possible token, subtract its draft probability from its main-model probability and replace negative results with zero. Divide the remaining values by their total so they sum to 1. This is renormalisation. Sample the replacement from those corrected probabilities.

Later guesses assumed the rejected token was part of the text, so discard them. If all guesses pass, sample one extra token from the main model.

Change the draft’s probability of “on”. Should the final answer probabilities change? What happens to “the” when “on” is rejected?

Keep the main model’s probabilities

Target probabilities for “on”, “in” and “under” are 20%, 60% and 20%. Draft probabilities start at 50%, 30% and 20%. Acceptance keeps 20%, 30% and 20% of all rounds. The rejected 30% becomes “in”. The final distribution is 20%, 60%, 20%.

The middle table rows count shares of all rounds: kept proposals plus replacements. At the initial setting, 30% of proposals need replacing, and every replacement is “in”. The final probabilities remain 20%, 60%, 20%, even when the draft changes.

This browser model implements the ideal rule. DeepInfer multiplies q by about 0.99995 when calculating replacements. This avoids a numerical corner case but allows a small, documented deviation from the main model’s probabilities. See deepinfer/model_executor/layers/tilelang_ops/common.py:3-19, :53.

When do more guesses stop helping?

A later guess is useful only if all earlier guesses pass. Let K be the number of guesses. Let a be each guess’s acceptance chance, given that earlier guesses passed. Average progress over many rounds is 1 + a + a² + … + aᴷ tokens. The 1 is the replacement or extra token. The second guess contributes a² because the first two must pass.

For this cost model, a round takes 10 milliseconds, or ms, plus 1.5 ms per guess. These costs are invented. More guesses help only when their extra tokens repay that cost.

Useful progress per unit of work

Analytical toy: a main-model round costs 10 ms plus 1.5 ms per proposed token. With zero acceptance, extra guesses add cost and no progress. With perfect acceptance, K guesses commit K+1 tokens. These invented costs are not DeepInfer measurements.

DeepInfer estimates acceptance chances to choose how many guesses to check. Its real costs depend on batch size and recorded GPU execution shapes. See confidence-scheduled verification.

The same odds are not the same answer

Imagine two fair coin tosses. Both obey the same distribution. They need not produce the same outcome. Preserving a model’s token probabilities and reproducing an identical token sequence are different promises.

A seed is a number that lets the program reproduce its random draws. But a draw selects a token using calculated probabilities. If those probabilities change, the same draw can select a different token.

Computers store numbers with limited precision. Grouping additions differently can change the last digit. Suppose token A gets draws below a boundary, and B gets the rest. The boundary should be 0.1 + 0.2 + 0.3. What happens to a draw of exactly 0.6?

One draw, two addition orders

(0.1 + 0.2) + 0.3 is 0.6000000000000001. Adding 0.1 + (0.2 + 0.3) gives 0.6. With a draw of exactly 0.6 and the rule draw < boundary, the first selects A and the second B.

This is a deliberately sensitive counterexample, not an estimate of how often real tokens flip. GPU kernels add much larger arrays of numbers. Batch shapes can change how the kernels group those additions. Batch-invariant inference prevents other requests in the batch from changing a request’s numerical result.

DeepInfer’s documented recipe for NVIDIA GPUs combines DEEPINFER_BATCH_INVARIANT=1, a fixed seq_seed and identical input tokens and sampling settings. It also requires DEEPINFER_ENABLE_MTP_VARLEN=0. The last setting fixes how many drafts the model checks. This removes another way the sampling path can differ. This contract does not guarantee repeats across hardware or versions. See README.md:27-28 and the correctness walkthrough.

Put the pieces together

The common quantity is expensive work per committed token. An idle gap, a repeated memory read, an overloaded GPU or discarded guesses can all increase it. None of the techniques fixes every bottleneck.

Five conceptual checks with explanations. With JavaScript disabled, use the questions and worked answers at the end of Pass 0 and Pass 1.

A GPU has no local requests, but holds the busiest expert. Why can it still determine everyone’s delay? Why might adding a copy fail to fix it?

Compare your explanation with the mechanism

Other GPUs send work to that expert, so an empty local request queue does not make this GPU idle. The group waits for its expert work before continuing. A copy moves some of that work to another GPU. If the destination already has enough work, it can become even slower. The result depends on every GPU’s load, because the group waits for the slowest.

Where the small models end

The release supports offline inference. A complete service also needs a shared cache, routing across engine copies, and a way to move cached state between prefill and decode machines. We did not run the engine on GPUs. The deeper passes name the authors and configurations behind each benchmark figure.