Skip to content

Commit 125a636

Browse files
authored
Fix chat endpoint support for Ray in-framework MBridge deployment (#572)
Signed-off-by: Abhishree <abhishreetm@gmail.com>
1 parent 3c261f6 commit 125a636

3 files changed

Lines changed: 42 additions & 32 deletions

File tree

‎nemo_deploy/llm/megatronllm_deployable.py‎

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -205,12 +205,12 @@ def apply_chat_template(self, messages, add_generation_prompt=True):
205205
Works when model's tokenizer has chat template (typically chat models).
206206
"""
207207
try:
208-
tokenizer_chat_template = self.mcore_tokenizer.tokenizer.tokenizer.chat_template
208+
tokenizer_chat_template = self.mcore_tokenizer._tokenizer.chat_template
209209

210-
# Try to get bos_token, handle different tokenizer types
210+
# Try to get bos_token
211211
bos_token = None
212212
try:
213-
bos_token = self.mcore_tokenizer.tokenizer.tokenizer.bos_token
213+
bos_token = self.mcore_tokenizer._tokenizer.bos_token
214214
except AttributeError:
215215
# Some tokenizers might not have bos_token, use empty string as fallback
216216
bos_token = ""
@@ -223,11 +223,11 @@ def apply_chat_template(self, messages, add_generation_prompt=True):
223223
)
224224

225225
template = Template(tokenizer_chat_template)
226-
except AttributeError:
226+
except AttributeError as e:
227227
# If the tokenizer does not have chat_template
228228
raise ValueError(
229-
"The tokenizer does not have chat template, if you would like to evaluate chat model \
230-
ensure your model's tokenizer has a chat template"
229+
f"The tokenizer does not have chat template ({e}), if you would like to evaluate chat model "
230+
"ensure your model's tokenizer has a chat template"
231231
)
232232
# Render the template with the provided messages
233233
rendered_output = template.render(
@@ -240,14 +240,14 @@ def apply_chat_template(self, messages, add_generation_prompt=True):
240240

241241
def remove_eos_token(self, text):
242242
"""Removes eos token if it exists in the output, otherwise does nothing."""
243-
# Handle different tokenizer types
243+
# MBridge tokenizer: access underlying HF tokenizer via _tokenizer
244244
try:
245-
eos_token = self.mcore_tokenizer.tokenizer.tokenizer.eos_token
245+
eos_token = self.mcore_tokenizer._tokenizer.eos_token
246246
except AttributeError:
247247
# Fallback for TiktokenTokenizer and similar tokenizers
248248
try:
249-
eos_id = self.mcore_tokenizer.tokenizer.tokenizer.eos_id
250-
eos_token = self.mcore_tokenizer.tokenizer.tokenizer.special_tokens[eos_id]
249+
eos_id = self.mcore_tokenizer._tokenizer.eos_id
250+
eos_token = self.mcore_tokenizer._tokenizer.inv_vocab[eos_id]
251251
except AttributeError:
252252
# If neither approach works, return text unchanged
253253
return text

‎nemo_deploy/llm/megatronllm_deployable_ray.py‎

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -352,15 +352,25 @@ async def chat_completions(self, request: Dict[Any, Any]):
352352
# Extract parameters from the request dictionary
353353
messages = request.get("messages", [])
354354

355+
# Get sampling parameters
356+
temperature = request.get("temperature", 0.0)
357+
top_p = request.get("top_p", 0.0)
358+
top_k = request.get("top_k", 0)
359+
360+
# Greedy sampling check: when both temperature and top_p are 0, set top_k to 1
361+
if temperature == 0.0 and top_p == 0.0:
362+
LOGGER.warning("Both temperature and top_p are 0. Setting top_k to 1 to ensure greedy sampling.")
363+
top_k = 1
364+
355365
# Prepare inference parameters
356366
# For chat templates, we need to pass the entire messages list as a single prompt
357367
# so that apply_chat_template receives the full conversation context
358368
inference_inputs = {
359-
"prompts": [messages], # Wrap messages in a list so apply_chat_template gets the full conversation
360-
"max_length": request.get("max_tokens", 256),
361-
"temperature": request.get("temperature", 1.0),
362-
"top_k": request.get("top_k", 0),
363-
"top_p": request.get("top_p", 0.0),
369+
"prompts": [messages],
370+
"max_length": request.get("max_tokens", 512),
371+
"temperature": temperature,
372+
"top_k": top_k,
373+
"top_p": top_p,
364374
"compute_logprob": True if request.get("logprobs") == 1 else False,
365375
"apply_chat_template": request.get("apply_chat_template", True),
366376
}
@@ -370,7 +380,6 @@ async def chat_completions(self, request: Dict[Any, Any]):
370380

371381
# Extract generated texts from results
372382
generated_texts = results["sentences"]
373-
374383
# Calculate token counts
375384
prompt_tokens = sum(len(str(msg).split()) for msg in messages)
376385
completion_tokens = sum(len(r.split()) for r in generated_texts)

‎tests/unit_tests/deploy/test_megatronllm_deployable.py‎

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,11 @@ def mock_engine_and_tokenizer():
2828
mock_engine = MagicMock()
2929
mock_model = MagicMock()
3030
mock_tokenizer = MagicMock()
31-
mock_tokenizer.tokenizer.tokenizer = MagicMock()
32-
mock_tokenizer.tokenizer.tokenizer.chat_template = "{{messages}}"
33-
mock_tokenizer.tokenizer.tokenizer.bos_token = "<bos>"
34-
mock_tokenizer.tokenizer.tokenizer.eos_token = "<eos>"
31+
# MBridge tokenizer uses _tokenizer to access the underlying HF tokenizer
32+
mock_tokenizer._tokenizer = MagicMock()
33+
mock_tokenizer._tokenizer.chat_template = "{{messages}}"
34+
mock_tokenizer._tokenizer.bos_token = "<bos>"
35+
mock_tokenizer._tokenizer.eos_token = "<eos>"
3536

3637
return mock_engine, mock_model, mock_tokenizer
3738

@@ -70,7 +71,7 @@ def test_dict_to_str():
7071
@pytest.mark.run_only_on("GPU")
7172
def test_apply_chat_template_none_template(deployable):
7273
"""Test chat template application when tokenizer has no chat template."""
73-
deployable.mcore_tokenizer.tokenizer.tokenizer.chat_template = None
74+
deployable.mcore_tokenizer._tokenizer.chat_template = None
7475
messages = [{"role": "user", "content": "Hello"}]
7576

7677
with pytest.raises(ValueError, match="The tokenizer does not have a chat template defined"):
@@ -81,7 +82,7 @@ def test_apply_chat_template_none_template(deployable):
8182
def test_apply_chat_template_attribute_error(deployable):
8283
"""Test chat template application when tokenizer raises AttributeError."""
8384
# Remove the chat_template attribute to trigger AttributeError
84-
del deployable.mcore_tokenizer.tokenizer.tokenizer.chat_template
85+
del deployable.mcore_tokenizer._tokenizer.chat_template
8586
messages = [{"role": "user", "content": "Hello"}]
8687

8788
with pytest.raises(ValueError, match="The tokenizer does not have chat template"):
@@ -441,7 +442,7 @@ def test_apply_chat_template(deployable):
441442
@pytest.mark.run_only_on("GPU")
442443
def test_remove_eos_token(deployable):
443444
"""Test EOS token removal covering all code paths."""
444-
# Test normal case - eos_token accessible via self.mcore_tokenizer.tokenizer.tokenizer.eos_token
445+
# Test normal case - eos_token accessible via self.mcore_tokenizer._tokenizer.eos_token
445446
texts = ["Hello<eos>", "World", "Test<eos>"]
446447
cleaned_texts = deployable.remove_eos_token(texts)
447448
assert cleaned_texts == ["Hello", "World", "Test"]
@@ -452,7 +453,7 @@ def test_remove_eos_token_comprehensive(deployable):
452453
"""Test remove_eos_token method covering all code paths and edge cases."""
453454

454455
# Test case 1: Normal case - eos_token accessible
455-
deployable.mcore_tokenizer.tokenizer.tokenizer.eos_token = "<eos>"
456+
deployable.mcore_tokenizer._tokenizer.eos_token = "<eos>"
456457
texts = ["Hello<eos>", "World", "Test<eos>"]
457458
cleaned_texts = deployable.remove_eos_token(texts)
458459
assert cleaned_texts == ["Hello", "World", "Test"]
@@ -477,10 +478,10 @@ def test_remove_eos_token_comprehensive(deployable):
477478
cleaned_texts = deployable.remove_eos_token(texts)
478479
assert cleaned_texts == ["", "Hello", ""]
479480

480-
# Test case 6: Fallback case 1 - AttributeError on eos_token, but eos_id and special_tokens work
481-
del deployable.mcore_tokenizer.tokenizer.tokenizer.eos_token
482-
deployable.mcore_tokenizer.tokenizer.tokenizer.eos_id = 2
483-
deployable.mcore_tokenizer.tokenizer.tokenizer.special_tokens = {2: "</s>"}
481+
# Test case 6: Fallback case 1 - AttributeError on eos_token, but eos_id and inv_vocab work
482+
del deployable.mcore_tokenizer._tokenizer.eos_token
483+
deployable.mcore_tokenizer._tokenizer.eos_id = 2
484+
deployable.mcore_tokenizer._tokenizer.inv_vocab = {2: "</s>"}
484485

485486
texts = ["Hello</s>", "World", "Test</s>"]
486487
cleaned_texts = deployable.remove_eos_token(texts)
@@ -492,16 +493,16 @@ def test_remove_eos_token_comprehensive(deployable):
492493
assert cleaned_texts == ["Hello</s>World"]
493494

494495
# Test case 8: Fallback case 2 - Both AttributeErrors, return unchanged
495-
del deployable.mcore_tokenizer.tokenizer.tokenizer.eos_id
496+
del deployable.mcore_tokenizer._tokenizer.eos_id
496497

497498
texts = ["Hello<eos>", "World</s>", "Test"]
498499
cleaned_texts = deployable.remove_eos_token(texts)
499500
assert cleaned_texts == ["Hello<eos>", "World</s>", "Test"] # Should return unchanged
500501

501502
# Test case 9: Fallback case 2 - AttributeError on eos_id but not on eos_token access
502-
# First delete the special_tokens to trigger AttributeError in second try block
503-
deployable.mcore_tokenizer.tokenizer.tokenizer.eos_id = 2
504-
del deployable.mcore_tokenizer.tokenizer.tokenizer.special_tokens
503+
# First delete the inv_vocab to trigger AttributeError in second try block
504+
deployable.mcore_tokenizer._tokenizer.eos_id = 2
505+
del deployable.mcore_tokenizer._tokenizer.inv_vocab
505506

506507
texts = ["Hello<eos>", "World"]
507508
cleaned_texts = deployable.remove_eos_token(texts)

0 commit comments

Comments
 (0)