DeepInfer Explained

What an LLM does

BeginnerLearn the basicsabout 7 min read + 5 min deep dives

Code version: DeepInfer @ 9bd9cf2 · 2026-10-21

In one sentence

An LLM reads a list of tokens and gives a probability to every possible next token. It writes text by repeating this, one token at a time.

The idea#

Text in, one token out#

A large language model (LLM) does not work on letters or words. It works on tokens: small pieces of text, each with an integer id. The page Tokens and tokenizers shows how text becomes ids.

The model is a mathematical function with a fixed list of inputs and outputs:

  • Input: a list of token ids, for example the ids of "The cat sat on".
  • Output: one probability for each token in the vocabulary. The vocabulary is the list of all tokens that the model knows. A modern vocabulary often has between 32,000 and 256,000 tokens.

Take "The cat sat on". The model can give "the" a probability of 0.41, "a" 0.22 and "his" 0.09. Every other token gets a very small probability. These numbers are made up, but real outputs look like this. The model does not give one answer. It gives a probability distribution over all possible next tokens.

Writing text is a loop#

To write an answer, an inference engine runs the model in a loop. This kind of generation is autoregressive:

  1. Run the model on all tokens so far. Get the probabilities of the next token.
  2. Select one token from these probabilities. This is sampling. The page Sampling: temperature, top-k, top-p explains the rules.
  3. Add the selected token to the end of the input.
  4. Stop if the token is the end-of-sequence token (EOS), or if the answer has the maximum number of tokens. Else go back to step 1.

So an answer of 200 tokens needs 200 runs of the model. Each run makes exactly one new token. (Speculative decoding can make more than one token per run. DeepInfer uses it, but the idea stays the same.)

Where the knowledge is#

The model has billions of learned numbers, the weights. Training set them: the model read a very large amount of text and learned to predict the next token. All the "knowledge" of the model is in these numbers.

Generation does not change the weights. Each run reads all of them from device memory and uses them for the math. This is why LLM inference needs large and fast accelerators. It is also why an engine such as DeepInfer uses each read of the weights for many requests at the same time.

An everyday comparison#

Think of the word suggestions on a phone keyboard. You type "See you", and the keyboard suggests "soon", "tomorrow" or "later". When you tap one word, the keyboard adds it and suggests the next word. An LLM does the same thing, one token at a time.

The comparison stops here. A keyboard looks at the last few words and shows three choices. An LLM looks at all the text so far, often thousands of tokens. It gives a probability to every token in its vocabulary. Then the sampler, not the user, makes the choice.

What an inference engine adds#

The loop above is simple. An engine such as DeepInfer makes it fast for many users at the same time:

  • It keeps the intermediate results of earlier tokens, so each run computes only the new token. This memory is the KV cache (The KV cache).
  • It splits the work into two phases: prefill reads the whole prompt once, and each decode step adds one token (Prefill and decode).
  • It runs the tokens of many requests in one step, so one read of the weights serves all of them (Batching).
  • A scheduler decides in each step which requests run.

The picture#

Figure: one step of an LLM (panel 1) and the loop of steps that writes an answer (panel 2). Ids and probabilities are illustrative. Click to zoom.
Open full size ↗

Read panel 1 from left to right: text, ids, probabilities, one token. Panel 2 shows the loop. Each row is one run of the model. The green token of each row becomes the last input token of the next row. The loop stops at EOS.

In DeepInfer#

This is a basics page, so this part is short. The tour Tour: Life of a request follows one request through all of these places.

Step 1: the user calls generate()#

The offline API is the class LLM. generate() adds one request per prompt. If you give no SamplingParams, it uses the default values (deepinfer/entrypoints/llm.py:125-127). Then it runs the loop:

        # Run the engine.
        outputs: dict[str, list[RequestOutput]] = defaultdict(list)
        while True:
            unfinished = self.llm_engine.has_unfinished_requests()
            step_outputs = self.llm_engine.step() if unfinished else self.llm_engine.flush_inflight_reqs()
            for raw_output in step_outputs:
                # clone step_outputs since step_outputs will be released in the next step
                output = RequestOutput.clone(raw_output)
                outputs[output.request_id].append(output)
                if output.finished and use_tqdm:
                    pbar.update(1)
            if not unfinished:
                break
The loop of the engine. Each call of step() is one run of the model for all active requests. The loop ends when no request is unfinished.

Step 2: the engine stops a request#

After each step, C++ code checks each sequence for the stop rules of the loop:

    void _check_stop(Sequence &seq) {
        int token_id = seq.get_last_token_id();

        if (!seq.generation.stopping.ignore_eos && token_id == eos_token_id) {
            seq.schedule.status = SequenceStatus::FINISHED_STOPPED;
            return;
        }

        if (seq.input.tokens.get_len() > max_model_len) {
            seq.schedule.status = SequenceStatus::FINISHED_LENGTH_CAPPED;
            return;
        }

        if (seq.get_output_len() == seq.generation.stopping.max_tokens) {
            seq.schedule.status = SequenceStatus::FINISHED_LENGTH_CAPPED;
            return;
        }
Three stop rules: the EOS token (unless ignore_eos is set), the maximum model length, and max_tokens.

The status becomes a finish reason in the output: "stop" for EOS and "length" for the two length limits (csrc/engine/sequence.hpp:45-50).

Step 3: ids become text again#

At the end, LLM collects the tokens of each request. With decode_sequence=True (the default), it turns the token ids back into text with the same tokenizer (deepinfer/entrypoints/llm.py:187-190, deepinfer/entrypoints/llm.py:64).

Deep dive#

The sections below give the full detail. Open them one at a time.

Deep dive

The math of next-token prediction

Write the tokens of a text as x1, x2, …, xn. An LLM gives the probability of the whole text as a product of next-token probabilities (the chain rule of probability):

P(x1, x2, ..., xn) = P(x1) · P(x2 | x1) · P(x3 | x1, x2) · ... · P(xn | x1 ... xn-1)

The model computes each factor P(xt+1 | x1 … xt) in the same way:

  1. Input: token ids, an integer tensor of shape [T].
  2. The layers of the model make one hidden state vector per token: shape [T, d], where d is the hidden size.
  3. The output head multiplies each hidden state by a matrix of shape [d, V], where V is the vocabulary size. The result is the logits: shape [T, V], one raw score per token.
  4. Softmax turns the logits of one row into probabilities: pi = exp(zi) / Σj exp(zj).

Row t of the logits predicts token t + 1. During training, the model uses all rows: every position predicts its next token. During generation, only the last row matters, because only that row predicts a token that does not exist yet.

DeepInfer uses this fact. In a prefill step, the sampler selects only the last row of each sequence (csrc/engine/prepare_sample.cpp:197). The sampler then computes the logits only for the selected rows, in float32 (deepinfer/model_executor/layers/sampler.py:140-142, deepinfer/model_executor/layers/sampler_utils.py:25). A request with prompt logprobs is an exception: it also needs the other rows. For a prompt of 1,000 tokens and V = 129,280 (inferred, see Tokens and tokenizers), this saves 999 × 129,280 ≈ 129 million logits per sequence.

Worked example. These probabilities are made up. Take P("The") = 0.02, P("cat" | "The") = 0.01, P("sat" | "The cat") = 0.10, P("on" | "The cat sat") = 0.50:

P("The cat sat on") = 0.02 × 0.01 × 0.10 × 0.50 = 0.00001
log P               = ln 0.02 + ln 0.01 + ln 0.10 + ln 0.50
                    = −3.912 − 4.605 − 2.303 − 0.693 = −11.513

Products of many small numbers become too small for floating point. So engines work with the sum of log probabilities. One such value per token is a logprob. DeepInfer can return the logprob of each output token and of the best alternatives (top_logprobs, deepinfer/sampling_params.py:31-33).

Deep dive

How much work is one token?

A common rule of thumb: one forward pass costs about 2 × N floating-point operations (FLOPs) per token. N is the number of weights that the token uses. Each weight takes part in one multiply and one add.

Illustrative example (not a DeepInfer model): a dense model with 10 billion weights and 2-byte numbers.

  1. Compute per token: 2 × 10 × 109 = 2 × 1010 FLOPs.
  2. Weights to read per step: 10 × 109 × 2 bytes = 20 GB.
  3. An answer of 500 tokens needs 500 steps. Each step reads the 20 GB again, if the engine runs this one request alone.

So the engine reads the same weights again and again. If 64 requests share each step, one read of 20 GB serves 64 tokens. This is the main reason for batching.

Mixture of experts. A MoE model has many more weights than one token uses. A router selects a few experts per token. So N in the rule is the number of active weights per token, not all weights. DeepSeek V4.1, the model of DeepInfer, is an MoE model (Mixture of Experts (MoE)).

Deep dive

The generation loop in DeepInfer, line by line

        if prompts is None and prompt_token_ids is None:
            raise ValueError("Either prompts or prompt_token_ids must be provided.")
        if isinstance(prompts, str):
            # Convert a single prompt to a list.
            prompts = [prompts]
            multi_modal_data = [multi_modal_data]
        if prompts is not None and prompt_token_ids is not None and len(prompts) != len(prompt_token_ids):
            raise ValueError("The lengths of prompts and prompt_token_ids must be the same.")
        if sampling_params is None:
            # Use default sampling params.
            sampling_params = SamplingParams()

        # Add requests to the engine.
        num_requests = len(prompts) if prompts is not None else len(prompt_token_ids)
        for i in range(num_requests):
            prompt = prompts[i] if prompts is not None else None
            token_ids = None if prompt_token_ids is None else prompt_token_ids[i]

            self._add_request(
                prompt,
                # 共享同一个 SamplingParams 时无需在此复制:_add_request 内部会 deepcopy
                sampling_params if isinstance(sampling_params, SamplingParams) else sampling_params[i],
                token_ids,
                None if multi_modal_data is None else multi_modal_data[i],
            )
        return self._run_engine(use_tqdm)
  1. Lines 117-118: you must give text prompts or token ids. Else generate() raises ValueError.
  2. Lines 119-122: a single string becomes a list of one prompt.
  3. Lines 125-127: no sampling_params means SamplingParams() with its defaults. The default max_tokens is 16 (deepinfer/sampling_params.py:53).
  4. Lines 131-141: one request per prompt. A list of SamplingParams gives each prompt its own settings.
  5. Line 142: _run_engine runs the loop of steps.

_add_request copies the parameters, gives the request the next integer id as a string, and calls the tokenizer if you gave text (deepinfer/entrypoints/llm.py:151-157). The ids become a NumPy int32 array.

The step results. Each step() returns output objects. The loop clones each one, because the engine reuses their memory in the next step (deepinfer/entrypoints/llm.py:179-182). With zero-overhead scheduling (ZOS), a step returns the results of the previous step. So after the last step, the loop calls flush_inflight_reqs() once to collect the last results (deepinfer/entrypoints/llm.py:178). The page Zero-overhead scheduling (ZOS) explains why.

The result. generate() sorts the requests by id. It returns one string per prompt, or the list of output objects if decode_sequence=False (deepinfer/entrypoints/llm.py:187-190).

Deep dive

Stop conditions and their settings

RuleSettingDefaultFinish reasonSource
The last token is EOS (id 1)ignore_eos=True turns it offFalse"stop"csrc/engine/scheduler.hpp:1038-1041
The text is longer than the model limitmax_model_len (engine)from the engine config"length"csrc/engine/scheduler.hpp:1043-1046
The output has max_tokens tokensmax_tokens (per request)16"length"csrc/engine/scheduler.hpp:1048-1051, deepinfer/sampling_params.py:53

Edge cases:

What can go wrong#

Why it matters. Every later page builds on this loop. Prefill and decode are the first run and the later runs. The KV cache saves the work of earlier tokens. Batching and the scheduler decide which loops share a step. Sampling is step 2 of the loop.

Check yourself#

  1. 1
    An LLM gets the tokens of "The cat sat on". What does one run of the model give back?
    easy
    Show answer

    One probability for every token of the vocabulary: the chance that this token comes next. The model does not give one word. A separate sampling step selects one token from these probabilities.

  2. 2
    An answer has 300 tokens. How many runs of the model does the plain loop need, without speculative decoding?
    easy
    Show answer

    300. Each run makes exactly one new token, which becomes part of the input of the next run.

  3. 3
    You call llm.generate("Hello") with no sampling parameters. Why is the answer so short?
    medium
    Show answer

    generate() uses SamplingParams() (deepinfer/entrypoints/llm.py:125-127). Its default max_tokens is 16 (deepinfer/sampling_params.py:53). The scheduler stops the request with the reason "length" after 16 output tokens (csrc/engine/scheduler.hpp:1048-1051).

  4. 4
    A prompt has 2,000 tokens, and the vocabulary has 129,280 tokens (inferred). How many logits does the DeepInfer sampler compute for this sequence in its prefill step, and why not 2,000 rows?
    hard
    Show answer

    One row: 129,280 logits. Only the last row predicts a new token. The sampler input selects only the last token row of each prefill sequence (csrc/engine/prepare_sample.cpp:197). Sampler.forward gathers these rows and only then computes the logits (deepinfer/model_executor/layers/sampler.py:140-142). The other 1,999 rows would need 1,999 × 129,280 ≈ 258 million float32 values. (This assumes that the request asks for no prompt logprobs.)