Skip to content

feat(agentic): add Sample-native multi-turn rollout and training - #214

Merged
celve merged 66 commits into
Tencent-Hunyuan:mainfrom
celve:agentic
Jul 27, 2026
Merged

feat(agentic): add Sample-native multi-turn rollout and training#214
celve merged 66 commits into
Tencent-Hunyuan:mainfrom
celve:agentic

Conversation

@celve

@celve celve commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

Summary

This draft proposes a Sample-native agentic RL integration for UniRL.

  • Replace the rollout request/response boundary with the lineage-preserving Sample -> Sample / Sample -> list[Sample] contract and migrate existing model, rollout, reward, and training paths onto Sample / Part.
  • Add synchronous multi-turn agent execution with role-aware conditioning, tool/environment observations, a rank-zero rollout coordinator, and thread-safe SGLang inner engines.
  • Add barrier, colocated partial-rollout, and disaggregated asynchronous agentic trainers, with ALFWorld and calculator/deep-research workflows.
  • Add migration and agent-loop documentation plus runnable example recipes.

The draft is intentionally opened for architecture and sequencing review before the remaining final-HEAD GPU matrix and patch-size cleanup are complete.

Test Plan

Completed on the current draft tip (159627bd):

  • SKIP=no-commit-to-branch pre-commit run --all-files --show-diff-on-failure
  • python3 -m compileall -q unirl
  • python3 scripts/check_recipe_targets.py — 1,965 Hydra _target_ paths resolved
  • Hydra API composition plus OmegaConf.resolve for all six barrier/partial/async ALFWorld and calculator/deep-research recipes

Real-GPU evidence from integration ancestors:

  • Qwen3-8B agentic rollout on two H20 workers: multi-turn calculator execution, ragged trajectories, cross-worker aggregation, and driver hydration passed (b4ed9375).
  • Qwen3-8B ALFWorld GRPO: fixed eight-game set, up to ten environment turns, reward approximately 0.53 -> 0.85 with a 0.94 peak (9cd7f3ab).
  • Qwen3-1.7B deep research with a Qwen2.5-72B judge: batch 128, eight siblings per prompt, reward approximately 0.22 -> 0.50 over 19 rollouts before an infrastructure failure (ce13a48d).
  • Stable Diffusion 3 was exercised end-to-end through all three rollout paths: trainside, vLLM-Omni, and SGLang diffusion.
  • The original autoregressive (AR) recipe completed multi-rollout training and produced an increasing reward trend (W&B).
  • The original Prompt Enhancement (PE) recipe completed multi-rollout joint training and produced an increasing reward trend (W&B).
  • FLUX.2 Klein 4B image editing (IT2I) passed on the current draft tip (159627bd) on 8× H20 with a local PickScore smoke override: 8 source-image prompts × 2 edits, the three-Part text → source image → generated image lineage and typed image_latent/image_latent_ids replay conditions passed on every rank, and one optimizer step completed with train/has_backward=1 and nonzero grad norm (W&B).
  • The original checked-in BAGEL + EditReward IT2I recipe (diffusion/bagel/bagel_editreward) completed on 8× H20 with EditReward isolated on a second pod: the default step-0 deterministic evaluation ran, followed by 3 full rollouts at the default 8 prompts × 8 edits (192 training edits total), 14 denoising steps, and 2 optimizer updates per rollout (6 total). All 32 recipe scoring batches, plus the separate two-turn preflight, returned HTTP 200; rollout rewards were -0.7940, -0.9308, and -0.7629; and eight 1024×512 source|edited previews synced successfully (W&B). This validation used two working-tree fixes not yet committed to the PR: hydrating the source-image TensorRef before media-preview indexing and treating EditReward's blank output_dir as its /tmp inference default.

Pending before ready-for-review:

  • Re-run the agentic barrier, partial-rollout, and asynchronous recipes on the final draft tip after the sync-only engine refactor and latest-main merge.
  • Re-run a final-tip AR job covering the merged unrestricted top_k behavior.
  • Re-run the original BAGEL IT2I recipe on the committed current-head fixes so that the successful hardware result is final-tip evidence.
  • Record exact supported Python/CUDA/dependency environments, commands, datasets, hardware, and logs for the final GPU matrix.

Compatibility / Risk

  • Breaking public API change: RolloutReq, RolloutResp, and RolloutTrack are removed without compatibility aliases. See unirl/types/README.md for field/helper mappings.
  • The patch spans the Sample/Part foundation plus every existing rollout consumer; choosing either side of the migration piecemeal would leave incompatible engine/model/trainer contracts.
  • SGLang and vLLM-Omni require separate pinned CUDA environments. The final clean-install matrix is pending.
  • Agentic evaluation is not implemented. Barrier/partial variants require evaluation disabled; asynchronous variants currently force it off.
  • Partial carry is supported only for resumable/stateless environments. Stateful ALFWorld episodes and stateful tool sessions use the drop policy.
  • Checkpoints restore model/optimizer state, not in-flight rollout buffers, carried environment episodes, or tool sessions.

Reviewer Notes

Suggested review order:

  1. unirl/types/ Sample/Part lineage and migration contract.
  2. Existing model, rollout, reward, and trainer migrations.
  3. SGLang concurrency plus unirl/rollout/loop/ and tools.
  4. Agentic barrier/partial/async trainers, environments, and recipes.

Overlap/sequence needs maintainer agreement:

Temporary integration-only smoke scripts and the top-level test tree used during development were intentionally removed from the upstream patch; the retained validation evidence is summarized above. Before ready-for-review, we will complete the final-tip GPU matrix and perform a subsystem-by-subsystem human review. This integration was developed with substantial AI assistance (Codex); the human submitter will review and defend the final diff before the draft is promoted.

Checklist

  • I reviewed the changed code and removed unrelated/generated artifacts.
  • I updated tests, docs, and configs where needed, or explained why not.

Current-head maintainer validation (28a27e4)

  • Merged current main (fce4f4b) and resolved the Sample/Part migration conflicts; GitHub now reports the PR mergeable.
  • Committed the two fixes used by the successful BAGEL/EditReward run: hydrate the source-image transport reference before preview indexing, and use /tmp/editreward_output when the vendored config leaves output_dir blank.
  • Ported current-main Qwen3 agent-SFT message/tool support and prompt-content eval seeding to the Sample-native API. Explicit eval noise keys are a CONCAT field, so DP split/select preserves row alignment.
  • Removed all nine test files introduced by this PR. The final diff against main contains no tests/** changes.
  • Validation: pytest tests -q (25 passed); 32 deleted historical PR regression cases executed in memory (all passed, no files restored); compileall; full pre-commit; all seven new agentic recipes and four modified diffusion recipes composed and resolved.
  • Plain repository-wide pytest also runs the same 25 tests successfully, then mis-collects six existing command-line functions in unirl-reward-service/scripts/test_videoalign.py because they require CLI arguments; that script is unchanged relative to main.

This removes the textual merge conflict and known uncommitted BAGEL fixes, but does not make the patch low risk. Final-tip agentic/AR/BAGEL GPU reruns, dependency-matrix evidence, patch-size cleanup, and subsystem human review remain required before merge.

celve added 30 commits June 24, 2026 18:30
…-446)

Squash merge of origin/LIN-446/main into LIN-444/main. Combines:
- add Sample/Part endomorphism types
- derive Sample/Part lineage from the sample_ids path
Replace the RolloutReq/RolloutResp request/response pair with the unified
recursive Sample/Part types across the dedicated rollout engines —
generate: Sample → Sample, in place, no bridge. The model pipelines, the
trainside engine, the trainers, reward, and the train stack stay on the old
types for a later pass, so this branch is intentionally not runnable as a
full training loop yet.

Engines converted:
- sglang (AR text/vlm), sglang_diffusion (image), vllm_omni (sd3 / qwen_image
  / hv15 single-DiT + HI3 multi-stage), composed (PE AR→diffusion chain).
- base.py ABC flipped to generate(self, sample: Sample) -> Sample;
  engine/__init__ chunked_engine_generate_req -> chunked_engine_generate
  (split-by-root -> regroup -> concat, the Sample analogue of slice-by-index).

Mechanics:
- Pre-forked gen shells are filled by sampling_params type; positional lineage
  replaces parent_track; σ pinned onto DiffusionSamplingParams.sigmas; the x_T
  noise key is derived from the path-id lineage (OD-2); stage_config moved to
  Part.control.
- Multi-input multimodal (chained input Parts) is gated with clear deferral
  raises — a deferred non-goal of this pass.

P0 type fields: Part.control / conditions / fill,
DiffusionSamplingParams.sigmas / init_noise_latent_shape,
LatentSegment.initial_latents.

Verification (rollout-only, no training):
- scripts/check_sample_roundtrip.py structural oracle (5/5 contracts).
- Real GPU rollout smokes pass for every converted engine and modality class:
  AR (sglang/Qwen3), diffusion (vllm_omni sd3, sglang_diffusion sd3), and the
  multi-stage composed PE chain (AR→diffusion).
Wire the previously-gated image+text / cot_text modalities (hi3_it2i,
hi3_i2t, the sglang vlm path, and hi3_dit_recaption) onto the
generate(Sample) -> Sample boundary. The type layer already supported it,
so this is mostly un-gating: a second input rides as a chained input Part
via the new Part.input_child(primitive) (branch-1 child, no sampling_params)
so only the head stays a root, and Sample.conditioning() surfaces both
primitives in turn order. Adapters locate the chained input by primitive
type (image_input_part / new cot_text_from_sample); hi3_dit_recaption gets
the ported cot_text build.

Also fixes sglang text.py build_response to preserve intermediate input
Parts (return [*parts[:-1], filled] instead of [parts[0], filled]) so a
multi-input chain's gen Part keeps a valid parent.

Verified engine-only with hand-built Samples: oracle check_sample_roundtrip
7/7 (incl. multi-input image-chain + cot_text fixtures), a real Qwen2.5-VL
VLM run, and a full HunyuanImage-3 80B IT2I GPU run (image+text -> AR
recaption -> edited image, chain filled).
Migrate the consumer side (all five trainers + train stack + wandb logging) from
RolloutReq/RolloutResp/RolloutTrack to the Sample/Part endomorphism model
(LIN-446) — the counterpart to LIN-454's rollout-engine conversion. Trainers now
build request Samples (Part.input + .fork) and consume response Samples (frontier
= parts[-1], stages located by sampling_params type); GRPO advantage /
reward-propagation / token-balancing run through the methods already on Part/Sample.

Scope: all trainers incl. it2i (multi-input via Part.input_child) and the HI3
unified two-engine path. Out of scope / unchanged: the reward service
(score_and_attach migrates on a separate branch — its (*, req, track) signature
does not yet match the trainers' score_and_attach(sample) call, so the train tail
is gated on that branch), the trainside engine, and the rollout-engine internals.

Highlights:
- Sample/Part navigation on the type: root_group_ids, gen_parts/gen_part/
  gen_part_index, with_parts, and tree-aware slice/select (a request Sample
  DP-scatters by prompt-tree, not by the parts list).
- Per-trainer request builders + the HI3 two-engine _run_rollout_one stitching
  (AR recaption -> DiT image); the DiT x_T noise key is re-rooted from the
  globally-unique lineage so it stays unique across dp>1 replicas.
- UnifiedModelTrainStack.train_track scatters the [input, ar, image] lineage as a
  unit so both stages shard at the same prompt boundaries.
- wandb on Sample/parts: diffusion stage named "diffusion" consistently;
  num_samples reports the generated-sample count; zero-std groups bucket by the
  advantage grouping (advantage_group_ids).
- disable_driver_xt flag on DiffusionSamplingParams (restores the unified DiT
  driver-x_T escape hatch).

Verification: scripts/check_sample_roundtrip.py (14 contracts) + SD3 (vllm-omni),
AR (sglang), and HI3 two-engine generation smokes pass on GPU. The dp>1 cluster
is contract-tested + A2 engine-verified; its multinode end-to-end behavior is not
yet verified, and the full train loop awaits the separate reward migration.
Convert the model bundle's rollout boundary from RolloutReq/RolloutResp to the
recursive Sample/Part types — the model-bundle half of the refactor, pairing with
LIN-480 (trainers / train-stack):

- SD3Pipeline.generate / Qwen3Pipeline.generate take a request Sample (pre-forked
  gen frontier) and fill it; a factored _conditions_for() is the single encode
  path shared by rollout and trainer-side replay (re-encode; Part.conditions left
  empty — no cache).
- TrainsideRolloutEngine.generate -> Sample -> Sample: _ensure_sample_sigmas pins
  sigma onto the gen part's DiffusionSamplingParams; Part.slice/concat chunking.
- NoiseRecipe.from_sample() — Sample-shaped x_T builder keyed on the gen part's
  path-id lineage.
- check_sample_roundtrip.py +2 contracts (from_sample lineage, generate-fills-
  frontier); trainside_{sd3,ar}_smoke.py real-GPU rollout+replay smokes.

Validated on H20 (pod unirl-gz-2): CPU oracle green; SD3 rollout->replay
mean|dlogp|=0 (ratio~1); Qwen3 AR replay self-consistency 0 (the
old_logp_source=replay contract — the in-process autoregress bf16 record vs
replay fp32 differs ~3-5, harmless since trainside recipes use replay for
old_logp). Stages (diffusion.py/ar.py) untouched.
Convert the reward adapter from RolloutReq/RolloutTrack to the Sample/Part
endomorphism model — the consumer-side counterpart LIN-480 left gated. The
trainers already call score_and_attach(sample); this matches that contract.

- RewardService.score_and_attach(*, req, track) -> RolloutTrack becomes
  score_and_attach(self, sample: Sample) -> Sample: score the frontier Part,
  build input primitives from Sample.conditioning() (nearest-ancestor caption),
  generated from frontier.primitive keyed by the backend preferred_input_kind,
  and root-sourced prompt metadata via the new Sample.root_metadata.
- AR truncation/overlong shaping preserved verbatim, re-gated on the scored
  frontier being ARSamplingParams.
- Delete the request/track expansion + metadata-normalization machinery
  (_KIND_TO_KEY, _normalize_prompt_metadata, _build_request_for_track): the
  Sample is row-aligned and DP-scatters by prompt-tree, nothing to reconcile.
- Add primitive_modality_key + Sample.root_metadata; refresh reward
  docstrings/README to Sample/Part.
- Add scripts/check_reward_roundtrip.py structural oracle (6 contracts;
  passes on-GPU-pod with real torch).
…n-stack replay

SD3Pipeline.generate left Part.conditions empty (a "replay re-encodes"
assumption), but the train stack's prepare_segment reads part.conditions
directly with no re-encode wiring — so trainside SD3 training crashed in
SD3Conditions.from_dict (text=None). Every sibling diffusion pipeline and
the SD3 sglang_diffusion adapter already populate conditions via
fill(conditions=...); bring trainside SD3 in line.

Surfaced by the SD3 trainside e2e (trainside_sd3_smoke bypassed this by
calling stage.replay directly, never going through train_track).
…rollout

texts_from_sample asserted prompt count == gen sample count, but the
request keeps the input Part un-fanned (one prompt per group) while the
gen shell fans out samples_per_prompt via Sample.fork — so SD3/qwen-image
served rollout crashed on rollout 1 with "prompt count 6 != gen sample
count 96". vllm_omni runs num_outputs_per_prompt=1 (one output per gen
sample), so tile each prompt across its group-by-parent-contiguous gen
siblings. A 1:1 request (no fan-out) is unchanged.

Surfaced by the SD3 sd3_vllmomni e2e (served LIN-454 path).
…feUnpickler

sglang 0.5.12's SafeUnpickler (CVE-2025-10164 guard) runs in the engine
process and blocked the colocate tensor weight-sync: update_weights_from_tensor
deserialization rejected unirl's vendored FlattenedTensorBucket /
_rebuild_cuda_tensor_modified (both under the unirl. namespace) with
"Blocked unsafe class loading". Register "unirl." on sglang's server-side
allowlist in SGLangRolloutEngine.__init__ (the tp_size=1/use_ray=False
scheduler runs in this process), mirroring what unirl's own SafeUnpickler
already permits.

Surfaced by the Qwen3 DRPO AR e2e (qwen3_drpo_4b_base_dapo_sglang): the
full vertical (sglang gen -> MathVerify -> DRPO -> FSDP train) ran and
crashed only at the first weight-sync.
… compat)

The prior allowlist patch (a1f33d1) registered "unirl." on the engine
process's SafeUnpickler, but sglang's SRT scheduler deserializes the
weight bucket in a SEPARATE spawned subprocess with a fresh allowlist, so
the patch never took (same "Blocked unsafe class loading" recurred).

Instead, serialize with sglang's NATIVE FlattenedTensorBucket /
MultiprocessingSerializer / monkey_patch_torch_reductions (verified
identical device-UUID IPC mapping) so the wire references sglang's own
allowlisted classes — matching what the sglang_diffusion engine already
does. Reverts the ineffective __init__ allowlist patch.

Surfaced by the Qwen3 DRPO AR e2e: full vertical ran, crashed only at the
initial colocate weight-sync into SRT.
…hape

Convert every model pipeline's generate() from RolloutReq → RolloutResp to the
recursive Sample/Part endomorphism, completing the model-bundle half of the
rollout refactor (pairs with the earlier LIN-454/479/480/481 engine/trainer/
train-stack/reward migrations), then retire the legacy types.

- Base contract: Pipeline.generate(self, sample: Sample) -> Sample; lift the
  request-side Sample readers out of the rollout layer into types/sample_ops.py
  so pipelines import them without a models→rollout inversion.
- 7 Tier-1 diffusion pipelines (qwen_image, z_image, wan21, wan22, hunyuan_video,
  hunyuan_video15, ltx2): read prompt via sample.conditioning(), gen params off
  the frontier Part, fill the frontier shell; factor _conditions_for() shared by
  rollout + trainer-side replay. i2v image arrives via the input_child chain.
- qwen_vl + flux2_klein (AR/edit with one extra input modality).
- pe, bagel, hunyuan_image3 (multi-stage 2-gen-part flows): PEPipeline consumes
  the pre-forked [input, ar_shell, diff_shell] Sample and drives the child
  pipelines (mirrors the migrated ComposedRolloutEngine); bagel dispatcher reads
  task from parts[0].control and fills one or both gen Parts; HI3 dispatcher + 5
  modes/ (incl. two-part t2ti).
- Retire unirl/types/rollout_req.py + rollout_resp.py; relocate PrimitiveValue to
  types/primitives.py; drop the dead NoiseRecipe.from_rollout_req; sweep doc
  breadcrumbs across conditions/algorithms/sde/engine docs.

All pipelines carry conditions=<conds>.to_dict() on frontier.fill — the train
stack reads Part.conditions (GRPO/FlowGRPO re-type via conditions_cls.from_dict);
it does NOT re-encode. (Caught by E2E pe-trainside on unirl-gz-2: an earlier
revision left conditions empty and crashed at the first AR train step with
"Qwen3ARConditions.from_dict: expected d['prompt'] ... got None".)

Validated on TaiJi unirl-gz-2 (8×H20, GZ): compileall green; sd3-trainside reward
grew 0.74→0.79 with ratio=1.0 / advantage_std≈0.49; pe-trainside (SD3+Qwen3-0.6B)
runs the full 3-part chain with both AR + diffusion stacks training.

Adds scripts/trainside_{qwen_image,qwen_vl,pe}_smoke.py (tier-representative GPU
rollout→replay smokes).
…ti-turn encode (both backends)

Squash of LIN-503/main (8 commits) onto LIN-444/main. Takes the rollout Sample from
single-turn-only to full multi-turn, multi-modal conditioning end-to-end.

- Role-aware trajectory layer on Sample/Part (Phase 1/2): Turn, Part.role /
  resolved_role, turns(), text_conditioning() / vision_conditioning() fail-loud
  renderers, replace_frontier / with_filled_frontier write-back; diffusion/omni
  engines wired; sample_ops retired.
- gap C — sglang AR engine encode consumes the trajectory: text.py / vlm.py build the
  chat conversation from text_/vision_conditioning() via a pure transpose + de-expand
  util (rollout/engine/sglang/utils/conversations.py); resolve_sampling fan-out fixed
  to the last-fork branch (parts[-2]).
- Trainside conjugate — qwen3 / qwen_vl in-process encoders consume turns() via
  models/types/conversations.py (transpose, no de-expand, inline-PIL VLM fusion);
  stale "_conditions_for re-tokenize" docstrings + trainside smokes fixed.
- Behavior-preserving on single-turn (byte-identical); parity-safe by construction
  (replay teacher-forces over the stored conditions["prompt"], not a re-encode).
- Verification: CPU oracles (engine 8/8, trainside 6/6, layer 7/7) + 4 GPU multi-turn
  smokes (sglang/trainside × text/VLM) green on Qwen3-4B-Instruct and
  Qwen2.5-VL-3B-Instruct — the captured prompt carries user→assistant→tool (+ image),
  replay ratio=1.
…t (LIN-492)

Add the agentic (multi-turn tool) rollout layer over the Sample/Part model, on top of
LIN-503 multi-turn conditioning. Scope is ROLLOUT only — trainer wiring, reward, and a
dataset for agentic GRPO are a follow-up (the trainer is still single-turn).

- AgentLoop (unirl/rollout/loop/): environment-driven synchronous loop —
  fork -> generate -> env.step -> observe, bounded by max_turns. Structural
  Environment / RolloutEnginePort protocols; the existing SGLang engine satisfies the
  port unchanged.
- ToolEnvironment + tools/ (Tool, CalculatorTool): parse <tool_call>, run a registered
  tool, feed the result back as a role="tool" observation, stop on a final answer (or
  max_turns). Safe ast-based calculator (no eval).
- Sample.observe(role="tool"): append the world-response as a mask-0 input Part tagged
  for LIN-503 role-aware rendering (<tool_response>). Additive — no existing code path
  changed; the whole merge is 1648 insertions, 0 deletions.

Verified:
- CPU oracles: scripts/tool_env_smoke.py (9 checks), scripts/rollout_loop_smoke.py (6),
  scripts/check_sample_roundtrip.py (16, regression) — all green.
- GPU (Qwen3-4B-Instruct): single-turn tool call (tool_env_ar_smoke); the closed loop —
  model calls the calculator, sees the exact result, and answers (tool_env_ar_loop_smoke);
  and a 72-trajectory reliability run at 97.2% correct with clean termination
  (tool_env_ar_reliability).

Known follow-ups: trainer integration (multi-gen-Part train step + reward + dataset),
per-sample termination for n>1 heterogeneous batches, more tools.
… agentic)

Squash-merge of LIN-499/main: the async per-group engine contract (agenerate core + sync generate facade + abort/pause/resume), all five engines migrated, Part.weight_version provenance, DevicePool.worker_max_concurrency knob, and the CPU async-contract test suite (11 tests pass on the merged tree).

Conflict in types/sample.py resolved by keeping BOTH agentic's Part.role (LIN-503) and LIN-499's Part.weight_version — adjacent new fields, not mutually exclusive.
…ator)

AgenticRolloutEngine drives multi-turn (tool-use) rollout: a rank-0 coordinator
(BROADCAST+RANK_ZERO, the NCCLWeightSync pattern) over a DP slab of per-worker
persistent drain loops. Each worker runs one run_until_complete that pulls
single-trajectory tasks from rank 0 and runs them as multi-turn agent loops on its
inner engine's event loop (continuous-batched via the inner backend's semaphore).
generate returns a flat List[Sample] of variable-depth trajectories; the GRPO
n-group is recovered by bucketing on the shared prompt root id.

- base: widen generate to RolloutOutput = Sample | List[Sample] (single-turn
  returns Sample, agentic returns List[Sample]); per-turn seams stay -> Sample.
- engine/agentic: coordinator (set_workers/generate/next_task) + per-worker drain
  (run_drain/_drain/_run_one/_pull); _run_coro delegates to the inner so the drain
  and weight-sync verbs share one lock (the quiesce boundary); a per-worker
  trajectory cap distinct from the backend request semaphore; lifecycle/weight-sync
  verbs delegate to the inner engine.
- loop: add async astep to the Environment protocol + ToolEnvironment (non-blocking
  tool boundary); make step re-entrant (turn derived from the sample, not a mutable
  counter) so one env serves concurrent trajectories; add agenerate to
  RolloutEnginePort.
- tests: CPU contract tests (ragged List[Sample] + bucket-by-root, cap saturation,
  two distinct bounds, pull load-balancing, failure isolation, FIFO next_task) +
  astep parity / re-entrancy / slow-tool-yields.
- scripts: multi-worker GPU smoke. Validated on a 2-worker Qwen3-8B H20 slab:
  ragged List[Sample] with correct tool-use answers, and cross-worker rank-0
  aggregation proven safe (gen segments produced on 2 distinct workers, all spans
  plasma-backed in Ray's global object store, 48 TensorRefs hydrate from the driver)
  — risk #1 closed, no run_drain materialization needed.

LIN-522
…earch tools, ALFWorld

Land the full multi-turn agentic RL capability on top of AgenticRolloutEngine.

AgenticTrainer (unirl/trainer/agentic.py)
  GRPO over variable-depth trajectory lists: group-relative advantage across the n
  siblings of a prompt, every assistant turn concatenated into ONE on-policy
  train_track step (ratio ~ 1). The per-trajectory reward step (_rewards_and_groups)
  is overridable so tasks swap only the reward SOURCE.

Deep-research task (answer-graded)
  M1 calculator + MathVerify and M2 search/visit tools + LLM-judge reward, with
  recipes and the train_deep_research entrypoint.

ALFWorld baseline (AgenticEnvTrainer, env-sourced reward)
  Stateful per-trajectory ALFWorld adapter, an engine hook that attaches the
  environment's terminal-success return to the last turn, admissible-action snapping
  to avoid TextWorld PDDL crashes, and crash-exclusion (NaN reward -> neutral, zero
  advantage) so env-bug crashes cannot corrupt the GRPO gradient. Verified on-pod:
  task-success reward rises ~0.53 -> ~0.85 (peak 0.94), on-policy, on a fixed 8-game
  set (genuinely multi-turn: up to 10 env steps per trajectory, n=8 GRPO group).

Testing: CPU contract tests cover the plumbing (reward attach, group advantages, env
adapter, train_step assembly). The GPU end-to-end path is validated empirically (the
rising curve), not yet in CI.
Add a StatefulTool seam so tools can hold per-trajectory state across
turns, with guaranteed teardown and a first out-of-process tool.

- StatefulTool(Tool): session_start -> execute_session -> session_end,
  keyed by a session id carried in the root control bag. ToolEnvironment
  dispatches on isinstance, so the stateless Tool path is unchanged.
- ToolEnvironment: reset mints a uuid4 session id per stateful tool and
  stamps it into control["tool_sessions"] via _part_with_field (returns
  the request unchanged when there are no stateful tools); step/_run
  dispatch execute_session; async aclose ends sessions in the executor,
  swallowing errors.
- AgenticRolloutEngine._run_one: finally-hook calls env.aclose on every
  path (success, crash, abort), duck-typed via getattr and wrapped so it
  can never re-raise into the drain.
- AlfworldEnv.aclose: reclaim the episode + pooled template — fixes a
  leak when a trajectory dies in the engine between turns.
- SandboxTool: persistent per-session Python REPL subprocess (lazy spawn
  in the executor, select-based timeout, killed on session_end);
  validates cross-turn variable reuse.
- Tests: StatefulTool lifecycle + teardown on success and forced
  exception, SandboxTool REPL, and an ALFWorld leak regression.
Over-generate, interrupt generation at a turn boundary on weight sync, commit the fast complete GRPO groups, carry/drop the slow tail.

- Engine: submit/poll/abort/drained over a background buffered drain; turn-boundary checkpoint-and-resume in _run_one.
- Colocate driver: AgenticPartialTrainer / AgenticEnvPartialTrainer — keeps all GPUs on generation while cutting the straggler tail.
- Disaggregated driver: AsyncAgenticTrainer / AsyncAgenticEnvTrainer.
- Tail policy: carry (resumable tool envs) vs drop (stateful envs, ALFWorld).
- Shared _advantage_train_and_log extracted from the barrier train_step (barrier path preserved); entrypoints + ALFWorld/deep-research recipes; per-rollout turn-histogram + committed/dropped instrumentation.

ALFWorld speed study: colocate-partial beats the barrier only with group-level depth variance — Qwen3-8B ~18% faster, Qwen3-0.6B 27% slower.

Integrates with LIN-533 teardown: the _run_one finally aclose now also fires on the abort/checkpoint path, releasing carried trajectories' episodes/sessions.
…udge)

Reproduces AReaL's tongyi_deepresearch deep-research agent in UniRL's
agentic stack (sync AgenticTrainer + AgenticRolloutEngine):

- Verbatim Tongyi SYSTEM_PROMPT for the M2 recipe (deep_research_search_judge)
- Hardened SearchTool: serper + serpapi providers, retries/backoff
- Hardened VisitTool toward AReaL tool_visit.py: Jina read retries,
  structured evidence/summary extraction, content truncation
- Robust LLM-judge verdict parsing (negative-first regex; fixes "not correct")
- Cross-config invariant guards (AgenticTrainer + AgenticRolloutEngine)
- prepare_asearcher: default ASearcherBase35k split, streaming load
- Committed GPU training smoke (scripts/train_deep_research_smoke.py)

Validated live: reward climbed 0.22 -> 0.50 over 19 rollouts (Qwen3-1.7B
policy, Qwen2.5-72B judge, batch 128, n=8) before an infra-driven collapse.

Follow-ups (not in this change): multi-turn token-recording reconciliation
(Miles-style trim/accumulate) and a trajectory context-token cap for
long-horizon runs. The shipped recipe defaults to max_turns=8 (safe).
CPU unit test for _parse_verdict guarding the negative-first parse against
the "incorrect" ⊃ "correct" substring trap and "not correct" / "wrong"
phrasings the prior substring test misread. 20 cases; the coverage Phase C
intended but the M2 squash omitted. Verified: 20 passed on agentic@ce13a48d.
Collapse the grouping/lineage duplication on the Sample/Part model:

- sample_id: add ancestor_id(sid, depth) — grouping labels are id-prefix
  projections. root_group_ids and Sample.split project directly; the
  _root_groups_per_part walk (re-deriving what __post_init__ validated)
  is deleted.
- compute_advantages: the group_ids label-list override becomes
  group_layer (None = immediate parent, 0 = root prompt; PE
  diffusion_group_scope="prompt" now passes group_layer=0 instead of
  threading root_group_ids labels). scope stays the normalization mode,
  with the historical global branch (unbiased std) kept verbatim for
  bit-parity with the shipped adv_normalization_scope: global recipes.
- delete Part.advantage_group_ids (its only reader, the zero-std wandb
  metrics, buckets by sibling group_ids) and the dead Part.split.
- kind signal: fork requires sampling_params (a paramless gen shell is
  unrepresentable); Part.is_gen is the single predicate behind
  gen_parts / resolved_role / the wandb gen filter. Ids stay pure
  addresses; role stays presentation-only.
- contracts: check_group_layer_advantages pins per-layer GRPO values,
  the unbiased-std global formula, and fail-loud on paramless fork and
  negative group_layer.
…backends

Drop agenerate/run_session/CoroutineFactory from the rollout-engine contract
(BaseRolloutEngine, BaseSingleTurnRolloutEngine) and make the SGLang backends
safely callable from concurrent threads, so the agentic drain can drive sync
generate from one thread per trajectory:

- native: replace the SessionRunner run_until_complete-under-drive-lock model
  with a serve/park LoopThread over SGLang's own engine.loop — callers submit
  coroutines threadsafe and stay in flight together; weight/memory verbs
  require quiesced generation and run with the loop parked (the Engine's sync
  wrappers still drive the idle loop themselves).
- http: pure sync (urllib + threading.Semaphore + per-batch thread fan-out);
  the httpx client and the backend-owned event loop are gone. Controls are
  bounded 10s best-effort POSTs.
- Backend protocol: generate is sync + thread-safe; async surface removed.

New CPU tests cover the LoopThread lifecycle (concurrent overlap, semaphore
bound, park/serve, quiesce guard, close-waits) and the sync HTTP backend
against a stdlib ThreadingHTTPServer stub.
trainside, vllm_omni, sglang_diffusion, composed: delete agenerate (a
to_thread wrapper over the locked sync path) and run_session + the
LocalAsyncRuntime each held only to serve it; their sync generate paths are
unchanged (the generate lock stays their concurrency story). Drop agenerate
from the RolloutEnginePort protocol (its only consumer, AgentLoop, is sync).
Delete unirl/rollout/engine/runtime.py and its test file; SessionRunner keeps
a local transitional CoroutineFactory alias until the test fakes move off it.
…suite

AgenticRolloutEngine now drives trajectories on a per-drive thread pool
(per_worker_concurrency threads = the trajectory cap; a thread holds its
trajectory across tool-wait, preserving the two-bounds design) instead of
coroutines on the inner engine's loop: _drain_worker pulls (blocking ray.get)
and runs a fully-sync _run_one (inner.generate per turn + env.step + duck-typed
env.close teardown). run_drain joins every drain thread and re-raises the first
failure; a failed worker sets _stopping so siblings checkpoint. The barrier
agenerate (test-only) is deleted; coordinator verbs and the turn-boundary
checkpoint/carry semantics are unchanged, and the weight-sync quiesce is now
'abort joined the drain threads => decode-idle'.

Environment protocol: astep/aclose (bridges that existed only for the deleted
loop model) are removed; sync close(sample) joins reset/step as the guaranteed
teardown hook (ToolEnvironment._end_sessions / AlfworldEnv._teardown_episode
bodies, now public). SessionRunner is deleted (backends/base.py is protocol-
only again). Tests: _fakes.py is a sync thread-safe backend/engine pair with a
hold-gate for deterministic overlap; the engine tests assert the same
contracts under threads (cap saturation, two distinct bounds, checkpoint/
resume, conservation, continuous batching); loop tests use step/close, with
thread-based re-entrancy replacing the astep suite.
Rewrite the README's generation-interface and extending sections for the
sync-only contract (threads, not asyncio; concurrent-caller requirement for
agentic inners; the loop survives only inside the native SGLang backend).
Update tool/env docstrings that still referenced the deleted astep/aclose/
SessionRunner, and guard the HTTP batch path against an empty wire (the old
gather path returned [] there; a zero-worker pool would raise).
Keep the Sample/Part architecture while porting main's model, rollout, reward, and training behavior. Migrate decoded outputs to typed primitive maps and remove retired rollout response APIs.
# Conflicts:
#	unirl/rollout/engine/sglang/utils/sampling.py
@celve celve mentioned this pull request Jul 27, 2026
2 tasks
@celve

celve commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator Author

PE:
image

@celve

celve commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator Author

qwen omni:
image

@celve

celve commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator Author

DRPO:
image

@celve

celve commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator Author

SD3 vLLM Omni:
image

celve and others added 6 commits July 27, 2026 20:23
The six agentic entrypoints were the only ones in the repo named after a
dataset. Every other entrypoint is named for its trainer -- trainer/ar.py ->
train_ar.py, whose docstring states outright that it serves both qwen_vl and
qwen3 recipes. Apply that existing convention:

  train_deep_research.py         -> train_agentic.py
  train_partial_deep_research.py -> train_agentic_partial.py
  train_async_deep_research.py   -> train_agentic_async.py
  train_alfworld.py              -> train_agentic_env.py
  train_partial_alfworld.py      -> train_agentic_env_partial.py
  train_async_alfworld.py        -> train_agentic_env_async.py

Each name now maps 1:1 onto its trainer module, and both axes are legible:
_env is the reward source (environment return vs graded terminal answer),
_partial/_async is the execution topology. Topology is a suffix so the whole
family groups under train_agentic*; train_async_ar.py keeps its prefix rather
than widen the diff.

Recipes stay benchmark-named -- the entrypoint names the capability, the
recipe names the task. Each docstring gains a train_ar.py-style line naming
the recipe family it serves, and the README tables become reward-source x
topology matrices instead of benchmark lists.

Bodies are unchanged: same trainer classes, same constructor arguments, same
config_name defaults. No behavioral change.

Note: python -m unirl.train_alfworld and train_deep_research no longer exist;
six user-facing module paths moved.
Three files added by adf70e8 were never run through ruff-format, so
`pre-commit run --all-files` -- which the Lint workflow runs on every PR to
main -- fails on this branch. Formatting only; no logic touched.
… barrier

7ad5b34 established that an infrastructure fault must be excluded from GRPO
rather than scored as a genuine miss -- "scoring an infrastructure fault as a
genuine miss manufactures a gradient for every sibling in the group" -- but it
only reached AgenticEnvTrainer. The colocate-partial and fully-async env
variants each carried their own copy of _rewards_and_groups and kept the
pre-fix 0.0, agentic_env_async.py even retaining the comment "gen-less /
failed trajectory stays a legit group member" that the fix says it reverses.

Concretely: when a trajectory faults before its first turn the engine attaches
no reward (_attach_env_reward returns early on an empty gen list), so the
trainer's missing-reward branch decides the sentinel. NaN is dropped from the
group's mean/std and given zero advantage; 0.0 instead drags the mean, shifts
the std, and hands every sibling a gradient the model never earned. A fault
AFTER turn one was already fine on all paths -- the engine attaches NaN there
and it passes straight through.

Rather than patch two literals, hoist the method into a shared
_EnvRewardSource mixin that all three env trainers mix in ahead of their
trainer base. One implementation, so the next sentinel change cannot land on
some paths and miss others. Net -83/+30 in the trainers; the reward SOURCE
becomes a named concept matching how the docstrings already describe it.

Tests, restoring coverage this branch lost:

- tests/rollout/test_agentic_failure_marking.py, deleted by c566479
  "test: remove tests directory" while the fix it guarded stayed. Restored
  verbatim; it still passes against current code.
- tests/trainer/test_agentic_env_reward_source.py, new. The gap that let this
  bug through: nothing ever asserted the env TRAINERS' sentinel. Parametrized
  over all three env paths, plus a structural test that they resolve to one
  shared function.

Verified against the pre-fix tree: exactly 3 failures, naming the two buggy
paths and the divergence itself. 18 pass after the fix.
Removes the 13 remaining test files (1117 lines), including the two added in
57edcca. No workflow in .github/ runs pytest and the root pyproject declares
no testpaths, so nothing in CI changes.
…opologies

deep_research_calc_mathverify{,_partial,_async}.yaml were bring-up scaffolding:
M1 existed to prove the agentic loop "WITHOUT external services (the existing
CalculatorTool + a rule-based MathVerify reward) ... so M2 only swaps in the
search/visit tools and the LLM-judge reward (config-only)". M2 landed and the
scaffold has no remaining purpose.

The catch: all three were the hydra config_name DEFAULTS for the three
answer-graded entrypoints, and search_judge existed in barrier flavour only.
Deleting them would have left train_agentic_partial and train_agentic_async
with no recipe. Repointing both at the barrier recipe is not an option either:
it declares TensorWeightSync, so the async entrypoint would compose and then
fail at runtime, since disaggregated training needs NCCLWeightSync to cross the
slab boundary.

So port M2 to the two missing topologies, grafting each one's topology block off
the calc recipe it replaces and leaving tools/reward/system-prompt identical:

  deep_research_search_judge_partial.yaml  colocate, TensorWeightSync,
    oversample_batch_size 6, partial_rollout, worker_max_concurrency 24
  deep_research_search_judge_async.yaml    disaggregated, NCCLWeightSync,
    train_fraction 0.5, oversample_batch_size 8, mem_fraction_static 0.8

tail_policy: carry is correct for both -- SearchTool and VisitTool subclass the
stateless Tool, not StatefulTool, so a carried Sample holds all resume state
(the same reason the calc recipes carried).

Verified: all six agentic entrypoints compose against their defaults with
--cfg job --resolve, and the resolved configs carry the right weight-sync class,
mem_fraction_static, and prompts_per_rollout == oversample_batch_size per
topology. check-recipe-targets resolves 2135 paths; hooks and bash -n clean.

Neither new recipe has been train-verified -- they are compose-checked ports.
CalculatorTool itself is left in place; no recipe uses it now, but it stays a
supported library tool.
@celve
celve merged commit 89115c9 into Tencent-Hunyuan:main Jul 27, 2026
5 checks passed
celve added a commit to celve/unirl that referenced this pull request Jul 27, 2026
Mirrors the z_image migration (Tencent-Hunyuan#214): generate() becomes the
Sample -> Sample endomorphism — read the prompt via
sample.conditioning(), take DiffusionSamplingParams (with pinned
sigmas) from the frontier gen Part, derive noise groups from
frontier.group_ids and x_T via NoiseRecipe.from_sample, then fill the
frontier shell with the LatentSegment, decoded images, and replay
conditions. User-supplied negatives are deferred (CFG synthesizes the
"" negative routed to the DROP system prompt). Adds load_vae for
separate-engine recipes (Optional VAE + decode guard).
celve added a commit to celve/unirl that referenced this pull request Jul 27, 2026
Mirrors the z_image migration (Tencent-Hunyuan#214): generate() becomes the
Sample -> Sample endomorphism — read the prompt via
sample.conditioning(), take DiffusionSamplingParams (with pinned
sigmas) from the frontier gen Part, derive noise groups from
frontier.group_ids and x_T via NoiseRecipe.from_sample, then fill the
frontier shell with the LatentSegment, decoded images, and replay
conditions. User-supplied negatives are deferred (CFG synthesizes the
"" negative routed to the DROP system prompt). Adds load_vae for
separate-engine recipes (Optional VAE + decode guard).
celve added a commit that referenced this pull request Jul 28, 2026
…PO recipe (#219)

* feat(boogu_image): vendor Boogu-Image model code verbatim @ 434fb56

Verbatim copy of the 8 model files from boogu-project/Boogu-Image at
434fb56d2a5f6ae6f30cce6a8b56e319e9cd1979 (transformer, attention
processors, rope, Lumina2 block, embeddings, components, import/teacache
utils). No modifications in this commit; mechanical import rewrites and
the SDPA pin follow separately for reviewable diffs.

* refactor(boogu_image): vendor mechanical edits — flatten imports, pin SDPA, stub caches

- flatten cross-package imports to same-dir relatives
- replace triton-RMSNorm / flash-swiglu env conditionals with the upstream
  default-env branches (torch.nn.RMSNorm, pure-torch swiglu)
- pin the four os.getenv("device")-gated attention-processor selections to
  the SDPA processors; keep Flash2Varlen classes for an optional post-load
  swap (identical param names, so checkpoints/LoRA stay backend-independent)
- route TaylorSeer/TeaCache helper imports to raising stubs (RL rollout and
  replay must be cache-free); teacache_util vendored verbatim because
  TeaCacheParams is instantiated unconditionally
- add vendor __init__ + VENDOR_COMMIT.txt documenting every edit and the
  re-vendor recipe

* feat(boogu_image): Base T2I model bundle package

Adds unirl/models/boogu_image/: config/bundle/conditions/text_embed/
diffusion/vae/pipeline implementing the typed pipeline contract for
Boogu-Image-0.1-Base (10.29B vendored DiT + frozen Qwen3-VL-8B mllm
encoder + FLUX.1 AutoencoderKL).

Key model-specific behavior, each mirroring the reference pipeline:
- Qwen3-VL chat-template embed stage with the fixed T2I system prompt;
  empty/whitespace prompts (the CFG negative "") route to the DROP
  system prompt; full right-padded sequence + mask, no repacking
- predict_noise: t = 1 - sigma in model dtype, positional forward with
  bare-tensor return, sequential negative branch gated on
  guidance_scale > 1.0 with plain linear combine, velocity negated to
  the sigma convention; cfg_range collapses to a per-step effective
  guidance scale via sampler_kwargs
- resolution-independent rotary tables cached per device on the stage
- static-v1 schedule pinned as FlowMatchSchedulePolicy.static_only
  (shift = e^1.15; Boogu's scheduler JSON uses custom field names the
  base loader would silently ignore)
- bundle: mllm lm_head strip, flock-serialized loading, trivial-case
  meta-init (zero buffers), optional flash2_varlen processor swap

* feat(boogu_image): trainside FlowGRPO recipe + einops dep

examples/diffusion/boogu_image/boogu_image_trainside.yaml: FlowGRPO +
LoRA + PickScore on 8 GPUs at 512^2 (CFG off in Boogu's convention,
guidance_scale=1.0; documented 1024^2 fallback overrides since Boogu's
native band is 1K-2K). FSDP block_class_names lists the 5 concrete
instantiated block classes (exact type-name matching; the DoubleStream
block is a separate class, not a subclass). LoRA targets the 12
attention projections incl. the double-stream processor-internal
img_*/instruct_* Linears (dot-boundary suffix matching cannot reach
them via to_q). master_dtype fp32 per bagel's reward-collapse fix;
old_logp_source=replay since forward_batch_size != micro_batch_size.

pyproject: add einops>=0.7 (imported by the vendored DiT; pure-python,
engine-agnostic).

* style(boogu_image): ruff-format glue files; exclude vendored tree from hooks

Vendored Boogu code joins bagel/vendor in the pre-commit exclude so
re-vendor diffs stay clean against upstream; the three package glue
files keep the repo formatting.

* refactor(boogu_image): migrate to the Sample-native rollout model

Mirrors the z_image migration (#214): generate() becomes the
Sample -> Sample endomorphism — read the prompt via
sample.conditioning(), take DiffusionSamplingParams (with pinned
sigmas) from the frontier gen Part, derive noise groups from
frontier.group_ids and x_T via NoiseRecipe.from_sample, then fill the
frontier shell with the LatentSegment, decoded images, and replay
conditions. User-supplied negatives are deferred (CFG synthesizes the
"" negative routed to the DROP system prompt). Adds load_vae for
separate-engine recipes (Optional VAE + decode guard).
CjhHa1 added a commit to zzhuoxin1508/UniRL that referenced this pull request Jul 29, 2026
Brings in the sample-native rollout boundary (Tencent-Hunyuan#214), which removed
unirl/types/rollout_req.py and unirl/types/rollout_resp.py along with the
RolloutReq / RolloutResp / RolloutTrack triplet.

Conflict resolution in unirl/trainer/diffusion.py takes main's version and
re-applies this branch's evaluate() seam (sync_weights / sleep_after) plus the
_train_fraction field on top of it.

unirl/trainer/async_diffusion.py still imports the deleted types at this
commit, so it does not import here; the next commit migrates it.
CjhHa1 added a commit to zzhuoxin1508/UniRL that referenced this pull request Jul 29, 2026
…sync runtime

Two things broke this branch against current main, and both are fixed here.

The trainer was written against the retired RolloutReq / RolloutResp /
RolloutTrack triplet, deleted by the sample-native rollout boundary (Tencent-Hunyuan#214). It
is now sample-native: the request is the Sample from _build_request_sample,
scoring is reward.score_and_attach(sample) on the self-contained filled Sample
instead of the old (req=, track=) pair, groups reassemble with Sample.concat,
and the RolloutResp(tracks=...) rebuild and its _track_key bookkeeping are
gone. The entry point's stage_config was likewise renamed to task_config.

The async buffer / generate seam this branch duplicated from AsyncARTrainer has
since been lifted into unirl/rollout/async_runtime.py, the follow-up refactor
this PR's description anticipated. _RolloutBuffer, _generate_async,
_collect_resp, _is_ready, _launch, _reap_ready and the _next_batch loop are all
replaced by AsyncRolloutScheduler + RayGenerationDispatcher, leaving only the
diffusion hooks: build a request Sample, score-and-split at reap time, and
advantage + FlowGRPO step.

Adopting that runtime needs one addition to it, because it launched before it
reaped and this path requires the opposite. Reaping pulls the trajectory
segment off the rollout slab as an NCCL send issued on the rollout workers, so
a generation launched ahead of that send blocks it -- the ~150s/rollout instead
of ~8s that reap-before-launch was introduced to fix. Reap-first at
max_inflight=1 hands the send idle workers while still launching before the
step returns, so the next generation overlaps the caller's train step. The new
reap_before_launch flag selects the order and defaults to the existing
launch-first behavior, so the AR path is unchanged.

Verified: ruff check and format clean, the trainer and entry point import
against current main, Hydra compose of the BAGEL async recipe passes, all
recipe _target_ paths resolve, both constructor guards fire before any Ray
construction, and a fake-dispatcher check confirms reap-first at
max_inflight=1 both keeps one generation in flight across every train step and
always reaps against idle rollout workers. Not re-run: the GPU reward-curve and
localize-timing validation in the PR description.
leviking98z-rgb added a commit that referenced this pull request Jul 29, 2026
…n RL (#192)

* feat(trainer): add AsyncDiffusionTrainer for disaggregated async diffusion RL

Diffusion sibling of AsyncARTrainer: subclasses DiffusionTrainer(layout=separate)
to reuse the two-slab build + NCCLWeightSync handshake, and overlays the async
rollout buffer loop (non-blocking generate, reap-time reward scoring off the train
critical path, buffer of scored GRPO groups, train consumes the freshest batch).
Knobs: max_inflight (overlap depth), buffer_max_staleness (0=on-policy).

Adds unirl/trainer/async_diffusion.py, unirl/train_async_diffusion.py, and
examples/diffusion/sd3/sd3_vllmomni_async.yaml. Purely additive.

* Overlap async diffusion rollout with training via reap-before-launch segment transfer, add BAGEL async recipe, drop SD3 async recipe

* chore(trainer): make train_async_diffusion.py executable to match train_diffusion.py

* fix(trainer): point async diffusion entry at BAGEL recipe with stale=2

Default Hydra config still pointed at the dropped SD3 async yaml. Point
train_async_diffusion at bagel_vllmomni_async and set buffer_max_staleness=2
(the throughput-optimal knob from the PR validation table).

* fix(trainer): keep async diffusion evaluation policy-stable

Evaluate the resident rollout policy without syncing or offloading the async engine, while preserving synchronous defaults and forwarding configured eval suites.

* docs(trainer): clarify async reward scoring boundary

State consistently that generation overlaps training while reap-time reward scoring remains synchronous.

* fix(trainer): reject unsupported async diffusion depth

Fail before worker construction unless max_inflight is exactly one, preserving the idle-worker window required by reap-time transfer.

* fix(trainer): align async diffusion policy metadata

Record the train slab fraction and describe the actual remote LoRA sync and bounded policy-lag ratio semantics without changing runtime behavior.

* fix(trainer): harden async diffusion result handling

* refactor(trainer): async diffusion on the Sample API and the shared async runtime

Two things broke this branch against current main, and both are fixed here.

The trainer was written against the retired RolloutReq / RolloutResp /
RolloutTrack triplet, deleted by the sample-native rollout boundary (#214). It
is now sample-native: the request is the Sample from _build_request_sample,
scoring is reward.score_and_attach(sample) on the self-contained filled Sample
instead of the old (req=, track=) pair, groups reassemble with Sample.concat,
and the RolloutResp(tracks=...) rebuild and its _track_key bookkeeping are
gone. The entry point's stage_config was likewise renamed to task_config.

The async buffer / generate seam this branch duplicated from AsyncARTrainer has
since been lifted into unirl/rollout/async_runtime.py, the follow-up refactor
this PR's description anticipated. _RolloutBuffer, _generate_async,
_collect_resp, _is_ready, _launch, _reap_ready and the _next_batch loop are all
replaced by AsyncRolloutScheduler + RayGenerationDispatcher, leaving only the
diffusion hooks: build a request Sample, score-and-split at reap time, and
advantage + FlowGRPO step.

Adopting that runtime needs one addition to it, because it launched before it
reaped and this path requires the opposite. Reaping pulls the trajectory
segment off the rollout slab as an NCCL send issued on the rollout workers, so
a generation launched ahead of that send blocks it -- the ~150s/rollout instead
of ~8s that reap-before-launch was introduced to fix. Reap-first at
max_inflight=1 hands the send idle workers while still launching before the
step returns, so the next generation overlaps the caller's train step. The new
reap_before_launch flag selects the order and defaults to the existing
launch-first behavior, so the AR path is unchanged.

Verified: ruff check and format clean, the trainer and entry point import
against current main, Hydra compose of the BAGEL async recipe passes, all
recipe _target_ paths resolve, both constructor guards fire before any Ray
construction, and a fake-dispatcher check confirms reap-first at
max_inflight=1 both keeps one generation in flight across every train step and
always reaps against idle rollout workers. Not re-run: the GPU reward-curve and
localize-timing validation in the PR description.

---------

Co-authored-by: Jianghai <72591262+CjhHa1@users.noreply.github.com>
Co-authored-by: Haonan Wang <haonan.wang@u.nus.edu>
Co-authored-by: aimicahchen <aimicahchen@tencent.com>
Co-authored-by: leviking98z-rgb <leviking98z@gmail.com>
Co-authored-by: CjhHa1 <cjh18671720497@outlook.com>
leviking98z-rgb pushed a commit to NancyFyong/UniRL that referenced this pull request Jul 29, 2026
…lloutReq

The video adapter module still imports unirl.types.rollout_req, which Tencent-Hunyuan#214 removed when Sample/Part replaced the RolloutReq/RolloutResp triplet. The import is unguarded and adapters/__init__ pulls the whole module in for its registration side-effects, so importing the sglang_diffusion engine raises ModuleNotFoundError and every video family goes down with it — WAN 2.1/2.2, HunyuanVideo and Mochi as well as LTX-2.

Ltx2T2VAdapter carries two more leftovers behind that import: NoiseRecipe.from_rollout_req, renamed to from_sample, and req.sigmas, a field Sample does not have. Both are ports of what image.py and the VideoAdapter base already do, so the sigma schedule now comes from sample.frontier_gen_part(DiffusionSamplingParams).sampling_params.sigmas.
leviking98z-rgb added a commit that referenced this pull request Jul 29, 2026
…lloutReq (#272)

The video adapter module still imports unirl.types.rollout_req, which #214 removed when Sample/Part replaced the RolloutReq/RolloutResp triplet. The import is unguarded and adapters/__init__ pulls the whole module in for its registration side-effects, so importing the sglang_diffusion engine raises ModuleNotFoundError and every video family goes down with it — WAN 2.1/2.2, HunyuanVideo and Mochi as well as LTX-2.

Ltx2T2VAdapter carries two more leftovers behind that import: NoiseRecipe.from_rollout_req, renamed to from_sample, and req.sigmas, a field Sample does not have. Both are ports of what image.py and the VideoAdapter base already do, so the sigma schedule now comes from sample.frontier_gen_part(DiffusionSamplingParams).sampling_params.sigmas.

Co-authored-by: LeviKing <leviking98z@gmail.com>
haonan3 added a commit to YSunLIN/UniRL that referenced this pull request Jul 30, 2026
…oles DSL

- Rebase onto current main (Tencent-Hunyuan#214): RolloutReq/RolloutInputs are gone; the
  actor role now consumes Texts/Images primitives plus per-sample metadata
  records straight from the data-source Sample.
- Drop recipes/common/ (role-list orchestration): REFLTrainer subclasses
  BaseTrainer directly and wires actor + reward with placement()+remote_hydra,
  mirroring RewardBackpropTrainer (the SD3 image-ReFL driver). ReflActorRole
  mirrors ReFLPolicy's family-agnostic contract (pipeline_target +
  model_config + from_config; FSDPBackend composed in initialize()).
- Re-root both configs from the roles: list to the repo-wide flat schema
  (actor:/reward:/data_source:/sampling:/logging:), the same shape as
  examples/diffusion/refl_sd3.yaml.
- KL correctness across DP shards: diffuse_with_grad now returns per-sample
  [B] KL (concat field) instead of a per-shard scalar shared field, so
  DP_SCATTER merge/re-shard round-trips each shard's own KL. Previously the
  driver collapsed all shards to one value (loss/logging skew; gradient flow
  was unaffected because dKL/dkl is the constant kl_weight).
- I2V condition assembly moves out of the role into
  Wan21/Wan22ReflPipeline.build_refl_conditions (mirrors each mainline
  pipeline's generate); negative prompts ride sampler_kwargs.
- Seed scheme now matches ReFLPolicy (base + 1000*rollout_id + dp_rank);
  previously every rollout redrew the same init noise.
- reward/service.py: restore the List typing import lost in the merge.
CjhHa1 added a commit to HaitaoWuTJU/UniRL that referenced this pull request Jul 31, 2026
The package did not import: Tencent-Hunyuan#214 replaced RolloutReq / RolloutResp with
Sample / Part, and the main merge into this branch did not conflict because
pipeline.py exists only on the feature side, so it kept importing
unirl.types.rollout_req. Every validation number in the PR description was
produced before that merge.

generate is now Sample -> Sample. Task selection reads parts[0].control["task"]
and otherwise infers from Sample.has_image_input(). i2t pulls its turns from
vision_conditioning() and t2i from turns(); both fail loud on a multi-turn
trajectory, which the single-user-turn Janus chat template cannot encode.
Sampling params come off the frontier gen shell rather than a request-level
dict, and t2i requires JanusProImageARSamplingParams because the image grid,
CFG weight, and token count all ride on it. Results go back through
Sample.with_filled_frontier, so the input chain and reward_compute_s survive.

The stages, conditions, bundle, config, chat_template, and image_prompt were
already API-agnostic and are untouched.
haonan3 added a commit to KemingWu/UniRL that referenced this pull request Aug 2, 2026
…rework)

Adopts the core-primitive DiffusionOPD implementation in place of the
original implementation on this branch; the tree is taken entirely from
the rework side (-s ours), the original commits stay in history.

What changed relative to the original approach:
- teachers are backend-owned frozen LoRA adapters
  (backend.lora_cfg.frozen_adapters, injected pre-FSDP-wrap, so FSDP
  shards them and LoRA checkpoints stay save/load-symmetric)
- teacher selection is data-driven via metadata["domain"] stamped by
  MultiDomainRLDataSource (replaces the data-source/algorithm round-robin
  counter pair, which desynchronized on checkpoint resume and on the
  branch's post-Tencent-Hunyuan#214 rebase broke entirely: unirl.types.prompts was
  removed upstream)
- eval covers every domain in one pass (budget split across domains)
- local mmdet GenEval scorer and the 2-node launcher snapshot are
  dropped; classical GenEval stays in unirl-reward-service
  (configs/geneval_service.yaml)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

need review Ready and waiting for review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants