[Rollout] Add streaming vLLM rollout (port slime #1921) - #117
Conversation
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>
There was a problem hiding this comment.
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.
| 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})) |
There was a problem hiding this comment.
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"There was a problem hiding this comment.
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.
|
Closing for now (slime→vime sync being restructured). Branch is preserved — can reopen anytime. |
…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>
…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>
* [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][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][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>
…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>
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-paththat consumes/inference/v1/generateas an SSE stream (stream: true) and writes each chunk's tokens/logprobs onto thesampleimmediately. The win is on abort: a partial-rollout / weight-update abort mid-generation leaves a coherent partial sample behind, without depending on an/abort_requestresponse.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 thee2e-test-megatronmatrix (j2 + rendered yml).vime vs slime port notes
meta_info.output_token_logprobs; vLLM'sGenerateResponseStreamChoicechunks carry DELTAtoken_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_logprobsparses it unchanged.vllm_rollout.generate: align logprobs → buildmeta→update_from_meta_info→_apply_vllm_routed_experts(routing replay).max_new_tokensmath +consistent_hashx-session-idheader match the non-streaming path.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./inference/v1/generatevia SSE, and the Rustvllm_routerforwards SSE transparently (Body::from_stream,text/event-stream).Validation
--rmcontainer from the latest cumulative CI imagevime-vllm-cu129-sync1916): module import OK, test-file import OK,test_megatron_argument_validation7 passed, plugin_contracts 34 passed (rollout 6 / runtime-hook 10 / path-loading 14 / generate 4).Upstream: THUDM/slime#1921. Part of the slime→vime sync tracked in #107 (not ticking the box pending sign-off).