Fix spec v2 stop output boundary (#25980)

Co-authored-by: gss <2783977641@qq.com>
Co-authored-by: hnyls2002 <lsyincs@gmail.com>
Co-authored-by: Liangsheng Yin <hnyls2002@gmail.com>
This commit is contained in:
gq112
2026-06-09 00:32:21 -07:00
committed by GitHub
co-authored by gss hnyls2002 Liangsheng Yin
parent d981b7b9c4
commit 2218622f50
4 changed files with 207 additions and 27 deletions
@@ -169,7 +169,7 @@ class DetokenizerManager(MultiHttpWorkerDetokenizerMixin):
def trim_matched_stop(
self, output: Union[str, List[int]], finished_reason: Dict, no_stop_trim: bool
):
if no_stop_trim or not finished_reason:
if not finished_reason:
return output
matched = finished_reason.get("matched", None)
@@ -181,10 +181,15 @@ class DetokenizerManager(MultiHttpWorkerDetokenizerMixin):
# Trim stop str.
if isinstance(matched, str) and isinstance(output, str):
pos = output.find(matched)
return output[:pos] if pos != -1 else output
if pos == -1:
return output
end = pos + len(matched)
return output[:end] if no_stop_trim else output[:pos]
# Trim stop token.
if isinstance(matched, int) and isinstance(output, list):
if no_stop_trim:
return output
# 200012 <|call|> is the tool call token and one of eos tokens for gpt-oss model
if output[-1] == 200012 and self.is_tool_call_parser_gpt_oss:
return output
+49 -12
View File
@@ -1192,6 +1192,17 @@ class Req(ReqDllmMixin):
return self.surr_and_decode_ids, self.read_offset - self.surr_offset
def _stop_match_tail_len(self, new_accepted_len: int) -> int:
max_len_tail_str = max(
self.sampling_params.stop_str_max_len + 1,
self.sampling_params.stop_regex_max_len + 1,
)
# Cover all newly accepted tokens so an early stop string is not missed
# when speculative decoding accepts multiple tokens per step.
return min(
max_len_tail_str + max(new_accepted_len - 1, 0), len(self.output_ids)
)
def tail_str(self, new_accepted_len: int = 1) -> str:
# Check stop strings and stop regex patterns together
if (
@@ -1200,17 +1211,7 @@ class Req(ReqDllmMixin):
):
return ""
max_len_tail_str = max(
self.sampling_params.stop_str_max_len + 1,
self.sampling_params.stop_regex_max_len + 1,
)
# Spec decode accepts multiple tokens per step; widen the window to cover
# the whole accepted chunk so a stop string landing mid-chunk (with more
# tokens accepted after it) is not pushed out of view.
tail_len = min(
max_len_tail_str + max(new_accepted_len - 1, 0), len(self.output_ids)
)
tail_len = self._stop_match_tail_len(new_accepted_len)
return self.tokenizer.decode(self.output_ids[-tail_len:])
def check_match_stop_str_prefix(self) -> bool:
@@ -1266,6 +1267,34 @@ class Req(ReqDllmMixin):
return False
def _locate_str_stop_finished_len(
self,
new_accepted_len: int,
*,
stop_str: Optional[str] = None,
stop_regex: Optional[str] = None,
) -> int:
"""Map a matched stop string/regex to output_ids length (stop included)."""
def matched(text: str) -> bool:
if stop_str is not None:
return stop_str in text
return re.search(stop_regex, text) is not None
tail_len = self._stop_match_tail_len(new_accepted_len)
start = len(self.output_ids) - tail_len
token_window = self.output_ids[start:]
# Old prefixes were checked in the previous step.
for token_count in range(
max(1, len(token_window) - new_accepted_len + 1), len(token_window)
):
if matched(self.tokenizer.decode(token_window[:token_count])):
return start + token_count
# The full tail window is already known to match by the caller.
return len(self.output_ids)
def _check_str_based_finish(self, new_accepted_len: int = 1):
if (
len(self.sampling_params.stop_strs) > 0
@@ -1276,8 +1305,13 @@ class Req(ReqDllmMixin):
# Check stop strings
if len(self.sampling_params.stop_strs) > 0:
for stop_str in self.sampling_params.stop_strs:
if stop_str in tail_str or stop_str in self.decoded_text:
stop_str_in_tail = stop_str in tail_str
if stop_str_in_tail or stop_str in self.decoded_text:
self.finished_reason = FINISH_MATCHED_STR(matched=stop_str)
if stop_str_in_tail:
self.finished_len = self._locate_str_stop_finished_len(
new_accepted_len, stop_str=stop_str
)
return True
# Check stop regex
@@ -1287,6 +1321,9 @@ class Req(ReqDllmMixin):
self.finished_reason = FINISHED_MATCHED_REGEX(
matched=stop_regex_str
)
self.finished_len = self._locate_str_stop_finished_len(
new_accepted_len, stop_regex=stop_regex_str
)
return True
return False