Files
wehub-resource-sync 59a0a3844c
PR Test AMD / cancel-on-close (push) Has been skipped
PR Test NVIDIA ARM / scan (push) Has been skipped
PR Test NVIDIA / cancel-on-close (push) Has been skipped
PR Test AMD / scan (push) Has been skipped
PR Test NVIDIA ARM / cancel-on-close (push) Has been skipped
PR Test NVIDIA / scan (push) Has been skipped
Release Docker Images / build (cu129-torch-2.11.0) (push) Has been skipped
Release Docker Images / build (cu130-torch-2.11.0) (push) Has been skipped
Release PyPI / publish (push) Has been skipped
Scheduler Python Test / test (push) Successful in 27m19s
Docs / build (push) Successful in 28m8s
Scheduler C++ Test / test (push) Successful in 28m19s
Scheduler C++ Test / test-flat (push) Successful in 28m18s
Docs / deploy (push) Has been cancelled
PR Test AMD / finish (push) Has been cancelled
PR Test NVIDIA / finish (push) Has been cancelled
PR Test NVIDIA ARM / finish (push) Has been cancelled
PR Test NVIDIA ARM / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test AMD / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test NVIDIA / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:32:31 +08:00

354 lines
14 KiB
Python
Executable File

# Copyright (c) 2026 LightSeek Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import copy
import time
from typing import Any
import torch
from tokenspeed.runtime.cache.req_to_token_pool import (
ReqToTokenPoolInfo,
)
from tokenspeed.runtime.engine.request_types import ( # noqa: F401
ABORT_CODE,
FINISH_ABORT,
FINISH_LENGTH,
FINISH_MATCHED_STR,
FINISH_MATCHED_TOKEN,
INIT_INCREMENTAL_DETOKENIZATION_OFFSET,
BaseFinishReason,
)
from tokenspeed.runtime.grammar.base_grammar_backend import BaseGrammarObject
from tokenspeed.runtime.metrics.collector import TimeStats
from tokenspeed.runtime.sampling.sampling_params import SamplingParams
from tokenspeed.runtime.utils import get_colorful_logger
logger = get_colorful_logger(__name__)
class Req:
"""The input and output status of a request."""
def __init__(
self,
rid: str,
origin_input_text: str,
origin_input_ids: tuple[int],
sampling_params: SamplingParams,
return_logprob: bool = False,
top_logprobs_num: int = 0,
token_ids_logprob: list[int] = None,
stream: bool = False,
origin_input_ids_unpadded: tuple[int] | None = None,
input_embeds: list[list[float]] | None = None,
input_extra_infos: list[dict] | None = None,
session_id: str | None = None,
custom_logit_processor: str | None = None,
return_hidden_states: bool = False,
eos_token_ids: set[int] | None = None,
bootstrap_host: str | None = None,
bootstrap_port: int | None = None,
bootstrap_room: int | None = None,
origin_input_multi_ids: list[list[int]] | None = None,
created_time: float | None = None,
):
# Input and output info
self.rid = rid
self.origin_input_text = origin_input_text
self.origin_input_ids_unpadded = (
origin_input_ids_unpadded
if origin_input_ids_unpadded
else origin_input_ids # Before image padding
)
self.origin_input_ids = origin_input_ids
self.origin_input_multi_ids = origin_input_multi_ids
# Each decode stage's output ids
self.output_ids = []
self.output_multi_ids = []
# fill_ids = origin_input_ids + output_ids. Updated if chunked.
self.fill_ids = None
self.fill_multi_ids = None
self.fill_input_embeds = None
# For Eagle and chunked prefill, remove first token when chunked prefill
self.draft_fill_ids = None
self.session_id = session_id
self.input_embeds = input_embeds
self.input_extra_infos = input_extra_infos
# Sampling info
if isinstance(sampling_params.custom_params, dict):
sampling_params = copy.copy(sampling_params)
sampling_params.custom_params = sampling_params.custom_params | {
"__req__": self
}
self.sampling_params = sampling_params
self.custom_logit_processor = custom_logit_processor
self.return_hidden_states = return_hidden_states
# Memory pool info
self.req_pool_idx: int | None = None
self.req_to_token_pool_info: ReqToTokenPoolInfo | None = None
# substitute for prefix_indices
self.prefix_page_ids = []
self.prefix_len = 0
# Check finish
self.tokenizer = None
# Cached tokenizer-related ids to avoid repeated HF attribute lookups in check_finished().
self._eos_token_id_cached: int | None = None
self._additional_stop_token_ids_cached: set[int] | None = None
self.finished_reason = None
# Whether this request has finished output
self.finished_output = None
# If we want to abort the request in the middle of the event loop, set this to true
# Note: We should never set finished_reason in the middle, the req will get filtered and never respond
self.to_abort = False
# This carries the error message for `.to_abort` and will be attached to the finished_reason at the end of the event loop
self.to_abort_message: str = "Unknown error"
self.stream = stream
self.eos_token_ids = eos_token_ids
# For incremental decoding
# ----- | --------- read_ids -------|
# ----- | surr_ids |
# xxxxx | xxxxxxxxxxx | xxxxxxxxxxx |
# ----- ^ ----------- ^ ----------- ^
# ----- 1 ----------- 2 ----------- 3
# 1: surr_offset
# 2: read_offset
# 3: last token
self.surr_offset = None # Surrounding offset to defeat the cleanup algorithm
self.read_offset = None
self.decoded_text = ""
# Prefix info
# The indices to kv cache for the shared prefix.
self.prefix_indices = []
# Number of tokens to run prefill.
self.extend_input_len = 0
# The relative logprob_start_len in an extend batch
self.extend_logprob_start_len = 0
self.last_node = None
# Whether or not if it is chunked. It increments whenever
# it is chunked, and decrement whenever chunked request is
# processed.
self.is_chunked = 0
# For retraction
self.is_retracted = False
# Incremental streamining
self.send_token_offset: int = 0
self.send_decode_id_offset: int = 0
# because the decode server does not have the first output token logprobs
self.send_output_token_logprobs_offset: int = 0
# Logprobs (arguments)
self.return_logprob = return_logprob
# Start index to compute logprob from.
self.logprob_start_len = 0
self.top_logprobs_num = top_logprobs_num
self.token_ids_logprob = token_ids_logprob
# Logprobs (return values)
self.input_logprob_sent: bool = False
self.input_token_logprobs_val: list[float] | None = None
self.input_token_logprobs_idx: list[int] | None = None
self.input_top_logprobs_val: list[float] | None = None
self.input_top_logprobs_idx: list[int] | None = None
self.input_token_ids_logprobs_val: list[float] | None = None
self.input_token_ids_logprobs_idx: list[int] | None = None
# Temporary holder to store input_token_logprobs.
self.input_token_logprobs: list[tuple[int]] | None = None
self.temp_input_top_logprobs_val: list[torch.Tensor] | None = None
self.temp_input_top_logprobs_idx: list[int] | None = None
self.temp_input_token_ids_logprobs_val: list[float] | None = None
self.temp_input_token_ids_logprobs_idx: list[int] | None = None
if return_logprob:
self.output_token_logprobs_val = []
self.output_token_logprobs_idx = []
self.output_top_logprobs_val = []
self.output_top_logprobs_idx = []
self.output_token_ids_logprobs_val = []
self.output_token_ids_logprobs_idx = []
else:
self.output_token_logprobs_val = self.output_token_logprobs_idx = (
self.output_top_logprobs_val
) = self.output_top_logprobs_idx = self.output_token_ids_logprobs_val = (
self.output_token_ids_logprobs_idx
) = None
self.hidden_states = []
# Embedding (return values)
self.embedding = None
# Constrained decoding
self.grammar: BaseGrammarObject | None = None
# The number of cached tokens that were already cached in the KV cache
self.cached_tokens = 0
self.already_computed = 0
self.last_host_node: Any = None
self.host_hit_length = 0
# The number of verification forward passes in the speculative decoding.
# This is used to compute the average acceptance length per request.
self.spec_verify_ct = 0
# Time of obj created
# Use the created_time from tokenizer if provided, otherwise use current time
if created_time is not None:
self.created_time = created_time
else:
self.created_time = time.time()
# Calculate the time from receiving the request at TokenizerManager to reaching process_input_requests in the scheduling process
self.tokenizer_to_scheduler_latency = time.time() - self.created_time
# For metrics
self.time_stats: TimeStats = TimeStats()
self.has_log_time_stats: bool = False
self.queue_time_start = None
self.queue_time_end = None
self.last_tic = time.monotonic()
self.first_latency_recorded = (
False # Flag to track if first latency has been recorded
)
self.prefill_waiting_recorded = False
self.first_chunk_forward_start_time = None
self.reserve_num_tokens = 0
# For disaggregation
self.bootstrap_host: str = bootstrap_host
self.bootstrap_port: int | None = bootstrap_port
self.bootstrap_room: int | None = bootstrap_room
# the start index of the sent kv cache
# We want to send it chunk by chunk for chunked prefill.
# After every chunk forward, we do the following:
# kv_send(req.input_ids[req.start_send_idx:len(req.fill_ids)])
# start_send_idx = len(req.fill_ids)
self.start_send_idx: int = 0
# For overlap schedule, we delay the kv transfer until `process_batch_result_disagg_prefill` rather than `process_prefill_chunk` in non-overlap
# This is because kv is not ready in `process_prefill_chunk`.
# We use `tmp_end_idx` to store the end index of the kv cache to send.
self.tmp_end_idx: int = -1
self.metadata_buffer_index: int = -1
# Only meaningful in speculative reasoning.
self.accept_draft_tokens: float | None = None
self.output_extra_info: dict[str, Any] = {}
def set_tokenizer(self, tokenizer):
"""Assign tokenizer and cache ids needed by check_finished()."""
self.tokenizer = tokenizer
if tokenizer is None:
self._eos_token_id_cached = None
self._additional_stop_token_ids_cached = None
return
eos_id = getattr(tokenizer, "eos_token_id", None)
self._eos_token_id_cached = int(eos_id) if eos_id is not None else None
extra = getattr(tokenizer, "additional_stop_token_ids", None)
self._additional_stop_token_ids_cached = (
set(int(x) for x in extra) if extra else None
)
@property
def seqlen(self):
return len(self.origin_input_ids) + len(self.output_ids)
def finished(self) -> bool:
# Whether request reached finished condition
return self.finished_reason is not None
def init_incremental_detokenize(self):
first_iter = self.surr_offset is None or self.read_offset is None
if first_iter:
self.read_offset = len(self.origin_input_ids_unpadded)
self.surr_offset = max(
self.read_offset - INIT_INCREMENTAL_DETOKENIZATION_OFFSET, 0
)
# self.surr_offset = self.read_offset
all_ids = self.origin_input_ids_unpadded + self.output_ids
return all_ids[self.surr_offset :], self.read_offset - self.surr_offset
def check_finished(self):
if self.finished():
return
if self.to_abort:
self.finished_reason = FINISH_ABORT(
message=self.to_abort_message,
)
return
if len(self.output_ids) >= self.sampling_params.max_new_tokens:
self.finished_reason = FINISH_LENGTH(
length=self.sampling_params.max_new_tokens
)
return
if self.grammar is not None:
if self.grammar.is_terminated():
self.finished_reason = FINISH_MATCHED_TOKEN(matched=self.output_ids[-1])
return
last_token_id = self.output_ids[-1]
if not self.sampling_params.ignore_eos:
matched_eos = False
# Check stop token ids
if self.sampling_params.stop_token_ids:
matched_eos = last_token_id in self.sampling_params.stop_token_ids
if self.eos_token_ids:
matched_eos |= last_token_id in self.eos_token_ids
if self.tokenizer is not None and self._eos_token_id_cached is None:
self.set_tokenizer(self.tokenizer)
if self._eos_token_id_cached is not None:
matched_eos |= last_token_id == self._eos_token_id_cached
if self._additional_stop_token_ids_cached:
matched_eos |= last_token_id in self._additional_stop_token_ids_cached
if matched_eos:
self.finished_reason = FINISH_MATCHED_TOKEN(matched=last_token_id)
return
# Check stop strings
if len(self.sampling_params.stop_strs) > 0:
tail_str = self.tokenizer.decode(
self.output_ids[-(self.sampling_params.stop_str_max_len + 1) :]
)
for stop_str in self.sampling_params.stop_strs:
if stop_str in tail_str or stop_str in self.decoded_text:
self.finished_reason = FINISH_MATCHED_STR(matched=stop_str)
return
def __repr__(self):
return (
f"Req(rid={self.rid}, "
f"input_ids={len(self.origin_input_ids)}, output_ids={len(self.output_ids)})"
)