03. Multi-Role Training (OpenRLHF)
November 3, 2025 · View on GitHub
This guide demonstrates how to express and submit a multi-role OpenRLHF training job. The examples below follow the patterns used in examples/unified/rl/openrlhf/ppo/main.py.
Overview
Multi-role training lets you define and orchestrate multiple specialized workloads—such as actors, critics, rollout engines, trainers, and reward models—each with its own resource needs and execution entry point. By composing these roles into a single distributed configuration, you can flexibly scale and manage complex reinforcement learning workflows.
Paradigm Note: This OpenRLHF example demonstrates the remote_call (RPC) paradigm for inter-role communication, where each role exposes and invokes RPC methods for distributed coordination. For an alternative approach using the Proxy/WorkerGroup paradigm, see the verl example (04. Multi-Role Training Example: verl).
For RL scenarios, two core training workloads—Actor and Critic—must be trained simultaneously, reflecting the greater complexity of RL compared to standard supervised fine-tuning (SFT) pipelines.
Key components:
- Submitter: Parses command-line arguments,
assembles workload descriptors
(
ResourceDesc,ElasticWorkloadDesc,SimpleWorkloadDesc), constructs aDLConfig, and launches the control plane viaPrimeMaster. - Trainer: Acts as the central RL controller, manages the training loop, aggregates experiences, performs optimization steps, handles checkpointing, and exposes monitoring endpoints.
- Actor (Policy): Executes policy rollouts using elastic GPU-backed instances.
- Critic: Runs value-function estimation workers, typically as elastic GPU jobs for PPO.
- Rollout Engines: Performs environment simulations
or policy evaluations (e.g., VLLM), often modeled
as
SimpleWorkloadDesc. - Value & Reward Models: Evaluates value or reward functions using multi-GPU elastic workers.
- Reference Models (Optional): Supports behavior cloning or KL-control via secondary policy evaluations.
Example Implementation
This section walks through the concrete implementation in
examples/unified/rl/openrlhf/ppo, showing how each key component
maps to its source file, how workloads are declared in main.py,
and where the trainer and runtime communication is handled.
This example is based on the OpenRLHF PPO, LICENSEd under Apache 2.0. Requirements include torch, vllm, and openrlhf. Computing Resources includes 8*80G GPUs, or equivalent.
cd examples/unified/
bash rl/openrlhf/ppo/run.sh
---
For a practical multi-role training example using verl, see [03B. Multi-Role Training Example: verl](03b-multi-role-training-verl.md).
It's not necessary to run this example, we will explain the key concepts and patterns used in this multi-role training job. You can use DLRover to submit your own multi-role jobs by following the same patterns.
DLRover itself is not an algorithm framework, and therefore does not natively support any specific RL algorithm implementation. As a result, a complete RL computation requires users to leverage the fundamental capabilities provided by DLRover to implement the corresponding algorithm and ensure its execution.
Directory Structure
-
main.py: The entrypoint to submit the multi-role job. -
ppo_trainer.py: Implements the Trainer, managing the RL training loop.trainer.py: The core logic for trainer.
-
remote_call.py: Defines all RPC for roles to communicate. -
RL Roles:
ppo_actor.py: Implements the Actor role, handling policy rollouts.ppo_critic.py: Implements the Critic role, estimating value functions.ppo_rollout.py: Implements the Rollout Engine, simulating environments.ppo_reward.py: Implements the Reward Model, evaluating rewards.ppo_reference.py: Implements the Reference Model, used for behavior cloning.
-
Misc
common.py: Contains common utilities for roles.run.sh: A script to runmain.pywith appropriate arguments.utils/experience_maker.py: A class for generating and managing experiences.
-
Submitter / Entrypoint:
examples/unified/rl/openrlhf/ppo/main.py -
Actor (Policy Worker):
examples/unified/rl/openrlhf/ppo/ppo_actor.py -
Critic (Value Estimator):
examples/unified/rl/openrlhf/ppo/ppo_critic.py -
Rollout Engine:
examples/unified/rl/openrlhf/ppo/ppo_rollout.py -
Trainer (RL Controller):
examples/unified/rl/openrlhf/ppo/ppo_trainer.py -
Reward Model:
examples/unified/rl/openrlhf/ppo/ppo_reward.py -
Reference Model:
examples/unified/rl/openrlhf/ppo/ppo_reference.py
Workload Declaration and Submission (main.py)
main.py Contains argument parsing, workload assembly, and job submission logic.
The core part is the submit() function, which defines the multi-role workloads
and submits them to the PrimeMaster.
# examples/unified/rl/openrlhf/ppo/main.py
# Inside submit()
# 1. Create a job builder
builder = RLJobBuilder().config(args_2_omega_conf(args))
# 2. Define workloads / roles
builder.trainer(f"{P}.ppo_trainer.PPOTrainerActor").resource(cpu=4)
(
builder.actor(f"{P}.ppo_actor.PolicyModelActor")
.resource(accelerator=1)
.nnodes(args.actor_num_nodes)
.nproc_per_node(args.actor_num_gpus_per_node)
)
(
builder.critic(f"{P}.ppo_critic.CriticModelRayActor")
.resource(accelerator=0.4)
.nnodes(args.critic_num_nodes)
.nproc_per_node(args.critic_num_gpus_per_node)
)
(
builder.rollout(f"{P}.ppo_rollout.VLLMActor")
.resource(accelerator=1)
.total(args.vllm_num_engines)
)
# optional reference role
if args.init_kl_coef != 0:
(
builder.role(RLRoleType.REFERENCE.name)
.train("{P}.ppo_reference.PPOReferenceActor")
.resource(accelerator=0.4)
.nnodes(args.ref_num_nodes)
.nproc_per_node(args.ref_num_gpus_per_node)
)
(
builder.role(RLRoleType.REWARD.name)
.train("{P}.ppo_reward.RewardModelRayActor")
.resource(accelerator=0.4)
.nnodes(args.reward_num_nodes)
.nproc_per_node(args.reward_num_gpus_per_node)
)
# 3. Build and submit
job = builder.build()
# optional modifications before submission
if args.skip_node_check:
for workload in job.workloads.values():
if workload.backend == "elastic":
workload.comm_pre_check = False
print(job.model_dump_json(indent=2))
if not args.dry_run:
job.submit(args.job_name)
Trainer & Runtime Communication
After submitting, DLRover orchestrates all roles, then invokes user entry points. In this case, we use trainer-driven pattern to manage the training loop. All other roles will quickly setup, and wait for rpc calls.
The training loop is defined in ppo_trainer.py, which handles the
communication between the trainer and other roles like Actor, Critic, Reward Model,
and Reference Model. The trainer orchestrates the entire training process,
including generating samples, computing rewards, updating policies, and managing
checkpoints.
The sequence diagram below illustrates the communication flow between the roles during training:
sequenceDiagram
participant S as DLRover
participant T as Trainer
participant A as Actor
participant C as Critic
participant R as Reward Model
participant Ref as Reference Model
participant Rollout as Rollout Engine
S->> T: start
note over T: prepare dataset
note over T: init_workers
par
T->> Ref: reference_init
and
T->> R: reward_init
and
T->> C: critic_init
and
T->> A: actor_init
end
note over T: fit
loop Training Loop
T->> Rollout: generate_samples
Rollout--> T: samples
par
T->> R: reward_forward
R-->> T: rewards
and
T->> A: actor_forward
A-->> T: log_prob
and
T->> C: critic_forward
C-->> T: values
and
T->> Ref: reference_forward
Ref-->> T: ref_log_prob
end
par
T->>A: append_experience
and
T->>C: append_experience
end
par
T->>C: critic_update
and
T->>A: actor_update
end
T->>A: broadcast_to_vllm
A->>Rollout: weight_sync
opt Checkpointing
par
T->>C: save_checkpoint
and
T->>A: save_checkpoint
end
end
end
par
T->> A: end_job
and
T->> C: end_job
and
T->> R: end_job
and
T->> Ref: end_job
and
T->> Rollout: end_job
end
T -->> S: end
How the RPC Works
The communication between the roles is handled through remote procedure calls (RPCs) defined in remote_call.py. Each role implements its own RPC methods to interact with the trainer and other roles.
-
Define RPC methods in
remote_call.py:- File name and function name just for reference, you can use any name.
- Async methods are recommended returning
Future[xxx]. - Batch methods input lots
Sequenceand returnFutureSequence[xxx]. Will only blocking when iterating the result.
# remote_call.py def vllm_wakeup() -> None:... def vllm_generate( prompt_token_ids: Sequence[List[int]], params: "SamplingParams" ) -> FutureSequence["RequestOutput"]: ... def actor_forward( sequences: Sequence[torch.Tensor], action_mask: Sequence[torch.BoolTensor], attention_mask: Sequence[torch.LongTensor], ) -> Sequence[torch.Tensor]:... -
Use the function in any place where you need to call the remote method:
# utils/experience_maker.py from .. import remote_call class SamplesGenerator: def __generate_vllm(self, *args, **kwargs): #... all_outputs = remote_call.vllm_generate( all_prompt_token_ids, sampling_params ) #... -
Implement and bind the RPC methods in Role classes:
- Here
remote_callis used for RPC name, if not provided, it will use the method name as the RPC name. And you need keep consistent in next step. - All
@rpcmethods in entrypoint module top-level and entrypoint class will be automatically exported. - You could use
@rpc(export=True)to export any method in any place. But be careful about the timing, method should be exported before the RPC call.
# ppo_rollout.py class VLLMActor: @rpc(remote_call.vllm_wakeup) def wake_up(self): self.llm.wake_up() @rpc(remote_call.vllm_generate) def generate(self, prompt_token_ids, params): ... - Here
-
Implement the interface orchestration in
remote_call.py:- In pervious steps, we finished the RPC method definition and binding. The last step is to orchestrate the interface calls.
- Keep in mind that all functions is normal PYTHON functions, you can use any logic to implement them.
# remote_call.py # 1. Simple and Blocking call def vllm_wakeup() -> None: group(RLRoleType.ROLLOUT).call(vllm_wakeup).result() # 2. Async broadcast call, return Future def reference_init(strategy, model_path: str) -> Future: return group(RLRoleType.REFERENCE).call( reference_init, strategy, model_path ) # 3. Batch call, return FutureSequence(subclass of Sequence) def vllm_generate( prompt_token_ids: Sequence[List[int]], params: "SamplingParams" ) -> Sequence["RequestOutput"]: return group(RLRoleType.ROLLOUT).call_batch( vllm_generate, len(prompt_token_ids), prompt_token_ids, params, ) # 4. Call multiple roles in parallel, and wait def end_job() -> None: """End the job by calling the end_job method on the group.""" futures = [ group(role).call(end_job) for role in [ RLRoleType.ROLLOUT, RLRoleType.REFERENCE, RLRoleType.REWARD, RLRoleType.ACTOR, RLRoleType.CRITIC, ] ] for future in futures: future.result() # 4. Utility to cache RoleGroup @lru_cache(maxsize=None) def group(role: RLRoleType) -> RoleGroup: """Get the role group for the given role.""" return RoleGroup(role.name) # 5. Also, you could add any custom remote_call utilities here @contextmanager def vllm_running(enable_sleep: bool): """Context manager to handle vLLM sleep and wakeup.""" if enable_sleep: vllm_wakeup() try: yield finally: if enable_sleep: vllm_sleep()