DeepInfer Explained

deepinfer/sampling_params.py: SamplingParams

BeginnerCode mapabout 7 min read + 3 min deep dives

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

Purpose#

SamplingParams holds the settings of one request: how to choose each token, when to stop, and which extra values to return.

Filedeepinfer/sampling_params.py, 119 lines. The header says "Derived from vLLM; modified by DeepSeek." (deepinfer/sampling_params.py:2)
Main classSamplingParams (deepinfer/sampling_params.py:13). The constant SAMPLING_EPS = 1e-5 (deepinfer/sampling_params.py:10).
Runs inThe engine process, in Python, once per request. It imports torch only for the int32 limit (deepinfer/sampling_params.py:8).
Called byUser scripts, LLM.generate (default object), LLMEngine.add_request (from a dict), and two dummy runs in the model runner.
CallsOnly random.getrandbits and torch.iinfo.
Not in this fileAny sampling. The C++ code reads the values; the sampler uses them.

Where it sits#

Figure: the path of each field. C++ reads the numbers once, when it makes the sequence (panel 1). Each step copies them into flat arrays for the sampler (panel 2). Click to zoom.
Open full size ↗

Who calls it#

Who reads the values#

Walk through the main methods#

The file is short, so this part also serves as the list of key names.

The fields#

    def __init__(
        self,
        temperature: float = 1.0,
        top_p: float = 1.0,
        top_k: int = -1,
        ignore_eos: bool = False,
        max_tokens: int = 16,
        logprobs: bool = False,
        prompt_logprobs: bool = False,
        top_logprobs: int = 0,
        first_n_token_no_eos: int = 0,
        eos_id: int = 0,
        so_args: dict | None = None,
        golden_token_ids: list[int] | None = None,
        seq_seed: int | None = None,
    ) -> None:
        del logprobs

        self.temperature = temperature
        self.top_p = top_p
        self.top_k = top_k
        self.ignore_eos = ignore_eos
        self.max_tokens = max_tokens
        if self.max_tokens == 0:
            self.max_tokens = 1
        self.prompt_logprobs = prompt_logprobs
        self.top_logprobs = top_logprobs
        self.seq_seed = self._make_seed(seq_seed)
        self._verify_args()
        if self.temperature < SAMPLING_EPS:
            self.temperature = SAMPLING_EPS
            self.top_p = 1.0
            self.top_k = -1
        self.first_n_token_no_eos = first_n_token_no_eos
        self.eos_id = eos_id
        self.so_args = so_args
        self.golden_token_ids = golden_token_ids
ArgumentTypeDefaultMeaningRule (deepinfer/sampling_params.py:93-105)
temperaturefloat1.0temperature: divides the logits.≥ 0. Below 1e-5 it becomes 1e-5, and top-p and top-k turn off (lines 76-79).
top_pfloat1.0top-p filter.0 < top_p ≤ 1.
top_kint-1top-k filter; -1 = off.-1, or 1 … 2,147,483,647. 0 raises.
ignore_eosboolFalseGo on after the EOS token.No check.
max_tokensint16Most output tokens.0 becomes 1 (lines 70-71). Then 1 … 2,147,483,647.
logprobsboolFalse"Compatibility parameter; ignored" (line 30).Deleted at line 63; not stored.
prompt_logprobsboolFalseReturn prompt logprobs.No check. Turned off under YOCO truncation.
top_logprobsint0How many best alternatives per token.0 … 1000.
first_n_token_no_eosint0With 1: the first output token cannot be EOS.No check. Only the value 1 has an effect.
eos_idint0The EOS id for the rule above and for draft stops.No check. The engine overwrites it.
so_argsdict | NoneNoneGrammar of a structured output.Checked later, by the grammar builder.
golden_token_idslist[int] | NoneNonegolden tokens: forced output tokens.No check.
seq_seedint | NoneNoneseq_seed: the random seed of the request.None: 64 random bits. Other int: masked to 64 bits. Not an int: TypeError.

Order of the work in __init__#

  1. Line 63: drop logprobs.
  2. Lines 65-73: store the values; a max_tokens of 0 becomes 1.
  3. Line 74: make the seed (see below).
  4. Line 75: _verify_args() checks the ranges. It runs before the temperature clamp, so it sees the values that you gave.
  5. Lines 76-79: a temperature below SAMPLING_EPS becomes 1e-5, and top_p = 1.0, top_k = -1.
  6. Lines 80-83: store the four fields that have no check.

_make_seed: the seed rules#

    @staticmethod
    def _make_seed(seed: int | None) -> int:
        if seed is None:
            return random.getrandbits(64)
        if isinstance(seed, int):
            return seed & 0xFFFF_FFFF_FFFF_FFFF
        raise TypeError()
  1. Line 88: no seed gives 64 random bits from Python's random module. So two runs give different tokens, unless you pass a seed or seed the random module.
  2. Line 90: an int is masked to 64 bits. A negative seed is valid: -1 becomes 18,446,744,073,709,551,615. True is an int in Python, so it gives seed 1.
  3. Line 91: any other type raises TypeError() with an empty message. A NumPy integer is not a Python int, so np.int64(7) raises too.

Because of the mask, the range check on seq_seed at lines 104-105 can never fail.

so_args: the grammar dict#

The docstring describes the keys in Chinese (deepinfer/sampling_params.py:39-43). Translated and split into a list:

  • "schema_type" and "schema": the grammar.
  • Optional "trigger_token_ids" (one list[int]): the token sequence that turns the grammar on. Before the full sequence appears, the output is free.
  • Optional "bound_token_id": a non-negative integer; 0 is valid. Missing or None means no bound. A list is not valid. It needs a non-empty trigger_token_ids.
  • When the bound is active, free tokens are no longer allowed. The output first follows the trigger sequence token by token, then enters the grammar.

The class does not check the dict. The grammar builder checks it in build(), and then compiles the grammar on a thread pool (deepinfer/structure_output/so_matcher_builder.py:58-95):

The page Structured outputs explains the grammar matcher. examples/benchmark.py shows a regex example: {"schema_type": "regex", "schema": r"Hello, my name is [a-z]+"} (examples/benchmark.py:280).

What the engine changes after the constructor#

        if isinstance(sampling_params, dict):
            sampling_params = SamplingParams(**{k: v for k, v in sampling_params.items() if v is not None})
        sampling_params.eos_id = self.eos_id

        if self.yoco_prefill_truncation_mode != "off" and sampling_params.prompt_logprobs:
            # mask / compact 任一模式下显式关闭 prompt logprobs。
            logger.warning(
                "Request %s: prompt_logprobs is forced to False under YoCo prefill truncation "
                "(DEEPINFER_YOCO_PREFILL_TRUNCATION_MODE != 'off'): compaction cannot compute clipped rows; "
                "mask returns degenerate logp for pre-rectangle tokens",
                request_id,
            )
            sampling_params.prompt_logprobs = False
  1. Line 527: eos_id always becomes the tokenizer's EOS id. The docstring calls the field a "HACK" with default 0 "only for warm up phase" (deepinfer/sampling_params.py:36-38). So a value that you set has no effect through add_request.
  2. Lines 529-537: with DEEPINFER_YOCO_PREFILL_TRUNCATION_MODE not off, prompt_logprobs becomes False, with a warning in the log.

The engine changes the object that it gets. LLM passes a deep copy, so your object stays as it was. A direct caller of LLMEngine.add_request sees its own object change.

__repr__#

The text form shows 9 of the 13 fields (deepinfer/sampling_params.py:107-119). It leaves out first_n_token_no_eos, eos_id and golden_token_ids. logprobs is not stored, so it cannot show.

Deep dive#

The sections below give the full detail. Open them one at a time, or use Open all deep dives at the top of the page.

Deep dive

How the values reach the sampler, step by step

  1. Once, in the Sequence constructor. C++ reads attributes by name from the Python object:
    struct SamplingScalars {
        double temperature{0};
        double top_p{0};
        int top_k{-1};
        uint64_t seq_seed{0};
        int top_logprobs{0};
        bool need_prompt_logprobs{false};
        int first_n_token_no_eos{0};
        int64_t eos_id{0};
        bool ignore_eos{false};
    
        static SamplingScalars capture(const py::object &sampling_params) {
            SamplingScalars scalars;
            scalars.temperature = sampling_params.attr("temperature").cast<double>();
            scalars.top_p = sampling_params.attr("top_p").cast<double>();
            scalars.top_k = sampling_params.attr("top_k").cast<int>();
            scalars.seq_seed = sampling_params.attr("seq_seed").cast<uint64_t>();
            scalars.top_logprobs = sampling_params.attr("top_logprobs").cast<int>();
            scalars.need_prompt_logprobs = sampling_params.attr("prompt_logprobs").cast<bool>();
            scalars.first_n_token_no_eos = sampling_params.attr("first_n_token_no_eos").cast<int>();
            scalars.eos_id = sampling_params.attr("eos_id").cast<int64_t>();
            scalars.ignore_eos = sampling_params.attr("ignore_eos").cast<bool>();
            return scalars;
        }
    and max_tokens and ignore_eos into StoppingCriteria (csrc/engine/sequence.hpp:181-190). The Python object stays in the sequence as generation.params, but C++ never reads these numbers from it again.
  2. Each step, in the metadata. Each SequenceMetadata copies the SamplingScalars struct (csrc/engine/sequence.hpp:380).
  3. Each step, in prepare_sampler_inputs. The function fills one entry per sequence in pinned CPU tensors: temperatures and top_ps as float32, top_ks as int32, seq_seeds as uint64 (csrc/engine/prepare_sample.cpp:82-88, csrc/engine/prepare_sample.cpp:157-161). max_top_logprobs is the largest top_logprobs of the batch (line 157).
  4. EOS rules. In a prefill, a sequence with first_n_token_no_eos == 1 in its last prompt chunk adds a row and its eos_id to the suppression list (csrc/engine/prepare_sample.cpp:20-22, csrc/engine/prepare_sample.cpp:192-195). In a decode with drafts, stop_token_ids[i] = [eos_id, -1], or [-1, -1] with ignore_eos (csrc/engine/prepare_sample.cpp:217-219).
  5. Prompt logprobs. With prompt_logprobs, the prompt rows get the same temperature, top-p and top-k (csrc/engine/prepare_sample.cpp:181-189).
  6. To the device. All arrays go to the device with non_blocking=True (csrc/engine/prepare_sample.cpp:257-264).
  7. Stop check. After the step, the scheduler stops a sequence on EOS (unless ignore_eos), at max_model_len, or at max_tokens output tokens (csrc/engine/scheduler.hpp:1035-1052).

Worked example. R1 has temperature=0.6, top_p=0.95, seq_seed=7. R2 has temperature=0 and no seed. In one decode step: temperatures = [0.6, 1e-5], top_ps = [0.95, 1.0], top_ks = [-1, -1], seq_seeds = [7, <64 random bits>]. Note that 0.6 as float32 is 0.60000002384.

Deep dive

Where max_tokens matters besides the stop

Deep dive

Error paths

InputResultWhere
temperature=-0.1ValueError("temperature must be non-negative ...")deepinfer/sampling_params.py:94-95
top_p=0 or top_p=1.5ValueError("top_p must be in (0, 1] ...")deepinfer/sampling_params.py:96-97
top_k=0 or top_k=-2ValueError("top_k must be -1 (disable), or between 1 and int32 max ...")deepinfer/sampling_params.py:98-99
max_tokens=-5ValueError("max_tokens must be >= 1 ...")deepinfer/sampling_params.py:100-101
top_logprobs=1001ValueError("top_logprobs must be in [0, 1000] ...")deepinfer/sampling_params.py:102-103
seq_seed="7" or np.int64(7)TypeError(), no messagedeepinfer/sampling_params.py:91
logprobs=Trueno error, no effectdeepinfer/sampling_params.py:63
prompt_logprobs=True and a forkRuntimeError "Forking a cached prefix does not support prompt logprobs"csrc/engine/scheduler.hpp:1064
A type that C++ cannot cast (for example top_k=2.5)error when the engine makes the Sequence (a pybind11 cast error)csrc/engine/sequence.hpp:107

The checks run in the constructor. If you change an attribute later, for example params.top_p = 0, nothing checks the new value.

Deep dive

Settings outside the class that change sampling

SettingDefaultEffectSource
DEEPINFER_YOCO_PREFILL_TRUNCATION_MODEoffNot off: prompt_logprobs is forced to False.deepinfer/environ.py:188, deepinfer/engine/llm_engine.py:529-537
DEEPINFER_SAMPLER_FORCE_TOPK0 (off)Samples only from the top N logits, whatever top_k says.deepinfer/environ.py:150, deepinfer/model_executor/layers/sampler.py:91
DEEPINFER_DECODE_TOKEN_BLACKLISTemptyToken ids that are never sampled.deepinfer/environ.py:151, deepinfer/model_executor/layers/sampler.py:107-110
DEEPINFER_BATCH_INVARIANToffWith DEEPINFER_ENABLE_MTP_VARLEN=0 and a fixed seq_seed: the same outputs for the same inputs (NVIDIA only).deepinfer/environ.py:197, README.md:28

The page The sampler explains each of these settings in its deep dives.

Check yourself#

  1. 1
    What are the defaults of temperature, top_p, top_k and max_tokens?
    easy
    Show answer

    1.0, 1.0, -1 (off) and 16 (deepinfer/sampling_params.py:49-53).

  2. 2
    You write SamplingParams(temperature=0, top_p=0.5, top_k=40). Which values does the sampler get?
    medium
    Show answer

    Temperature 1e-5, top-p 1.0, top-k -1. The checks pass first (0 ≥ 0, 0.5 in range, 40 in range), then lines 76-79 replace the three values (deepinfer/sampling_params.py:75-79).

  3. 3
    You set eos_id=42 and call LLM.generate. Which EOS id does the request use?
    medium
    Show answer

    The tokenizer's EOS id. add_request sets sampling_params.eos_id = self.eos_id for every request (deepinfer/engine/llm_engine.py:527, deepinfer/engine/llm_engine.py:225).

  4. 4
    After add_request, a script sets seq.sampling_params.temperature = 2.0. Does the next step sample with temperature 2.0?
    hard
    Show answer

    No. The C++ Sequence copied the value into SamplingScalars in its constructor (csrc/engine/sequence.hpp:280, csrc/engine/sequence.hpp:105). SequenceMetadata and prepare_sampler_inputs read that copy (csrc/engine/sequence.hpp:380, csrc/engine/prepare_sample.cpp:158). This is the deep dive "How the values reach the sampler".