What an LLM does
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:
- Run the model on all tokens so far. Get the probabilities of the next token.
- Select one token from these probabilities. This is sampling. The page Sampling: temperature, top-k, top-p explains the rules.
- Add the selected token to the end of the input.
- 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#
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:
breakstep() 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;
}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 diveThe 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:
- Input: token ids, an integer tensor of shape
[T]. - The layers of the model make one hidden state vector per token: shape
[T, d], wheredis the hidden size. - The output head multiplies each hidden state by a matrix of shape
[d, V], whereVis the vocabulary size. The result is the logits: shape[T, V], one raw score per token. - 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 diveHow 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.
- Compute per token: 2 × 10 × 109 = 2 × 1010 FLOPs.
- Weights to read per step: 10 × 109 × 2 bytes = 20 GB.
- 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 diveThe 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)- Lines 117-118: you must give text prompts or token ids. Else
generate()raisesValueError. - Lines 119-122: a single string becomes a list of one prompt.
- Lines 125-127: no
sampling_paramsmeansSamplingParams()with its defaults. The defaultmax_tokensis 16 (deepinfer/sampling_params.py:53). - Lines 131-141: one request per prompt. A list of
SamplingParamsgives each prompt its own settings. - Line 142:
_run_engineruns 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 diveStop conditions and their settings
| Rule | Setting | Default | Finish reason | Source |
|---|---|---|---|---|
| The last token is EOS (id 1) | ignore_eos=True turns it off | False | "stop" | csrc/engine/scheduler.hpp:1038-1041 |
| The text is longer than the model limit | max_model_len (engine) | from the engine config | "length" | csrc/engine/scheduler.hpp:1043-1046 |
The output has max_tokens tokens | max_tokens (per request) | 16 | "length" | csrc/engine/scheduler.hpp:1048-1051, deepinfer/sampling_params.py:53 |
Edge cases:
max_tokens=0becomes 1 (deepinfer/sampling_params.py:70-71). A negative value raisesValueError(deepinfer/sampling_params.py:100-101).first_n_token_no_eos=1forbids EOS as the first output token. The sampler sets the EOS logit to −∞ in the prefill step (deepinfer/model_executor/layers/sampler.py:183-185). The engine always sets the EOS id of the request to the EOS id of the tokenizer (deepinfer/engine/llm_engine.py:527).- A prompt that is longer than the prompt limit never runs. It ends with the reason
"prompt_length_exceeded"(csrc/engine/scheduler.hpp:377-384,csrc/engine/sequence.hpp:53-54).
What can go wrong#
- Very short answers. The default
max_tokensis 16 (deepinfer/sampling_params.py:53). Set a larger value for real answers. - Answers that never end early. With
ignore_eos=True, every request runs tomax_tokens. Benchmarks use this on purpose; normal use does not. - An empty prompt.
add_requestraisesValueError("prompt must not be empty")(deepinfer/engine/llm_engine.py:553-554). - A bad token id. If you pass a token id below 0, or at or above the valid limit,
add_requestlogs an info message and returnsNone. It does not raise (deepinfer/engine/llm_engine.py:580-589). The limit isvocab_size, plus 5 image ids if the request has multimodal data (deepinfer/multimodal_tokens.py:28).
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#
- 1An 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.
- 2An 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.
- 3You callmedium
llm.generate("Hello")with no sampling parameters. Why is the answer so short?Show answer
generate()usesSamplingParams()(deepinfer/entrypoints/llm.py:125-127). Its defaultmax_tokensis 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). - 4A 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.forwardgathers 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.)