Source code for furiosa_llm.sampling_params

# Copyright (c) 2023, The vLLM team.
# Copyright (c) 2023, FuriosaAI Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import copy
from dataclasses import dataclass
from enum import IntEnum
from functools import cached_property

from pydantic import BaseModel

from furiosa_llm.constants import StructuredOutputsBackend as StructuredOutputsBackend
from furiosa_llm.metadata.tasks import PoolingTask
from furiosa_llm.outputs import RequestOutputKind

_SAMPLING_EPS = 1e-5


class SamplingType(IntEnum):
    GREEDY = 0
    RANDOM = 1


# https://github.com/vllm-project/vllm/blob/6d8d0a24c02bfd84d46b3016b865a44f048ae84b/vllm/sampling_params.py#L30-L84
@dataclass
class StructuredOutputsParams:
    """
    Parameters for a pattern, corresponding to one state machine.

    These parameters also function as request JSON body parameters (in "structured_outputs" field) in OpenAI-compatible interfaces.
    """

    json: str | dict | None = None
    regex: str | None = None
    choice: list[str] | None = None
    grammar: str | None = None
    json_object: bool | None = None
    disable_any_whitespace: bool = False
    disable_additional_properties: bool = False
    whitespace_pattern: str | None = None
    structural_tag: str | None = None

    # XXX: Indicates if this parameter was generated from xgrammar’s harmony built-in structural tag.
    # This field is needed to work around the xgrammar limitation described in #3907.
    # It should not be set by the user, but should be managed internally and implicitly.
    _is_harmony_structural_tag: bool = False

    backend: str | None = None

    @staticmethod
    def from_optional(
        json: dict | BaseModel | str | None = None,
        regex: str | None = None,
        choice: list[str] | None = None,
        grammar: str | None = None,
        json_object: bool | None = None,
        whitespace_pattern: str | None = None,
        structural_tag: str | None = None,
        backend: str | None = None,
        disable_additional_properties: bool = False,
    ) -> "StructuredOutputsParams | None":
        if all(arg is None for arg in (json, regex, choice, grammar, json_object, structural_tag)):
            return None
        # Extract json schemas from pydantic models
        if isinstance(json, (BaseModel, type(BaseModel))):
            json = json.model_json_schema()

        return StructuredOutputsParams(
            json=json,
            regex=regex,
            choice=choice,
            grammar=grammar,
            json_object=json_object,
            whitespace_pattern=whitespace_pattern,
            structural_tag=structural_tag,
            disable_additional_properties=disable_additional_properties,
            backend=backend,
        )

    def __post_init__(self):
        """Validate that some fields are mutually exclusive."""
        guide_count = sum(
            [
                self.json is not None,
                self.regex is not None,
                self.choice is not None,
                self.grammar is not None,
                self.json_object is not None,
                self.structural_tag is not None,
            ]
        )
        if guide_count > 1:
            raise ValueError(
                "You can only use one kind of structured output but multiple are "
                f"specified: {self.__dict__}"
            )
        if guide_count < 1:
            raise ValueError("at least one structured output constraint must be specified")


# https://github.com/vllm-project/vllm/blob/v0.6.2/vllm/sampling_params.py#L46-L127
[docs] class SamplingParams: """Sampling parameters for text generation. Args: n: Number of output sequences to return for the given prompt. repetition_penalty: Float that penalizes new tokens based on whether they appear in the prompt and the generated text so far. Values > 1 encourage the model to use new tokens, while values < 1 encourage the model to repeat tokens. temperature: Float that controls the randomness of the sampling. Lower values make the model more deterministic, while higher values make the model more random. Zero means greedy sampling. top_p: Float that controls the cumulative probability of the top tokens to consider. Must be in (0, 1]. Set to 1 to consider all tokens. top_k: Integer that controls the number of top tokens to consider. Set to -1 to consider all tokens. min_p: Float that represents the minimum probability for a token to be considered, relative to the probability of the most likely token. Must be in [0, 1]. Set to 0 to disable this. stop: List or single non-empty string(s) that stop generation when the first string is matched. By default, the matched stop string is removed from the returned text. When streaming, Furiosa-LLM may hold back a trailing window up to the longest configured stop string to prevent a partial match from being emitted. stop_token_ids: Token IDs that stop the generation when they are generated. The returned output will contain the stop tokens unless the stop tokens are special tokens. include_stop_str_in_output: Whether to retain the matched stop string in the returned text. Defaults to False. When True, text after the match within the token that completed it is removed. Streaming does not need to hold back a trailing stop-string window in this mode. ignore_eos: Whether to ignore the EOS token and continue generating tokens after the EOS token is generated. max_tokens: Maximum number of tokens to generate per output sequence. If the value is None, it is capped to the maximum sequence length. min_tokens: Minimum number of tokens to generate per output sequence before EOS or stop_token_ids can be generated skip_special_tokens: Whether to skip special tokens in the output. logprobs: Number of log probabilities to return per output token. When set to None, no probability is returned. If set to a non-None value, the result includes the log probabilities of the specified number of most likely tokens, as well as the chosen tokens. Note that the implementation follows the OpenAI API: The API will always return the log probability of the sampled token, so there may be up to `logprobs+1` elements in the response. prompt_logprobs: Number of log probabilities to return per prompt token. When set to None (default), no prompt logprobs are returned. When set to a non-negative integer, returns the top-k log probabilities for each prompt token position, plus the actual token's logprob. Set to -1 to return log probabilities for all vocabulary tokens. **Warning**: Using -1 can cause significant memory and network overhead as it returns logprobs for the entire vocabulary (e.g., ~150K tokens for Qwen models) at each prompt position. structured_outputs: Parameter for specifying structured output requirements. reasoning_structured_outputs: Parameter that matches the reasoning part of model output. This will be used to strip the reasoning tokens before applying structured output requirements on the answer content. extra_args: Arbitrary additional arguments carried alongside the standard sampling parameters. All extra (undeclared) keys of an API request are collected here. Specific consumers extract the keys they care about; e.g. ``kv_transfer_params`` is extracted from this dict. kv_transfer_params: Access point of the remote KV connector, extracted from ``extra_args["kv_transfer_params"]``. There can be one of two ZMQ endpoints (``"host:port"``): ``"remote_decoder"`` and ``"remote_prefiller"``. If set, the worker connects to the given endpoint to enable PD-disaggregated prefill or decoding. ``"id"`` is the shared identifier of the P-D pair: it must be the same value on both the prefiller and the decoder, and the connector uses it to match the two sides. """ def __init__( self, *, n: int = 1, repetition_penalty: float = 1.0, temperature: float = 1.0, top_p: float = 1.0, top_k: int = -1, min_p: float = 0.0, stop: str | list[str] | None = None, stop_token_ids: list[int] | None = None, include_stop_str_in_output: bool = False, ignore_eos: bool = False, max_tokens: int | None = 16, min_tokens: int = 0, skip_special_tokens: bool = True, logprobs: int | None = None, prompt_logprobs: int | None = None, output_kind: RequestOutputKind = RequestOutputKind.CUMULATIVE, structured_outputs: StructuredOutputsParams | None = None, reasoning_structured_outputs: StructuredOutputsParams | None = None, extra_args: dict | None = None, ) -> None: # Filter empty strings and switch empty array to None # This is to keep vLLM behavior compatibility if isinstance(stop, str): stop = [stop] stop = [s for s in (stop or []) if s] stop = stop or None self.n = n self.repetition_penalty = repetition_penalty self.temperature = temperature self.top_p = top_p self.top_k = top_k self.min_p = min_p self.stop = stop self.stop_token_ids = stop_token_ids self.include_stop_str_in_output = include_stop_str_in_output self.ignore_eos = ignore_eos self.max_tokens = max_tokens self.min_tokens = min_tokens self.skip_special_tokens = skip_special_tokens # https://github.com/vllm-project/vllm/blob/v0.6.2/vllm/sampling_params.py#L253 self.logprobs = 1 if logprobs is True else logprobs self.prompt_logprobs = prompt_logprobs self.output_kind = output_kind self.structured_outputs = structured_outputs self.reasoning_structured_outputs = reasoning_structured_outputs self.extra_args = extra_args self.kv_transfer_params = (extra_args or {}).get("kv_transfer_params") self._verify_args() @classmethod def from_optional( cls, *, n: int | None = None, repetition_penalty: float | None = 1.0, temperature: float | None = None, top_p: float | None = None, top_k: int | None = None, min_p: float = 0.0, stop: str | list[str] | None = None, stop_token_ids: list[int] | None = None, include_stop_str_in_output: bool = False, ignore_eos: bool | None = None, max_tokens: int | None = None, min_tokens: int | None = None, skip_special_tokens: bool = True, logprobs: int | None = None, prompt_logprobs: int | None = None, output_kind: RequestOutputKind | None = None, structured_outputs: StructuredOutputsParams | None = None, reasoning_structured_outputs: StructuredOutputsParams | None = None, extra_args: dict | None = None, ) -> "SamplingParams": return cls( n=1 if n is None else n, repetition_penalty=1.0 if repetition_penalty is None else repetition_penalty, temperature=1.0 if temperature is None else temperature, top_p=1.0 if top_p is None else top_p, top_k=-1 if top_k is None else top_k, min_p=min_p, stop=stop, stop_token_ids=stop_token_ids, include_stop_str_in_output=include_stop_str_in_output, ignore_eos=False if ignore_eos is None else ignore_eos, max_tokens=max_tokens, min_tokens=0 if min_tokens is None else min_tokens, skip_special_tokens=skip_special_tokens, logprobs=logprobs, prompt_logprobs=prompt_logprobs, output_kind=RequestOutputKind.CUMULATIVE if output_kind is None else output_kind, structured_outputs=structured_outputs, reasoning_structured_outputs=reasoning_structured_outputs, extra_args=extra_args, ) def clone(self) -> "SamplingParams": return copy.deepcopy(self) def __eq__(self, other) -> bool: return ( isinstance(other, SamplingParams) and self.n == other.n and self.repetition_penalty == other.repetition_penalty and self.temperature == other.temperature and self.top_p == other.top_p and self.top_k == other.top_k and self.min_p == other.min_p and self.max_tokens == other.max_tokens and self.min_tokens == other.min_tokens and self.skip_special_tokens == other.skip_special_tokens and self.logprobs == other.logprobs and self.prompt_logprobs == other.prompt_logprobs and self.output_kind == other.output_kind and self.stop == other.stop and self.stop_token_ids == other.stop_token_ids and self.include_stop_str_in_output == other.include_stop_str_in_output and self.ignore_eos == other.ignore_eos and self.structured_outputs == other.structured_outputs and self.reasoning_structured_outputs == other.reasoning_structured_outputs ) def _verify_args(self) -> None: if self.n < 1: raise ValueError(f"n must be at least 1, got {self.n}.") if self.n > 1: raise ValueError(f"furiosa-llm currently does not support n > 1, got {self.n}.") if not 0.0 < self.repetition_penalty <= 2.0: raise ValueError( f"repetition_penalty must be in (0, 2], got {self.repetition_penalty}." ) if self.temperature < 0.0: raise ValueError(f"temperature must be non-negative, got {self.temperature}.") if not 0.0 < self.top_p <= 1.0: raise ValueError(f"top_p must be in (0, 1], got {self.top_p}.") if self.top_k < -1 or self.top_k == 0: raise ValueError(f"top_k must be -1 (disable), or at least 1, got {self.top_k}.") if not 0.0 <= self.min_p <= 1.0: raise ValueError(f"min_p must be in [0, 1], got {self.min_p}.") if self.stop is not None: if any(len(s) == 0 for s in self.stop): raise ValueError("stop strings must not be empty.") if self.max_tokens is not None and self.max_tokens < 1: raise ValueError(f"max_tokens must be at least 1, got {self.max_tokens}.") if self.min_tokens < 0: raise ValueError( f"min_tokens must be greater than or equal to 0, got {self.min_tokens}." ) if self.max_tokens is not None and self.min_tokens > self.max_tokens: raise ValueError( f"min_tokens must be less than or equal to " f"max_tokens={self.max_tokens}, got {self.min_tokens}." ) if self.logprobs is not None and self.logprobs < 0: raise ValueError(f"logprobs must be non-negative, got {self.logprobs}.") if ( self.prompt_logprobs is not None and self.prompt_logprobs != -1 and self.prompt_logprobs < 0 ): raise ValueError( f"prompt_logprobs must be -1 (all tokens) or non-negative, " f"got {self.prompt_logprobs}." ) if self.kv_transfer_params is not None: if not isinstance(self.kv_transfer_params, dict): raise ValueError( f"kv_transfer_params must be a dict or None, got {self.kv_transfer_params}." ) if "id" not in self.kv_transfer_params: raise ValueError( f"kv_transfer_params must contain an 'id' field, got {self.kv_transfer_params}." ) if ( "remote_prefiller" not in self.kv_transfer_params and "remote_decoder" not in self.kv_transfer_params ): raise ValueError( f"kv_transfer_params must contain either 'remote_prefiller' or 'remote_decoder' field, got {self.kv_transfer_params}." ) def structured_outputs_enabled(self): return self.structured_outputs is not None @cached_property def sampling_type(self) -> SamplingType: if self.temperature < _SAMPLING_EPS: return SamplingType.GREEDY return SamplingType.RANDOM def __repr__(self) -> str: return ( f"SamplingParams(n={self.n}, " f"repetition_penalty={self.repetition_penalty}, " f"temperature={self.temperature}, " f"top_p={self.top_p}, " f"top_k={self.top_k}, " f"min_p={self.min_p}, " f"max_tokens={self.max_tokens}, " f"min_tokens={self.min_tokens}, " f"skip_special_tokens={self.skip_special_tokens}, " f"logprobs={self.logprobs}, " f"prompt_logprobs={self.prompt_logprobs}, " f"structured_outputs={self.structured_outputs}, " f"reasoning_structured_outputs={self.reasoning_structured_outputs}, " f"output_kind={self.output_kind}, " f"stop={self.stop}, " f"stop_token_ids={self.stop_token_ids}, " f"include_stop_str_in_output={self.include_stop_str_in_output}, " f"ignore_eos={self.ignore_eos}, " f"extra_args={self.extra_args})" )
def validate_speculative_sampling_params( sampling_params: SamplingParams, speculative_decoding_enabled: bool ) -> None: """Reject request features not yet supported by speculative decoding.""" if not speculative_decoding_enabled: return if sampling_params.kv_transfer_params is not None: raise ValueError("PD disaggregation is not supported with speculative decoding") if ( sampling_params.structured_outputs is not None or sampling_params.reasoning_structured_outputs is not None ): raise ValueError("structured outputs are not supported with speculative decoding") if sampling_params.logprobs is not None or sampling_params.prompt_logprobs is not None: raise ValueError("logprobs and prompt_logprobs are not supported with speculative decoding")
[docs] class PoolingParams: """ API parameters for pooling models. Attributes: truncate_prompt_tokens: Controls prompt truncation. Set to -1 to use the model's default truncation size. Set to k to keep only the last k tokens (left truncation). Set to None to disable truncation. dimensions: Number of dimensions for the output embedding. If set, truncates the embedding to the first N dimensions (Matryoshka Representation Learning). Must be a positive integer. normalize: Whether to normalize the embeddings outputs. Only supported for embedding tasks. """ def __init__( self, truncate_prompt_tokens: int | None = None, dimensions: int | None = None, normalize: bool | None = True, task: PoolingTask | None = None, ): if truncate_prompt_tokens is not None and truncate_prompt_tokens < -1: raise ValueError( f"truncate_prompt_tokens must be -1, None, or a non-negative integer, " f"got {truncate_prompt_tokens}." ) # TODO: check if the model supports MRL before allowing dimension truncation if dimensions is not None and dimensions < 1: raise ValueError(f"dimensions must be a positive integer, got {dimensions}") self.truncate_prompt_tokens = truncate_prompt_tokens self.dimensions = dimensions self.normalize = normalize self.task = task