[BugFix] Preserve tokenizer worker fanout when skip_tokenizer_init is enabled (#30682)

Co-authored-by: hjzhang <zhanghjzzz@qq.com>
Co-authored-by: hnyls2002 <lsyincs@gmail.com>
This commit is contained in:
hjzhang
2026-07-16 01:29:46 -07:00
committed by GitHub
co-authored by hjzhang hnyls2002
parent 4d60c4540c
commit db7e6807de
5 changed files with 35 additions and 11 deletions
@@ -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
)
@@ -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),
]
)
+4 -8
View File
@@ -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
@@ -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]
@@ -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.