【vllm】(二)vLLM v1 Engine — 模块超深度逐行分析之二

七~十二、输入/输出处理与支撑模块(深度逐行解析)

本文档对 vllm/v1/engine/ 子目录中的输入处理、输出处理、反分词器、Logprobs计算、并行采样及类型定义等六大模块进行逐行级深度剖析。


七、InputProcessor 深度解析(逐行代码走读)

7.1 模块概览与设计定位

input_processor.py(444行)是 vLLM V1 引擎前端的请求入口守门人 ------所有从 API 层进入引擎的请求,必须经过 InputProcessor.process_inputs() 的完整验证、预处理、分词与多模态处理,最终被转化为标准化的 EngineCoreRequest,才能被送入 EngineCore 调度执行。

其核心职责:

  1. 参数验证:SamplingParams / PoolingParams 合法性校验
  2. LoRA 验证:LoRA 请求与配置一致性
  3. 多模态处理 :将多模态输入转化为 MultiModalFeatureSpec 列表
  4. 分词与输入预处理:原始提示 → 分词 → token ID 序列
  5. 请求ID随机化:确保内部请求ID全局唯一性
  6. 提示长度校验:防止超长提示进入引擎

7.2 导入依赖分析

python 复制代码
import time
from collections.abc import Mapping
from typing import Any, Literal

import vllm.envs as envs
from vllm.config import VllmConfig
from vllm.inputs import (
    EngineInput,
    PromptType,
    SingletonInput,
    split_enc_dec_input,
)
from vllm.inputs.preprocess import InputPreprocessor
from vllm.logger import init_logger
from vllm.lora.request import LoRARequest
from vllm.multimodal import MULTIMODAL_REGISTRY, MultiModalRegistry
from vllm.multimodal.encoder_budget import MultiModalBudget
from vllm.multimodal.inputs import MultiModalFeatureSpec
from vllm.multimodal.utils import argsort_mm_positions
from vllm.platforms import current_platform
from vllm.pooling_params import PoolingParams
from vllm.renderers import BaseRenderer, renderer_from_config
from vllm.sampling_params import SamplingParams
from vllm.tasks import GENERATION_TASKS, POOLING_TASKS, SupportedTask
from vllm.tokenizers import TokenizerLike
from vllm.utils import length_from_prompt_token_ids_or_embeds, random_uuid
from vllm.utils.jsontree import json_iter_leaves
from vllm.v1.engine import EngineCoreRequest

关键依赖解读

  • InputPreprocessor:负责原始提示的预处理(分词+多模态编码),是 InputProcessor 的内部组件
  • split_enc_dec_input:将编码器-解码器架构的输入拆分为 encoder/decoder 两部分
  • MultiModalBudget:计算多模态编码器缓存大小,决定是否跳过提示长度检查
  • argsort_mm_positions:按多模态占位符在输入序列中的位置排序,保证多模态特征按正确顺序排列
  • BaseRenderer / renderer_from_config:渲染器,用于 chat 模板渲染和分词(v0.18+ 的新架构)
  • json_iter_leaves:遍历 JSON 树叶子节点,用于验证 mm_hashes 结构
  • current_platform.validate_request:平台级请求校验(如特定硬件限制)

7.3 InputProcessor.init() 逐行解析

python 复制代码
class InputProcessor:
    def __init__(
        self,
        vllm_config: VllmConfig,
        renderer: BaseRenderer | None = None,
        *,
        mm_registry: MultiModalRegistry = MULTIMODAL_REGISTRY,
    ) -> None:

参数

  • vllm_config:全局配置对象,包含 model/cache/lora/scheduler/speculative/structured_outputs/observability 等子配置
  • renderer:可选的渲染器,如未提供则从配置自动创建------这是 v0.18 版本引入的新架构,将分词逻辑从 InputProcessor 中解耦
  • mm_registry:多模态注册表,默认使用全局单例 MULTIMODAL_REGISTRY
python 复制代码
        self.vllm_config = vllm_config
        self.model_config = model_config = vllm_config.model_config
        self.cache_config = vllm_config.cache_config
        self.lora_config = vllm_config.lora_config
        self.scheduler_config = vllm_config.scheduler_config
        self.speculative_config = vllm_config.speculative_config
        self.structured_outputs_config = vllm_config.structured_outputs_config
        self.observability_config = vllm_config.observability_config

将各子配置提取为实例属性,方便后续方法直接访问。设计意图 :避免每次调用时都穿透 vllm_config.xxx_config,减少属性查找开销。

python 复制代码
        self.generation_config_fields = model_config.try_get_generation_config()

从模型配置中提取 generation_config.json 的字段。这些字段用于后续将生成配置(如 bos/eos token、默认温度等)注入 SamplingParams。try_get_ 前缀暗示可能返回 None(部分模型无此配置)。

python 复制代码
        self.renderer = renderer or renderer_from_config(vllm_config)

渲染器初始化:如果外部未提供,则通过 renderer_from_config() 从配置自动创建。渲染器负责 chat 模板渲染和分词------这是 v0.18 架构演进的关键组件。

python 复制代码
        self.supports_mm_inputs = mm_registry.supports_multimodal_inputs(model_config)
        self.mm_encoder_cache_size = 0
        self.skip_prompt_length_check = False
        if self.supports_mm_inputs:
            mm_budget = MultiModalBudget(vllm_config, mm_registry)
            self.mm_encoder_cache_size = mm_budget.encoder_cache_size
            self.skip_prompt_length_check = (
                mm_budget.processor.info.skip_prompt_length_check
            )
            mm_budget.reset_cache()  # Not used anymore

多模态初始化逻辑

  1. supports_mm_inputs:查询注册表判断当前模型是否支持多模态输入
  2. 如果支持,创建 MultiModalBudget 计算:
    • encoder_cache_size:编码器缓存大小,用于后续验证单个多模态项的嵌入token数是否超出缓存
    • skip_prompt_length_check:某些多模态处理器需要先处理多模态内容才能确定最终token数(如图片可能产生不同数量的token),因此允许跳过严格的提示长度预检查
  3. mm_budget.reset_cache():预算计算完成后清空缓存------预算对象仅用于获取这两个参数,不保留状态

设计洞察skip_prompt_length_check 体现了多模态场景下的一个重要权衡------严格预检查可能导致合法请求被拒绝(因为多模态token数在处理前不确定),但跳过检查又可能导致处理时才发现超长。架构选择了信任多模态处理器的策略。

7.4 tokenizer 属性

python 复制代码
    @property
    def tokenizer(self) -> TokenizerLike | None:
        return self.renderer.tokenizer

    def get_tokenizer(self) -> TokenizerLike:
        return self.renderer.get_tokenizer()

两个 tokenzier 访问器:

  • tokenizer 属性:可能返回 None(渲染器无 tokenizer 时)
  • get_tokenizer() 方法:保证返回非 None 的 tokenizer(内部断言)

这反映了 vLLM 对"分词可选"场景的支持------某些模型/配置不需要分词(如直接使用 embeds 输入)。

7.5 _validate_params() 逐行解析

python 复制代码
    def _validate_params(
        self,
        params: SamplingParams | PoolingParams,
        supported_tasks: tuple[SupportedTask, ...],
    ) -> None:
        """Raise `ValueError` if SamplingParams or PoolingParams is not valid."""

参数

  • params:用户提供的采样/池化参数
  • supported_tasks:模型支持的任务类型元组

设计意图:将验证逻辑集中在此,而非分散在 API 层,是因为 InputProcessor 是引擎前端的唯一入口------任何绕过 API 层的调用也需要经过此验证。

python 复制代码
        if isinstance(params, SamplingParams):
            supported_generation_tasks = [
                task for task in supported_tasks if task in GENERATION_TASKS
            ]
            if not supported_generation_tasks:
                raise ValueError("This model does not support generation")

过滤出模型支持的生成类任务。如果模型不支持任何生成任务但用户传了 SamplingParams,则报错。例如:嵌入模型不支持生成。

python 复制代码
            params.verify(
                self.model_config,
                self.speculative_config,
                self.structured_outputs_config,
                self.tokenizer,
            )

调用 SamplingParams 自身的 verify() 方法进行深度校验------包括 max_tokens 范围、temperature 合法性、top_p/top_k 约束、guided decoding(JSON schema)兼容性等。将验证委托给参数对象本身,遵循了"信息专家"原则------参数对象最了解自己的约束。

python 复制代码
            if params.thinking_token_budget is not None and (
                self.vllm_config.reasoning_config is None
                or not self.vllm_config.reasoning_config.enabled
            ):
                raise ValueError(
                    "thinking_token_budget is set but reasoning_config is "
                    "not configured. Please set --reasoning-config to use "
                    "thinking_token_budget."
                )

思维链(Chain-of-Thought)token 预算验证:如果用户设置了 thinking_token_budget 但未启用推理配置,则拒绝。这确保了思维链功能仅在显式配置时才可用,避免用户误设参数导致非预期行为。

python 复制代码
        elif isinstance(params, PoolingParams):
            supported_pooling_tasks = [
                task for task in supported_tasks if task in POOLING_TASKS
            ]
            if not supported_pooling_tasks:
                raise ValueError("This model does not support pooling")

池化任务的类似验证逻辑。

python 复制代码
            if params.task is None:
                if "token_embed" in supported_pooling_tasks:
                    params.task = "token_embed"
                elif "token_classify" in supported_pooling_tasks:
                    params.task = "token_classify"
                elif "plugin" in supported_pooling_tasks:
                    params.task = "plugin"

自动推断池化任务类型:当用户未指定任务类型时,按优先级自动选择------token_embed > token_classify > plugin。这个优先级反映了最常见用例的统计分布。

python 复制代码
            if params.task not in supported_pooling_tasks:
                raise ValueError(
                    f"Unsupported task: {params.task!r} "
                    f"Supported tasks: {supported_pooling_tasks}"
                )

            params.verify(self.model_config)

验证推断(或显式指定)的任务类型是否被模型支持,然后委托 PoolingParams 自身验证。

python 复制代码
        else:
            raise TypeError(
                f"params must be either SamplingParams or PoolingParams, "
                f"but got {type(params).__name__}"
            )

类型守卫------防止传入了未预期的参数类型。

7.6 _validate_lora() 逐行解析

python 复制代码
    def _validate_lora(self, lora_request: LoRARequest | None) -> None:
        if lora_request is None:
            return

无 LoRA 请求,直接返回。

python 复制代码
        if not self.lora_config:
            raise ValueError(
                f"Got lora_request {lora_request} but LoRA is not enabled!"
            )

用户传了 LoRA 请求但引擎未启用 LoRA 配置------这是配置不一致错误。

python 复制代码
        if self.tokenizer is not None:
            logger.warning_once(
                "vLLM has deprecated support for supporting different "
                "tokenizers for different LoRAs. By default, vLLM uses base "
                "model's tokenizer. If you are using a LoRA "
                "with its own tokenizer, consider specifying `--tokenizer "
                "[lora_path]` to use the LoRA tokenizer."
            )

弃用警告 :vLLM 曾经支持为不同 LoRA 使用不同 tokenizer,但这带来了极大的工程复杂度(缓存一致性、分词不一致等),现已弃用。此警告仅在首次遇到时打印(warning_once)。

7.7 _get_mm_identifier() 逐行解析

python 复制代码
    def _get_mm_identifier(
        self,
        mm_hash: str,
        lora_request: LoRARequest | None,
    ) -> str:

设计背景 :多模态编码器的输出缓存使用 mm_hash 作为缓存键。但在启用 enable_tower_connector_lora 时,同一个多模态输入在不同 LoRA 下会产生不同的嵌入表示(因为 LoRA 可能修改编码器层)。因此,缓存键必须包含 LoRA 信息以避免错误的缓存命中。

python 复制代码
        if (
            lora_request is None
            or self.lora_config is None
            or not self.lora_config.enable_tower_connector_lora
        ):
            return mm_hash
        return f"{lora_request.lora_name}:{mm_hash}"

分支逻辑

  1. 无 LoRA 请求 / 未启用 LoRA / 未启用 tower_connector_lora → 使用原始 hash
  2. 启用了 tower_connector_lora → 在 hash 前拼接 lora_name: 前缀

格式选择:冒号分隔符简单直观,且 LoRA name 不太可能包含冒号。如果未来可能出现冒号,可考虑使用更复杂的分隔符或编码。

7.8 assign_request_id() 静态方法逐行解析

python 复制代码
    @staticmethod
    def assign_request_id(request: EngineCoreRequest):
        """Replace the externally supplied request ID with an internal request ID
        that adds 8 random characters in order to ensure uniqueness.
        """

设计动机:外部提供的 request_id(如 OpenAI API 兼容层中的用户定义 ID)不能保证全局唯一。多个客户端可能使用相同的 request_id,导致引擎内部状态冲突。因此,vLLM 在内部使用"外部ID + 随机后缀"作为唯一标识。

python 复制代码
        if request.external_req_id is not None:
            raise ValueError(
                "The external_req_id field should not be set on EngineCoreRequests"
                " passed to vLLM; use the request_id field."
            )

防御性断言external_req_id 是此方法的输出字段,如果输入时已设置,说明调用方误用了接口。

python 复制代码
        request.external_req_id = request.request_id

保存原始 request_id 到 external_req_id------这是在输出时返回给用户用的标识符。

python 复制代码
        if envs.VLLM_DISABLE_REQUEST_ID_RANDOMIZATION:
            logger.warning_once(
                "VLLM_DISABLE_REQUEST_ID_RANDOMIZATION is set and will be "
                "removed in a future release. Duplicate externally-provided "
                "request IDs may cause failures and/or subtle correctness errors."
            )

兼容性逃生舱:允许通过环境变量禁用随机化,但发出强烈警告------重复 ID 可能导致请求状态混乱。这是为了支持某些遗留系统的需求。

python 复制代码
        else:
            request.request_id = f"{request.external_req_id}-{random_uuid():.8}"

ID 生成策略{原始ID}-{8位随机十六进制}。8个十六进制字符 = 32位随机性 = 约43亿种可能。对于单引擎实例而言,碰撞概率极低。.8 格式化截断 UUID 到前8位。

设计权衡:为什么不用完整 UUID?因为 request_id 会在引擎内部大量传递和比较(每次调度迭代),过长的 ID 会增加内存和比较开销。8位后缀在单实例场景下足够安全。

7.9 process_inputs() 逐行解析------核心入口

这是 InputProcessor 最核心的方法,约70行代码,将原始请求完整转化为 EngineCoreRequest

python 复制代码
    def process_inputs(
        self,
        request_id: str,
        prompt: PromptType | EngineInput,
        params: SamplingParams | PoolingParams,
        supported_tasks: tuple[SupportedTask, ...],
        arrival_time: float | None = None,
        lora_request: LoRARequest | None = None,
        tokenization_kwargs: dict[str, Any] | None = None,
        trace_headers: Mapping[str, str] | None = None,
        priority: int = 0,
        data_parallel_rank: int | None = None,
        resumable: bool = False,
    ) -> EngineCoreRequest:

参数详解

参数 类型 说明
request_id str 外部请求ID(由调用方提供)
prompt PromptType | EngineInput 提示内容------可以是原始文本、token ID列表、或已处理的EngineInput
params SamplingParams | PoolingParams 采样/池化参数
supported_tasks tuple[SupportedTask, ...] 模型支持的任务类型
arrival_time float | None 请求到达时间戳(None则使用当前时间)
lora_request LoRARequest | None LoRA 适配器请求
tokenization_kwargs dict | None 分词额外参数(已弃用)
trace_headers Mapping | None 分布式追踪头(OpenTelemetry)
priority int 请求优先级(0=默认)
data_parallel_rank int | None 数据并行目标 rank
resumable bool 是否可恢复(流式输入模式)
python 复制代码
        self._validate_params(params, supported_tasks)
        self._validate_lora(lora_request)

先进行参数和 LoRA 验证------快速失败策略,尽早拒绝不合法请求,避免后续无效计算。

python 复制代码
        parallel_config = self.vllm_config.parallel_config
        dp_size = parallel_config.data_parallel_size
        dp_local_size = parallel_config.data_parallel_size_local
        num_ranks = dp_local_size if parallel_config.local_engines_only else dp_size
        if data_parallel_rank is not None and not (0 <= data_parallel_rank < num_ranks):
            raise ValueError(
                f"data_parallel_rank {data_parallel_rank} "
                f"is out of range [0, {num_ranks})."
            )

数据并行 Rank 验证

  • dp_size:全局数据并行大小
  • dp_local_size:本节点数据并行大小
  • local_engines_only:如果为 True,只在本节点内路由请求,因此 rank 范围是 dp_local_size
  • 否则,rank 范围是全局 dp_size

这体现了 vLLM 的多级数据并行架构------同一请求可以被路由到不同节点的不同 rank。

输入处理的双路径架构
python 复制代码
        if isinstance(prompt, dict) and "type" in prompt:
            if tokenization_kwargs:
                logger.warning_once(
                    "Passing tokenization_kwargs to InputProcessor is deprecated "
                    "and will be removed in v0.18. You should instead pass "
                    "them to Renderer.render_cmpl() or Renderer.render_chat()."
                )

            if arrival_time is None:
                arrival_time = prompt.get("arrival_time", time.time())

            processed_inputs: EngineInput = prompt

路径1:已处理的 EngineInput ------当 prompt 是带有 "type" 键的字典时,说明它已经过 Renderer 处理(这是 v0.18 推荐的新路径)。此时直接使用,跳过分词步骤。

tokenization_kwargs 的弃用警告:在新架构中,分词参数应在 Renderer 层传入,而非在 InputProcessor 层。

python 复制代码
        else:
            logger.warning_once(
                "Passing raw prompts to InputProcessor is deprecated "
                "and will be removed in v0.18. You should instead pass "
                "the outputs of Renderer.render_cmpl() or Renderer.render_chat()."
            )

            if arrival_time is None:
                arrival_time = time.time()

            processed_inputs = self.input_preprocessor.preprocess(
                prompt,
                tokenization_kwargs=tokenization_kwargs,
            )

路径2:原始提示 ------传统路径,调用 InputPreprocessor.preprocess() 进行分词和多模态处理。此路径已弃用,将在 v0.18 移除。

架构演进洞察:v0.18 将分词职责从 InputProcessor 分离到 Renderer,是为了:

  1. 让 InputProcessor 专注于验证和组装
  2. Renderer 可以在 API 层就完成分词,减少引擎前端的开销
  3. 支持 chat 模板渲染的更灵活控制
python 复制代码
        current_platform.validate_request(processed_inputs, params)

平台级验证------某些硬件平台可能对请求有特殊约束(如 TPU 的 batch size 限制、特定模型的输入格式要求等)。

python 复制代码
        encoder_inputs, decoder_inputs = split_enc_dec_input(processed_inputs)
        self._validate_model_inputs(encoder_inputs, decoder_inputs)

将输入拆分为编码器/解码器部分并验证。对于纯解码器模型(如 LLaMA),encoder_inputs 为 None。

python 复制代码
        if decoder_inputs["type"] == "embeds":
            prompt_token_ids = None
            prompt_embeds = decoder_inputs["prompt_embeds"]
        else:
            prompt_token_ids = decoder_inputs["prompt_token_ids"]
            prompt_embeds = None

互斥分支:token IDs 和 embeds 不能同时存在。embeds 模式用于直接提供嵌入向量的场景(如从外部编码器获取的嵌入)。

SamplingParams 后处理
python 复制代码
        sampling_params = None
        pooling_params = None
        if isinstance(params, SamplingParams):
            sampling_params = params.clone()

必须克隆:在多进程场景下(如 MPClient),原始 params 可能被多个子进程共享。克隆确保每个请求有独立的参数副本。

python 复制代码
            if sampling_params.max_tokens is None:
                seq_len = length_from_prompt_token_ids_or_embeds(
                    prompt_token_ids, prompt_embeds
                )
                sampling_params.max_tokens = self.model_config.max_model_len - seq_len

自动计算 max_tokens :如果用户未指定,则默认填满到模型最大长度。length_from_prompt_token_ids_or_embeds 统一处理 token IDs 和 embeds 两种输入的长度计算。

python 复制代码
            sampling_params.update_from_generation_config(
                self.generation_config_fields,
                self.renderer.get_eos_token_id(),
            )
            if self.tokenizer is not None:
                sampling_params.update_from_tokenizer(self.tokenizer)

将模型的 generation_config 和 tokenizer 信息注入 SamplingParams------例如设置默认的 eos_token_id、repetition_penalty 等。这确保了模型级默认参数不会被忽略。

python 复制代码
        else:
            pooling_params = params.clone()

池化参数直接克隆,无需额外的后处理。

多模态特征组装
python 复制代码
        mm_features: list[MultiModalFeatureSpec] | None = None

        if decoder_inputs["type"] == "multimodal":
            decoder_mm_inputs = decoder_inputs["mm_kwargs"]
            decoder_mm_positions = decoder_inputs["mm_placeholders"]
            decoder_mm_hashes = decoder_inputs["mm_hashes"]

仅当输入类型为 "multimodal" 时才处理多模态特征。三个关键字段:

  • mm_kwargs:多模态编码器的输入数据(如图像像素张量)
  • mm_placeholders:多模态占位符在token序列中的位置信息
  • mm_hashes:多模态内容的哈希值,用于编码器缓存命中
python 复制代码
            if not all(
                isinstance(leaf, str) for leaf in json_iter_leaves(decoder_mm_hashes)
            ):
                raise ValueError(
                    f"mm_hashes must contain only strings, got: {decoder_mm_hashes}. "
                    "This is likely due to an incorrect custom implementation of "
                    "MultiModalProcessor.apply method."
                )

哈希类型验证 :使用 json_iter_leaves 遍历嵌套的 mm_hashes 结构(可能是 {"image": ["hash1", "hash2"], "video": ["hash3"]} 形式),确保所有叶子节点都是字符串。这是防御性检查,防止自定义多模态处理器返回非法类型。

python 复制代码
            sorted_mm_idxs = argsort_mm_positions(decoder_mm_positions)

位置排序:多模态占位符必须按其在token序列中的位置排序。例如,如果文本是 "看这张图然后看这张",则 image1 必须在 image2 之前处理。

python 复制代码
            mm_features = []
            for modality, idx in sorted_mm_idxs:
                base_mm_hash = decoder_mm_hashes[modality][idx]
                mm_features.append(
                    MultiModalFeatureSpec(
                        data=decoder_mm_inputs[modality][idx],
                        modality=modality,
                        identifier=self._get_mm_identifier(
                            base_mm_hash,
                            lora_request,
                        ),
                        mm_position=decoder_mm_positions[modality][idx],
                        mm_hash=base_mm_hash,
                    )
                )

MultiModalFeatureSpec 组装

字段 来源 说明
data mm_kwargs[modality][idx] 多模态编码器的实际输入数据
modality 排序结果 模态类型("image"/"video"/"audio"等)
identifier _get_mm_identifier() 缓存键(可能包含 LoRA 前缀)
mm_position mm_placeholders[modality][idx] 占位符位置信息
mm_hash mm_hashes[modality][idx] 原始内容哈希

identifier vs mm_hash 的区别

  • mm_hash:纯内容哈希,用于跨请求的编码器缓存命中
  • identifier:可能包含 LoRA 信息的复合键,用于编码器缓存的精确查找
EngineCoreRequest 构建
python 复制代码
        return EngineCoreRequest(
            request_id=request_id,
            prompt_token_ids=prompt_token_ids,
            prompt_embeds=prompt_embeds,
            mm_features=mm_features,
            sampling_params=sampling_params,
            pooling_params=pooling_params,
            arrival_time=arrival_time,
            lora_request=lora_request,
            cache_salt=decoder_inputs.get("cache_salt"),
            priority=priority,
            data_parallel_rank=data_parallel_rank,
            trace_headers=trace_headers,
            resumable=resumable,
        )

最终将所有处理后的数据打包为 EngineCoreRequest。注意 cache_salt------这是一个 KV 缓存盐值,用于缓存隔离(不同前缀/配置的缓存不会冲突)。

7.10 _validate_prompt_len() 逐行解析

python 复制代码
    def _validate_prompt_len(
        self,
        prompt_len: int,
        prompt_type: Literal["encoder", "decoder"],
    ):
        if self.skip_prompt_length_check:
            return

多模态场景下可能跳过长度检查(如前所述)。

python 复制代码
        if prompt_len == 0 and prompt_type == "decoder":
            raise ValueError(f"The {prompt_type} prompt cannot be empty")

解码器提示不能为空(但编码器提示可以为空------某些编码器-解码器模型允许空的编码器输入)。

python 复制代码
        model_config = self.model_config
        max_prompt_len = (
            model_config.max_model_len
            if prompt_type == "decoder"
            else self.mm_encoder_cache_size
        )

不同的长度上限

  • 解码器:使用 max_model_len(模型支持的最大序列长度)
  • 编码器:使用 mm_encoder_cache_size(预分配的编码器缓存大小)
python 复制代码
        if prompt_len > max_prompt_len:
            if self.supports_mm_inputs:
                suggestion = (
                    "Make sure that `max_model_len` is no smaller than the "
                    "number of text tokens plus multimodal tokens. For image "
                    "inputs, the number of image tokens depends on the number "
                    "of images, and possibly their aspect ratios as well."
                )
            else:
                suggestion = (
                    "Make sure that `max_model_len` is no smaller than the "
                    "number of text tokens."
                )

            raise ValueError(
                f"The {prompt_type} prompt (length {prompt_len}) is "
                f"longer than the maximum model length of {max_prompt_len}. "
                f"{suggestion}"
            )

超长提示错误------附带场景化的修复建议。

python 复制代码
        elif prompt_len == max_prompt_len and model_config.runner_type == "generate":
            suggestion = (
                "Make sure that `max_model_len` is no smaller than the "
                "number of text tokens (prompt + requested output tokens)."
            )
            raise ValueError(
                f"The {prompt_type} prompt (length {prompt_len}) plus the number of "
                f"requested output tokens (at least 1) is longer than the maximum "
                f"model length of {max_prompt_len}. {suggestion}"
            )

精确等于最大长度的特殊处理:对于生成任务,即使提示恰好等于 max_model_len,也无法生成任何 token(至少需要1个生成token的位置)。因此也报错。但池化任务不需要生成,所以仅对 generate runner_type 检查。

7.11 _validate_model_input() 逐行解析

python 复制代码
    def _validate_model_input(
        self,
        prompt_input: SingletonInput,
        prompt_type: Literal["encoder", "decoder"],
    ) -> None:

验证单个模型输入(编码器或解码器)。

python 复制代码
        prompt_ids = (
            None
            if prompt_input["type"] == "embeds"
            else prompt_input["prompt_token_ids"]
        )
        prompt_embeds = (
            prompt_input["prompt_embeds"] if prompt_input["type"] == "embeds" else None
        )

        prompt_len = length_from_prompt_token_ids_or_embeds(prompt_ids, prompt_embeds)
        self._validate_prompt_len(prompt_len, prompt_type)

统一处理 token IDs 和 embeds 两种输入模式,获取长度后验证。

python 复制代码
        if prompt_input["type"] == "multimodal":
            decoder_mm_positions = prompt_input["mm_placeholders"]
            for modality, mm_positions in decoder_mm_positions.items():
                for mm_position in mm_positions:
                    num_embeds = mm_position.get_num_embeds()
                    if num_embeds > self.mm_encoder_cache_size:
                        raise ValueError(
                            f"The {prompt_type} prompt contains a(n) {modality} item "
                            f"with {num_embeds} embedding tokens, which exceeds the "
                            f"pre-allocated encoder cache size "
                            f"{self.mm_encoder_cache_size}. Please reduce the input "
                            f"size or increase the encoder cache size "
                            f"by setting --limit-mm-per-prompt at startup."
                        )

多模态嵌入数验证 :每个多模态项(如一张高分辨率图片可能产生数千个token)的嵌入数不能超过编码器缓存大小。否则运行时会 OOM。提供修复建议:减小输入尺寸或增大 --limit-mm-per-prompt

python 复制代码
        if prompt_ids and tokenizer is not None:
            max_input_id = max(prompt_ids, default=0)

            model_vocab_size = model_config.get_vocab_size()
            if max_input_id > max(tokenizer.max_token_id, model_vocab_size - 1):
                raise ValueError(f"Token id {max_input_id} is out of vocabulary")

Token ID 越界检查

  • 取 prompt 中最大的 token ID
  • 使用 max(tokenizer.max_token_id, model_vocab_size - 1) 作为阈值
  • 取两者最大值的原因在注释中详细说明:
    • Qwen3 模型:语言模型有 tokenizer 中不存在的额外 token
    • 多模态模型:tokenizer 中有模型嵌入层不存在的占位符 token
    • 因此仅用单一阈值会产生假阳性

设计洞察 :这个 max() 操作是一种保守策略------只要 token ID 在任一词汇表范围内,就认为合法。这避免了因模型/tokenizer 词汇表不一致导致的误拒。

7.12 _validate_model_inputs() 逐行解析

python 复制代码
    def _validate_model_inputs(
        self,
        encoder_input: SingletonInput | None,
        decoder_input: SingletonInput,
    ):
        if encoder_input is not None:
            self._validate_model_input(encoder_input, prompt_type="encoder")

        self._validate_model_input(decoder_input, prompt_type="decoder")

简单的委托方法------分别验证编码器和解码器输入。编码器输入可选(纯解码器模型无编码器)。

7.13 InputProcessor 整体架构总结

复制代码
┌──────────────────────────────────────────────┐
│            API Layer (OpenAI/...)             │
└──────────────┬───────────────────────────────┘
               │ raw request
               ▼
┌──────────────────────────────────────────────┐
│              InputProcessor                   │
│                                              │
│  ┌─────────────────┐  ┌──────────────────┐  │
│  │ _validate_params│  │  _validate_lora  │  │
│  └─────────────────┘  └──────────────────┘  │
│                                              │
│  ┌──────────────────────────────────────┐    │
│  │ process_inputs()                     │    │
│  │  ├─ 双路径: EngineInput / raw prompt │    │
│  │  ├─ split_enc_dec_input              │    │
│  │  ├─ _validate_model_inputs           │    │
│  │  ├─ SamplingParams 后处理            │    │
│  │  └─ 多模态特征组装                   │    │
│  └──────────────────────────────────────┘    │
│                                              │
│  ┌──────────────────────────────────────┐    │
│  │ assign_request_id()                  │    │
│  │  └─ 外部ID + 随机后缀 = 内部唯一ID  │    │
│  └──────────────────────────────────────┘    │
└──────────────┬───────────────────────────────┘
               │ EngineCoreRequest
               ▼
┌──────────────────────────────────────────────┐
│              EngineCore                       │
└──────────────────────────────────────────────┘

八、OutputProcessor 深度解析(逐行代码走读)

8.1 模块概览与设计定位

output_processor.py(812行)是 vLLM V1 引擎前端的出口枢纽 ------将 EngineCore 输出的原始 token ID 序列、logprob 张量、pooling 结果等,经过反分词、logprob 计算、流式/非流式输出组装,最终转化为用户可见的 RequestOutput / PoolingRequestOutput

核心职责:

  1. 请求状态管理:追踪每个活跃请求的解码/日志概率/统计状态
  2. 流式输出控制:stream interval、DELTA/FINAL_ONLY 模式
  3. 并行采样聚合:best-of-n 场景下的子请求输出合并
  4. 请求生命周期管理:添加、完成、中止、错误传播
  5. 统计与追踪:指标收集和 OpenTelemetry 追踪

8.2 导入依赖分析

python 复制代码
import asyncio
from collections import defaultdict, deque
from collections.abc import Iterable
from dataclasses import dataclass
from typing import Any, cast

import numpy as np
import torch
  • asyncio:用于异步输出收集器(RequestOutputCollector)
  • defaultdict, deque:请求ID映射和流式输入队列
  • numpy, torch:处理路由专家和嵌入输出
python 复制代码
from vllm.outputs import (
    STREAM_FINISHED,
    CompletionOutput,
    PoolingOutput,
    PoolingRequestOutput,
    RequestOutput,
)
from vllm.sampling_params import RequestOutputKind
  • RequestOutputKind:输出模式枚举(DELTA / FINAL_ONLY / CUMULATIVE)
  • STREAM_FINISHED:流式完成信号常量
python 复制代码
from vllm.v1.engine import EngineCoreOutput, EngineCoreRequest, FinishReason
from vllm.v1.engine.detokenizer import IncrementalDetokenizer
from vllm.v1.engine.logprobs import LogprobsProcessor
from vllm.v1.engine.parallel_sampling import ParentRequest

三大支撑组件的导入------反分词器、日志概率处理器、并行采样管理器。

8.3 RequestOutputCollector 逐行解析

python 复制代码
EMPTY_CPU_TENSOR = torch.empty(0, device="cpu")

共享的空 CPU 张量,用作中止请求时 pooling 输出的占位符------避免为每个中止请求分配新张量。

python 复制代码
class RequestOutputCollector:
    """
    Collects streamed RequestOutputs per individual request,
    for hand-off to the consuming asyncio generate task.

    When streaming deltas, RequestOutputs are merged if the
    producer gets ahead of the consumer.
    """

设计背景:在 AsyncLLM 中,引擎前端的输出处理线程(生产者)和用户的 asyncio 生成器(消费者)运行在不同的执行上下文中。RequestOutputCollector 是它们之间的有界缓冲区。

python 复制代码
    def __init__(self, output_kind: RequestOutputKind, request_id: str):
        self.aggregate = output_kind == RequestOutputKind.DELTA
        self.request_id = request_id
        self.output: RequestOutput | PoolingRequestOutput | Exception | None = None
        self.ready = asyncio.Event()

        self._input_stream_task: asyncio.Task | None = None

关键字段

  • aggregate:DELTA 模式下是否合并连续输出------如果消费者跟不上,多个增量输出可以合并为一个
  • output:单槽缓冲(不是队列!)------一次只持有一个输出,消费者取走后才接收下一个
  • ready:asyncio 事件,实现生产者-消费者同步
  • _input_stream_task:输入流任务引用,用于取消

为什么是单槽而非队列? 因为 DELTA 模式下,如果消费者落后,多个增量输出可以被合并为一个------不需要保留历史。这简化了实现并减少了内存使用。

python 复制代码
    def put(self, output: RequestOutput | PoolingRequestOutput | Exception) -> None:
        """Non-blocking put operation."""
        if self.output is None or isinstance(output, Exception):
            self.output = output
            self.ready.set()

空槽或异常直接放入。

python 复制代码
        elif isinstance(self.output, RequestOutput) and isinstance(
            output, RequestOutput
        ):
            self.output.add(output, aggregate=self.aggregate)

合并逻辑 :两个 RequestOutput 通过 add() 方法合并。aggregate=True 时执行增量合并(delta 累加),否则后者覆盖前者。

python 复制代码
        elif isinstance(self.output, PoolingRequestOutput) and isinstance(
            output, PoolingRequestOutput
        ):
            self.output = output

Pooling 输出总是后者覆盖前者(池化结果只有最终值有意义)。

python 复制代码
    async def get(self) -> RequestOutput | PoolingRequestOutput:
        """Get operation blocks on put event."""
        while (output := self.output) is None:
            await self.ready.wait()
        self.output = None
        self.ready.clear()
        if isinstance(output, Exception):
            raise output
        return output

异步获取:等待 ready 事件,取出输出,清空缓冲区,重置事件。异常直接抛出------让 generate() 协程处理错误。

python 复制代码
    def get_nowait(self) -> RequestOutput | PoolingRequestOutput | None:
        """Non-blocking get operation."""
        output = self.output
        if output is not None:
            self.output = None
            self.ready.clear()
        if isinstance(output, Exception):
            raise output
        return output

非阻塞版本------用于同步 API(LLMEngine)场景。

python 复制代码
    def close(self):
        if self._input_stream_task is not None:
            self._input_stream_task.cancel()
        self._input_stream_task = None

    def __del__(self):
        if (task := self._input_stream_task) is not None:
            task.get_loop().call_soon_threadsafe(task.cancel)
            self._input_stream_task = None

资源清理 :关闭时取消关联的输入流任务。__del__ 使用 call_soon_threadsafe 因为析构可能发生在非事件循环线程。

8.4 OutputProcessorOutput 数据类

python 复制代码
@dataclass
class OutputProcessorOutput:
    request_outputs: list[RequestOutput | PoolingRequestOutput]
    reqs_to_abort: list[str]

process_outputs() 的返回值:

  • request_outputs:本次迭代产生的输出列表(LLMEngine 同步模式使用)
  • reqs_to_abort:需要中止的请求ID列表(detokenizer 检测到 stop string 但 EngineCore 尚未标记完成的请求)

8.5 StreamingUpdate 数据类

python 复制代码
@dataclass
class StreamingUpdate:
    """Streaming input update data for output processor."""
    prompt: str | None
    prompt_token_ids: list[int] | None
    arrival_time: float
    final: bool = False

流式输入更新的数据容器------用于支持请求的增量提示追加。final=True 标记这是最后一个更新。

8.6 RequestState 逐行解析

RequestState 是输出处理器的核心数据结构,追踪每个活跃请求的完整状态。

python 复制代码
class RequestState:
    def __init__(
        self,
        request_id: str,
        external_req_id: str,
        parent_req: ParentRequest | None,
        request_index: int,
        lora_request: LoRARequest | None,
        output_kind: RequestOutputKind,
        prompt: str | None,
        prompt_token_ids: list[int] | None,
        prompt_embeds: torch.Tensor | None,
        logprobs_processor: LogprobsProcessor | None,
        detokenizer: IncrementalDetokenizer | None,
        max_tokens_param: int | None,
        arrival_time: float,
        queue: RequestOutputCollector | None,
        log_stats: bool,
        stream_interval: int,
        top_p: float | None = None,
        n: int | None = None,
        temperature: float | None = None,
        stream_input: bool = False,
    ):

参数分组

  1. 身份信息:request_id, external_req_id, parent_req, request_index
  2. 模型相关:lora_request, prompt, prompt_token_ids, prompt_embeds
  3. 输出处理组件:logprobs_processor, detokenizer
  4. 参数快照:max_tokens_param, top_p, n, temperature(用于追踪指标)
  5. 输出控制:output_kind, queue, stream_interval
  6. 统计:log_stats, arrival_time
  7. 流式输入:stream_input
python 复制代码
        self.request_id = request_id
        self.external_req_id = external_req_id
        self.parent_req = parent_req
        self.request_index = request_index
        self.lora_request = lora_request
        self.lora_name = lora_request.lora_name if lora_request is not None else None
        self.output_kind = output_kind
        self.prompt = prompt
        self.prompt_token_ids = prompt_token_ids
        self.prompt_embeds = prompt_embeds
        self.prompt_len = length_from_prompt_token_ids_or_embeds(
            self.prompt_token_ids, self.prompt_embeds
        )
        self.logprobs_processor = logprobs_processor
        self.detokenizer = detokenizer
        self.max_tokens_param = max_tokens_param
        self.top_p = top_p
        self.n = n
        self.temperature = temperature
        self.is_prefilling = True
        self.queue = queue
        self.num_cached_tokens = 0

关键状态字段

  • is_prefilling:当前是否处于 prefill 阶段------首次收到 EngineCoreOutput 后翻转为 False
  • num_cached_tokens:prefix caching 命中的 token 数
python 复制代码
        self.stats = RequestStateStats(arrival_time=arrival_time) if log_stats else None

        self.stream_interval = stream_interval
        self.sent_tokens_offset = 0

        self.streaming_input = stream_input
        self.input_chunk_queue: deque[StreamingUpdate] | None = (
            deque() if stream_input else None
        )
  • stats:请求级统计(仅在 log_stats=True 时创建,避免无谓开销)
  • stream_interval:流式输出间隔------每 N 个 token 输出一次
  • sent_tokens_offset:已发送的 token 偏移量(用于 DELTA 模式和 stream interval)
  • input_chunk_queue:流式输入队列------当请求支持增量提示追加时使用
apply_streaming_update()
python 复制代码
    def apply_streaming_update(self, update: StreamingUpdate) -> None:
        self.streaming_input = not update.final

如果更新是最终的,关闭流式输入模式。

python 复制代码
        if update.prompt:
            self.prompt = (
                (self.prompt + update.prompt) if self.prompt else update.prompt
            )
        if self.prompt_token_ids:
            self.prompt_token_ids.extend(update.prompt_token_ids or ())
        else:
            self.prompt_token_ids = update.prompt_token_ids or []
        assert self.prompt_token_ids is not None
        self.prompt_len = len(self.prompt_token_ids)

增量更新提示内容------拼接文本和 token IDs。

python 复制代码
        if self.stats is not None:
            self.stats.arrival_time = update.arrival_time
        self.is_prefilling = True

更新到达时间并重新标记为 prefilling(因为追加了新的输入)。

from_new_request() 工厂方法
python 复制代码
    @classmethod
    def from_new_request(
        cls,
        tokenizer: TokenizerLike | None,
        request: EngineCoreRequest,
        prompt: str | None,
        parent_req: ParentRequest | None,
        request_index: int,
        queue: RequestOutputCollector | None,
        log_stats: bool,
        stream_interval: int,
    ) -> "RequestState":

从 EngineCoreRequest 创建 RequestState 的工厂方法。

python 复制代码
        if sampling_params := request.sampling_params:
            if not sampling_params.detokenize:
                tokenizer = None
            output_kind = sampling_params.output_kind
            logprobs_processor = LogprobsProcessor.from_new_request(
                tokenizer=tokenizer,
                request=request,
            )
            detokenizer = IncrementalDetokenizer.from_new_request(
                tokenizer=tokenizer,
                request=request,
            )

条件组件创建

  • 如果 detokenize=False,将 tokenizer 设为 None,这样 detokenizer 和 logprobs_processor 都会使用空实现
  • LogprobsProcessor 和 IncrementalDetokenizer 都有工厂方法根据请求参数自动选择合适的实现
python 复制代码
            max_tokens_param = sampling_params.max_tokens
            top_p = sampling_params.top_p
            n = sampling_params.n
            temperature = sampling_params.temperature
        else:
            logprobs_processor = None
            detokenizer = None
            max_tokens_param = None
            top_p = None
            n = None
            temperature = None
            assert request.pooling_params is not None
            output_kind = request.pooling_params.output_kind

池化请求不需要 detokenizer 和 logprobs_processor------直接输出嵌入向量。

python 复制代码
        assert request.external_req_id is not None

确保外部请求ID已被赋值(assign_request_id 必须在此之前调用)。

make_request_output()------输出组装核心
python 复制代码
    def make_request_output(
        self,
        new_token_ids: list[int],
        pooling_output: torch.Tensor | None,
        finish_reason: FinishReason | None,
        stop_reason: int | str | None,
        kv_transfer_params: dict[str, Any] | None = None,
        routed_experts: np.ndarray | None = None,
    ) -> RequestOutput | PoolingRequestOutput | None:

返回 None 的情况:本次迭代不需要产生输出(如 stream interval 未达到、FINAL_ONLY 模式下请求未完成)。

python 复制代码
        finished = finish_reason is not None
        final_only = self.output_kind == RequestOutputKind.FINAL_ONLY

        if not finished and final_only:
            return None

FINAL_ONLY 模式:只返回最终输出,中间迭代返回 None。

python 复制代码
        if self.stream_interval > 1:
            assert self.detokenizer is not None

            if not (
                finished
                or self.sent_tokens_offset == 0
                or self.detokenizer.num_output_tokens() - self.sent_tokens_offset
                >= self.stream_interval
            ):
                return None

Stream Interval 过滤:仅在以下条件之一满足时产生输出:

  1. 请求已完成
  2. 是第一个 token(sent_tokens_offset == 0
  3. 累积的 token 数达到 stream interval 阈值

这避免了逐 token 输出时的过高网络开销。

python 复制代码
            if self.output_kind == RequestOutputKind.DELTA:
                new_token_ids = self.detokenizer.output_token_ids[
                    self.sent_tokens_offset :
                ]
                self.sent_tokens_offset = self.detokenizer.num_output_tokens()

DELTA 模式下的 token 截取:只取自上次发送以来新增的 token,并更新偏移量。

python 复制代码
        external_req_id = self.external_req_id

        if pooling_output is not None:
            return self._new_request_output(
                external_req_id,
                [self._new_pooling_output(pooling_output)],
                finished,
            )

Pooling 输出直接组装。

python 复制代码
        output = self._new_completion_output(
            new_token_ids, finish_reason, stop_reason, routed_experts
        )

        if self.parent_req is None:
            outputs = [output]
        else:
            outputs, finished = self.parent_req.get_outputs(self.request_id, output)
            if not outputs:
                return None
            external_req_id = self.parent_req.external_req_id

并行采样处理

  • 无 parent:直接输出单个 completion
  • 有 parent:通过 ParentRequest.get_outputs() 聚合子请求输出,可能返回 None(其他子请求尚未完成且模式为 FINAL_ONLY)
python 复制代码
        return self._new_request_output(
            external_req_id, outputs, finished, kv_transfer_params
        )

最终组装 RequestOutput。

_new_request_output()
python 复制代码
    def _new_request_output(
        self,
        external_req_id: str,
        outputs: list[CompletionOutput] | list[PoolingOutput],
        finished: bool,
        kv_transfer_params: dict[str, Any] | None = None,
    ) -> RequestOutput | PoolingRequestOutput:
        prompt_token_ids = self.prompt_token_ids
        if prompt_token_ids is None and self.prompt_embeds is not None:
            prompt_token_ids = [0] * len(self.prompt_embeds)
        assert prompt_token_ids is not None

embeds 到 token IDs 的适配 :当使用 embeds 输入时,没有实际的 token IDs,但输出格式要求提供。使用 [0] * len 作为占位符------这些 token IDs 仅用于长度信息,不会被实际解码。

python 复制代码
        first_output = outputs[0]
        if isinstance(first_output, PoolingOutput):
            assert len(outputs) == 1
            return PoolingRequestOutput(
                request_id=external_req_id,
                outputs=first_output,
                num_cached_tokens=self.num_cached_tokens,
                prompt_token_ids=prompt_token_ids,
                finished=finished,
            )

Pooling 输出路径------单个 PoolingOutput 封装为 PoolingRequestOutput。

python 复制代码
        assert self.logprobs_processor is not None
        if self.output_kind == RequestOutputKind.DELTA:
            prompt_logprobs = self.logprobs_processor.pop_prompt_logprobs()
        else:
            prompt_logprobs = self.logprobs_processor.prompt_logprobs

DELTA 模式的 prompt logprobs 特殊处理:DELTA 模式下,prompt logprobs 只在 prefill 结束时返回一次,之后就被"弹出"(pop),不再保留。这是为了遵循 OpenAI API 的 DELTA 语义------每个增量输出只包含新增信息。

python 复制代码
        return RequestOutput(
            request_id=external_req_id,
            lora_request=self.lora_request,
            prompt=self.prompt,
            prompt_token_ids=prompt_token_ids,
            prompt_logprobs=prompt_logprobs,
            outputs=cast(list[CompletionOutput], outputs),
            finished=finished,
            kv_transfer_params=kv_transfer_params,
            num_cached_tokens=self.num_cached_tokens,
            metrics=self.stats,
        )

最终构建 RequestOutput------注意使用 external_req_id(而非内部 request_id)作为返回给用户的标识符。

_new_completion_output()
python 复制代码
    def _new_completion_output(
        self,
        token_ids: list[int],
        finish_reason: FinishReason | None,
        stop_reason: int | str | None,
        routed_experts: np.ndarray | None = None,
    ) -> CompletionOutput:
        assert self.detokenizer is not None
        assert self.logprobs_processor is not None
        finished = finish_reason is not None
        delta = self.output_kind == RequestOutputKind.DELTA

        text = self.detokenizer.get_next_output_text(finished, delta)
        if not delta:
            token_ids = self.detokenizer.output_token_ids

文本获取

  • DELTA 模式:获取自上次发送以来的增量文本
  • 非DELTA 模式:获取全部输出文本,并使用 detokenizer 的完整 token ID 列表(而非增量 token IDs)
python 复制代码
        logprobs = self.logprobs_processor.logprobs
        if delta and logprobs:
            logprobs = logprobs[-len(token_ids) :]

DELTA 模式的 logprobs 截取:只保留与当前增量 token 对应的 logprobs 条目。

python 复制代码
        return CompletionOutput(
            index=self.request_index,
            text=text,
            token_ids=token_ids,
            routed_experts=routed_experts,
            logprobs=logprobs,
            cumulative_logprob=self.logprobs_processor.cumulative_logprob,
            finish_reason=str(finish_reason) if finished else None,
            stop_reason=stop_reason if finished else None,
        )

组装 CompletionOutput------注意 finish_reasonstop_reason 仅在请求完成时设置。

8.7 OutputProcessor 逐行解析

python 复制代码
class OutputProcessor:
    """Process EngineCoreOutputs into RequestOutputs."""

    def __init__(
        self,
        tokenizer: TokenizerLike | None,
        *,
        log_stats: bool,
        stream_interval: int = 1,
        tracing_enabled: bool = False,
    ):
        self.log_stats = log_stats
        self.tokenizer = tokenizer
        self.stream_interval = stream_interval
        self.request_states: dict[str, RequestState] = {}
        self.parent_requests: dict[str, ParentRequest] = {}
        self.external_req_ids: defaultdict[str, list[str]] = defaultdict(list)
        self.lora_states = LoRARequestStates(log_stats)
        self.tracing_enabled = tracing_enabled

核心数据结构

字段 类型 说明
request_states dict[str, RequestState] 内部请求ID → 请求状态映射
parent_requests dict[str, ParentRequest] 父请求ID → ParentRequest 映射(并行采样)
external_req_ids defaultdict[str, list[str]] 外部请求ID → 内部请求ID列表映射(一对多,因为并行采样)
lora_states LoRARequestStates LoRA 请求统计
abort_requests()
python 复制代码
    def abort_requests(self, request_ids: Iterable[str], internal: bool) -> list[str]:

中止请求的复杂逻辑------需要处理:

  1. 内部 vs 外部请求ID
  2. 并行采样的父子请求关系
  3. 输出队列通知(确保 generate() 协程能收到中止信号)
python 复制代码
        internal_req_ids = []
        for request_id in request_ids:
            if internal:
                internal_req_ids.append(request_id)
                if req_state := self.request_states.get(request_id):
                    external_req_id = req_state.external_req_id
                    internal_ids = self.external_req_ids[external_req_id]
                    internal_ids.remove(request_id)
                    if not internal_ids:
                        del self.external_req_ids[external_req_id]

内部ID路径:直接使用,并从外部ID映射中移除。

python 复制代码
            elif internal_ids := self.external_req_ids.pop(request_id, []):
                internal_req_ids.extend(internal_ids)

外部ID路径:一个外部ID可能对应多个内部ID(并行采样),全部取出并中止。

python 复制代码
        request_ids_to_abort = []
        for request_id in internal_req_ids:
            req_state = self.request_states.pop(request_id, None)
            if req_state is not None:
                self.lora_states.request_finished(request_id, req_state.lora_name)
                request_ids_to_abort.append(request_id)
                if req_state.queue is not None and (
                    request_output := req_state.make_request_output(
                        new_token_ids=[],
                        pooling_output=EMPTY_CPU_TENSOR
                        if req_state.detokenizer is None
                        else None,
                        finish_reason=FinishReason.ABORT,
                        stop_reason=None,
                        kv_transfer_params=None,
                    )
                ):
                    req_state.queue.put(request_output)

为已中止的请求生成最终输出:创建一个 finish_reason=ABORT 的输出,放入队列以解除 generate() 协程的等待。

EMPTY_CPU_TENSOR 的使用:对于 pooling 请求(detokenizer 为 None),需要一个非 None 的 pooling_output 来进入 pooling 分支。

python 复制代码
            elif parent := self.parent_requests.get(request_id):
                if parent.child_requests:
                    child_reqs = list(parent.child_requests)
                    child_reqs = self.abort_requests(child_reqs, internal=True)
                    request_ids_to_abort.extend(child_reqs)
                self.parent_requests.pop(request_id, None)

递归中止:如果请求ID是父请求,递归中止所有子请求。

add_request()
python 复制代码
    def add_request(
        self,
        request: EngineCoreRequest,
        prompt: str | None,
        parent_req: ParentRequest | None = None,
        request_index: int = 0,
        queue: RequestOutputCollector | None = None,
    ) -> None:
        request_id = request.request_id
        req_state = self.request_states.get(request_id)
        if req_state is not None:
            self._update_streaming_request_state(req_state, request, prompt)
            return

流式输入处理:如果请求ID已存在,说明这是同一个请求的增量更新(而非新请求),进入流式更新路径。

python 复制代码
        req_state = RequestState.from_new_request(...)
        self.request_states[request_id] = req_state
        if parent_req:
            self.parent_requests[parent_req.request_id] = parent_req
        self.external_req_ids[req_state.external_req_id].append(request_id)

新请求:创建状态,注册父子关系,建立外部ID映射。

_update_streaming_request_state()
python 复制代码
    def _update_streaming_request_state(
        self, req_state: RequestState, request: EngineCoreRequest, prompt: str | None
    ) -> None:
        if not request.resumable:
            if req_state.input_chunk_queue is None:
                self._finish_request(req_state)
                if req_state.queue is not None:
                    req_state.queue.put(STREAM_FINISHED)
            elif req_state.input_chunk_queue:
                req_state.input_chunk_queue[-1].final = True
            else:
                req_state.streaming_input = False
            return

非 resumable 的最终更新:三种情况------

  1. 引擎已完成(input_chunk_queue 为 None)→ 立即完成请求
  2. 有排队的更新 → 标记最后一个为 final
  3. 无排队更新 → 关闭流式输入模式
python 复制代码
        update = StreamingUpdate(
            prompt=prompt,
            prompt_token_ids=request.prompt_token_ids,
            arrival_time=request.arrival_time,
        )

        if req_state.input_chunk_queue is None:
            req_state.apply_streaming_update(update)
            req_state.input_chunk_queue = deque()
        else:
            req_state.input_chunk_queue.append(update)

resumable 更新的两种处理

  1. 引擎已完成当前输入 → 立即应用更新(不排队)
  2. 引擎仍在处理 → 排队等待

设计洞察input_chunk_queue 的状态有三种:

  • deque()(非空或空deque):引擎正在处理
  • None:引擎已完成当前输入,等待下一个
  • 无 deque(初始 None 后变成 deque):在流式输入期间动态切换
process_outputs()------输出处理主循环
python 复制代码
    def process_outputs(
        self,
        engine_core_outputs: list[EngineCoreOutput],
        engine_core_timestamp: float | None = None,
        iteration_stats: IterationStats | None = None,
    ) -> OutputProcessorOutput:

这是整个输出处理管线的主入口,处理一次调度迭代产生的所有 EngineCoreOutput。

python 复制代码
        request_outputs: list[RequestOutput | PoolingRequestOutput] = []
        reqs_to_abort: list[str] = []
        for engine_core_output in engine_core_outputs:
            req_id = engine_core_output.request_id
            req_state = self.request_states.get(req_id)
            if req_state is None:
                continue

遍历所有输出,跳过已中止请求的输出(可能在中止和输出之间存在竞争)。

python 复制代码
            self._update_stats_from_output(
                req_state, engine_core_output, engine_core_timestamp, iteration_stats
            )

步骤1:更新统计信息。

python 复制代码
            new_token_ids = engine_core_output.new_token_ids
            pooling_output = engine_core_output.pooling_output
            finish_reason = engine_core_output.finish_reason
            stop_reason = engine_core_output.stop_reason
            kv_transfer_params = engine_core_output.kv_transfer_params
            routed_experts = engine_core_output.routed_experts

提取 EngineCoreOutput 的各字段。

python 复制代码
            if req_state.is_prefilling:
                if engine_core_output.prefill_stats is not None:
                    req_state.num_cached_tokens = (
                        engine_core_output.prefill_stats.num_cached_tokens
                    )
                req_state.is_prefilling = False

Prefill 完成检测:首次收到输出时标记 prefill 完成,并记录 prefix caching 命中的 token 数。

python 复制代码
            if pooling_output is None:
                assert req_state.detokenizer is not None
                assert req_state.logprobs_processor is not None
                
                stop_string = req_state.detokenizer.update(
                    new_token_ids, finish_reason == FinishReason.STOP
                )
                if stop_string:
                    finish_reason = FinishReason.STOP
                    stop_reason = stop_string

步骤2:反分词和 stop string 检测------关键交互点:

  • Detokenizer 负责增量解码和 stop string 匹配
  • 如果检测到 stop string,覆盖 EngineCore 的 finish_reason
  • 这意味着 EngineCore 可能不知道 stop string 的匹配(它只检测 EOS token),detokenizer 在前端补充了这一层检测
python 复制代码
                req_state.logprobs_processor.update_from_output(engine_core_output)

步骤3:更新 logprobs。

python 复制代码
            if request_output := req_state.make_request_output(
                new_token_ids,
                pooling_output,
                finish_reason,
                stop_reason,
                kv_transfer_params,
                routed_experts,
            ):
                if req_state.streaming_input:
                    request_output.finished = False

                if req_state.queue is not None:
                    req_state.queue.put(request_output)
                else:
                    request_outputs.append(request_output)

步骤4:组装输出------

  • 流式输入模式下,即使 finish_reason 不为 None,也标记为未完成(因为后续可能还有输入更新)
  • AsyncLLM:放入队列
  • LLMEngine:添加到返回列表
python 复制代码
            if finish_reason is not None:
                if req_state.streaming_input:
                    if req_state.input_chunk_queue:
                        update = req_state.input_chunk_queue.popleft()
                        req_state.apply_streaming_update(update)
                    else:
                        req_state.input_chunk_queue = None

流式输入完成处理:当前子请求完成时,应用队列中的下一个更新(如果有)。

python 复制代码
                else:
                    self._finish_request(req_state)
                    if not engine_core_output.finished:
                        reqs_to_abort.append(req_id)

非流式输入完成处理

  1. 从请求状态字典中移除
  2. 关键 :如果 engine_core_output.finished 为 False 但 detokenizer 检测到 stop string 导致 finish_reason 不为 None,需要在 EngineCore 中中止此请求
python 复制代码
                    self._update_stats_from_finished(
                        req_state, finish_reason, iteration_stats
                    )
                    if self.tracing_enabled:
                        self.do_tracing(engine_core_output, req_state, iteration_stats)

更新完成统计和追踪。

do_tracing()------OpenTelemetry 追踪
python 复制代码
    def do_tracing(
        self,
        engine_core_output: EngineCoreOutput,
        req_state: RequestState,
        iteration_stats: IterationStats | None,
    ) -> None:

为完成的请求生成 OpenTelemetry span,包含延迟分解:

  • TIME_TO_FIRST_TOKEN:首 token 延迟
  • E2E:端到端延迟
  • TIME_IN_QUEUE:排队等待时间
  • TIME_IN_MODEL_PREFILL:prefill 模型推理时间
  • TIME_IN_MODEL_DECODE:decode 模型推理时间
  • TIME_IN_MODEL_INFERENCE:总推理时间

以及使用量指标:

  • PROMPT_TOKENS:提示 token 数
  • COMPLETION_TOKENS:生成 token 数

还有请求参数(top_p, max_tokens, temperature, n)作为属性。

8.8 OutputProcessor 整体架构总结

复制代码
┌──────────────────────────────────────────────┐
│              EngineCore                       │
│    EngineCoreOutput[] per iteration           │
└──────────────┬───────────────────────────────┘
               │
               ▼
┌──────────────────────────────────────────────┐
│            OutputProcessor                    │
│                                              │
│  ┌──────────────────────────────────────┐    │
│  │ process_outputs() 主循环             │    │
│  │  1) 更新统计                         │    │
│  │  2) Detokenizer.update() → stop检测  │    │
│  │  3) LogprobsProcessor.update()       │    │
│  │  4) RequestState.make_request_output │    │
│  │  5) 完成处理 / 流式更新             │    │
│  └──────────────────────────────────────┘    │
│                                              │
│  ┌──────────┐ ┌──────────────┐ ┌──────────┐ │
│  │RequestSta│ │ParentRequest │ │RequestOut│ │
│  │   tes    │ │  (并行采样)  │ │putCollect│ │
│  └──────────┘ └──────────────┘ └──────────┘ │
└──────────────┬───────────────────────────────┘
               │ RequestOutput / PoolingRequestOutput
               ▼
┌──────────────────────────────────────────────┐
│     AsyncLLM (queue.put) / LLMEngine (list)  │
└──────────────────────────────────────────────┘

九、Detokenizer 深度解析(逐行代码走读)

9.1 模块概览与设计定位

detokenizer.py(339行)负责将 token ID 序列增量转化为文本,并在转化过程中检测 stop string。这是 vLLM 流式输出的关键组件------用户看到的每一个增量文本片段,都经过此模块的增量解码逻辑。

核心设计:

  1. 三级继承体系:IncrementalDetokenizer(基类/空实现)→ BaseIncrementalDetokenizer(stop string 逻辑)→ Fast/SlowIncrementalDetokenizer(具体解码)
  2. 快慢双路径:Fast 使用 tokenizers 库的 DecodeStream(Rust 实现),Slow 使用 Python 增量解码
  3. Stop buffer 机制:为避免在 stop string 边界处截断,保留一定长度的文本缓冲

9.2 全局常量

python 复制代码
USE_FAST_DETOKENIZER = version.parse(tokenizers.__version__) >= version.parse("0.22.0")

INVALID_PREFIX_ERR_MSG = "Invalid prefix encountered"
  • USE_FAST_DETOKENIZER:tokenizers >= 0.22.0 支持带 prefill ids 的 DecodeStream,决定是否使用快速路径
  • INVALID_PREFIX_ERR_MSG:tokenizers 库的特定错误消息,用于异常恢复

9.3 IncrementalDetokenizer 基类

python 复制代码
class IncrementalDetokenizer:
    def __init__(self):
        self.token_ids: list[int] = []

    @property
    def output_token_ids(self) -> list[int]:
        return self.token_ids

    def num_output_tokens(self) -> int:
        return len(self.token_ids)

    def update(self, new_token_ids: list[int], stop_terminated: bool) -> str | None:
        self.token_ids.extend(new_token_ids)
        return None

    def get_next_output_text(self, finished: bool, delta: bool) -> str:
        return ""

空实现/占位符 :当不需要反分词时(detokenize=False),使用此基类。所有方法返回空/None,但 token_ids 仍然被记录(用于长度计算)。

python 复制代码
    @classmethod
    def from_new_request(
        cls,
        tokenizer: TokenizerLike | None,
        request: EngineCoreRequest,
    ) -> "IncrementalDetokenizer":
        assert request.sampling_params is not None

        if tokenizer is None:
            return IncrementalDetokenizer()

        if USE_FAST_DETOKENIZER and isinstance(tokenizer, PreTrainedTokenizerFast):
            return FastIncrementalDetokenizer(tokenizer, request)

        return SlowIncrementalDetokenizer(tokenizer, request)

工厂方法的三条路径

  1. 无 tokenizer → 空实现
  2. Fast tokenizer + tokenizers >= 0.22 → 快速路径
  3. 其他 → 慢速路径

9.4 BaseIncrementalDetokenizer 逐行解析

python 复制代码
class BaseIncrementalDetokenizer(IncrementalDetokenizer, ABC):
    def __init__(self, request: EngineCoreRequest):
        super().__init__()
Stop string 配置
python 复制代码
        params = request.sampling_params
        assert params is not None
        if params.stop is None:
            self.stop = []
        elif isinstance(params.stop, str):
            self.stop = [params.stop]
        else:
            self.stop = params.stop
        self.min_tokens = params.min_tokens
        self.include_stop_str_in_output = params.include_stop_str_in_output

参数归一化stop 可能是 None、str 或 list[str],统一为 list。

python 复制代码
        if self.stop and not self.include_stop_str_in_output:
            self.stop_buffer_length = max(len(s) for s in self.stop) - 1
        else:
            self.stop_buffer_length = 0
        self._last_output_text_offset: int = 0

Stop buffer 机制

核心问题:增量解码时,stop string 可能跨越多个 token。例如 stop="End",当前 token 解码为 "En",下一个 token 可能是 "d"。如果立即输出 "En",之后发现 "End" 匹配,就需要回溯------这是不可接受的。

解决方案:保留 max_stop_string_length - 1 个字符不输出。只有当请求完成时(确保不会再有更多 token),才输出全部文本。

_last_output_text_offset:DELTA 模式下记录上次输出到的位置。

python 复制代码
        self.output_text = ""

累积的输出文本。

update()------增量解码与 stop 检测核心
python 复制代码
    def update(self, new_token_ids: list[int], stop_terminated: bool) -> str | None:
        if not new_token_ids:
            return None

空 token 列表直接返回。

python 复制代码
        if stop_terminated and not self.include_stop_str_in_output:
            skipped_stop_token_id = new_token_ids[-1]
            new_token_ids = new_token_ids[:-1]
        else:
            skipped_stop_token_id = None

Stop token 处理:如果 EngineCore 检测到 stop(EOS token 匹配)但用户不想在输出中包含 stop string,则跳过最后一个 token 的解码。但 token ID 仍然被记录(用于 token_ids 列表的完整性)。

python 复制代码
        stop_check_offset = len(self.output_text)
        for new_token_id in new_token_ids:
            self.token_ids.append(new_token_id)
            self.output_text += self.decode_next(new_token_id)
            if self.min_tokens and self.num_output_tokens() <= self.min_tokens:
                stop_check_offset = len(self.output_text)

逐 token 增量解码

  1. 将 token ID 追加到列表
  2. 调用 decode_next()(由子类实现)获取解码文本
  3. 累积到 output_text

min_tokens 的 stop 检查偏移 :在 min_tokens 未达到之前,不应该检测 stop string。因此将 stop_check_offset 推进到当前位置------这意味着 stop string 搜索只在新产生的字符范围内进行。

python 复制代码
        if skipped_stop_token_id is not None:
            self.token_ids.append(skipped_stop_token_id)

将跳过的 stop token ID 追加到列表(但不解码)。

python 复制代码
        stop_string = None
        if self.stop and self.num_output_tokens() > self.min_tokens:
            stop = check_stop_strings(
                output_text=self.output_text,
                new_char_count=len(self.output_text) - stop_check_offset,
                stop=self.stop,
                include_in_output=self.include_stop_str_in_output,
            )
            if stop is not None:
                stop_string, truncate_to = stop
                if truncate_to != -1:
                    self.output_text = self.output_text[:truncate_to]

Stop string 检测:仅当超过 min_tokens 阈值时才检查。如果检测到,截断输出文本。

get_next_output_text()------增量文本获取
python 复制代码
    def get_next_output_text(self, finished: bool, delta: bool) -> str:
        buffer_length = 0 if finished else self.stop_buffer_length
        if not delta:
            if not buffer_length:
                return self.output_text
            return self.output_text[:-buffer_length]

非 DELTA 模式:返回全部文本(扣除 stop buffer)。

python 复制代码
        length = len(self.output_text) - buffer_length
        last_offset = self._last_output_text_offset
        if last_offset < length:
            self._last_output_text_offset = length
            return self.output_text[last_offset:length]
        return ""

DELTA 模式:只返回自上次调用以来新增的文本(扣除 stop buffer)。这是流式 SSE 输出的核心------每个增量只包含新增内容。

9.5 FastIncrementalDetokenizer 逐行解析

python 复制代码
class FastIncrementalDetokenizer(BaseIncrementalDetokenizer):
    def __init__(self, tokenizer: PreTrainedTokenizerFast, request: EngineCoreRequest):
        super().__init__(request)

        sampling_params = request.sampling_params
        assert sampling_params is not None

        self.request_id = request.request_id
        self.skip_special_tokens = sampling_params.skip_special_tokens

        self.tokenizer: Tokenizer = tokenizer._tokenizer

获取底层的 tokenizers 库 Tokenizer 对象(不是 transformers 的封装)。

python 复制代码
        self.stream = DecodeStream(
            ids=request.prompt_token_ids,
            skip_special_tokens=self.skip_special_tokens,
        )

DecodeStream 初始化:使用 prompt token IDs 作为 prefill------这是 tokenizers >= 0.22.0 的新特性,允许在创建 DecodeStream 时就提供已知的前缀 token,避免重复解码。

python 复制代码
        self.spaces_between_special_tokens = (
            sampling_params.skip_special_tokens
            or sampling_params.spaces_between_special_tokens
        )

        if not self.spaces_between_special_tokens:
            added_token_ids = getattr(self.tokenizer, "added_token_ids", None)
            if added_token_ids is None:
                self.tokenizer.added_token_ids = added_token_ids = {
                    tid: tok.content
                    for tid, tok in self.tokenizer.get_added_tokens_decoder().items()
                }

            if added_token_ids:
                self.last_special = False
                self.added_token_ids = added_token_ids
            else:
                self.spaces_between_special_tokens = True

Special token 间距处理

  • skip_special_tokens=True 时,自动不显示 special token,间距无意义
  • spaces_between_special_tokens=True 时,在 special token 之间插入空格
  • 当两者都为 False 时,需要精确控制间距------使用 added_token_ids 字典检测 special token,避免错误插入空格
decode_next()
python 复制代码
    def decode_next(self, next_token_id: int) -> str:
        token = self._protected_step(next_token_id)

        if not self.spaces_between_special_tokens:
            special_token = self.added_token_ids.get(next_token_id)
            is_special = special_token is not None
            if is_special and self.last_special:
                token = special_token
            self.last_special = is_special

        return token or ""

Special token 处理逻辑

  • 如果当前 token 是 special token 且上一个也是 special token,不插入空格------直接使用 token 的原始内容
  • 否则,使用 DecodeStream 的输出(可能包含前缀空格)
_protected_step()------带异常恢复的步进
python 复制代码
    def _protected_step(self, next_token_id: int) -> str | None:
        try:
            token = self.stream.step(self.tokenizer, next_token_id)
        except (OverflowError, TypeError):
            logger.exception("Encountered invalid token id: %r", next_token_id)
            token = None
        except Exception as e:
            if not str(e).startswith(INVALID_PREFIX_ERR_MSG):
                raise e
            logger.warning(
                "Encountered invalid prefix detokenization error"
                " for request %s, resetting decode stream.",
                self.request_id,
            )
            self.stream = DecodeStream(skip_special_tokens=self.skip_special_tokens)
            token = self.stream.step(self.tokenizer, next_token_id)
        return token

三层异常处理

  1. OverflowError/TypeError:token ID 越界(如超大的无效 ID),记录并返回 None
  2. Invalid prefix error :tokenizers 库在遇到非单调 UTF-8 输出时抛出此错误------这通常是因为某些 token 的组合产生了无效的 UTF-8 前缀。恢复策略:重置 DecodeStream(不带 prefill),然后重新步进当前 token
  3. 其他异常:向上传播

设计洞察:Invalid prefix 错误的恢复是"有损"的------重置后丢失了 prompt 的解码上下文。但对于后续 token 的解码,这通常不影响正确性,因为 UTF-8 字符边界很快会重新对齐。

9.6 SlowIncrementalDetokenizer 逐行解析

python 复制代码
class SlowIncrementalDetokenizer(BaseIncrementalDetokenizer):
    def __init__(self, tokenizer: TokenizerLike, request: EngineCoreRequest):
        super().__init__(request)

        self.tokenizer = tokenizer
        params = request.sampling_params
        assert params is not None

        self.prompt_len = length_from_prompt_token_ids_or_embeds(
            request.prompt_token_ids, request.prompt_embeds
        )

慢速解码器需要区分 prompt token 和 output token------因为 token_ids 列表包含两者。

python 复制代码
        if request.prompt_token_ids is not None:
            self.tokens, self.prefix_offset, self.read_offset = (
                convert_prompt_ids_to_tokens(
                    tokenizer=tokenizer,
                    prompt_ids=request.prompt_token_ids,
                    skip_special_tokens=params.skip_special_tokens,
                )
            )
        else:
            self.tokens = [""] * self.prompt_len
            self.prefix_offset = 0
            self.read_offset = 0

Prompt token 预处理:将 prompt token IDs 转换为 token 字符串列表,并计算增量解码所需的偏移量。对于 embeds 输入,使用空字符串占位。

python 复制代码
        self.token_ids.extend(request.prompt_token_ids or [0] * self.prompt_len)

token_ids 包含 prompt token------这是与 FastIncrementalDetokenizer 的关键区别。Fast 使用 DecodeStream 的 prefill 功能,不需要在 token_ids 中包含 prompt;Slow 需要将 prompt 和 output token 统一在 token_ids 列表中,因为增量解码需要完整的前缀。

python 复制代码
        self.skip_special_tokens = params.skip_special_tokens
        self.spaces_between_special_tokens = params.spaces_between_special_tokens
output_token_ids 属性覆盖
python 复制代码
    @property
    def output_token_ids(self) -> list[int]:
        if self.prompt_len:
            return self.token_ids[self.prompt_len :]
        return self.token_ids

    def num_output_tokens(self) -> int:
        return len(self.token_ids) - self.prompt_len

关键覆盖:因为 token_ids 包含 prompt token,output_token_ids 需要切片排除 prompt 部分。

decode_next()
python 复制代码
    def decode_next(self, next_token_id: int) -> str:
        new_tokens, decoded_text, prefix_offset, read_offset = detokenize_incrementally(
            tokenizer=self.tokenizer,
            all_input_ids=self.token_ids,
            prev_tokens=self.tokens,
            prefix_offset=self.prefix_offset,
            read_offset=self.read_offset,
            skip_special_tokens=self.skip_special_tokens,
            spaces_between_special_tokens=self.spaces_between_special_tokens,
        )

        self.tokens.extend(new_tokens)
        self.prefix_offset = prefix_offset
        self.read_offset = read_offset

        return decoded_text

调用 detokenize_incrementally() ------这是一个基于前缀比较的增量解码算法:

  • 维护 prefix_offsetread_offset 两个指针
  • 每次只解码自上次以来新增的部分
  • 利用 UTF-8 字符边界的重叠来处理多字节字符的跨 token 分割

9.7 check_stop_strings() 函数逐行解析

python 复制代码
def check_stop_strings(
    output_text: str,
    new_char_count: int,
    stop: list[str],
    include_in_output: bool,
) -> tuple[str, int] | None:
    if not new_char_count or not stop:
        return None

提前退出:无新字符或无 stop string。

python 复制代码
    for stop_str in stop:
        stop_string_len = len(stop_str)
        stop_index = output_text.find(stop_str, 1 - new_char_count - stop_string_len)
        if stop_index == -1:
            continue

优化的搜索范围1 - new_char_count - stop_string_len 作为搜索起始位置------只搜索新字符可能触及的范围。这是因为之前的字符已经被搜索过了(增量检查)。

python 复制代码
        if include_in_output:
            stop_index += stop_string_len
            if stop_index >= len(output_text):
                return stop_str, -1

包含 stop string 在输出中:截断点移到 stop string 末尾。如果 stop string 恰好在输出末尾,不需要截断(返回 -1)。

python 复制代码
        return stop_str, stop_index
    return None

不包含 stop string:截断点在 stop string 开头。

9.8 Detokenizer 整体架构总结

复制代码
                    ┌──────────────────────┐
                    │ IncrementalDetokenizer│ ← 空实现(detokenize=False)
                    │   (基类/占位符)       │
                    └──────────┬───────────┘
                               │
                    ┌──────────┴───────────┐
                    │BaseIncrementalDetoken│
                    │   (stop string 逻辑)  │
                    │  ├─ stop_buffer      │
                    │  ├─ min_tokens       │
                    │  └─ get_next_output  │
                    └──────┬───────┬───────┘
                           │       │
            ┌──────────────┘       └──────────────┐
            │                                      │
┌───────────┴───────────┐          ┌──────────────┴──────────┐
│FastIncrementalDetoken │          │SlowIncrementalDetoken   │
│  (tokenizers Rust)    │          │  (Python incremental)   │
│  ├─ DecodeStream      │          │  ├─ detokenize_increm.  │
│  ├─ _protected_step   │          │  ├─ prefix/read offset  │
│  └─ special token     │          │  └─ prompt/output split │
│    handling           │          │                         │
└───────────────────────┘          └─────────────────────────┘
相关推荐
无忧智库2 小时前
智能工厂信息化顶层架构设计:一套真正落地的体系该长什么样(PPT)
架构
易生一世2 小时前
Kiro CLI的Windows安装及认证
windows·ai·kiro
梦想的旅途22 小时前
解构私域自动化架构:基于 RPA 协议的中台设计与实现
架构·自动化·rpa
WJJAGI3 小时前
烧了 30 亿 token 之后,我决定不让 Hermes 自己改配置了
架构
疯狂的魔鬼3 小时前
从 5 个 Hooks 到注册表模式:Vue 3 复杂详情页的架构演进与原则沉淀
前端·架构
ekuoleung3 小时前
Spring Boot 3.4 + Java 21 在量化平台中的架构实践
java·架构
gao_tjie3 小时前
鱼音频生成 API 集成指南
ai
中小企业实战军师刘孙亮4 小时前
组织赋能+体系搭建,破解中小企业增长困局-佛山鼎策创局破局增长咨询
架构·产品运营·音视频·制造·业界资讯
前端摸鱼匠4 小时前
【AI大模型春招面试题25】掩码自注意力(Masked Self-Attention)与普通自注意力的区别?适用场景?
人工智能·ai·面试·大模型·求职招聘