Skip to main content
Module: rllm.trainer.algorithms.advantage
This page covers:
  1. The estimator interface and its data contract
  2. Built-in estimators and what rLLM does (and doesn’t) support today
  3. Role-level estimator overrides
  4. Registering a custom estimator
  5. A worked example: porting OPO from Verl

Core concept

In the unified trainer, advantages are computed on TrajectoryGroups. Groups are partitioned by group_role (for example, solver and judge); each role’s estimator is invoked once with all the groups belonging to that role. The orchestrator passes three things to the estimator:
  • rewards: list[np.ndarray] — outer list aligned with the role’s TrajectoryGroups, inner array indexed by trajectory.
  • algorithm_config: AlgorithmConfig — the resolved algorithm config, so the estimator can pull whatever it needs (for example, norm_adv_by_std_in_grpo).
  • traj_groups: list[TrajectoryGroup] — same outer shape as rewards, exposed for estimators that need per-trajectory metadata (response lengths, step counts, etc.).

What rewards looks like

Concretely, if a training step produced 4 solver groups with 8 trajectories each, the solver call receives:
  • rewards — a length-4 list of 1-D numpy arrays of shape (8,).
  • rewards[i][j] is the scalar reward for trajectory j in group i.
The output (advantages_by_group, returns_by_group) must align with rewards:
  • advantages_by_group[i].shape == rewards[i].shape
  • one scalar advantage per trajectory; the unified trainer broadcasts it across the trajectory’s response tokens later.
The estimator interface is trajectory-level scalar in, trajectory-level scalar out. Per-token signals (returns over time, GAE, K3 detector, etc.) cannot be expressed through this hook today. For per-token signals, see Pre-computing advantage in workflow.

Tinker loss mapping

For the Tinker backend, rLLM also maps each estimator to a default loss function in rllm/trainer/tinker/tinker_policy_trainer.py: Override anytime via:

Built-in estimators

For REINFORCE++ baseline, normalization uses batch-level statistics across all centered rewards in the role.

What rLLM supports today

The four estimators above are intentionally a subset of Verl’s full catalog. We expose what fits the rLLM hook contract — scalar reward per trajectory, scalar advantage per trajectory — so that the same code path serves both Tinker and Verl backends. Verl’s broader catalog includes GAE, REINFORCE++ (proper, per-token discounted reward-to-go), REMAX, OPO, GPG, GRPO-passk, GDPO, and OTB / TIR-OTB. Of these:
  • Scalar-per-trajectory estimators (OPO, GPG, GRPO-passk, GDPO) can be expressed through the current hook. The OPO worked example below walks through one such port. We plan to add more on request.
  • Per-token or critic-dependent estimators (GAE, REINFORCE++ proper, OTB, TIR-OTB) need an interface extension and are a larger follow-up. If you need one of these today, write a per-token signal in your workflow and use use_precomputed_advantage to bypass the estimator hook entirely.

Setting role-level estimators

The most powerful feature of the rLLM path is assigning different estimators to different trajectory roles in one training job. This is especially useful for multi-agent workflows where roles have different reward distributions. In a solver-judge workflow, for example, there are typically multiple solver trajectories per rollout (2 solver trajectories per rollout × N rollouts = 2N solver trajectories), so GRPO is a good fit for the solver. The judge depends on the solver’s outputs, so cross-rollout grouping is less meaningful for it; a vanilla REINFORCE is often the better choice. Configure this via traj_group_adv_estimator_map in the trainer constructor:
Global default is set in yaml:

Custom estimators

Use the registry helpers to register and retrieve custom advantage estimators:
  • register_rllm_adv_estimator(name)
  • get_rllm_adv_estimator(name)
The canonical signature is:
**kwargs carries traj_groups: list[TrajectoryGroup] aligned with rewards. Pull it when you need per-trajectory metadata; ignore it otherwise. A toy custom estimator that subtracts the role-batch mean:
Use the custom estimator as a global default:
Or as a role-specific override in traj_group_adv_estimator_map:

Worked example: porting OPO from Verl

OPO (https://arxiv.org/abs/2505.23585) computes a length-weighted baseline per group: baseline=ileniscoreiileni,advantagei=scoreibaseline\text{baseline} = \frac{\sum_i \text{len}_i \cdot \text{score}_i}{\sum_i \text{len}_i}, \qquad \text{advantage}_i = \text{score}_i - \text{baseline} Verl’s reference implementation is compute_opo_outcome_advantage in verl/trainer/ppo/core_algos.py. It reads response lengths from response_mask. In rLLM, response lengths come from traj.steps[*].response_ids, which are reachable through traj_groups in **kwargs.
Use it as a global default:
Or per role:
The same pattern works for any scalar-per-trajectory estimator: pull what you need from algorithm_config and traj_groups, return a list of advantage arrays aligned with rewards.