Explainer for NVIDIA-NeMo/RL #4634.
RL permalinks pin the PR head 5b6fa89; vLLM links pin v0.29.0, the version RL uses.
None instead of building those objects. Everything that is saved or sent stays the same.Background. During Gym rollouts, RL's vLLM server records the exact token IDs and log-probabilities of each model call. This is token capture. It writes them to a shared store (it stages them), then removes them from the HTTP reply to Gym. Dotted words have a short explanation: hover or tap them.
vLLM's chat handler formats the reply in chat_completion_full_generator.
For each output it calls self._create_chat_logprobs(...),
which builds one object per generated token. For a 32k-token answer, that is 32k objects, built on the server's event loop.
Capture never reads them. RL attaches the raw arrays from the engine output
(utils.py:470),
and Gym's capture code reads those message fields first (adapter L34-42).
Then RL deletes the objects (vllm_worker_async.py:880).
The two paths join again at step 5. From there on, nothing differs: the same arrays are staged, and
Gym gets the same reply. The only change a client could see is step 4. That payload never leaves the
server, because step 6 removes logprobs before sending.
Where it lands: RL only, one new module plus a gate in the worker.
captured_chat_logprobs.py overrides vLLM's
_create_chat_logprobs; vllm_worker_async.py decides when.
No Gym or vLLM change: Gym already reads the message fields first, and vLLM already accepts a None there (protocol.py:106).
@@ vllm_worker_async.py base L1245-L1250 โ head L1251-L1263 ยท NeMoRLOpenAIServingChatMixin.chat_completion_full_generator - response = await super().chat_completion_full_generator( - request, - capture_result_generator(), - *args, - **kwargs, - ) + bypass_logprobs = bool( + not request.stream + and request.logprobs + and return_as_token_id + and id(request) in worker_self._capture_calls + ) + with self._bypass_chat_logprobs(enabled=bypass_logprobs): + response = await super().chat_completion_full_generator( + request, + capture_result_generator(), + *args, + **kwargs, + ) ... if request.logprobs and return_as_token_id: response = attach_token_information_to_chat_response_choices(
| Condition | Why it is needed |
|---|---|
not request.stream | Streaming replies use a different vLLM function, so they are never touched. |
request.logprobs and return_as_token_id | The same test as the attach step below it, so the raw arrays are always attached when the objects are skipped. |
id(request) in _capture_calls | Only requests that capture accepted. Their logprobs are always deleted at step 6. |
The on/off value is kept per server object, asyncio task and thread. So a second request on the same server, a child task, or a call from a worker thread still builds logprobs normally. The PR's tests check each of these.
_create_chat_logprobs
from the full generator, the speedup turns off with no error. The review asks for a one-line test to catch that.
๐ฌ review comment โ test_vllm_captured_chat_logprobs.py:122