Skip to content

feat(RL): add vLLM Tokens-in-Tokens-Out and RL related response support - #9651

Merged
biswapanda merged 25 commits into
mainfrom
bis/nvext-tito-vllm
Jun 6, 2026
Merged

feat(RL): add vLLM Tokens-in-Tokens-Out and RL related response support#9651
biswapanda merged 25 commits into
mainfrom
bis/nvext-tito-vllm

Conversation

@biswapanda

@biswapanda biswapanda commented May 16, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add vLLM worker-side support for nvext Tokens-in-Tokens-Out requests
  • forward RL/TITO flags through the vLLM CLI/runtime config
  • preserve nvext.cache_salt on both decode and disaggregated prefill prompts
  • expose requested nvext.engine_data, completion token IDs/logprobs, prompt logprobs, and related TITO parity fields
  • add focused vLLM parity/unit coverage for cache salt, generation defaults, engine data, logprob flattening, and skip-special-token behavior

Stack

Where To Review

  • components/src/dynamo/vllm/handlers.py
  • components/src/dynamo/vllm/tests/test_vllm_tito_parity.py
  • components/src/dynamo/vllm/args.py
  • components/src/dynamo/vllm/backend_args.py
  • components/src/dynamo/vllm/main.py

Local Tests

  • python -m py_compile components/src/dynamo/vllm/handlers.py components/src/dynamo/vllm/tests/test_vllm_tito_parity.py
  • PYTHONPATH=components/src uv run --no-project --with pytest --with pytest-benchmark python -m pytest -c /dev/null components/src/dynamo/vllm/tests/test_vllm_tito_parity.py -q
  • git diff --check

Notes

After #9649 lands, this PR can be retargeted to main or rebased so the diff remains limited to the vLLM files.

supersedes #9382

Summary by CodeRabbit

Release Notes

New Features

  • Added --enable-rl configuration option for reinforcement learning-style defaults.
  • Enhanced nvext request field support including cache_salt and engine_data accumulation.
  • Added support for bad_words_token_ids and skip_special_tokens sampling parameters.
  • Improved prompt logprobs serialization and accumulation in streaming responses.

Tests

  • Added comprehensive test coverage for logprobs handling and configuration options.

Review Change Stack

@biswapanda
biswapanda requested review from a team as code owners May 16, 2026 00:18
@biswapanda biswapanda self-assigned this May 16, 2026
@github-actions github-actions Bot added feat backend::vllm Relates to the vllm backend labels May 16, 2026
@biswapanda biswapanda changed the title feat(vllm): add nvext tokens-in-tokens-out support feat(RL): add vllm tokens-in-tokens-out support May 16, 2026
@biswapanda biswapanda changed the title feat(RL): add vllm tokens-in-tokens-out support feat(vllm): add nvext Tokens-in-Tokens-Out support May 16, 2026
Comment thread components/src/dynamo/vllm/handlers.py Outdated
@biswapanda

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 16, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented May 16, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

This PR implements RL-mode support with conditional logprobs-mode switching and nvext-aware token handling. It tracks explicit --logprobs-mode flags, adds an --enable-rl backend option that conditionally defaults logprobs mode, introduces nvext utilities for prompt cache_salt injection and engine data accumulation, and updates sampling parameter construction for token-in requests and output options.

Changes

vLLM RL mode and nvext token handling

Layer / File(s) Summary
Explicit logprobs-mode flag tracking
components/src/dynamo/vllm/args.py
Adds sys import and new Config.logprobs_mode_explicitly_set flag. Introduces _arg_was_provided() helper to detect --logprobs-mode in argv (both --option and --option=... forms).
RL enablement option
components/src/dynamo/vllm/backend_args.py
Adds --enable-rl CLI argument with DYN_ENABLE_RL env var and DynamoVllmConfig.enable_rl configuration field.
RL logprobs-mode conditional defaulting
components/src/dynamo/vllm/main.py
Implements configure_rl_logprobs_mode() that switches logprobs mode to processed_logprobs when RL is enabled and mode was not explicitly set. Hooks into worker startup.
nvext helper functions
components/src/dynamo/vllm/handlers.py (lines 296–461)
Adds utilities for serializing prompt logprobs to Rust/serde-compatible format, applying nvext cache_salt to prompts, flattening logprobs, accumulating engine data per token, and detecting token-in requests.
Sampling params and output options handling
components/src/dynamo/vllm/handlers.py (lines 500–604)
Updates build_sampling_params to use raw vLLM defaults for token-in requests, special-cases bad_words_token_ids, and adds skip_special_tokens mapping from request output options.
Token generation and engine_data integration
components/src/dynamo/vllm/handlers.py (lines 2360–2839)
Wires nvext helpers into decode and prefill flows: emits serialized prompt_logprobs on completion, applies cache_salt to constructed prompts, and accumulates per-chunk token ids/logprobs into final emitted chunks when engine_data is requested.
Configuration and RL mode unit tests
components/src/dynamo/vllm/tests/test_vllm_unit.py (lines 333–373)
Tests explicit flag detection, RL logprobs-mode defaulting when not explicitly set, and CLI parsing that --logprobs-mode sets the explicit flag.
nvext handler utility tests
components/src/dynamo/vllm/tests/test_vllm_tito_parity.py
Comprehensive test suite with six test classes covering prompt logprobs serialization, cache_salt wiring, token-in sampling defaults, logprobs flattening, engine_data accumulation, and skip_special_tokens handling with regressions.

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 41.30% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and specifically describes the main change: adding vLLM support for Tokens-in-Tokens-Out (TITO) and RL-related response features, which aligns with the primary objectives across all modified files.
Description check ✅ Passed The PR description covers overview (summary of changes), detailed sections for each component, specific files to review, and local test commands, mostly aligning with the template structure.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
components/src/dynamo/vllm/tests/test_vllm_unit.py (1)

361-372: ⚡ Quick win

Add coverage for --logprobs-mode=<value> flag form.

_arg_was_provided() handles both split and = forms; this test currently exercises only split args. Covering both prevents regressions in explicit-flag tracking.

✅ Suggested test tweak
-def test_logprobs_mode_flag_is_tracked(mock_vllm_cli):
-    mock_vllm_cli(
-        "--model",
-        "Qwen/Qwen3-0.6B",
-        "--logprobs-mode",
-        "raw_logprobs",
-    )
-
-    config = parse_args()
-
-    assert config.logprobs_mode_explicitly_set is True
+@pytest.mark.parametrize(
+    "logprobs_args",
+    [
+        ("--logprobs-mode", "raw_logprobs"),
+        ("--logprobs-mode=raw_logprobs",),
+    ],
+)
+def test_logprobs_mode_flag_is_tracked(mock_vllm_cli, logprobs_args):
+    mock_vllm_cli(
+        "--model",
+        "Qwen/Qwen3-0.6B",
+        *logprobs_args,
+    )
+
+    config = parse_args()
+
+    assert config.logprobs_mode_explicitly_set is True
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/src/dynamo/vllm/tests/test_vllm_unit.py` around lines 361 - 372,
The test test_logprobs_mode_flag_is_tracked currently only exercises the
split-argument form; add a complementary test (or extend this one) that calls
mock_vllm_cli with the equals form (e.g., "--logprobs-mode=raw_logprobs"), then
call parse_args() and assert config.logprobs_mode_explicitly_set is True to
ensure _arg_was_provided() correctly recognizes the '=' form as explicitly set.
components/src/dynamo/vllm/handlers.py (1)

398-406: ⚡ Quick win

Avoid quadratic flattening in _flatten_logprobs.

pending.pop(0) plus pending[0:0] = item makes this O(n²). On long RL/TITO logprob payloads this helper can become a hot path for no real benefit.

♻️ Suggested change
-    pending = list(log_probs)
+    pending = list(reversed(log_probs))
     while pending:
-        item = pending.pop(0)
+        item = pending.pop()
         if isinstance(item, (int, float)):
             out.append(float(item))
         elif isinstance(item, list):
-            pending[0:0] = item
+            pending.extend(reversed(item))
         elif isinstance(item, dict) and "logprob" in item:
             try:
                 out.append(float(item["logprob"]))
             except (TypeError, ValueError):
                 continue
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/src/dynamo/vllm/handlers.py` around lines 398 - 406, Replace the
quadratic list-front-insert pattern in _flatten_logprobs: instead of using
pending as a list with pending.pop(0) and pending[0:0] = item (which causes
O(n^2)), make pending a collections.deque (initialized from log_probs), use
pending.popleft() instead of pop(0), and when encountering a list item use
pending.extendleft(reversed(item)) to prepend the sublist efficiently; keep the
same handling for numeric types and dicts with "logprob" and append floats to
out as before.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@components/src/dynamo/vllm/handlers.py`:
- Around line 2616-2623: The current code sets request_prompt_token_ids from
request.get("token_ids"), which is incorrect for the pre-rendered multimodal
path; change the assignment so that when want_engine_data is true you prefer the
built prompt's token ids (the expanded sequence's prompt["prompt_token_ids"])
and only fall back to request.get("token_ids") otherwise; update the logic that
populates request_prompt_token_ids (the variable used to populate
engine_data.prompt_token_ids) so it reads from prompt["prompt_token_ids"] when
present to ensure engine_data echoes the actual prompt the engine consumed.

In `@components/src/dynamo/vllm/tests/test_vllm_tito_parity.py`:
- Around line 13-22: The pytest module-level marker list pytestmark (which
already includes pytest.mark.vllm) is missing the required single component
marker; update the pytestmark list in test_vllm_tito_parity.py to include
exactly one component marker (choose one of pytest.mark.multimodal,
pytest.mark.router, pytest.mark.kvbm, or pytest.mark.core) alongside the
existing markers so the module has a framework marker (vllm) and exactly one
component marker.

---

Nitpick comments:
In `@components/src/dynamo/vllm/handlers.py`:
- Around line 398-406: Replace the quadratic list-front-insert pattern in
_flatten_logprobs: instead of using pending as a list with pending.pop(0) and
pending[0:0] = item (which causes O(n^2)), make pending a collections.deque
(initialized from log_probs), use pending.popleft() instead of pop(0), and when
encountering a list item use pending.extendleft(reversed(item)) to prepend the
sublist efficiently; keep the same handling for numeric types and dicts with
"logprob" and append floats to out as before.

In `@components/src/dynamo/vllm/tests/test_vllm_unit.py`:
- Around line 361-372: The test test_logprobs_mode_flag_is_tracked currently
only exercises the split-argument form; add a complementary test (or extend this
one) that calls mock_vllm_cli with the equals form (e.g.,
"--logprobs-mode=raw_logprobs"), then call parse_args() and assert
config.logprobs_mode_explicitly_set is True to ensure _arg_was_provided()
correctly recognizes the '=' form as explicitly set.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: cfb6dcd4-cd82-48c4-aed4-79646146e466

📥 Commits

Reviewing files that changed from the base of the PR and between a711882 and 0b30947.

📒 Files selected for processing (6)
  • components/src/dynamo/vllm/args.py
  • components/src/dynamo/vllm/backend_args.py
  • components/src/dynamo/vllm/handlers.py
  • components/src/dynamo/vllm/main.py
  • components/src/dynamo/vllm/tests/test_vllm_tito_parity.py
  • components/src/dynamo/vllm/tests/test_vllm_unit.py

Comment thread components/src/dynamo/vllm/handlers.py
Comment thread components/src/dynamo/vllm/tests/test_vllm_tito_parity.py
@biswapanda biswapanda changed the title feat(vllm): add nvext Tokens-in-Tokens-Out support feat(RL): add vllm Tokens-in-Tokens-Out support May 16, 2026
@biswapanda biswapanda changed the title feat(RL): add vllm Tokens-in-Tokens-Out support feat(RL): add vLLM Tokens-in-Tokens-Out and RL related response support May 16, 2026
Comment thread components/src/dynamo/vllm/args.py Outdated
@biswapanda
biswapanda force-pushed the bis/nvext-tito-rl branch from 565fd7c to 2256c2a Compare May 16, 2026 01:12
@biswapanda
biswapanda requested a review from a team May 16, 2026 01:12
@biswapanda
biswapanda force-pushed the bis/nvext-tito-vllm branch from 8cec894 to a836fc9 Compare May 16, 2026 01:13
Comment thread components/src/dynamo/vllm/args.py Outdated
Emit dtype alongside data/shape so the consumer decodes the raw base85 bytes
with the correct element type instead of assuming int32 (KrishnanPrash, codex).
detokenize is already forced False unconditionally at the end of the function;
the earlier duplicate assignment was dead.
build_sampling_params forces detokenize=False, so vLLM never reads
skip_special_tokens; Dynamo detokenizes in the Rust backend, which reads it
from the request output_options (backend.rs). Replace the no-op assignment with
a NOTE and update the tests to assert it is not forwarded (KrishnanPrash).
Capture the raw routed_experts per output index during streaming and base85-
encode it once on the finish chunk, instead of serializing on every chunk and
discarding all but the last. Non-final chunks now only do a getattr.
Skip a non-int prompt-logprob token-id key instead of aborting the whole
prompt_logprobs payload (kthui), and drop a malformed completion token id
instead of killing the generation stream, in _accumulate_engine_data.
The two metadata/engine_data extra_fields tests ran with enable_rl=False, which
short-circuits the RL branch so they validated nothing about the gate. Run them
with enable_rl=True so they confirm metadata-only requests are not treated as
token-in even when RL is active.
Lock the -inf/nan -> finite sentinel behavior in _finite_logprob,
_flatten_logprobs (incl. bool drop), and _serialize_prompt_logprobs, and assert
the sentinel is JSON-safe so the serde_json transport never nulls it.
@biswapanda

Copy link
Copy Markdown
Contributor Author

rebase on top of latest main

dict(out.get('disaggregated_params') or {}) tripped mypy's arg-type check
(the value union includes list/int). Narrow with isinstance before dict(),
which is also runtime-safer. Fixes the dynamo-runtime/mypy CI failure.
prefill_result.get('disaggregated_params', {}) returns None (not {}) when the
key is present-but-None, which prefill produces on its error path and when
_build_disaggregated_params returns None for empty params. The chained .get()
then raised AttributeError. Use 'or {}' so missing / None / empty all degrade
to an empty mapping.
VllmLLMEngine.generate (unified path) had the same prefill_result
.get('disaggregated_params', {}).get(...) trap as the legacy handler: a
present-but-None value -> AttributeError. Use 'or {}' so it falls through to
the existing kv_params ValueError instead.
Absorbed from the RL worker-admin stack so the binding lives in the base PR.
Workers use it for their RL request-plane route descriptor instead of deriving
the system URL from static env vars. Uses ip_resolver::local_ip_for_advertise
(renamed from get_local_ip_for_advertise on current main).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend::vllm Relates to the vllm backend feat frontend `python -m dynamo.frontend` and `dynamo-run in=http|text|grpc` size/XXL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants