From db7e6807dea92e11ac048eafe0d6e6daf0ab3bc5 Mon Sep 17 00:00:00 2001 From: hjzhang <499213894@qq.com> Date: Thu, 16 Jul 2026 16:29:46 +0800 Subject: [PATCH] [BugFix] Preserve tokenizer worker fanout when `skip_tokenizer_init` is enabled (#30682) Co-authored-by: hjzhang Co-authored-by: hnyls2002 --- .../scheduler_components/ipc_channels.py | 3 ++- python/sglang/srt/managers/tokenizer_manager.py | 2 ++ python/sglang/srt/server_args.py | 12 ++++-------- .../tokenizer/test_skip_tokenizer_init.py | 13 +++++++++++-- .../unit/server_args/test_server_args.py | 16 ++++++++++++++++ 5 files changed, 35 insertions(+), 11 deletions(-) diff --git a/python/sglang/srt/managers/scheduler_components/ipc_channels.py b/python/sglang/srt/managers/scheduler_components/ipc_channels.py index 43da8ccf6..fdddc345e 100644 --- a/python/sglang/srt/managers/scheduler_components/ipc_channels.py +++ b/python/sglang/srt/managers/scheduler_components/ipc_channels.py @@ -53,7 +53,8 @@ class SchedulerIpcChannels: context, zmq.PUSH, port_args.tokenizer_ipc_name, False ) if skip_tokenizer_init: - # Directly send to the TokenizerManager + # No decode work: send outputs straight to the tokenizer side + # (MultiTokenizerRouter fans out when tokenizer_worker_num > 1). send_to_detokenizer_raw = get_zmq_socket( context, zmq.PUSH, port_args.tokenizer_ipc_name, False ) diff --git a/python/sglang/srt/managers/tokenizer_manager.py b/python/sglang/srt/managers/tokenizer_manager.py index 6d066b198..f96660fb0 100644 --- a/python/sglang/srt/managers/tokenizer_manager.py +++ b/python/sglang/srt/managers/tokenizer_manager.py @@ -601,6 +601,8 @@ class TokenizerManager(TokenizerControlMixin, TokenizerManagerScoreMixin): (FreezeGCReq, lambda x: None), # For handling case when scheduler skips detokenizer and forwards back to the tokenizer manager, we ignore it. (HealthCheckOutput, lambda x: None), + # Same skip-detokenizer forwarding case as above. + (ConfigureLoggingReq, lambda x: None), (ActiveRanksOutput, self.update_active_ranks), ] ) diff --git a/python/sglang/srt/server_args.py b/python/sglang/srt/server_args.py index 90f29b841..0e7532857 100644 --- a/python/sglang/srt/server_args.py +++ b/python/sglang/srt/server_args.py @@ -6089,16 +6089,12 @@ class ServerArgs: ) if self.skip_tokenizer_init: - if self.tokenizer_worker_num != 1: - logger.warning( - "skip_tokenizer_init=True disables tokenizer workers; forcing tokenizer_worker_num=1 " - f"(requested {self.tokenizer_worker_num})." - ) - self.tokenizer_worker_num = 1 + # Tokenizer workers still serve HTTP / state / output work, so + # their fanout is preserved; detokenizer workers only decode. if self.detokenizer_worker_num != 1: logger.warning( - "skip_tokenizer_init=True disables detokenizer workers; forcing detokenizer_worker_num=1 " - f"(requested {self.detokenizer_worker_num})." + "skip_tokenizer_init=True leaves no decode work for detokenizer workers; " + f"forcing detokenizer_worker_num=1 (requested {self.detokenizer_worker_num})." ) self.detokenizer_worker_num = 1 diff --git a/test/registered/tokenizer/test_skip_tokenizer_init.py b/test/registered/tokenizer/test_skip_tokenizer_init.py index 72802bedc..babf1dec5 100644 --- a/test/registered/tokenizer/test_skip_tokenizer_init.py +++ b/test/registered/tokenizer/test_skip_tokenizer_init.py @@ -36,7 +36,12 @@ class TestSkipTokenizerInit(CustomTestCase): cls.model, cls.base_url, timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH, - other_args=["--skip-tokenizer-init", "--incremental-streaming-output"], + other_args=[ + "--skip-tokenizer-init", + "--incremental-streaming-output", + "--tokenizer-worker-num", + "4", + ], ) cls.eos_token_id = [119690] cls.tokenizer = AutoTokenizer.from_pretrained( @@ -216,7 +221,11 @@ class TestSkipTokenizerInitVLM(TestSkipTokenizerInit): cls.model, cls.base_url, timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH, - other_args=["--skip-tokenizer-init"], + other_args=[ + "--skip-tokenizer-init", + "--tokenizer-worker-num", + "4", + ], ) cls.eos_token_id = [cls.tokenizer.eos_token_id] diff --git a/test/registered/unit/server_args/test_server_args.py b/test/registered/unit/server_args/test_server_args.py index 353efb421..3367a2147 100644 --- a/test/registered/unit/server_args/test_server_args.py +++ b/test/registered/unit/server_args/test_server_args.py @@ -232,6 +232,22 @@ class TestLoadBalanceMethod(unittest.TestCase): self.assertEqual(server_args.disaggregation_transfer_backend, "mooncake") +class TestSkipTokenizerInit(unittest.TestCase): + def test_skip_tokenizer_worker_counts(self): + server_args = ServerArgs( + model_path="dummy", + skip_tokenizer_init=True, + tokenizer_worker_num=4, + detokenizer_worker_num=3, + ) + + server_args._handle_tokenizer_batching() + + # Tokenizer fanout preserved; detokenizer coerced to 1 (no decode work). + self.assertEqual(server_args.tokenizer_worker_num, 4) + self.assertEqual(server_args.detokenizer_worker_num, 1) + + class TestHiSparseDsaBackendPolicy(unittest.TestCase): # The backend selection moved to the resolution pipeline; these policy # tests drive the pass through its read-only view.