Skip to content

Harden the token-space media rebuild: per-run media, dtype cast, prefill prefix, startup checks - #33

Open
celve wants to merge 3 commits into
rockdu/vlm-02-token-spacefrom
LIN-2519/harden-media-rebuild
Open

celve wants to merge 3 commits into
rockdu/vlm-02-token-spacefrom
LIN-2519/harden-media-rebuild

Conversation

@celve

@celve celve commented Nov 1, 2026 •

Copy link
Copy Markdown
Owner

Layer P15 of the CUA upstreaming stack (the Rockdu sync). It is built and reviewed in celve, and goes upstream with the whole stack once every layer is finished. Depends on radixark/miles radixark#4272 (Kangrui Du / Rockdu, branch rockdu/vlm-02-token-space at f9e7a9f645 in radixark/miles, stacked on radixark#4240 rockdu/vlm-01-media-refs at 3080d5fb1c), which we expect to land. This PR builds on his live head and carries none of his commits. Its celve base is a ref-for-ref mirror of his radixark branch, needed only because a celve PR can't use a radixark branch as base. The mirror is never edited, and it is re-synced, with this stack rebased, whenever radixark#4272 moves. Upstream, this PR opens against radixark's rockdu/vlm-02-token-space and is retargeted to main when radixark#4272 merges. It follows his 2026-10-29 SGLang token-space API. Issue: LIN-2519, hub LIN-2504.

These are the CUA fixes we ran on top of a backport of his stack (#11), ported onto his current code. There are three commits, one per concern.

Problem.

  1. Media written under --shared-media-dir is content-addressed and never deleted. Screenshots from every run pile up on the shared filesystem: one crashed CUA run left 3.7 GB, and each 1080p round adds about 0.6 GB.
  2. Train data can carry both media references and dense multimodal_train_inputs, for example from a sample builder that still sets the tensors. When that happens it silently pays the hand-off that references replace, and the trainer then rebuilds the same tensors anyway.
  3. SGLang's processor returns fp32 pixels, and get_rollout_data holds every sample's rebuilt tensors on the GPU for the whole step. At 1080p with two screenshots that is 95.6 MiB a sample. Round 0 of a CUA run ran out of GPU memory on every H200 rank, at 134.8 of 139.8 GiB.
  4. Prefill scoring (--recompute-logprobs-via-prefill) sends media references but no mm_token_expansion_start_len. In token space the tokens were already expanded during rollout, so SGLang would expand the placeholders again.
  5. The trainer builds one processor from the settings all rollout engines share (RolloutEngineProcessorArgs.from_sglang_config). SglangConfig.common_value only finds engine groups that disagree when the first training step reads those settings, which is after a whole rollout round. Separately, a user-set --custom-inference-engine-provider-path means external engines (_compute_rollout_external), and the startup check does not refuse it.

Fix.

  1. run_media_dir(args) returns <--shared-media-dir>/<run_uuid>. The dataset, eval and geo3k multi-turn call sites write there. release_run_media(args) removes that directory. train.py and train_async.py register it first on the disposer, so it runs last, after the rollout executor and the trainers are gone. The directory is keyed by run, not by rollout step, because a sample generated in one step can train in the next. The --shared-media-dir help and cli-reference.md say so.
  2. convert_samples_to_train_data raises when its output carries both multimodal_inputs and multimodal_train_inputs. This applies to the default converter and to a custom one.
  3. compute_multimodal_train_inputs takes the training dtype and casts each sample's floating tensors as they leave the processor, in the per-sample rebuild both the round rebuild and a later per-micro-batch rebuild share. get_rollout_data passes bf16, or fp16 under --fp16; fp32 runs are left as they are. The vision tower runs in the training dtype, so the cast changes nothing it sees.
  4. In token space with media, _build_prefill_scoring_payload sets mm_token_expansion_start_len = len(sample.tokens).
  5. _check_multimodal_token_space_processing_args now calls RolloutEngineProcessorArgs.from_sglang_config while arguments are parsed, and turns a disagreement into a ValueError naming the field. It also refuses external engines by _compute_rollout_external, not only by --rollout-external-engine-addrs. The cli-reference.md row lists both.

Dropped from the origin commits because Rockdu's 9de9319b83 and bbddd9e654 already cover them:

  • our validate_token_space_engine_settings in launch_settings.py. That module does not exist on this branch.
  • its fixed expectations that the engines run the auto image backend and the base --sglang-mm-process-config. The trainer now follows the engines' own settings.
  • its per-group enable_token_space_processor check (c95dc47af9). common_value("enable_token_space_processor") covers it.
  • its --rollout-external-engine-addrs refusal.

Also dropped:

Tests (CPU devbox; SGLang sglang-miles b0ed761650, which has the 10-29 token-space API). This base predates radixark/miles radixark#4548, so its chat_template_utils/deepseek.py imports encoding_dsv4/encoding_dsv41 from SGLang, and b0ed761650 has removed them. To get past that, both modules were taken from SGLang e22a530859^ and appended to the package path through a test-only sitecustomize. No code in this PR changes for it.

  • Regression tests: each one fails with the fix's source files reverted to f9e7a9f645 (9 failed, 8 passed) and passes with the fix (17 passed). Failures without the fix:
    • test_train.py::TestRunMedia drives train() and fails with assert not True: the run's directory is still there. It also checks that the media still exists when the rollout executor is disposed, and that another run's directory is kept.
    • test_train_data_conversion.py has two tests, for the default and a custom converter. Both fail with DID NOT RAISE ValueError.
    • test_data.py::TestGetRolloutDataMediaRebuild drives get_rollout_data through the real compute_multimodal_train_inputs, with only the SGLang processor faked to return fp32 pixels. On f9e7a9f645 it fails with assert torch.float32 == torch.bfloat16 and == torch.float16 (2 failed, 1 passed). The fp32 case and the integer image_grid_thw stay unchanged.
    • test_prefill_logprobs.py::test_prefill_scoring_of_media_marks_the_expanded_tokens_as_prefix[True] fails with KeyError: 'mm_token_expansion_start_len'.
    • test_arguments.py::TestMultimodalTokenSpaceProcessingArgs: the custom provider test and the two group-disagreement tests (an mm_process_config override and an image_processor_backend override) fail with DID NOT RAISE ValueError.
  • pytest tests/fast/utils/test_media_refs.py tests/fast/test_train.py tests/fast/test_train_async.py tests/fast/backends/training_utils/test_data.py tests/fast/ray/rollout/test_train_data_conversion.py tests/fast/rollout/generate_utils/test_prefill_logprobs.py tests/fast/backends/training_utils/data/test_token_space_media_process.py tests/fast/utils/test_arguments.py: 657 passed. After the cast moved into the per-sample rebuild (head d809e5bf9c), pytest tests/fast/backends/training_utils/test_data.py tests/fast/backends/training_utils/data tests/fast/utils/test_arguments.py tests/fast/rollout/generate_utils/test_prefill_logprobs.py tests/fast/ray/rollout/test_train_data_conversion.py: 613 passed.
  • pytest tests/fast/rollout/generate_utils tests/fast/rollout/test_data_source.py tests/fast/rollout/test_sglang_rollout.py tests/fast/rollout/inference_rollout/test_inference_rollout_common.py tests/fast/rollout/inference_rollout/test_compatibility.py tests/fast/utils/test_data.py tests/fast/ray/rollout/test_train_data_conversion.py (the changed call sites): 343 passed.
  • pre-commit run on the changed files: passed.

Limits.

  • Run media is removed only when the run ends. A long run still holds every round's screenshots until then, at about 0.6 GB a round at 1080p. Deleting them per round would need to know when every sample that references them has trained.
  • Only train.py and train_async.py release run media. train_multi_policy.py and serve_tinker.py do not.
  • A run killed without teardown (SIGKILL, node loss) leaves its directory behind.

Related upstream work.

Origin commits (#11, on LIN-1985/investigate-existing-recipes-for): 1d5956e99d, a79b5f04fb, d2a5e6d631, 6d10f16ef4, ad65487654, c95dc47af9 (cf2eb9df40 dropped).

… run ends

Media written under --shared-media-dir was content-addressed but never deleted, so screenshots from every run accumulated on the shared filesystem (a crashed CUA run left 3.7 GB, and a 1080p round adds about 0.6 GB). Each run now writes under <dir>/<run_uuid>, and the training drivers remove that directory as their last teardown step, after the rollout executor and trainers are gone. The directory is keyed by run rather than by rollout step because a sample generated in one step can train in the next.
Copilot AI balanced review requested due to automatic review settings November 1, 2026 10:35
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-01T10:36:47.840240Z f7f0e28 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f7f0e286e8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread miles/utils/media_refs.py
Comment on lines +49 to +51
def release_run_media(args) -> None:
if (media_dir := run_media_dir(args)) is not None:
shutil.rmtree(media_dir, ignore_errors=True)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve media referenced by saved rollout state

When a multimodal run saves rollout checkpoints or replay dumps, teardown now deletes files those artifacts still reference. FullyAsyncRolloutFn.save() persists pending and completed Sample objects, and save_debug_rollout_data() serializes sample.to_dict(); neither copies the referenced media into the artifact. After a normal shutdown or an exception that runs the disposer, resuming buffered samples or replaying a dump therefore encounters missing media paths. Rebuilding the dataset does not repair the absolute references already stored in those samples. Preserve media reachable from saved artifacts, or copy it into artifact-owned storage and rewrite the references before deleting the run directory.

Useful? React with 👍 / 👎.

celve added 2 commits November 3, 2026 14:33
- Refuse train data that carries both media references and dense multimodal_train_inputs, from the default or a custom converter: the trainer rebuilds the tensors from the references, so shipping both silently pays the hand-off the references replace.
- Cast each sample's rebuilt floating tensors to the training dtype as they leave the processor. SGLang's processor returns fp32 pixels, and get_rollout_data holds every sample's tensors for the whole step: at 1080p with two screenshots a sample that was 95.6 MiB, and round 0 ran out of GPU memory on every H200 rank. The vision tower runs in bf16, so the cast changes nothing it sees.
- Mark the whole sequence as an expanded prefix when prefill scoring sends media references in token space. The tokens were already expanded during rollout, so without mm_token_expansion_start_len SGLang would expand the placeholders again.
The trainer builds one processor from the settings every rollout engine shares, and SglangConfig.common_value only notices engine groups that disagree when the first training step reads them, after a whole rollout round. Read those settings during argument parsing so a group override of the image processor, its config or the model fails at launch. A custom inference engine provider also implies external engines whose server args Miles never sees, so refuse it alongside --rollout-external-engine-addrs.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants