Skip to content

Commit e9a30b1

Browse files
committed
fix(vllm): validate replaced prompt length
1 parent a1a79a6 commit e9a30b1

2 files changed

Lines changed: 46 additions & 1 deletion

File tree

nemo_rl/models/generation/vllm/vllm_worker_async.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -753,6 +753,23 @@ def model_post_init(self, context):
753753
return super().model_post_init(context)
754754

755755
class NeMoRLOpenAIServingMixin:
756+
def _validate_chat_completion_prompt_token_ids(
757+
self, request, prompt_token_ids: list[int]
758+
) -> None:
759+
"""Validate the final prompt after authoritative token replacement.
760+
761+
vLLM validates the chat-template rendering before this mixin replaces
762+
its prefix with authoritative trajectory tokens. The replacement can
763+
make the actual engine prompt longer than the rendering that vLLM
764+
checked, so every chat-completion return path must validate the final
765+
token IDs again. Tokenize requests intentionally remain unrestricted.
766+
"""
767+
if isinstance(request, NeMoRLChatCompletionRequest):
768+
validate_prompt_token_count(
769+
prompt_token_count=len(prompt_token_ids),
770+
max_prompt_tokens=self.model_config.max_model_len - 1,
771+
)
772+
756773
@staticmethod
757774
def _set_max_tokens(request, max_tokens: int) -> None:
758775
"""Set the request's max output tokens.
@@ -950,12 +967,16 @@ async def preprocess_chat(
950967
not hasattr(request, "required_prefix_token_ids")
951968
or request.required_prefix_token_ids is None
952969
):
970+
final_prompt_token_ids = res[1][0]["prompt_token_ids"]
971+
self._validate_chat_completion_prompt_token_ids(
972+
request, final_prompt_token_ids
973+
)
953974
# Clamp the request's max output tokens so that input + output <= max_model_len.
954975
if actual_request_max_tokens is not None:
955976
self._clamp_max_tokens(
956977
request,
957978
actual_request_max_tokens,
958-
res[1][0]["prompt_token_ids"],
979+
final_prompt_token_ids,
959980
)
960981
return res
961982

@@ -1006,6 +1027,10 @@ async def preprocess_chat(
10061027

10071028
engine_prompt["prompt_token_ids"] = final_prompt_token_ids
10081029

1030+
self._validate_chat_completion_prompt_token_ids(
1031+
request, final_prompt_token_ids
1032+
)
1033+
10091034
# Clamp after prefix replacement since the prompt length may have changed.
10101035
if actual_request_max_tokens is not None:
10111036
self._clamp_max_tokens(

tests/unit/models/generation/test_vllm_generation.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2637,6 +2637,26 @@ async def test_vllm_http_server_correct_merged_tokens_matches_baseline(
26372637
vllm_http_server_generated_token["token"].removeprefix("token_id:")
26382638
)
26392639

2640+
# vLLM validates the shorter chat-template rendering before NeMo RL
2641+
# replaces it with the authoritative trajectory prefix. Reject an
2642+
# oversized replacement before it reaches the engine's fixed token buffer.
2643+
oversized_authoritative_prefix = [
2644+
*initial_tokenized_query_ids_prefix,
2645+
*([initial_tokenized_ids[0]] * 1024),
2646+
]
2647+
oversized_response = requests.post(
2648+
url=f"{base_urls[0]}/chat/completions",
2649+
json=body | {"required_prefix_token_ids": oversized_authoritative_prefix},
2650+
)
2651+
assert oversized_response.status_code == 400
2652+
assert "maximum context length" in oversized_response.json()["error"]["message"]
2653+
2654+
# The rejection must leave EngineCore healthy for subsequent requests.
2655+
healthy_response = requests.post(
2656+
url=f"{base_urls[0]}/chat/completions", json=body_with_reference_token_ids
2657+
)
2658+
assert healthy_response.status_code == 200
2659+
26402660
# A prompt tokenized by this server can be reused directly by the next
26412661
# generation request. The chat conversation is still rendered for response
26422662
# and tool parsing, but prompt encoding is bypassed.

0 commit comments

Comments
 (0)