Skip to content

Add --render-endpoint to prepare_data.py for vLLM-delegated tokenization (#652)#665

Draft
WindChimeRan wants to merge 15 commits into
vllm-project:mainfrom
WindChimeRan:652-render-preprocessing
Draft

Add --render-endpoint to prepare_data.py for vLLM-delegated tokenization (#652)#665
WindChimeRan wants to merge 15 commits into
vllm-project:mainfrom
WindChimeRan:652-render-preprocessing

Conversation

@WindChimeRan

@WindChimeRan WindChimeRan commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Purpose

Adds a --render-endpoint flag to prepare_data.py that delegates tokenization to a running vLLM render server instead of doing it locally with HF apply_chat_template.

The --render-endpoint should point at the same vllm serve instance used for hidden-state extraction. The render endpoint runs on the same server with zero GPU overhead. A separate vllm launch render instance is only needed for GPU-less offline preprocessing.

vLLM prerequisite PR has been merged, bun not released yet: vllm-project/vllm#46846

When --render-endpoint is set, raw conversations are sent to POST /v1/chat/completions/render and the returned token_ids + loss_mask are used directly, eliminating the need for local chat-template rendering, assistant-pattern detection, and regex-based loss masking.

Motivation: online training already requires a vLLM instance, so tokenization should happen once on the vLLM side rather than being duplicated between HF and vLLM. See RFC #652 for discussion.

Tokenization

  • Render + server mask (template has {% generation %}): token_ids and mask both from /render → 1 tokenization, server-side, no local re-tokenization.
  • Render + regex fallback (no gen-tags): server token_ids kept as input_ids; mask built from one local decode → re-tokenize for offsets only (row dropped if it doesn't round-trip) → 2 tokenizations (1 server + 1 local, offsets-only).
  • Default HF path (no --render-endpoint, unchanged): single local apply_chat_template → 1 tokenization, no server.

Corner cases

The current renderer endpoint + fallback may not fully guarantee the correctness of some corner cases,

e.g., #659 , #645, #690 need to investigate further

Tests

Limitation:

  • [Future work] No concurrency: the render path is a sequential for loop of blocking HTTP calls.
  • The headline benefit is niche. For the common models (Qwen2.5/3/VL — no {% generation %} tags), the render path falls to the same local regex the default path uses.

Checklist

I have filled in:

  • The purpose of the PR, such as "Fix some issue (link existing issues this PR will resolve)".
  • The test plan/results, such as providing test command and pasting the results.
  • (Optional) The necessary documentation update.
  • I (a human) have written or reviewed the code in this pr to the best of my ability.

Signed-off-by: Ranran Haoran Zhang <ranzhang@redhat.com>
@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ac633738-4d6e-4c1f-a6e7-9ca6f764d23c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a vLLM render endpoint preprocessing path to the data generation pipeline. A new render_client.py module provides render_conversation for POSTing to /v1/chat/completions/render. build_dataset_from_render in preprocessing.py uses this client per dataset row, with server-provided or regex-fallback loss masks. load_and_preprocess_dataset and prepare_data.py are extended with a --render-endpoint option to select this path.

Changes

vLLM Render Endpoint Preprocessing

Layer / File(s) Summary
render_conversation client and RenderError
src/speculators/data_generation/render_client.py
New module defines DEFAULT_RENDER_TIMEOUT, RenderError, and render_conversation which POSTs to /v1/chat/completions/render, raises RenderError on non-200, and returns token_ids plus loss_mask mapped from assistant_tokens_mask.
build_dataset_from_render and load_and_preprocess_dataset extensions
src/speculators/data_generation/preprocessing.py
Adds build_dataset_from_render iterating rows, calling render_conversation, applying server-provided or regex-fallback loss mask, filtering by minimum_valid_tokens, and preserving messages for multimodal processors. Extends load_and_preprocess_dataset with render_endpoint and chat_template_kwargs parameters, conditionalizes chat-template validation, and routes to build_dataset_from_render or build_eagle3_dataset. Updates __all__ and adds httpx import.
CLI --render-endpoint argument
scripts/prepare_data.py
Adds --render-endpoint CLI argument and passes args.render_endpoint into load_and_preprocess_dataset.
Unit tests for render_client and build_dataset_from_render
tests/unit/data_generation/test_render_client.py, tests/unit/data_generation/test_build_dataset_from_render.py
Unit tests verify render_conversation token parsing, loss_mask passthrough, RenderError on HTTP failures, and request body field forwarding. build_dataset_from_render tests cover server mask passthrough, regex fallback, over-length row dropping, RenderError containment, multimodal messages column preservation, and text-only processor exclusion.
Integration tests: render truncation and multimodal alignment
tests/integration/datagen/test_render_truncation.py, tests/integration/datagen/test_render_multimodal.py
Truncation test launches a real vLLM render server and asserts token_ids from the render endpoint with truncate_prompt_tokens match HF apply_chat_template truncated output. Multimodal test verifies over-length rows are dropped and emitted samples have matching input_ids and loss_mask lengths.

Possibly related PRs

  • vllm-project/speculators#495: Modifies load_and_preprocess_dataset in preprocessing.py with multimodal/ProcessorLike refactoring that this PR's render_endpoint routing and build_dataset_from_render are built on top of.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 43.24% 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
Title check ✅ Passed The title accurately summarizes the main change: adding a render endpoint option for vLLM-delegated tokenization.
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.
Description check ✅ Passed The description matches the changeset by explaining the new render-endpoint flow, preprocessing updates, and added tests.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@mergify

mergify Bot commented Jun 26, 2026

Copy link
Copy Markdown

The quality checks have failed. Please run make style and make quality under
the root directory to address the lint failures. You will need to install the
dev optional install to get the required linting packages:
https://github.com/vllm-project/speculators/blob/main/CONTRIBUTING.md

Signed-off-by: Ranran Haoran Zhang <ranzhang@redhat.com>
Signed-off-by: Ranran Haoran Zhang <ranzhang@redhat.com>
Signed-off-by: Ranran Haoran Zhang <ranzhang@redhat.com>
Signed-off-by: Ranran Haoran Zhang <ranzhang@redhat.com>
Signed-off-by: Ranran Haoran Zhang <ranzhang@redhat.com>
@WindChimeRan WindChimeRan marked this pull request as ready for review June 30, 2026 16:02
Signed-off-by: Ranran Haoran Zhang <ranzhang@redhat.com>

@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 (5)
scripts/prepare_data.py (1)

193-206: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

chat_template_kwargs is not exposed via the CLI.

load_and_preprocess_dataset accepts chat_template_kwargs and forwards it into build_dataset_from_render (e.g. for enable_thinking), but prepare_data.py never passes it, so it is always None from the CLI. If template kwargs are meant to be usable with the render path, add a corresponding argument; otherwise this is fine to defer.

🤖 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 `@scripts/prepare_data.py` around lines 193 - 206, The CLI path in
prepare_data.py is dropping chat template options, so
load_and_preprocess_dataset never receives chat_template_kwargs even though it
forwards them to build_dataset_from_render. Expose a matching CLI argument in
the prepare_data entrypoint, pass it through the load_and_preprocess_dataset
call, and wire it into the existing argument parsing alongside the other
dataset/render options so template-specific flags like enable_thinking can be
used from the command line.
src/speculators/data_generation/preprocessing.py (1)

686-714: 🚀 Performance & Scalability | 🔵 Trivial

Serial per-row render requests will bottleneck large datasets.

Each conversation triggers a blocking render_conversation call inside a single-threaded loop, so throughput is gated by per-request latency (the PR notes the render path has no concurrency). For multi-million-row corpora this is materially slower than the parallel dataset.map(num_proc=...) default path. Consider batching or a bounded ThreadPoolExecutor over rows (the render endpoint is I/O-bound), or document the expected throughput limitation for operators.

🤖 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 `@src/speculators/data_generation/preprocessing.py` around lines 686 - 714, The
per-row rendering in the preprocessing loop is still single-threaded and will
bottleneck large datasets. Update the render path around the loop that calls
_normalize_conversation, _adapt_conv_for_vllm, and render_conversation to use
bounded concurrency (for example, a ThreadPoolExecutor) or batched requests so
multiple conversations can render in parallel without overloading the endpoint.
Keep the existing per-conversation error handling and dropped_count/log.error
behavior intact when refactoring.
tests/unit/data_generation/test_render_client.py (1)

84-86: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider asserting retry behavior is not triggered for client errors.

test_error_raises only verifies a RenderError is raised on 500. Given retries wrap this function, a test confirming the request count for a non-retriable error (e.g. 400) would lock in the intended retry policy referenced in the render_client.py review.

🤖 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 `@tests/unit/data_generation/test_render_client.py` around lines 84 - 86, The
current `test_error_raises` only checks that `_call_render` raises
`RenderError`, but it does not verify that client errors are not retried; update
this test in `test_render_client.py` to assert the retry policy explicitly by
using `_make_transport` with a non-retriable status like 400 and confirming the
request is attempted only once. Keep the existing `RenderError` assertion, and
add a request-count check around `_call_render` so the behavior of the retry
wrapper in the render client stays locked in.
tests/integration/datagen/test_render_truncation.py (1)

23-23: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Make the vLLM interpreter path configurable.

VLLM_PYTHON is a hardcoded developer-specific absolute path. The test will silently skip on any other machine via the shutil.which guard, so it effectively only runs in one environment. The sibling multimodal test already supports a RENDER_ENDPOINT env override; consider mirroring that here (e.g. read VLLM_PYTHON from the environment with this path as a fallback) so the test is portable.

♻️ Suggested change
-VLLM_PYTHON = "/home/ran/workspace/vllm/.venv/bin/python"
+VLLM_PYTHON = os.environ.get(
+    "VLLM_PYTHON", "/home/ran/workspace/vllm/.venv/bin/python"
+)
🤖 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 `@tests/integration/datagen/test_render_truncation.py` at line 23, The
hardcoded VLLM_PYTHON absolute path in test_render_truncation.py makes the test
machine-specific and can cause it to skip elsewhere; update the test setup so
VLLM_PYTHON is read from an environment override with the current path as a
fallback, similar to the sibling multimodal test’s RENDER_ENDPOINT handling, and
keep the existing shutil.which guard working against the resolved value.
tests/integration/datagen/test_render_multimodal.py (1)

35-38: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Avoid hardcoded developer path and /tmp constants.

Two portability/hygiene issues here:

  • VLLM_PYTHON is a developer-specific absolute path (same as the truncation test); prefer reading it from the environment with a fallback.
  • MEDIA_DIR/IMG_PATH hardcode /tmp (Ruff S108) and the generated image is never cleaned up. Since the image fixture is module-scoped, consider deriving the path from tmp_path_factory so the file is isolated and auto-removed.
🤖 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 `@tests/integration/datagen/test_render_multimodal.py` around lines 35 - 38,
Replace the hardcoded developer-specific runtime and temp paths in
test_render_multimodal by sourcing VLLM_PYTHON from an environment variable with
a safe fallback, and stop using fixed /tmp constants for MEDIA_DIR and IMG_PATH.
Update the multimodal test setup and any related fixture usage to derive the
image location from tmp_path_factory so the generated file is isolated per run
and cleaned up automatically, while keeping MODEL and the image fixture wiring
intact.

Source: Linters/SAST tools

🤖 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 `@src/speculators/data_generation/render_client.py`:
- Around line 49-53: The response parsing in render_client.py currently lets a
missing token_ids field raise a bare KeyError, which escapes the RenderError
handling used by build_dataset_from_render. Update the logic around resp.json()
and the return in the render client function to validate required fields and
catch malformed payloads, then re-raise any missing/invalid response data as
RenderError so the caller’s per-row exception handling can drop only the bad
row.
- Around line 44-47: The render client currently raises RenderError for every
non-OK response, which causes `@with_retries` to replay deterministic 4xx
failures. Update the request handling in render_client.py’s render flow so that
client errors (4xx) are treated as non-retriable, either by raising a distinct
exception or otherwise bypassing the retry decorator, while keeping transient
transport/5xx cases retriable. Make sure the behavior is applied in the same
place where resp.status_code is checked and RenderError is constructed.

---

Nitpick comments:
In `@scripts/prepare_data.py`:
- Around line 193-206: The CLI path in prepare_data.py is dropping chat template
options, so load_and_preprocess_dataset never receives chat_template_kwargs even
though it forwards them to build_dataset_from_render. Expose a matching CLI
argument in the prepare_data entrypoint, pass it through the
load_and_preprocess_dataset call, and wire it into the existing argument parsing
alongside the other dataset/render options so template-specific flags like
enable_thinking can be used from the command line.

In `@src/speculators/data_generation/preprocessing.py`:
- Around line 686-714: The per-row rendering in the preprocessing loop is still
single-threaded and will bottleneck large datasets. Update the render path
around the loop that calls _normalize_conversation, _adapt_conv_for_vllm, and
render_conversation to use bounded concurrency (for example, a
ThreadPoolExecutor) or batched requests so multiple conversations can render in
parallel without overloading the endpoint. Keep the existing per-conversation
error handling and dropped_count/log.error behavior intact when refactoring.

In `@tests/integration/datagen/test_render_multimodal.py`:
- Around line 35-38: Replace the hardcoded developer-specific runtime and temp
paths in test_render_multimodal by sourcing VLLM_PYTHON from an environment
variable with a safe fallback, and stop using fixed /tmp constants for MEDIA_DIR
and IMG_PATH. Update the multimodal test setup and any related fixture usage to
derive the image location from tmp_path_factory so the generated file is
isolated per run and cleaned up automatically, while keeping MODEL and the image
fixture wiring intact.

In `@tests/integration/datagen/test_render_truncation.py`:
- Line 23: The hardcoded VLLM_PYTHON absolute path in test_render_truncation.py
makes the test machine-specific and can cause it to skip elsewhere; update the
test setup so VLLM_PYTHON is read from an environment override with the current
path as a fallback, similar to the sibling multimodal test’s RENDER_ENDPOINT
handling, and keep the existing shutil.which guard working against the resolved
value.

In `@tests/unit/data_generation/test_render_client.py`:
- Around line 84-86: The current `test_error_raises` only checks that
`_call_render` raises `RenderError`, but it does not verify that client errors
are not retried; update this test in `test_render_client.py` to assert the retry
policy explicitly by using `_make_transport` with a non-retriable status like
400 and confirming the request is attempted only once. Keep the existing
`RenderError` assertion, and add a request-count check around `_call_render` so
the behavior of the retry wrapper in the render client stays locked in.
🪄 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: 20ed4253-978d-49dd-8fcb-31df9476f1f2

📥 Commits

Reviewing files that changed from the base of the PR and between 669b878 and 8b1938f.

📒 Files selected for processing (7)
  • scripts/prepare_data.py
  • src/speculators/data_generation/preprocessing.py
  • src/speculators/data_generation/render_client.py
  • tests/integration/datagen/test_render_multimodal.py
  • tests/integration/datagen/test_render_truncation.py
  • tests/unit/data_generation/test_build_dataset_from_render.py
  • tests/unit/data_generation/test_render_client.py

Comment thread src/speculators/data_generation/render_client.py
Comment thread src/speculators/data_generation/render_client.py
@WindChimeRan WindChimeRan mentioned this pull request Jun 30, 2026
10 tasks
@rahul-tuli rahul-tuli self-requested a review July 2, 2026 13:00

@rahul-tuli rahul-tuli left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thank you for working on this @WindChimeRan, the diff looks good to me pending a few small comments, could you respond and update with latest main, and I will approve the workflows to run, great job on this!

Comment thread scripts/prepare_data.py Outdated
Comment thread src/speculators/data_generation/preprocessing.py
Comment thread src/speculators/data_generation/preprocessing.py
Comment thread src/speculators/data_generation/preprocessing.py Outdated
@mergify

mergify Bot commented Jul 2, 2026

Copy link
Copy Markdown

This pull request has merge conflicts that must be resolved before it can be
merged. Please rebase the PR, @WindChimeRan.

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

@mergify mergify Bot added the needs-rebase label Jul 2, 2026
Comment thread tests/integration/datagen/test_render_multimodal.py Outdated
Comment thread src/speculators/data_generation/render_client.py
Signed-off-by: Ranran Haoran Zhang <ranzhang@redhat.com>
Signed-off-by: Ranran Haoran Zhang <ranzhang@redhat.com>
Signed-off-by: Ranran Haoran Zhang <ranzhang@redhat.com>
…ssing

# Conflicts:
#	src/speculators/data_generation/preprocessing.py
@WindChimeRan

Copy link
Copy Markdown
Contributor Author

Thanks for the review! All addressed @rahul-tuli @shanjiaz

@mergify mergify Bot removed the needs-rebase label Jul 3, 2026
Comment thread tests/integration/datagen/test_render_multimodal.py Outdated
WindChimeRan and others added 2 commits July 7, 2026 10:23
Signed-off-by: Ranran Haoran Zhang <ranzhang@redhat.com>
@rahul-tuli

Copy link
Copy Markdown
Collaborator

LGTM, pending CI!

Fixes the failing quality-checks CI job (ruff format --check).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WTsu9vFfCfPsTTE8PgfEyi
Signed-off-by: Ranran Haoran Zhang <ranranhaoranzhang@gmail.com>
@mergify mergify Bot removed the quality-failed label Jul 8, 2026
@rahul-tuli rahul-tuli enabled auto-merge (squash) July 8, 2026 13:01
@mergify

mergify Bot commented Jul 8, 2026

Copy link
Copy Markdown

This pull request has merge conflicts that must be resolved before it can be
merged. Please rebase the PR, @WindChimeRan.

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

@mergify mergify Bot added the needs-rebase label Jul 8, 2026
Resolve conflicts from RFC vllm-project#583 (vllm-project#690):
- prepare_data.py: drop duplicate token_freq_path (upstream moved it
  above the overwrite guard); keep chat_template_kwargs.
- preprocessing.py: docstring only; keep render-path wording and add
  upstream's allow_empty_output entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JkFcT8oXWTMmSWc93JuDbd
auto-merge was automatically disabled July 8, 2026 23:53

Head branch was pushed to by a user without write access

@mergify mergify Bot removed the needs-rebase label Jul 9, 2026
@orestis-z orestis-z added the ready This PR is ready for review label Jul 10, 2026
@WindChimeRan WindChimeRan marked this pull request as draft July 12, 2026 17:24
@WindChimeRan

Copy link
Copy Markdown
Contributor Author

this has design conflicts with #774

@dsikka dsikka mentioned this pull request Jul 13, 2026
2 tasks
@mergify

mergify Bot commented Jul 14, 2026

Copy link
Copy Markdown

This pull request has merge conflicts that must be resolved before it can be
merged. Please rebase the PR, @WindChimeRan.

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

@mergify mergify Bot added the needs-rebase label Jul 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-rebase ready This PR is ready for review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants