Skip to content

[Rollout] Add streaming vLLM rollout (port slime #1921) - #117

Closed
aoshen02 wants to merge 1 commit into
mainfrom
sync/slime-pr-1921
Closed

[Rollout] Add streaming vLLM rollout (port slime #1921)#117
aoshen02 wants to merge 1 commit into
mainfrom
sync/slime-pr-1921

Conversation

@aoshen02

@aoshen02 aoshen02 commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator

Ports THUDM/slime PR #1921 (streaming rollout) onto vime's vLLM rollout seam.

What

  • vime/rollout/vllm_streaming_rollout.py — a drop-in --custom-generate-function-path that consumes /inference/v1/generate as an SSE stream (stream: true) and writes each chunk's tokens/logprobs onto the sample immediately. The win is on abort: a partial-rollout / weight-update abort mid-generation leaves a coherent partial sample behind, without depending on an /abort_request response.
  • tests/test_qwen3_4B_streaming_partial_rollout.py — 8-GPU GRPO smoke (Qwen3-4B + dapo-math) with --over-sampling-batch-size > --rollout-batch-size + --partial-rollout, so the loop must abort in-flight requests every step and exercise the streaming abort path. Registered in the e2e-test-megatron matrix (j2 + rendered yml).

vime vs slime port notes

  • slime's sglang stream sends cumulative meta_info.output_token_logprobs; vLLM's GenerateResponseStreamChoice chunks carry DELTA token_ids + logprobs, so this accumulates deltas (+=) per chunk. Each delta choice is the same shape as the non-streaming choice, so _inference_generate_tokens_and_logprobs parses it unchanged.
  • Finalization mirrors vllm_rollout.generate: align logprobs → build metaupdate_from_meta_info_apply_vllm_routed_experts (routing replay).
  • Partial-continuation max_new_tokens math + consistent_hash x-session-id header match the non-streaming path.
  • Multimodal samples raise NotImplementedError (the MM render→generate flow is a two-call dance that doesn't map onto a single streamed call) rather than silently dropping images.
  • Passthrough confirmed end-to-end in source: vLLM-fork engine streams /inference/v1/generate via SSE, and the Rust vllm_router forwards SSE transparently (Body::from_stream, text/event-stream).

Validation

  • cpu gate (fresh --rm container from the latest cumulative CI image vime-vllm-cu129-sync1916): module import OK, test-file import OK, test_megatron_argument_validation 7 passed, plugin_contracts 34 passed (rollout 6 / runtime-hook 10 / path-loading 14 / generate 4).
  • lint: pinned pre-commit (ruff/isort/black) clean on changed files.
  • GPU CI: not run (per request).

Upstream: THUDM/slime#1921. Part of the slime→vime sync tracked in #107 (not ticking the box pending sign-off).

Port THUDM/slime PR #1921 (streaming rollout) to vime's vLLM rollout seam.

Adds vime/rollout/vllm_streaming_rollout.py: a drop-in
--custom-generate-function-path that consumes /inference/v1/generate as an SSE
stream (stream=true) and writes each chunk's tokens/logprobs onto the sample
immediately, so a partial-rollout / weight-update abort mid-generation leaves a
coherent partial sample behind without depending on an /abort_request response.

Unlike sglang's cumulative meta_info, vLLM's GenerateResponseStreamChoice
chunks carry DELTA token_ids+logprobs, so deltas are accumulated (+=) per chunk
(reusing _inference_generate_tokens_and_logprobs, same choice shape as the
non-streaming path). Finalization mirrors vllm_rollout.generate: align logprobs,
build meta, update_from_meta_info, then apply routed-experts replay. Multimodal
samples are routed to the non-streaming generate (NotImplementedError) rather
than silently dropping images.

Adds tests/test_qwen3_4B_streaming_partial_rollout.py (8-GPU, registered in the
e2e-test-megatron matrix): Qwen3-4B GRPO on dapo-math with
--over-sampling-batch-size > --rollout-batch-size + --partial-rollout so the
loop must abort in-flight requests every step, exercising the streaming abort
path. vime counterpart of slime's test, swapping sglang engine args for vLLM.

Signed-off-by: aoshen02 <aoshen@inferact.ai>

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces a streaming vLLM rollout implementation along with a corresponding CI smoke test. The streaming rollout consumes vLLM's SSE stream incrementally to handle mid-generation aborts gracefully. Feedback on the implementation points out a lack of retry logic for HTTP connection and stream consumption, which could cause training runs to crash on transient network glitches. It is recommended to wrap the stream connection in a retry loop and restore the sample state on failure.

Comment on lines +129 to +203
base_tokens = list(sample.tokens)
base_response = sample.response or ""
base_response_length = sample.response_length
base_log_probs = list(sample.rollout_log_probs or [])
base_loss_mask = list(sample.loss_mask) if sample.loss_mask is not None else None

skip_sp = params.get("skip_special_tokens")
skip_decode = True if skip_sp is None else bool(skip_sp)

call_tokens: list[int] = []
call_log_probs: list[float] = []
last_choice: dict[str, Any] | None = None
last_usage: dict[str, Any] | None = None
finish_reason: Any = None

client = http_utils._http_client
assert client is not None, "http client not initialized; call init_http_client first"

with trace_span(
sample, "vllm_inference_generate_stream", attrs={"max_new_tokens": params["max_new_tokens"]}
) as span:
async with client.stream("POST", url, json=payload, headers=headers) as response:
response.raise_for_status()
async for raw_line in response.aiter_lines():
if not raw_line or not raw_line.startswith("data:"):
continue
data_str = raw_line[len("data:") :].strip()
if not data_str or data_str == "[DONE]":
continue
try:
chunk = json.loads(data_str)
except json.JSONDecodeError:
logger.warning("vllm_streaming: skipping non-JSON chunk: %r", data_str[:120])
continue

choices = chunk.get("choices") or []
if not choices:
# usage-only / keepalive chunk
if chunk.get("usage"):
last_usage = chunk["usage"]
continue
choice = choices[0]
last_choice = choice
if chunk.get("usage"):
last_usage = chunk["usage"]
if choice.get("finish_reason"):
finish_reason = choice["finish_reason"]

# Each streamed choice carries only this chunk's *delta* tokens
# (GenerateResponseStreamChoice), so accumulate.
delta_tokens, delta_log_probs = _inference_generate_tokens_and_logprobs(choice)
if delta_tokens:
call_tokens += delta_tokens
call_log_probs += delta_log_probs

# Surface partial state on the sample immediately. If the outer
# abort path cuts us, whatever we've written so far is what
# survives. Decode the *accumulated* tokens (not the per-chunk
# delta) so multi-token characters straddling a chunk boundary
# decode correctly.
sample.tokens = base_tokens + call_tokens
sample.response = base_response + (
state.tokenizer.decode(call_tokens, skip_special_tokens=skip_decode) if call_tokens else ""
)
sample.response_length = base_response_length + len(call_tokens)
sample.rollout_log_probs = base_log_probs + call_log_probs
if base_loss_mask is not None:
assert args.partial_rollout and args.mask_offpolicy_in_partial_rollout
sample.loss_mask = base_loss_mask + [1] * len(call_tokens)

if state.aborted:
break

if finish_reason and last_choice is not None:
span.update(build_vllm_meta_trace_attrs({"choices": [last_choice], "usage": last_usage}))

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.

high

The streaming rollout implementation lacks retry logic for the HTTP connection and stream consumption. In distributed LLM training, transient network glitches, router restarts, or temporary vLLM engine overloads are common. Without retries, any such transient error will immediately raise an exception, propagate through asyncio.gather, and crash the entire training run.

We should wrap the stream connection and consumption in a retry loop (e.g., up to 60 retries with a 1-second delay, matching the non-streaming post behavior). If a retry is triggered, we must restore the sample object to its pre-call snapshot state to avoid accumulating duplicate or corrupted tokens/responses. Additionally, if the stream finishes normally but finish_reason is not explicitly set in the chunks, we should default it to 'stop' to ensure the sample is properly finalized rather than being left in a PENDING state.

    base_tokens = list(sample.tokens)
    base_response = sample.response or ""
    base_response_length = sample.response_length
    base_log_probs = list(sample.rollout_log_probs) if sample.rollout_log_probs is not None else None
    base_loss_mask = list(sample.loss_mask) if sample.loss_mask is not None else None

    skip_sp = params.get("skip_special_tokens")
    skip_decode = True if skip_sp is None else bool(skip_sp)

    call_tokens: list[int] = []
    call_log_probs: list[float] = []
    last_choice: dict[str, Any] | None = None
    last_usage: dict[str, Any] | None = None
    finish_reason: Any = None

    client = http_utils._http_client
    assert client is not None, "http client not initialized; call init_http_client first"

    max_retries = 60
    retry_count = 0
    while retry_count < max_retries:
        call_tokens = []
        call_log_probs = []
        last_choice = None
        last_usage = None
        finish_reason = None

        try:
            with trace_span(
                sample, "vllm_inference_generate_stream", attrs={"max_new_tokens": params["max_new_tokens"]}
            ) as span:
                async with client.stream("POST", url, json=payload, headers=headers) as response:
                    response.raise_for_status()
                    async for raw_line in response.aiter_lines():
                        if not raw_line or not raw_line.startswith("data:"):
                            continue
                        data_str = raw_line[len("data:") :].strip()
                        if not data_str or data_str == "[DONE]":
                            continue
                        try:
                            chunk = json.loads(data_str)
                        except json.JSONDecodeError:
                            logger.warning("vllm_streaming: skipping non-JSON chunk: %r", data_str[:120])
                            continue

                        choices = chunk.get("choices") or []
                        if not choices:
                            if chunk.get("usage"):
                                last_usage = chunk["usage"]
                            continue
                        choice = choices[0]
                        last_choice = choice
                        if chunk.get("usage"):
                            last_usage = chunk["usage"]
                        if choice.get("finish_reason"):
                            finish_reason = choice["finish_reason"]

                        delta_tokens, delta_log_probs = _inference_generate_tokens_and_logprobs(choice)
                        if delta_tokens:
                            call_tokens += delta_tokens
                            call_log_probs += delta_log_probs

                        sample.tokens = base_tokens + call_tokens
                        sample.response = base_response + (
                            state.tokenizer.decode(call_tokens, skip_special_tokens=skip_decode) if call_tokens else ""
                        )
                        sample.response_length = base_response_length + len(call_tokens)
                        sample.rollout_log_probs = (base_log_probs or []) + call_log_probs
                        if base_loss_mask is not None:
                            assert args.partial_rollout and args.mask_offpolicy_in_partial_rollout
                            sample.loss_mask = base_loss_mask + [1] * len(call_tokens)

                        if state.aborted:
                            break

                if finish_reason and last_choice is not None:
                    span.update(build_vllm_meta_trace_attrs({"choices": [last_choice], "usage": last_usage}))
            break
        except Exception as e:
            retry_count += 1
            if retry_count >= max_retries:
                logger.error("Max retries reached for streaming generate, failing...")
                raise e
            logger.warning("Streaming generate error: %s, retrying (%d/%d)...", e, retry_count, max_retries)
            sample.tokens = list(base_tokens)
            sample.response = base_response
            sample.response_length = base_response_length
            sample.rollout_log_probs = list(base_log_probs) if base_log_probs is not None else None
            if base_loss_mask is not None:
                sample.loss_mask = list(base_loss_mask)
            await asyncio.sleep(1)

    if not finish_reason and last_choice is not None and not state.aborted:
        finish_reason = "stop"

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Valid. The streaming path lacks the non-streaming post() retry/backoff, so a transient router/engine glitch would propagate through asyncio.gather and crash the run; the snapshot-restore-on-retry and finish_reason default are both right.

Note: #117 is intentionally deferred to draft — there's no current streaming-partial-rollout need, and streaming can't carry R3 routing-replay (the vLLM GenerateResponseStreamChoice SSE schema has no routed_experts field, unlike sglang's terminal meta_info). When it's resumed, this retry+restore+finish_reason fix will be applied alongside the routing-replay/image → non-streaming fallbacks and the stream usage chunk.

@aoshen02
aoshen02 marked this pull request as draft June 2, 2026 08:01
@aoshen02

aoshen02 commented Jun 3, 2026

Copy link
Copy Markdown
Collaborator Author

Closing for now (slime→vime sync being restructured). Branch is preserved — can reopen anytime.

@aoshen02 aoshen02 closed this Jun 3, 2026
aoshen02 added a commit that referenced this pull request Jun 5, 2026
…ang->vLLM

Port THUDM/slime PR-G examples to vime's vLLM rollout:
- #1921 streaming: vime/rollout/vllm_streaming_rollout.py consumes the vLLM
  /inference/v1/generate SSE stream. Key translation: sglang streams CUMULATIVE
  output_token_logprobs (overwrite per chunk); vLLM SSE sends DELTA token_ids+
  logprobs per chunk -> accumulate (+=) and re-decode accumulated tokens each
  chunk (multi-byte safe), finalize via _vllm_meta_from_generate_choice. MM
  raises NotImplementedError (two-call render->generate doesn't fit one stream
  -> use non-streaming generate). + CI test test_qwen3_4B_streaming_partial_rollout.
  (streaming file is the reviewed sync/slime-pr-1921 / draft #117 design.)
- #1861 retool: re-add examples/retool/generate_with_retool.py (vime lacked
  examples/retool/) from slime base + #1861 hardening; desync guard
  `if not meta_info.get("output_token_logprobs")`. NOTE: depends on sibling
  tool_sandbox.py (not yet ported -> follow-up; example partial until then).

Based on PR-A (#145) branch (stacked).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: aoshen02 <aoshen@inferact.ai>
aoshen02 added a commit that referenced this pull request Jun 6, 2026
…ang->vLLM

Port THUDM/slime PR-G examples to vime's vLLM rollout:
- #1921 streaming: vime/rollout/vllm_streaming_rollout.py consumes the vLLM
  /inference/v1/generate SSE stream. Key translation: sglang streams CUMULATIVE
  output_token_logprobs (overwrite per chunk); vLLM SSE sends DELTA token_ids+
  logprobs per chunk -> accumulate (+=) and re-decode accumulated tokens each
  chunk (multi-byte safe), finalize via _vllm_meta_from_generate_choice. MM
  raises NotImplementedError (two-call render->generate doesn't fit one stream
  -> use non-streaming generate). + CI test test_qwen3_4B_streaming_partial_rollout.
  (streaming file is the reviewed sync/slime-pr-1921 / draft #117 design.)
- #1861 retool: re-add examples/retool/generate_with_retool.py (vime lacked
  examples/retool/) from slime base + #1861 hardening; desync guard
  `if not meta_info.get("output_token_logprobs")`. NOTE: depends on sibling
  tool_sandbox.py (not yet ported -> follow-up; example partial until then).

Based on PR-A (#145) branch (stacked).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: aoshen02 <aoshen@inferact.ai>
CalvinXKY pushed a commit that referenced this pull request Jun 7, 2026
* [Sync][G] streaming rollout + retool example (slime #1921/#1861), sglang->vLLM

Port THUDM/slime PR-G examples to vime's vLLM rollout:
- #1921 streaming: vime/rollout/vllm_streaming_rollout.py consumes the vLLM
  /inference/v1/generate SSE stream. Key translation: sglang streams CUMULATIVE
  output_token_logprobs (overwrite per chunk); vLLM SSE sends DELTA token_ids+
  logprobs per chunk -> accumulate (+=) and re-decode accumulated tokens each
  chunk (multi-byte safe), finalize via _vllm_meta_from_generate_choice. MM
  raises NotImplementedError (two-call render->generate doesn't fit one stream
  -> use non-streaming generate). + CI test test_qwen3_4B_streaming_partial_rollout.
  (streaming file is the reviewed sync/slime-pr-1921 / draft #117 design.)
- #1861 retool: re-add examples/retool/generate_with_retool.py (vime lacked
  examples/retool/) from slime base + #1861 hardening; desync guard
  `if not meta_info.get("output_token_logprobs")`. NOTE: depends on sibling
  tool_sandbox.py (not yet ported -> follow-up; example partial until then).

Based on PR-A (#145) branch (stacked).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: aoshen02 <aoshen@inferact.ai>

* [Sync][G] add retool tool_sandbox.py (in-process tool registry, verbatim)

generate_with_retool.py imports `from tool_sandbox import SEMAPHORE, TOOL_CONFIGS,
tool_registry`; this in-process math/code tool-execution sandbox is pure Python
(no sglang/slime deps) -> copied verbatim from slime. Completes the retool example
import (was the flagged missing sibling).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: aoshen02 <aoshen@inferact.ai>

* feat(mega-G): support multimodal samples in streaming rollout (render-dance)

Streaming only changes how output is returned (SSE deltas vs one JSON); the
image render (input prep: images→features) is identical to the non-streaming
path. Replace the NotImplementedError stub with the same render-dance
vllm_rollout.generate uses:
  POST /v1/chat/completions/render → _mm_render_response_to_generate_body
  → _align_mm_feature_placeholders_to_tokens → token_ids + sampling_params
  → set stream:True and stream the generate call.

mm placeholders live in the prompt prefix (stable across partial-rollout
continuations), so re-rendering + re-aligning to the current token_ids holds
when an aborted MM sample resumes.

NOTE: needs MM e2e validation on a real vLLM MM server (no unit coverage for
the streaming MM path; can't run GPU/MM here).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Signed-off-by: aoshen02 <aoshen@inferact.ai>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
aoshen02 added a commit that referenced this pull request Jun 7, 2026
* [Sync][G] streaming rollout + retool example (slime #1921/#1861), sglang->vLLM

Port THUDM/slime PR-G examples to vime's vLLM rollout:
- #1921 streaming: vime/rollout/vllm_streaming_rollout.py consumes the vLLM
  /inference/v1/generate SSE stream. Key translation: sglang streams CUMULATIVE
  output_token_logprobs (overwrite per chunk); vLLM SSE sends DELTA token_ids+
  logprobs per chunk -> accumulate (+=) and re-decode accumulated tokens each
  chunk (multi-byte safe), finalize via _vllm_meta_from_generate_choice. MM
  raises NotImplementedError (two-call render->generate doesn't fit one stream
  -> use non-streaming generate). + CI test test_qwen3_4B_streaming_partial_rollout.
  (streaming file is the reviewed sync/slime-pr-1921 / draft #117 design.)
- #1861 retool: re-add examples/retool/generate_with_retool.py (vime lacked
  examples/retool/) from slime base + #1861 hardening; desync guard
  `if not meta_info.get("output_token_logprobs")`. NOTE: depends on sibling
  tool_sandbox.py (not yet ported -> follow-up; example partial until then).

Based on PR-A (#145) branch (stacked).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: aoshen02 <aoshen@inferact.ai>

* [Sync][G] add retool tool_sandbox.py (in-process tool registry, verbatim)

generate_with_retool.py imports `from tool_sandbox import SEMAPHORE, TOOL_CONFIGS,
tool_registry`; this in-process math/code tool-execution sandbox is pure Python
(no sglang/slime deps) -> copied verbatim from slime. Completes the retool example
import (was the flagged missing sibling).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: aoshen02 <aoshen@inferact.ai>

* feat(mega-G): support multimodal samples in streaming rollout (render-dance)

Streaming only changes how output is returned (SSE deltas vs one JSON); the
image render (input prep: images→features) is identical to the non-streaming
path. Replace the NotImplementedError stub with the same render-dance
vllm_rollout.generate uses:
  POST /v1/chat/completions/render → _mm_render_response_to_generate_body
  → _align_mm_feature_placeholders_to_tokens → token_ids + sampling_params
  → set stream:True and stream the generate call.

mm placeholders live in the prompt prefix (stable across partial-rollout
continuations), so re-rendering + re-aligning to the current token_ids holds
when an aborted MM sample resumes.

NOTE: needs MM e2e validation on a real vLLM MM server (no unit coverage for
the streaming MM path; can't run GPU/MM here).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Signed-off-by: aoshen02 <aoshen@inferact.ai>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
aoshen02 added a commit that referenced this pull request Jun 7, 2026
* [Sync][G] streaming rollout + retool example (slime #1921/#1861), sglang->vLLM

Port THUDM/slime PR-G examples to vime's vLLM rollout:
- #1921 streaming: vime/rollout/vllm_streaming_rollout.py consumes the vLLM
  /inference/v1/generate SSE stream. Key translation: sglang streams CUMULATIVE
  output_token_logprobs (overwrite per chunk); vLLM SSE sends DELTA token_ids+
  logprobs per chunk -> accumulate (+=) and re-decode accumulated tokens each
  chunk (multi-byte safe), finalize via _vllm_meta_from_generate_choice. MM
  raises NotImplementedError (two-call render->generate doesn't fit one stream
  -> use non-streaming generate). + CI test test_qwen3_4B_streaming_partial_rollout.
  (streaming file is the reviewed sync/slime-pr-1921 / draft #117 design.)
- #1861 retool: re-add examples/retool/generate_with_retool.py (vime lacked
  examples/retool/) from slime base + #1861 hardening; desync guard
  `if not meta_info.get("output_token_logprobs")`. NOTE: depends on sibling
  tool_sandbox.py (not yet ported -> follow-up; example partial until then).

Based on PR-A (#145) branch (stacked).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: aoshen02 <aoshen@inferact.ai>

* [Sync][G] add retool tool_sandbox.py (in-process tool registry, verbatim)

generate_with_retool.py imports `from tool_sandbox import SEMAPHORE, TOOL_CONFIGS,
tool_registry`; this in-process math/code tool-execution sandbox is pure Python
(no sglang/slime deps) -> copied verbatim from slime. Completes the retool example
import (was the flagged missing sibling).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: aoshen02 <aoshen@inferact.ai>

* feat(mega-G): support multimodal samples in streaming rollout (render-dance)

Streaming only changes how output is returned (SSE deltas vs one JSON); the
image render (input prep: images→features) is identical to the non-streaming
path. Replace the NotImplementedError stub with the same render-dance
vllm_rollout.generate uses:
  POST /v1/chat/completions/render → _mm_render_response_to_generate_body
  → _align_mm_feature_placeholders_to_tokens → token_ids + sampling_params
  → set stream:True and stream the generate call.

mm placeholders live in the prompt prefix (stable across partial-rollout
continuations), so re-rendering + re-aligning to the current token_ids holds
when an aborted MM sample resumes.

NOTE: needs MM e2e validation on a real vLLM MM server (no unit coverage for
the streaming MM path; can't run GPU/MM here).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Signed-off-by: aoshen02 <aoshen@inferact.ai>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CalvinXKY pushed a commit that referenced this pull request Jun 7, 2026
…ariable global batch + group_ids (slime #1926/#1930/#1933/#1941/#1959/#1965/#1969/#1984/#1962) (#145)

* [Sync][A] rollout data-model: micro-batch sched rollout-side + variable global batch + group_ids (slime #1926/#1930/#1933/#1941/#1959/#1965/#1969/#1984/#1962)

Port THUDM/slime mega-PR A (rollout data-model + train-split):
- #1926 move micro-batch scheduling train->rollout side (dp_schedule.py, first-fit packing)
- #1930+#1933 variable global batch size + per-token-loss reduction fix (cp_utils
  reduce_train_step_metrics / get_sum_of_sample_mean sample_denoms; step_global_batch_size
  denominator). CP-invariance + rollout==train equality validated by test_metric_report_dist
  / test_loss_cp_invariance (57/57 multi-proc gloo across CP/DP in {1,2,4}); test_dp_schedule 8/8.
- #1959 forge_load replay (vime/rollout/forge_load.py, --load-forge-rollout-data)
- #1965 group_ids fall back to range(), not sample.index
- #1969 --save-hf for raw mode (hf_checkpoint_saver.py)
- #1941 multi-sample fanout test + helpers (sglang_rollout->vllm_rollout)
- #1984 rename rollout_ids->group_ids (NON-agent files; agent half deferred to PR-D)
- #1962 lint (forge_load)

sglang->vLLM translated per arg-map (--sglang-*->--vllm-*, vllm_speculative_config).
NOTE: test_sample depends on args.vllm_speculative_config existing -> relies on PR-C #1938
getattr guard (stack order F+C+B+A satisfies this). py_compile clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: aoshen02 <aoshen@inferact.ai>

* test(A): port test_hf_checkpoint_saver + wire A numeric tests into cpu CI job

Oracle found A#145 added the data-model numeric tests + hf_checkpoint_saver
impl but did NOT wire them into the cpu CI job (e2e-test-plugin-contracts),
and the hf_checkpoint_saver unit test (slime #1969) was never ported.

- port tests/utils/test_hf_checkpoint_saver.py (slime->vime)
- add dp_schedule/cp_utils/metric_report{,_dist}/loss_cp_invariance/sample
  + utils/test_hf_checkpoint_saver to the cpu job; regen pr-test.yml

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* [Sync][A] revert rollout_id->group_id rename (port slime #2013)

slime #2013 reverts #1984 (the rollout_id->group_id rename) because it
conflicts with internal tooling. A #145 ported #1984, so mirror the revert to
stay byte-faithful and realign with main + post-#2013 slime:
  group_id -> rollout_id, group_ids -> rollout_ids,
  group_mask_sums -> rollout_mask_sums, num_groups_in_rollout ->
  num_rollouts_in_rollout, and drop the Sample.rollout_id deprecation guard
  (__getattribute__/__setattr__ + import warnings) #1984 had added.

Per-file churn matches slime #2013 exactly (actor/data/loss/model/rollout/
_fanout_test_helpers/forge_load/dp_schedule/types + 3 tests). The unrelated
compute_pass_rate(num_groups=...) kwarg is left as-is (slime keeps it too).

NOTE: #2013 also reverts D-side files not present on A
(examples/coding_agent_rl/*, vime/agent/trajectory.py, docs .../agent.md, the
group_id sections of docs customization.md + multi_agent stamping). Those must
get the same revert when D #148 is rebased onto this A.

* [Sync][G] streaming rollout + retool example (slime #1921/#1861) (#147)

* [Sync][G] streaming rollout + retool example (slime #1921/#1861), sglang->vLLM

Port THUDM/slime PR-G examples to vime's vLLM rollout:
- #1921 streaming: vime/rollout/vllm_streaming_rollout.py consumes the vLLM
  /inference/v1/generate SSE stream. Key translation: sglang streams CUMULATIVE
  output_token_logprobs (overwrite per chunk); vLLM SSE sends DELTA token_ids+
  logprobs per chunk -> accumulate (+=) and re-decode accumulated tokens each
  chunk (multi-byte safe), finalize via _vllm_meta_from_generate_choice. MM
  raises NotImplementedError (two-call render->generate doesn't fit one stream
  -> use non-streaming generate). + CI test test_qwen3_4B_streaming_partial_rollout.
  (streaming file is the reviewed sync/slime-pr-1921 / draft #117 design.)
- #1861 retool: re-add examples/retool/generate_with_retool.py (vime lacked
  examples/retool/) from slime base + #1861 hardening; desync guard
  `if not meta_info.get("output_token_logprobs")`. NOTE: depends on sibling
  tool_sandbox.py (not yet ported -> follow-up; example partial until then).

Based on PR-A (#145) branch (stacked).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: aoshen02 <aoshen@inferact.ai>

* [Sync][G] add retool tool_sandbox.py (in-process tool registry, verbatim)

generate_with_retool.py imports `from tool_sandbox import SEMAPHORE, TOOL_CONFIGS,
tool_registry`; this in-process math/code tool-execution sandbox is pure Python
(no sglang/slime deps) -> copied verbatim from slime. Completes the retool example
import (was the flagged missing sibling).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: aoshen02 <aoshen@inferact.ai>

* feat(mega-G): support multimodal samples in streaming rollout (render-dance)

Streaming only changes how output is returned (SSE deltas vs one JSON); the
image render (input prep: images→features) is identical to the non-streaming
path. Replace the NotImplementedError stub with the same render-dance
vllm_rollout.generate uses:
  POST /v1/chat/completions/render → _mm_render_response_to_generate_body
  → _align_mm_feature_placeholders_to_tokens → token_ids + sampling_params
  → set stream:True and stream the generate call.

mm placeholders live in the prompt prefix (stable across partial-rollout
continuations), so re-rendering + re-aligning to the current token_ids holds
when an aborted MM sample resumes.

NOTE: needs MM e2e validation on a real vLLM MM server (no unit coverage for
the streaming MM path; can't run GPU/MM here).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Signed-off-by: aoshen02 <aoshen@inferact.ai>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* [Sync][D] coding_agent_rl: agent-in-sandbox RL subsystem (slime #1923/#1956/#1960/#1954/#1957/#1958/#1963/#1979/#1981/#1982/#1961) (#148)

* [Sync][D] coding_agent_rl: agent-in-sandbox RL subsystem (slime #1923/#1956/#1960/#1954/#1957/#1958/#1963/#1979/#1981/#1982/#1961), sglang->vLLM

Add the agent-in-sandbox RL subsystem vime lacked. New vime/agent/ (trajectory,
sandbox, parsing, adapters: common/openai/anthropic) + examples/coding_agent_rl/
(demo, generate, sandbox, aiohttp_threaded, run script). Copied slime's final
state (all 11 D PRs applied) + translated.

3 sglang->vLLM seams:
- adapters/common.py: call_sglang_generate -> call_engine_generate; POST /generate
  -> /inference/v1/generate; body input_ids->token_ids + max_new_tokens->max_tokens
  + drop return_logprob + add logprobs:1 + model=; response output_token_logprobs
  -> choices[0].token_ids + logprobs.content[].logprob (via canonical field map);
  per-rid /abort_request -> task.cancel() + connection teardown on cancel.
- coding_agent_rl/generate.py: sglang_router_ip/port -> vllm_router_ip/port.
- parsing.py: NO change (ReasoningParser/FunctionCallParser lazy; coding_agent_rl
  defaults to XML fallback).

#1984 agent-half rename (rollout_id->group_id in trajectory.py) already present
(copied slime HEAD; consistent with PR-A's Sample.group_id). EAGLE spec left
commented (needs draft-model path). Based on PR-A (#145) (stacked).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: aoshen02 <aoshen@inferact.ai>

* test(D): port agent adapter/trajectory tests + wire agent-adapter CI job

Oracle found D#148's agent subsystem (vime/agent/adapters + trajectory) shipped
without its tests, and the cpu CI job had no agent-adapter coverage.

- port tests/test_agent_{adapters,sdk_adapters,trajectory}.py (slime->vime:
  slime.agent->vime.agent, SGLANG_URL_KEY->ENGINE_URL_KEY, sglang_url->engine_url)
- add agent-adapter-test cpu job (openai/openai-agents/anthropic) + extra_pip_deps
  template support; regen pr-test.yml

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci(D): don't wire test_agent_adapters yet (3/14 mock sglang /generate schema)

11/14 pass; the 3 (uses_sglang_tokens_for_training_segment x2, generate_
posts_input_ids_and_extracts_logprobs) assert against sglang's meta_info.
output_token_logprobs response shape and need rewriting to vime's vLLM
response format. File stays ported; trajectory + sdk_adapters wired+green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(D): clean .j2 (drop invalid python comment, regen pr-test.yml)

prior commit left a '#' comment inside the jinja dict literal -> regen
failed -> pr-test.yml stale (still wired the 3 red sglang-schema cases).
Remove comment; rationale lives in commit msg + OVERNIGHT_REPORT.

* fix(mega-D): translate agent-adapter sglang→vllm, not engine/杂名

Audit finding (bidirectional translation rule): the agent adapters had
translated slime's `sglang` identifiers/prose to generic `engine`/`rollout
engine` aliases instead of `vllm` specifically. Per the ground principle
(vime = slime modulo sglang→vllm, 1:1), fix to vllm:

  SGLANG_URL_KEY  → VLLM_URL_KEY        (AppKey "sglang_url"→"vllm_url")
  sglang_url      → vllm_url
  call_sglang_generate → call_vllm_generate
  (new helper)    → _vllm_sampling_body (was _engine_sampling_body)
  "sglang upstream {status}" → "vllm upstream {status}"
  docstrings "SGLang /generate" → "vLLM /inference/v1/generate"

Also wire the 3 previously-untranslated test cases in test_agent_adapters.py
from sglang wire schema to vLLM /inference/v1/generate schema:
  FakeSGLang → FakeVLLM  (returns choices[].token_ids + logprobs.content,
                          reads x-session-id header, serves /inference/v1/generate)
  request asserts: input_ids→token_ids, sampling_params.max_new_tokens→max_tokens,
                   return_logprob→sampling_params.logprobs==1, X-SMG-Routing-Key→x-session-id
  test names: *_uses_sglang_tokens_* → *_uses_vllm_tokens_*,
              *_posts_input_ids_* → *_posts_token_ids_*
  + MODEL_KEY in the direct-_generate test app (vime adds GenerateRequest.model).

14/14 test_agent_adapters pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(mega-D): drop the upstream `model` passthrough (mirror slime; vLLM doesn't need it)

Audit (vime-added field, no slime counterpart): the adapter had added a `model`
param + MODEL_KEY + `payload["model"]=model` sending model upstream to
/inference/v1/generate. Source check: vLLM `GenerateRequest.model` is
`str | None = None` (optional) and the disagg generate/serve_tokens handler
never reads it; the vllm-router doesn't route /generate by body.model. So it is
unused upstream — an unjustified divergence from slime (whose adapter has no
model param). Remove it to mirror slime exactly.

Kept: the OpenAI-response echo `body.get("model", "vime-actor")` — slime has the
identical line (slime-actor). Test no longer needs MODEL_KEY. 14/14 pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci(mega-D): wire test_agent_adapters.py into agent-adapter-test (was a dead test)

The agent-adapter-test job ran test_agent_sdk_adapters.py + test_agent_trajectory.py
but NOT test_agent_adapters.py — so the 14 adapter cases (incl. the 3 sglang→vLLM
schema ports) never ran in CI. slime wires test_agent_adapters.py in its
agent-adapter-test matrix; add it here (yml + j2). Deps (openai/anthropic/aiohttp)
already installed by this job.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* [Sync][D] revert rollout_id->group_id in agent files (port slime #2013, D-side)

Rebased D's own commits onto the updated A #145 (which now carries slime #2013).
A #145 reverted the shared data-model to rollout_id; this completes the same
revert for D's own files that A doesn't carry: examples/coding_agent_rl/
{README.md,generate.py} and vime/agent/trajectory.py. After this the whole
A+D surface is consistent on rollout_id, matching post-#2013 slime.

* fix(D): translate engine_url -> vllm_url at AnthropicAdapter call site (coding_agent_rl)

The adapter modules (agent/adapters/{anthropic,common,openai}.py) were
translated sglang_url -> vllm_url (commit 457d21c), but the call site in
examples/coding_agent_rl/generate.py was missed: it still built engine_url and
passed AnthropicAdapter(engine_url=...). Since the adapter __init__ now takes
vllm_url, this kwarg mismatch raises TypeError at construction. Rename the
local var + kwarg to vllm_url, completing the slime sglang_url -> vllm_url
translation and fixing the call.

---------

Signed-off-by: aoshen02 <aoshen@inferact.ai>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Signed-off-by: aoshen02 <aoshen@inferact.ai>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@aoshen02
aoshen02 deleted the sync/slime-pr-1921 branch June 8, 2026 14:20
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.

1 participant