Loss Functions
PyTRIO provides built-in loss functions for supervised learning and reinforcement learning.
You can select a loss function by passing a string to forward_backward:
future = training_client.forward_backward(
data,
loss_fn="cross_entropy",
)
result = future.result()Built-In Loss Functions
PyTRIO currently supports the following built-in loss functions:
| Loss function | Scenario | Description |
|---|---|---|
cross_entropy | Supervised learning | Standard cross-entropy loss for classification tasks. It computes the negative log-likelihood from model logits and target labels. |
importance_sampling | Offline reinforcement learning | Corrects off-policy data with importance sampling by weighting gradients with the probability ratio between the behavior policy and the target policy. |
ppo | Online reinforcement learning | Proximal Policy Optimization loss. It clips the probability ratio to limit the policy update magnitude and improve training stability. |
cispo | Online/offline reinforcement learning | Clipped Importance Sampling Policy Optimization. It weights the policy gradient with a clipped importance ratio and is useful for async or off-policy settings. |
dro | Offline reinforcement learning | Direct Reward Optimization. It adds a quadratic penalty to the reward term to constrain policy updates relative to the sampling policy. |
cross_entropy
For supervised learning, PyTRIO implements the standard cross-entropy loss, also known as negative log-likelihood. This loss optimizes the policy to maximize the log probability of token :
Here weights are 0 or 1 and are usually generated by renderer.build_supervised_example(), which returns (model_input, weights) and is used to specify the target assistant turns that should be trained.
The implementation is:
# Apply weights and compute elementwise loss
elementwise_loss = -target_logprobs * weights
# Apply sum reduction to get the total loss
loss = elementwise_loss.sum() # scalarThe cross_entropy loss requires the following fields in Datum.loss_fn_inputs:
target_tokens: array[(N,), int] | array[(N, K), int]: target token IDsweights: array[(N,), float] | array[(N, K), float]: token-level loss weights, usually from the renderer
Output:
logprobs: array[(N,), float] | array[(N, K), float]: log probabilities of the requested target tokens
Metric:
loss_sum: aggregated loss returned by the SDK, a scalar
importance_sampling
For reinforcement learning, PyTRIO implements a common variant of the policy-gradient objective for practical cases where the learning policy and the sampling policy differ, for example because of non-determinism that makes the data off-policy.
The issue is that the objective:
can become biased when from the sampler is not exactly the same as the desired from the learner. To correct this bias, PyTRIO uses the improved importance-sampling objective:
This objective gives the correct expected reward. In the formula:
- (
target_logprobs) comes from the learner and is computed during the forward pass inforward_backward. - (
sampling_logprobs) comes from the sampler and is recorded during sampling as the correction term.
The implementation is:
# Compute probability ratio
prob_ratio = torch.exp(target_logprobs - sampling_logprobs)
# Compute importance-weighted loss
loss = -(prob_ratio * advantages).sum()The importance_sampling loss requires the following fields in Datum.loss_fn_inputs:
target_tokens: array[(N,), int]: target token IDs from samplerlogprobs: array[(N,), float]: token-levelsampling_logprobsadvantages: array[(N,), float]: RL advantage values. Positive values reinforce the token, while negative values suppress it.
Output:
logprobs: array[(N,), float]: token-leveltarget_logprobs
Metric:
loss_sum: aggregated loss returned by the SDK, a scalar
ppo
PPO (Schulman et al., 2017) addresses issues in standard policy-gradient methods by introducing a clipped objective function. It restricts policy updates to the neighborhood of the sampling distribution, preventing excessive policy drift when multiple gradient updates are applied to the same rollout distribution.
The objective prevents overly large policy updates by clipping the importance ratio , where is the learner policy and is the sampling policy. Note that PPO clipping and loss computation are both performed independently at the token level.
The PPO clipped objective is:
The final PPO loss combines the clipped and unclipped objectives:
Here and are hyperparameters, currently fixed to 0.2 in PyTRIO.
The implementation is:
# Compute probability ratio
prob_ratio = torch.exp(target_logprobs - sampling_logprobs)
# Apply clipping
clipped_ratio = torch.clamp(prob_ratio, clip_low_threshold, clip_high_threshold)
# Compute both objectives
unclipped_objective = prob_ratio * advantages
clipped_objective = clipped_ratio * advantages
# Take minimum (most conservative)
ppo_objective = torch.min(unclipped_objective, clipped_objective)
# PPO loss is negative of objective
loss = -ppo_objective.sum()The ppo loss requires the following fields in Datum.loss_fn_inputs:
target_tokens: array[(N,), int]: target token IDs from samplerlogprobs: array[(N,), float]: token-levelsampling_logprobsadvantages: array[(N,), float]: RL advantage values
Output:
logprobs: array[(N,), float]: token-leveltarget_logprobs
Metric:
loss_sum: aggregated loss returned by the SDK, a scalar
You can also customize the clipping thresholds with loss_fn_config:
fwd_bwd_future = await training_client.forward_backward_async(
data=data,
loss_fn="ppo",
loss_fn_config={"clip_low_threshold": 0.9, "clip_high_threshold": 1.1}
)
fwd_bwd_result = await fwd_bwd_futurecispo
CISPO (Clipped Importance Sampling Policy Optimization) is a policy-gradient method. Like PPO, it uses the importance ratio . The difference is that PPO clips the objective directly, while CISPO clips the importance ratio and uses the clipped ratio as the coefficient on target_logprobs.
The CISPO objective is:
Here means stop-gradient. The clipped ratio is detached, so it only controls the gradient coefficient and does not backpropagate through the ratio itself.
The implementation is:
# Compute probability ratio
prob_ratio = torch.exp(target_logprobs - sampling_logprobs)
# Apply clipping
clipped_ratio = torch.clamp(prob_ratio, clip_low_threshold, clip_high_threshold)
# Compute CISPO objective (detach the clipped ratio)
cispo_objective = clipped_ratio.detach() * target_logprobs * advantages
# CISPO loss is negative of objective
loss = -cispo_objective.sum()The cispo loss requires the following fields in Datum.loss_fn_inputs:
target_tokens: array[(N,), int]: target token IDs from samplerlogprobs: array[(N,), float]: token-levelsampling_logprobsadvantages: array[(N,), float]: RL advantage values
Output:
logprobs: array[(N,), float]: token-leveltarget_logprobs
Metric:
loss_sum: aggregated loss returned by the SDK, a scalar
CISPO uses one-sided clipping by default: no lower bound and an upper bound only, with clip_low_threshold=0.0 and clip_high_threshold=4.0. You can also set these explicitly with loss_fn_config:
fwd_bwd_future = await training_client.forward_backward_async(
data=data,
loss_fn="cispo",
loss_fn_config={"clip_low_threshold": 0.0, "clip_high_threshold": 4.0}
)In async or off-policy training, the sampling policy may lag behind the current learner policy . A high lower bound can keep stale tokens weighted strongly even after the current policy has moved away from them, weakening the attenuation that importance sampling is meant to provide. Leaving the lower bound disabled is usually more robust.
dro
DRO (Direct Reward Optimization) is a general off-policy and even offline reinforcement learning method. It adds a quadratic penalty to the reward-weighted logprob term to constrain how far the learner policy moves from the sampling policy .
The DRO objective is:
Here controls the strength of the quadratic penalty. Note that DRO uses a softer formulation of advantage estimation, and those advantages need to be constructed on the client side before being passed in.
The implementation is:
# Compute quadratic penalty term
quadratic_term = (target_logprobs - sampling_logprobs) ** 2
# Compute DRO objective
dro_objective = target_logprobs * advantages - 0.5 * beta * quadratic_term
# DRO loss is negative of objective
loss = -dro_objective.sum()The dro loss requires the following fields in Datum.loss_fn_inputs:
target_tokens: array[(N,), int]: target token IDs from samplerlogprobs: array[(N,), float]: token-levelsampling_logprobsadvantages: array[(N,), float]: RL advantage values
Output:
logprobs: array[(N,), float]: token-leveltarget_logprobs
Metric:
loss_sum: aggregated loss returned by the SDK, a scalar
You can customize with loss_fn_config:
fwd_bwd_future = await training_client.forward_backward_async(
data=data,
loss_fn="dro",
loss_fn_config={"beta": 0.05}
)