Clean up detokenizer and remove dead multimodal_gen code (#21588)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lianmin Zheng
2026-03-28 21:44:40 -07:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 516cff97a3
commit ba6b501f3a
13 changed files with 53 additions and 103 deletions
-14
View File
@@ -178,12 +178,6 @@ class ModelConfig:
self.is_multimodal = enable_multimodal and is_multimodal_model(
self.hf_config.architectures
)
self.is_multimodal_gen = enable_multimodal and is_multimodal_gen_model(
self.hf_config.architectures
)
self.is_image_gen = enable_multimodal and is_image_gen_model(
self.hf_config.architectures
)
self.is_audio_model = enable_multimodal and is_audio_model(
self.hf_config.architectures
)
@@ -1351,14 +1345,6 @@ def is_multimodal_model(model_architectures: List[str]):
return False
def is_multimodal_gen_model(model_architectures: List[str]):
return False
def is_image_gen_model(model_architectures: List[str]):
return False
def is_audio_model(model_architectures: List[str]):
models = [
"WhisperForConditionalGeneration",
+2
View File
@@ -8,3 +8,5 @@ GPU_MEMORY_ALL_TYPES = [
GPU_MEMORY_TYPE_WEIGHTS,
GPU_MEMORY_TYPE_CUDA_GRAPH,
]
HEALTH_CHECK_RID_PREFIX = "HEALTH_CHECK"
+2 -1
View File
@@ -59,6 +59,7 @@ from fastapi.exceptions import RequestValidationError
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import ORJSONResponse, Response, StreamingResponse
from sglang.srt.constants import HEALTH_CHECK_RID_PREFIX
from sglang.srt.disaggregation.utils import FAKE_BOOTSTRAP_HOST, DisaggregationMode
from sglang.srt.entrypoints.anthropic.protocol import (
AnthropicCountTokensRequest,
@@ -509,7 +510,7 @@ async def health_generate(request: Request) -> Response:
return Response(status_code=200)
sampling_params = {"max_new_tokens": 1, "temperature": 0.0}
rid = f"HEALTH_CHECK_{time.time()}"
rid = f"{HEALTH_CHECK_RID_PREFIX}_{time.time()}"
if _global_state.tokenizer_manager.is_image_gen:
gri = _global_state.tokenizer_manager.get_image_gen_health_check_request(
@@ -25,10 +25,10 @@ import pybase64
import setproctitle
import zmq
from sglang.srt.constants import HEALTH_CHECK_RID_PREFIX
from sglang.srt.environ import envs
from sglang.srt.managers.io_struct import (
BatchEmbeddingOutput,
BatchMultimodalDecodeReq,
BatchStrOutput,
BatchTokenIDOutput,
FreezeGCReq,
@@ -88,16 +88,9 @@ class DetokenizerManager(MultiHttpWorkerDetokenizerMixin):
# Init running status
self.init_running_status(server_args)
if server_args.enable_metrics:
start_cpu_monitor_thread("detokenizer")
# Init dispatcher
self.init_request_dispatcher()
@staticmethod
def is_health_check_request(rid: Optional[str]) -> bool:
return isinstance(rid, str) and rid.startswith("HEALTH_CHECK")
def init_ipc_channels(self, port_args: PortArgs):
context = zmq.Context(2)
self.recv_from_scheduler = get_zmq_socket(
@@ -120,9 +113,8 @@ class DetokenizerManager(MultiHttpWorkerDetokenizerMixin):
def init_running_status(self, server_args: ServerArgs):
self.decode_status = LimitedCapacityDict(capacity=DETOKENIZER_MAX_STATES)
self.is_dummy = False
self.is_tool_call_parser_gpt_oss = server_args.tool_call_parser == "gpt-oss"
self.disable_tokenizer_batch_decode = server_args.disable_tokenizer_batch_decode
self.is_tool_call_parser_gpt_oss = server_args.tool_call_parser == "gpt-oss"
self.soft_watchdog = Watchdog.create(
debug_name="DetokenizerManager",
@@ -131,12 +123,14 @@ class DetokenizerManager(MultiHttpWorkerDetokenizerMixin):
test_stuck_time=envs.SGLANG_TEST_STUCK_DETOKENIZER.get(),
)
if server_args.enable_metrics:
start_cpu_monitor_thread("detokenizer")
def init_request_dispatcher(self):
self._request_dispatcher = TypeBasedDispatcher(
[
(BatchEmbeddingOutput, self.handle_batch_embedding_out),
(BatchTokenIDOutput, self.handle_batch_token_id_out),
(BatchMultimodalDecodeReq, self.handle_multimodal_decode_req),
(FreezeGCReq, self.handle_freeze_gc_req),
]
)
@@ -190,8 +184,6 @@ class DetokenizerManager(MultiHttpWorkerDetokenizerMixin):
) -> List[str]:
"""Batch decode with grouping by (skip_special_tokens, spaces_between_special_tokens)."""
assert self.tokenizer is not None
# fast path
first_skip, first_space = skip_list[0], space_list[0]
if all(s == first_skip for s in skip_list) and all(
@@ -236,9 +228,7 @@ class DetokenizerManager(MultiHttpWorkerDetokenizerMixin):
surr_offset=0,
read_offset=recv_obj.read_offsets[i],
)
if not self.is_health_check_request(rid):
# for health check requests, we do not store the decode status
self.decode_status[rid] = s
self.decode_status[rid] = s
else:
s = self.decode_status[rid]
s.decode_ids.extend(recv_obj.decode_ids[i])
@@ -254,22 +244,16 @@ class DetokenizerManager(MultiHttpWorkerDetokenizerMixin):
# Decode token ids to strings
if not self.disable_tokenizer_batch_decode:
if not self.is_dummy:
# Run normal batch decode
surr_texts = self._grouped_batch_decode(
surr_ids,
recv_obj.skip_special_tokens,
recv_obj.spaces_between_special_tokens,
)
read_texts = self._grouped_batch_decode(
read_ids,
recv_obj.skip_special_tokens,
recv_obj.spaces_between_special_tokens,
)
else:
# If it is dummy weights, just return dummy strings to prevent potential detokenization edge cases
surr_texts = ["dog" for _ in surr_ids]
read_texts = ["cat" for _ in read_ids]
surr_texts = self._grouped_batch_decode(
surr_ids,
recv_obj.skip_special_tokens,
recv_obj.spaces_between_special_tokens,
)
read_texts = self._grouped_batch_decode(
read_ids,
recv_obj.skip_special_tokens,
recv_obj.spaces_between_special_tokens,
)
else:
# Do not use batch decode to prevent some detokenization edge cases (e.g., gpt-oss).
surr_texts = [
@@ -297,25 +281,17 @@ class DetokenizerManager(MultiHttpWorkerDetokenizerMixin):
output_strs = []
for i in range(bs):
rid = recv_obj.rids[i]
if self.is_health_check_request(rid):
s = DecodeStatus(
decoded_text=recv_obj.decoded_texts[i],
decode_ids=recv_obj.decode_ids[i],
surr_offset=0,
read_offset=recv_obj.read_offsets[i],
try:
s = self.decode_status[rid]
except KeyError:
raise RuntimeError(
f"Decode status not found for request {rid}. "
"It may be due to the request being evicted from the decode status due to memory pressure. "
"Please increase the maximum number of requests by setting "
"the SGLANG_DETOKENIZER_MAX_STATES environment variable to a bigger value than the default value. "
f"The current value is {DETOKENIZER_MAX_STATES}. "
"For more details, see: https://github.com/sgl-project/sglang/issues/2812"
)
else:
try:
s = self.decode_status[rid]
except KeyError:
raise RuntimeError(
f"Decode status not found for request {rid}. "
"It may be due to the request being evicted from the decode status due to memory pressure. "
"Please increase the maximum number of requests by setting "
"the SGLANG_DETOKENIZER_MAX_STATES environment variable to a bigger value than the default value. "
f"The current value is {DETOKENIZER_MAX_STATES}. "
"For more details, see: https://github.com/sgl-project/sglang/issues/2812"
)
new_text = read_texts[i][len(surr_texts[i]) :]
if recv_obj.finished_reasons[i] is None:
# Streaming chunk: update the decode status
@@ -335,6 +311,7 @@ class DetokenizerManager(MultiHttpWorkerDetokenizerMixin):
recv_obj.finished_reasons[i],
recv_obj.no_stop_trim[i],
)
# Incrementally send text.
incremental_output = output_str[s.sent_offset :]
s.sent_offset = len(output_str)
@@ -404,14 +381,15 @@ class DetokenizerManager(MultiHttpWorkerDetokenizerMixin):
time_stats=recv_obj.time_stats,
)
def handle_multimodal_decode_req(self, recv_obj: BatchMultimodalDecodeReq):
raise NotImplementedError()
def handle_freeze_gc_req(self, recv_req: FreezeGCReq):
freeze_gc("Detokenizer Manager")
return None
def is_health_check_request(rid: Optional[str]) -> bool:
return isinstance(rid, str) and rid.startswith(HEALTH_CHECK_RID_PREFIX)
class LimitedCapacityDict(OrderedDict):
def __init__(self, capacity: int, *args, **kwargs):
super().__init__(*args, **kwargs)
+2 -1
View File
@@ -39,6 +39,7 @@ from torch.distributed import barrier
from sglang.jit_kernel.ngram_embedding import update_token_table
from sglang.srt.configs.model_config import ModelConfig
from sglang.srt.constants import HEALTH_CHECK_RID_PREFIX
from sglang.srt.constrained.grammar_manager import GrammarManager
from sglang.srt.disaggregation.decode import (
DecodePreallocQueue,
@@ -3412,7 +3413,7 @@ class IdleSleeper:
def is_health_check_generate_req(recv_req):
rid = getattr(recv_req, "rid", None)
return rid is not None and rid.startswith("HEALTH_CHECK")
return rid is not None and rid.startswith(HEALTH_CHECK_RID_PREFIX)
def is_work_request(recv_req):
@@ -953,10 +953,6 @@ class SchedulerOutputProcessorMixin:
if req is skip_req:
continue
# Multimodal partial stream chunks break the detokenizer, so drop aborted requests here.
if self.model_config.is_multimodal_gen and req.to_finish:
continue
if req.finished():
if req.finished_output:
# With the overlap schedule, a request will try to output twice and hit this line twice
@@ -975,8 +971,7 @@ class SchedulerOutputProcessorMixin:
# origin stream_interval logic
should_output = (
len(req.output_ids) % stream_interval == 1
if not self.model_config.is_multimodal_gen
and stream_interval > 1
if stream_interval > 1
else len(req.output_ids) % stream_interval == 0
)
@@ -986,8 +981,6 @@ class SchedulerOutputProcessorMixin:
else:
should_output = (
len(req.output_ids) % DEFAULT_FORCE_STREAM_INTERVAL == 0
if not self.model_config.is_multimodal_gen
else False
)
if should_output:
@@ -1003,10 +996,7 @@ class SchedulerOutputProcessorMixin:
decoded_texts.append(req.decoded_text)
decode_ids, read_offset = req.init_incremental_detokenize()
if self.model_config.is_multimodal_gen:
decode_ids_list.append(decode_ids)
else:
decode_ids_list.append(decode_ids[req.send_decode_id_offset :])
decode_ids_list.append(decode_ids[req.send_decode_id_offset :])
# Exclude the tokens after stop condition
output_ids_ = req.output_ids_through_stop
@@ -1132,8 +1122,6 @@ class SchedulerOutputProcessorMixin:
# Send to detokenizer
if reqs or is_idle_batch:
if self.model_config.is_multimodal_gen:
return
self.send_to_detokenizer.send_output(
BatchTokenIDOutput(
rids=rids,
@@ -231,7 +231,7 @@ class TokenizerManager(TokenizerCommunicatorMixin, TokenizerManagerMultiItemMixi
self.served_model_name = server_args.served_model_name
self.model_config = model_config_class.from_server_args(server_args)
self.is_generation = self.model_config.is_generation
self.is_image_gen = self.model_config.is_image_gen
self.is_image_gen = getattr(self.model_config, "is_image_gen", False)
self.context_len = self.model_config.context_len
self.image_token_id = self.model_config.image_token_id
self.max_req_input_len = None # Will be set later in engine.py
@@ -1194,7 +1194,6 @@ class TokenizerManager(TokenizerCommunicatorMixin, TokenizerManagerMultiItemMixi
self.request_logger.log_finished_request(
obj,
out,
is_multimodal_gen=self.model_config.is_multimodal_gen,
request=request,
)
@@ -7,6 +7,7 @@ from abc import ABC, abstractmethod
from datetime import datetime
from typing import List, Optional, Union
from sglang.srt.constants import HEALTH_CHECK_RID_PREFIX
from sglang.srt.managers.io_struct import EmbeddingReqInput, GenerateReqInput
from sglang.srt.server_args import ServerArgs
@@ -128,7 +129,7 @@ class FileRequestMetricsExporter(RequestMetricsExporter):
self, obj: Union[GenerateReqInput, EmbeddingReqInput], out_dict: dict
):
# Do not log health check requests, since they don't represent real user requests.
if isinstance(obj.rid, str) and "HEALTH_CHECK" in obj.rid:
if isinstance(obj.rid, str) and HEALTH_CHECK_RID_PREFIX in obj.rid:
return
try:
+1 -4
View File
@@ -20,6 +20,7 @@ import asyncio
import builtins
import ctypes
import functools
import gc
import importlib
import inspect
import io
@@ -2945,8 +2946,6 @@ def configure_gc_warning(warn_threshold_secs):
def freeze_gc(context: str):
import gc
g0_before, g1_before, g2_before = gc_object_counts()
gc.freeze()
g0_after, g1_after, g2_after = gc_object_counts()
@@ -2961,8 +2960,6 @@ def freeze_gc(context: str):
def configure_gc_logger():
logger.info("Enable GC Logger")
import gc
gc_start_time = {}
def gc_callback(phase, info):
+4 -10
View File
@@ -162,7 +162,6 @@ class RequestLogger:
self,
obj: Union["GenerateReqInput", "EmbeddingReqInput"],
out: Any,
is_multimodal_gen: bool = False,
request: Optional["fastapi.Request"] = None,
) -> None:
if not self.log_requests:
@@ -181,20 +180,15 @@ class RequestLogger:
}
if headers:
log_data["headers"] = headers
if not is_multimodal_gen:
log_data["out"] = _transform_data_for_logging(
out, max_length, out_skip_names
)
log_data["out"] = _transform_data_for_logging(
out, max_length, out_skip_names
)
log_json(self.targets, "request.finished", log_data)
else:
obj_str = _dataclass_to_string_truncated(
obj, max_length, skip_names=skip_names
)
out_str = (
""
if is_multimodal_gen
else f", out={_dataclass_to_string_truncated(out, max_length, skip_names=out_skip_names)}"
)
out_str = f", out={_dataclass_to_string_truncated(out, max_length, skip_names=out_skip_names)}"
headers_str = f", headers={headers}" if headers else ""
self._log(f"Finish: obj={obj_str}{headers_str}{out_str}")
@@ -8,6 +8,7 @@ from pathlib import Path
from sglang.bench_serving import run_benchmark
from sglang.benchmark.utils import parse_custom_headers
from sglang.srt.constants import HEALTH_CHECK_RID_PREFIX
from sglang.srt.utils import kill_process_tree
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
from sglang.test.test_utils import (
@@ -80,7 +81,7 @@ class TestBenchServingFunctionality(CustomTestCase):
continue
text = obj.get("obj", {}).get("text")
rid = obj.get("rid", "")
if text and not rid.startswith("HEALTH_CHECK"):
if text and not rid.startswith(HEALTH_CHECK_RID_PREFIX):
reqs.append(text)
self.assertGreaterEqual(len(reqs), NUM_CONVERSATIONS * NUM_TURNS)
@@ -58,6 +58,7 @@ import tempfile
import unittest
from unittest.mock import MagicMock, patch
from sglang.srt.constants import HEALTH_CHECK_RID_PREFIX
from sglang.srt.observability.request_metrics_exporter import (
FileRequestMetricsExporter,
RequestMetricsExporter,
@@ -243,7 +244,7 @@ class TestFileRequestMetricsExporter(unittest.TestCase):
def test_write_record_skips_health_check(self):
exporter = self._make_exporter()
obj = _GenerateReqInput(rid="HEALTH_CHECK_123", text="ping")
obj = _GenerateReqInput(rid=f"{HEALTH_CHECK_RID_PREFIX}_123", text="ping")
asyncio.run(exporter.write_record(obj, {}))
files = os.listdir(self.tmp_dir)
+2 -1
View File
@@ -8,6 +8,7 @@ from pathlib import Path
import requests
from sglang.srt.constants import HEALTH_CHECK_RID_PREFIX
from sglang.srt.utils import kill_process_tree
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
from sglang.test.test_utils import (
@@ -197,7 +198,7 @@ class TestRequestLoggerJson(BaseTestRequestLogger, CustomTestCase):
continue
rid = data.get("rid", "")
if rid.startswith("HEALTH_CHECK"):
if rid.startswith(HEALTH_CHECK_RID_PREFIX):
continue
if data.get("event") == "request.received":