Image-Text-to-Text
Transformers
Safetensors
qwen3_5
vllm
video
multimodal
reinforcement-learning
temporal-grounding
object-tracking
video-segmentation
visual-question-answering
spatial-reasoning
qwen3.5
conversational
Instructions to use OraRL/Video-ORA-4B with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use OraRL/Video-ORA-4B with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("image-text-to-text", model="OraRL/Video-ORA-4B") messages = [ { "role": "user", "content": [ {"type": "image", "url": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/p-blog/candy.JPG"}, {"type": "text", "text": "What animal is on the candy?"} ] }, ] pipe(text=messages)# Load model directly from transformers import AutoProcessor, AutoModelForMultimodalLM processor = AutoProcessor.from_pretrained("OraRL/Video-ORA-4B") model = AutoModelForMultimodalLM.from_pretrained("OraRL/Video-ORA-4B", device_map="auto") messages = [ { "role": "user", "content": [ {"type": "image", "url": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/p-blog/candy.JPG"}, {"type": "text", "text": "What animal is on the candy?"} ] }, ] inputs = processor.apply_chat_template( messages, add_generation_prompt=True, tokenize=True, return_dict=True, return_tensors="pt", ).to(model.device) outputs = model.generate(**inputs, max_new_tokens=40) print(processor.decode(outputs[0][inputs["input_ids"].shape[-1]:])) - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use OraRL/Video-ORA-4B with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "OraRL/Video-ORA-4B" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "OraRL/Video-ORA-4B", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Describe this image in one sentence." }, { "type": "image_url", "image_url": { "url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" } } ] } ] }'Use Docker
docker model run hf.co/OraRL/Video-ORA-4B
- SGLang
How to use OraRL/Video-ORA-4B with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "OraRL/Video-ORA-4B" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "OraRL/Video-ORA-4B", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Describe this image in one sentence." }, { "type": "image_url", "image_url": { "url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" } } ] } ] }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "OraRL/Video-ORA-4B" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "OraRL/Video-ORA-4B", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Describe this image in one sentence." }, { "type": "image_url", "image_url": { "url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" } } ] } ] }' - Docker Model Runner
How to use OraRL/Video-ORA-4B with Docker Model Runner:
docker model run hf.co/OraRL/Video-ORA-4B
| # Copyright 2022 The HuggingFace Team | |
| # Copyright 2024 Bytedance Ltd. and/or its affiliates | |
| # | |
| # Licensed under the Apache License, Version 2.0 (the "License"); | |
| # you may not use this file except in compliance with the License. | |
| # You may obtain a copy of the License at | |
| # | |
| # http://www.apache.org/licenses/LICENSE-2.0 | |
| # | |
| # Unless required by applicable law or agreed to in writing, software | |
| # distributed under the License is distributed on an "AS IS" BASIS, | |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| # See the License for the specific language governing permissions and | |
| # limitations under the License. | |
| """ | |
| Core functions to implement PPO algorithms. | |
| The function implemented in this file should be used by trainer with different distributed strategies to | |
| implement PPO | |
| """ | |
| from abc import ABC, abstractmethod | |
| from collections import defaultdict | |
| from enum import Enum | |
| from typing import TYPE_CHECKING, Any, Literal, Optional | |
| import numpy as np | |
| import torch | |
| import torch.nn.functional as F | |
| from ..utils import torch_functional as VF | |
| if TYPE_CHECKING: | |
| from .config import AlgorithmConfig | |
| class KLController(ABC): | |
| kl_coef: float | |
| """KL coefficient.""" | |
| def update(self, current_kl: float, n_steps: int): | |
| """Update kl_coef according to current KL.""" | |
| ... | |
| class AdaptiveKLController(KLController): | |
| """Adaptive KL controller described in: https://arxiv.org/pdf/1909.08593.pdf | |
| Copied from https://github.com/huggingface/trl/blob/v0.11.0/trl/trainer/utils.py#L54""" | |
| def __init__(self, init_kl_coef: float, target_kl: float, horizon: float): | |
| self.kl_coef = init_kl_coef | |
| self.target = target_kl | |
| self.horizon = horizon | |
| def update(self, current_kl: float, n_steps: int): | |
| target = self.target | |
| proportional_error = np.clip(current_kl / target - 1, -0.2, 0.2) | |
| mult = 1 + proportional_error * n_steps / self.horizon | |
| self.kl_coef *= mult | |
| class FixedKLController(KLController): | |
| """Fixed KL controller. | |
| Copeid from https://github.com/huggingface/trl/blob/v0.11.0/trl/trainer/utils.py#L72""" | |
| def __init__(self, init_kl_coef: float): | |
| self.kl_coef = init_kl_coef | |
| def update(self, current_kl: float, n_steps: int): | |
| pass | |
| class AdvantageEstimator(str, Enum): | |
| """ | |
| Using an enumeration class to avoid spelling errors in adv_estimator | |
| """ | |
| GAE = "gae" | |
| GRPO = "grpo" | |
| GRPO_PASSK = "grpo_passk" | |
| REINFORCE_PLUS_PLUS = "reinforce_plus_plus" | |
| REMAX = "remax" | |
| RLOO = "rloo" | |
| ADV_ESTIMATOR_MAP: dict[str, Any] = {} | |
| def get_kl_controller(algorithm_config: "AlgorithmConfig") -> KLController: | |
| """Adapted from https://github.com/huggingface/trl/blob/v0.11.0/trl/trainer/ppo_trainer.py#L319""" | |
| if algorithm_config.kl_type == "fixed": | |
| kl_ctrl = FixedKLController(init_kl_coef=algorithm_config.kl_coef) | |
| elif algorithm_config.kl_type == "adaptive": | |
| assert algorithm_config.kl_horizon > 0, f"horizon must be larger than 0. Got {algorithm_config.kl_horizon}." | |
| kl_ctrl = AdaptiveKLController( | |
| init_kl_coef=algorithm_config.kl_coef, | |
| target_kl=algorithm_config.kl_target, | |
| horizon=algorithm_config.kl_horizon, | |
| ) | |
| else: | |
| raise ValueError(f"Unknown kl type: {algorithm_config.kl_type}.") | |
| return kl_ctrl | |
| def register_adv_estimator(name: AdvantageEstimator): | |
| """Decorator to register a advantage estimator function with a given name.""" | |
| def decorator(fn): | |
| wrapped_fn = torch.no_grad()(fn) | |
| ADV_ESTIMATOR_MAP[getattr(name, "value", name)] = wrapped_fn | |
| return wrapped_fn | |
| return decorator | |
| def compute_advantage_return(name: AdvantageEstimator, **kwargs) -> tuple[torch.Tensor, torch.Tensor]: | |
| """Compute advantage and return for a given advantage estimator.""" | |
| return ADV_ESTIMATOR_MAP[getattr(name, "value", name)](**kwargs) | |
| def compute_gae_advantage_return( | |
| token_level_rewards: torch.Tensor, | |
| values: torch.Tensor, | |
| response_mask: torch.Tensor, | |
| gamma: torch.Tensor, | |
| lam: torch.Tensor, | |
| **kwargs, | |
| ) -> tuple[torch.Tensor, torch.Tensor]: | |
| """Adapted from https://github.com/huggingface/trl/blob/v0.16.0/trl/trainer/ppo_trainer.py#L513 | |
| Args: | |
| token_level_rewards: `(torch.Tensor)` | |
| shape: (bs, response_length) | |
| values: `(torch.Tensor)` | |
| shape: (bs, response_length) | |
| response_mask: `(torch.Tensor)` | |
| shape: (bs, response_length). The token after eos tokens have mask zero. | |
| gamma: `(float)` | |
| discounted factor used in RL | |
| lam: `(float)` | |
| lambda value when computing Generalized Advantage Estimation (https://arxiv.org/abs/1506.02438) | |
| Returns: | |
| advantages: `(torch.Tensor)` | |
| shape: (bs, response_length) | |
| returns: `(torch.Tensor)` | |
| shape: (bs, response_length) | |
| """ | |
| nextvalues = 0 | |
| lastgaelam = 0 | |
| advantages_reversed = [] | |
| gen_len = token_level_rewards.shape[-1] | |
| for t in reversed(range(gen_len)): | |
| delta = token_level_rewards[:, t] + gamma * nextvalues - values[:, t] | |
| gaelam = delta + gamma * lam * lastgaelam | |
| if response_mask[:, t]: # skip values and TD-error on observation tokens | |
| nextvalues = values[:, t] | |
| lastgaelam = gaelam | |
| advantages_reversed.append(lastgaelam) | |
| advantages = torch.stack(advantages_reversed[::-1], dim=1) | |
| returns = advantages + values | |
| advantages = VF.masked_whiten(advantages, response_mask) | |
| return advantages, returns | |
| def compute_grpo_outcome_advantage( | |
| token_level_rewards: torch.Tensor, response_mask: torch.Tensor, index: torch.Tensor, eps: float = 1e-6, **kwargs | |
| ) -> tuple[torch.Tensor, torch.Tensor]: | |
| """ | |
| Compute advantage for GRPO, operating only on Outcome reward (with only one scalar reward for each response). | |
| ``scale_rewards=True`` uses standard GRPO whitening: | |
| A_i = (r_i - μ) / (σ + ε) | |
| ``scale_rewards=False`` uses raw-centered advantages: | |
| A_i = r_i - μ | |
| Oracle-aware statistics (active when ``is_oracle_row`` is supplied and | |
| ``oracle_excluded_baseline=True``): | |
| Both μ and σ come from the on-policy rollouts only. Excluding the | |
| oracle from μ keeps the baseline action-independent and an honest | |
| estimate of E_{y~π_θ}[R]; a perfect-reward oracle inside μ would push | |
| the best on-policy rollout to a negative advantage on hard prompts and | |
| corrupt any sign-aware selector. Excluding it from σ stops it from | |
| inflating the denominator and shrinking on-policy gradients. | |
| On-policy σ is numerically unsafe on its own: when every on-policy | |
| reward collapses to one value (typical when a shaping floor swallows | |
| all failures), σ_op is exactly 0 and the oracle advantage diverges. | |
| Groups with σ_op < SIGMA_OP_FALLBACK_THRESHOLD therefore fall back to | |
| the full-group σ, which stays positive because the oracle is a | |
| high-reward outlier. | |
| Args: | |
| token_level_rewards, response_mask, index, eps: standard. | |
| is_oracle_row (kwargs): np.ndarray[bool] of shape (bs,); True marks the | |
| annotation-derived oracle row. | |
| oracle_excluded_baseline (kwargs): build μ and σ from on-policy rows. | |
| directional_gain (kwargs): scale on-policy utilities by the clipped | |
| oracle-gap ratio (σ_g/σ_op)^γ. | |
| directional_gain_recenter (kwargs): after a positive-only gain, | |
| subtract the transformed on-policy group mean, preserving the | |
| directional preference while restoring a zero-mean group. | |
| """ | |
| is_oracle_row = kwargs.get("is_oracle_row", None) | |
| scale_rewards = bool(kwargs.get("scale_rewards", True)) | |
| exclude_oracle = bool(kwargs.get("oracle_excluded_baseline", False)) | |
| # The oracle-gap gain multiplies the base advantage by | |
| # clip((σ_g/σ_op)^γ, 1, 4). σ_g includes the oracle and therefore measures | |
| # how far it lies outside the on-policy reward distribution, so the gain | |
| # grows exactly on the groups the policy has not solved. It requires the | |
| # oracle-excluded baseline μ_op; σ_op stays available even in raw mode. | |
| use_gain = bool(kwargs.get("directional_gain", False)) | |
| gain_gamma = float(kwargs.get("directional_gain_gamma", 0.25)) | |
| gain_pos_only = bool(kwargs.get("directional_gain_positive_only", False)) | |
| gain_recenter = bool(kwargs.get("directional_gain_recenter", False)) | |
| if use_gain: | |
| exclude_oracle = True | |
| # Cap (σ_g/σ_op)^γ so a near-degenerate σ_op cannot blow the gain up. | |
| GAIN_MAX: float = 4.0 | |
| scores = token_level_rewards.sum(dim=-1) | |
| bsz = scores.shape[0] | |
| # Build two per-group score lists in a single pass: | |
| # id2score_full = ALL rows (σ_op fallback source, and vanilla GRPO | |
| # mean/std when exclude_oracle=False). | |
| # id2score_op = on-policy only (μ and σ source when exclude_oracle). | |
| id2score_full: dict[Any, list[torch.Tensor]] = defaultdict(list) | |
| id2score_op: dict[Any, list[torch.Tensor]] = defaultdict(list) | |
| for i in range(bsz): | |
| id2score_full[index[i]].append(scores[i]) | |
| if exclude_oracle and is_oracle_row is not None and bool(is_oracle_row[i]): | |
| continue | |
| id2score_op[index[i]].append(scores[i]) | |
| # 1e-3 is far below any realistic reward std (≥ ~0.05 even on very hard | |
| # prompts), so the σ_g fallback only fires on truly degenerate groups. | |
| SIGMA_OP_FALLBACK_THRESHOLD: float = 1e-3 | |
| sigma_op_fallback_count = 0 | |
| id2mean: dict[Any, torch.Tensor] = {} | |
| id2std: dict[Any, torch.Tensor] = {} | |
| # Per-group gain g_idx = clip((σ_g/σ_op)^γ, ≤ GAIN_MAX), applied to | |
| # on-policy rows only. Empty unless use_gain. | |
| id2gain: dict[Any, torch.Tensor] = {} | |
| for idx, full_lst in id2score_full.items(): | |
| if not exclude_oracle: | |
| assert len(full_lst) > 1, "GRPO needs rollout.n > 1." | |
| id2mean[idx] = torch.mean(torch.tensor(full_lst)) | |
| id2std[idx] = torch.std(torch.tensor(full_lst)) | |
| continue | |
| op_lst = id2score_op.get(idx, []) | |
| # ---- Mean source: on-policy ONLY (with edge-case fallbacks) ---- | |
| if len(op_lst) == 0: | |
| # Pathological: the group holds only the oracle row. The append | |
| # pipeline keeps all n on-policy rollouts, so this cannot happen. | |
| id2mean[idx] = torch.zeros((), dtype=scores.dtype, device=scores.device) | |
| elif len(op_lst) == 1: | |
| # Single on-policy row → its own mean → its own advantage ≈ 0. | |
| id2mean[idx] = op_lst[0] | |
| else: | |
| id2mean[idx] = torch.mean(torch.tensor(op_lst)) | |
| # ---- Std sources ---- | |
| # σ_g (full group, oracle included) is always computed: it is the | |
| # degenerate-group fallback and also feeds the oracle-gap gain. σ_op | |
| # (on-policy only) is the base whitening std. | |
| sigma_g = ( | |
| torch.std(torch.tensor(full_lst)) | |
| if len(full_lst) >= 2 | |
| else torch.zeros((), dtype=scores.dtype, device=scores.device) | |
| ) | |
| sigma_op = torch.std(torch.tensor(op_lst)) if len(op_lst) >= 2 else None | |
| op_degenerate = sigma_op is None or float(sigma_op.item()) < SIGMA_OP_FALLBACK_THRESHOLD | |
| if op_degenerate: | |
| id2std[idx] = sigma_g | |
| sigma_op_fallback_count += 1 | |
| else: | |
| id2std[idx] = sigma_op | |
| # ---- Oracle-gap gain (σ_g/σ_op)^γ ---- | |
| # Scaled mode neutralizes the gain when σ_op degenerates, because its | |
| # whitening denominator has already fallen back to σ_g. Raw mode can | |
| # follow the clipped-gain formula even for tiny σ_op: eps and the upper | |
| # cap keep g finite, while the unwhitened centered reward still tends to | |
| # zero with σ_op. | |
| if use_gain: | |
| if op_degenerate and scale_rewards: | |
| id2gain[idx] = torch.ones((), dtype=scores.dtype, device=scores.device) | |
| else: | |
| sigma_op_for_gain = ( | |
| sigma_op | |
| if sigma_op is not None | |
| else torch.zeros((), dtype=scores.dtype, device=scores.device) | |
| ) | |
| ratio = sigma_g / (sigma_op_for_gain + eps) | |
| id2gain[idx] = torch.clamp( | |
| ratio ** gain_gamma, | |
| min=1.0, | |
| max=GAIN_MAX, | |
| ) | |
| gain_on_policy = 0 | |
| gain_amplified = 0 | |
| for i in range(bsz): | |
| m = id2mean.get(index[i]) | |
| s = id2std.get(index[i]) | |
| if m is None: | |
| continue | |
| centered_reward = scores[i] - m | |
| adv = centered_reward / (s + eps) if scale_rewards else centered_reward | |
| if use_gain: | |
| g = id2gain.get(index[i], None) | |
| # Amplify ON-POLICY rows only. The oracle row keeps its raw | |
| # (r_oracle-μ_op)/σ_op here; it is overwritten downstream by the | |
| # detached anchor. When positive_only, amplify only improving | |
| # (adv > 0, toward-oracle) rows; negatives stay at base scale. | |
| if g is not None and not (is_oracle_row is not None and bool(is_oracle_row[i])): | |
| gain_on_policy += 1 | |
| if (not gain_pos_only) or (adv > 0): | |
| adv = adv * g | |
| gain_amplified += 1 | |
| scores[i] = adv | |
| # Positive-only gain is an asymmetric utility transform. Without this | |
| # second baseline, each on-policy group has a positive sum | |
| # (g - 1) * sum(max(A_base, 0)), | |
| # which stacks an unconditional positive bias on top of oracle anchoring and | |
| # sign-balanced selection. Re-centering preserves ranking and the boosted | |
| # positive-vs-negative margin while restoring mean_op(A)=0. | |
| recenter_shifts: list[float] = [] | |
| if use_gain and gain_recenter: | |
| id2transformed_op: dict[Any, list[torch.Tensor]] = defaultdict(list) | |
| for i in range(bsz): | |
| if is_oracle_row is not None and bool(is_oracle_row[i]): | |
| continue | |
| id2transformed_op[index[i]].append(scores[i]) | |
| id2recenter_shift: dict[Any, torch.Tensor] = {} | |
| for idx, transformed in id2transformed_op.items(): | |
| if transformed: | |
| shift = torch.mean(torch.stack(transformed)) | |
| id2recenter_shift[idx] = shift | |
| recenter_shifts.append(abs(float(shift.item()))) | |
| for i in range(bsz): | |
| if is_oracle_row is not None and bool(is_oracle_row[i]): | |
| continue | |
| shift = id2recenter_shift.get(index[i]) | |
| if shift is not None: | |
| scores[i] = scores[i] - shift | |
| # Stash the fallback count on a side-channel kwarg if the caller passed an | |
| # empty dict for telemetry; this is opt-in to keep the function API clean. | |
| telemetry = kwargs.get("_telemetry_out", None) | |
| if telemetry is not None: | |
| telemetry["scale_rewards"] = float(scale_rewards) | |
| telemetry["sigma_op_fallback_count"] = sigma_op_fallback_count | |
| if use_gain and id2gain: | |
| gains = [float(g.item()) for g in id2gain.values()] | |
| telemetry["directional_gain_mean"] = sum(gains) / len(gains) | |
| telemetry["directional_gain_max"] = max(gains) | |
| telemetry["directional_gain_positive_only"] = float(gain_pos_only) | |
| telemetry["directional_gain_recenter"] = float(gain_recenter) | |
| if recenter_shifts: | |
| telemetry["directional_gain_recenter_abs_shift_mean"] = ( | |
| sum(recenter_shifts) / len(recenter_shifts) | |
| ) | |
| telemetry["directional_gain_recenter_abs_shift_max"] = max(recenter_shifts) | |
| if gain_on_policy > 0: | |
| telemetry["directional_gain_amplified_frac"] = ( | |
| gain_amplified / gain_on_policy | |
| ) | |
| returns = scores.unsqueeze(-1) * response_mask | |
| return returns, returns | |
| def compute_grpo_passk_outcome_advantage( | |
| token_level_rewards: torch.Tensor, response_mask: torch.Tensor, index: torch.Tensor, eps: float = 1e-6, **kwargs | |
| ) -> tuple[torch.Tensor, torch.Tensor]: | |
| """ | |
| Compute advantage for Pass@k using a GRPO-style outcome reward formulation. | |
| Only the best response per group gets a non-zero advantage: r_max - r_second_max. | |
| Implemented as described in https://arxiv.org/abs/2503.19595. | |
| Args: | |
| token_level_rewards: `(torch.Tensor)` | |
| shape: (bs, response_length) | |
| response_mask: `(torch.Tensor)` | |
| shape: (bs, response_length) | |
| index: `(torch.Tensor)` | |
| shape: (bs,) | |
| eps: `(float)` | |
| epsilon value to avoid division by zero | |
| Returns: | |
| advantages: `(torch.Tensor)` | |
| shape: (bs, response_length) | |
| returns: `(torch.Tensor)` | |
| shape: (bs, response_length) | |
| """ | |
| scores = token_level_rewards.sum(dim=-1) | |
| advantages = torch.zeros_like(scores) | |
| id2score = defaultdict(list) | |
| id2indices = defaultdict(list) | |
| bsz = scores.shape[0] | |
| for i in range(bsz): | |
| id2score[index[i]].append(scores[i]) | |
| id2indices[index[i]].append(i) | |
| for idx in id2score: | |
| assert len(id2score[idx]) > 1, "GRPO needs rollout.n > 1." | |
| rewards = torch.tensor(id2score[idx]) | |
| topk, topk_idx = torch.topk(rewards, k=2) | |
| r_max, r_second_max = topk[0], topk[1] | |
| i_max = id2indices[idx][topk_idx[0]] | |
| advantages[i_max] = (r_max - r_second_max) / (torch.std(torch.tensor(id2score[idx])) + eps) | |
| returns = advantages.unsqueeze(-1) * response_mask | |
| return returns, returns | |
| def compute_rloo_outcome_advantage( | |
| token_level_rewards: torch.Tensor, response_mask: torch.Tensor, index: torch.Tensor, **kwargs | |
| ) -> tuple[torch.Tensor, torch.Tensor]: | |
| """ | |
| Compute advantage for RLOO based on https://arxiv.org/abs/2402.14740 | |
| Args: | |
| token_level_rewards: `(torch.Tensor)` | |
| shape: (bs, response_length) | |
| response_mask: `(torch.Tensor)` | |
| shape: (bs, response_length) | |
| index: `(torch.Tensor)` | |
| shape: (bs,) | |
| Returns: | |
| advantages: `(torch.Tensor)` | |
| shape: (bs, response_length) | |
| returns: `(torch.Tensor)` | |
| shape: (bs, response_length) | |
| """ | |
| scores = token_level_rewards.sum(dim=-1) | |
| id2score = defaultdict(list) | |
| id2sum = {} | |
| bsz = scores.shape[0] | |
| for i in range(bsz): | |
| id2score[index[i]].append(scores[i]) | |
| for idx in id2score: | |
| id2sum[idx] = torch.sum(torch.tensor(id2score[idx])) | |
| for i in range(bsz): | |
| sample_num = len(id2score[index[i]]) | |
| assert sample_num > 1, "RLOO needs rollout.n > 1." | |
| baseline = (id2sum[index[i]] - scores[i]) / (sample_num - 1) | |
| scores[i] = scores[i] - baseline | |
| returns = scores.unsqueeze(-1) * response_mask | |
| return returns, returns | |
| def compute_reinforce_plus_plus_outcome_advantage( | |
| token_level_rewards: torch.Tensor, response_mask: torch.Tensor, gamma: torch.Tensor, **kwargs | |
| ) -> tuple[torch.Tensor, torch.Tensor]: | |
| """ | |
| Compute advantage for REINFORCE++. | |
| This implementation is based on the paper: https://arxiv.org/abs/2501.03262 | |
| Args: | |
| token_level_rewards: `(torch.Tensor)` | |
| shape: (bs, response_length) | |
| response_mask: `(torch.Tensor)` | |
| shape: (bs, response_length) | |
| Returns: | |
| advantages: `(torch.Tensor)` | |
| shape: (bs, response_length) | |
| returns: `(torch.Tensor)` | |
| shape: (bs, response_length) | |
| """ | |
| returns = torch.zeros_like(token_level_rewards) | |
| running_return = 0 | |
| for t in reversed(range(token_level_rewards.shape[1])): | |
| running_return = token_level_rewards[:, t] + gamma * running_return | |
| returns[:, t] = running_return | |
| # Reset after EOS | |
| running_return = running_return * response_mask[:, t] | |
| advantages = VF.masked_whiten(returns, response_mask) | |
| return advantages, returns | |
| def compute_remax_outcome_advantage( | |
| token_level_rewards: torch.Tensor, reward_baselines: torch.Tensor, response_mask: torch.Tensor, **kwargs | |
| ) -> tuple[torch.Tensor, torch.Tensor]: | |
| """ | |
| Compute advantage for ReMax, operating only on Outcome reward | |
| This implementation is based on the paper: https://arxiv.org/abs/2310.10505 | |
| (with only one scalar reward for each response). | |
| Args: | |
| token_level_rewards: `(torch.Tensor)` | |
| shape: (bs, response_length) | |
| reward_baselines: `(torch.Tensor)` | |
| shape: (bs,) | |
| response_mask: `(torch.Tensor)` | |
| shape: (bs, response_length) | |
| Returns: | |
| advantages: `(torch.Tensor)` | |
| shape: (bs, response_length) | |
| returns: `(torch.Tensor)` | |
| shape: (bs, response_length) | |
| """ | |
| advantages = (token_level_rewards.sum(dim=-1) - reward_baselines) * response_mask | |
| returns = (token_level_rewards * response_mask).flip(dims=(-1,)).cumsum(dim=-1).flip(dims=(-1,)) | |
| return advantages, returns | |
| def compute_rewards( | |
| token_level_scores: torch.Tensor, | |
| log_probs: torch.Tensor, | |
| ref_log_probs: torch.Tensor, | |
| kl_ratio: float, | |
| ) -> torch.Tensor: | |
| kl = log_probs - ref_log_probs | |
| return token_level_scores - kl * kl_ratio | |
| def average_loss( | |
| values: torch.Tensor, mask: torch.Tensor, mode: Literal["token", "seq"], eps: float = 1e-8 | |
| ) -> torch.Tensor: | |
| """Average the policy loss. | |
| Args: | |
| values: `(torch.Tensor)` | |
| shape: (bs, response_length) | |
| mask: `(torch.Tensor)` | |
| shape: (bs, response_length) | |
| mode: `(Literal["token", "seq"])` | |
| "token": average the loss in the whole batch | |
| "seq": average the loss in each sequence then average the mean of the means | |
| eps: `(float)` | |
| epsilon value | |
| Returns: | |
| loss: `a scalar torch.Tensor` | |
| """ | |
| if mode == "token": | |
| return VF.masked_mean(values, mask, eps=eps) | |
| elif mode == "seq": | |
| return ((values * mask).sum(-1) / (mask.sum(-1) + eps)).mean() | |
| else: | |
| raise NotImplementedError(f"Unknown mode: {mode}.") | |
| def compute_policy_loss( | |
| old_log_probs: torch.Tensor, | |
| log_probs: torch.Tensor, | |
| advantages: torch.Tensor, | |
| response_mask: torch.Tensor, | |
| clip_ratio_low: float, | |
| clip_ratio_high: float, | |
| clip_ratio_dual: float, | |
| loss_type: Literal["default", "gspo", "gspo_token", "cispo"], | |
| loss_avg_mode: Literal["token", "seq"], | |
| **kwargs, | |
| ) -> tuple[torch.Tensor, dict[str, float]]: | |
| """Compute the clipped policy objective and related metrics for PPO. | |
| Adapted from https://github.com/huggingface/trl/blob/v0.15.0/trl/trainer/ppo_trainer.py#L568 | |
| Args: | |
| old_log_prob: `(torch.Tensor)` | |
| shape: (bs, response_length) | |
| log_prob: `(torch.Tensor)` | |
| shape: (bs, response_length) | |
| advantages: `(torch.Tensor)` | |
| shape: (bs, response_length) | |
| response_mask: `(torch.Tensor)` | |
| shape: (bs, response_length) | |
| clip_ratio_low: (float) | |
| The lower clip range used in PPO. See https://arxiv.org/abs/1707.06347 | |
| clip_ratio_high: (float) | |
| The higher clip range used in DAPO. See https://arxiv.org/pdf/2503.14476 | |
| clip_ratio_dual: (float) | |
| The dual clip range used in Dual-clip PPO. See https://arxiv.org/pdf/1912.09729 | |
| loss_avg_mode: (Literal["token", "seq"]) | |
| "token": average the loss in the whole batch | |
| "seq": average the loss in each sequence then average the mean of the means | |
| Returns: | |
| pg_loss: `a scalar torch.Tensor` | |
| policy gradient loss computed via PPO | |
| pg_clipfrac_higher: (float) | |
| a float number indicating the fraction of policy gradient loss being clipped to a higher value | |
| pg_clipfrac_lower: (float) | |
| a float number indicating the fraction of policy gradient loss being clipped to a lower value | |
| ppo_kl: (float) | |
| a float number indicating the mean KL divergence between the old policy and the new policy | |
| entropy_loss: (float) | |
| a float number indicating the mean entropy loss | |
| """ | |
| negative_approx_kl = log_probs - old_log_probs | |
| if loss_type in ["gspo", "gspo_token"]: | |
| # compute sequence-level importance ratio | |
| negative_approx_kl_in_seq = VF.masked_mean(negative_approx_kl, response_mask, dim=-1) | |
| # combined ratio at token level | |
| if loss_type == "gspo_token": | |
| log_importance_ratio = negative_approx_kl_in_seq.detach().unsqueeze(-1) + log_probs - log_probs.detach() | |
| else: | |
| log_importance_ratio = negative_approx_kl_in_seq.unsqueeze(-1) * response_mask | |
| else: | |
| log_importance_ratio = negative_approx_kl | |
| # clamp the ratio before exp to avoid nan grad | |
| # see: https://github.com/pytorch/pytorch/issues/10729 | |
| ratio = torch.exp(torch.clamp(log_importance_ratio, -20.0, 20.0)) | |
| clipped_ratio = torch.exp( | |
| torch.clamp(log_importance_ratio, np.log(1.0 - clip_ratio_low), np.log(1.0 + clip_ratio_high)) | |
| ) | |
| # pg metrics | |
| metrics = {"ppo_kl": -negative_approx_kl} | |
| # use negative log probs as an estimator of entropy loss | |
| metrics["entropy_loss"] = average_loss(-log_probs, response_mask, mode=loss_avg_mode) | |
| if loss_type == "cispo": | |
| final_pg_loss = -advantages * log_probs * clipped_ratio.detach() | |
| else: | |
| pg_loss = -advantages * ratio # -ratio * A | |
| pg_loss2 = -advantages * clipped_ratio # -clip(ratio, 1-clip_low, 1+clip_high) * A | |
| pg_loss3 = -advantages * clip_ratio_dual # -clip_dual * A | |
| clipped_pg_loss_higher = torch.max(pg_loss, pg_loss2) # clip if pg_loss < pg_loss2 | |
| metrics["pg_clipfrac_higher"] = (pg_loss < pg_loss2).float() | |
| clipped_pg_loss_lower = torch.min(clipped_pg_loss_higher, pg_loss3) # clip if pg_loss > pg_loss3 and adv < 0 | |
| final_pg_loss = torch.where(advantages < 0, clipped_pg_loss_lower, clipped_pg_loss_higher) | |
| metrics["pg_clipfrac_lower"] = (clipped_pg_loss_higher > pg_loss3).float() * (advantages < 0).float() | |
| final_pg_loss = average_loss(final_pg_loss, response_mask, mode=loss_avg_mode) | |
| metrics = {k: VF.masked_mean(v, response_mask).detach().item() for k, v in metrics.items()} | |
| return final_pg_loss, metrics | |
| def compute_value_loss( | |
| vpreds: torch.Tensor, | |
| returns: torch.Tensor, | |
| values: torch.Tensor, | |
| response_mask: torch.Tensor, | |
| cliprange_value: float, | |
| loss_avg_mode: Literal["token", "seq"], | |
| ) -> tuple[torch.Tensor, dict[str, float]]: | |
| """Compute the value loss. | |
| Adapted from https://github.com/huggingface/trl/blob/v0.15.0/trl/trainer/ppo_trainer.py#L556 | |
| Args: | |
| vpreds (`torch.FloatTensor`): | |
| Predicted values of the value head, shape (`batch_size`, `response_length`) | |
| returns: (`torch.FloatTensor`): | |
| Ground truth returns, shape (`batch_size`, `response_length`) | |
| values (`torch.FloatTensor`): | |
| Old values of value head, shape (`batch_size`, `response_length`) | |
| response_mask: `(torch.Tensor)` | |
| shape: (bs, response_length) | |
| cliprange_value: (float) | |
| The clip range for value net used in PPO. See https://arxiv.org/abs/1707.06347 | |
| loss_avg_mode: (Literal["token", "seq"]) | |
| "token": average the loss in the whole batch | |
| "seq": average the loss in each sequence then average the mean of the means | |
| Returns: | |
| vf_loss: a scalar (`torch.FloatTensor`): | |
| value function loss | |
| vf_clipfrac: a float | |
| The ratio of vf being clipped | |
| vpred_mean: a float | |
| The mean of predicted values | |
| """ | |
| vpredclipped = torch.clamp(vpreds, values - cliprange_value, values + cliprange_value) | |
| vf_loss1 = torch.square(vpreds - returns) | |
| vf_loss2 = torch.square(vpredclipped - returns) | |
| clipped_vf_losses = torch.max(vf_loss1, vf_loss2) # clip if vf_loss1 < vf_loss2 | |
| vf_loss = 0.5 * average_loss(clipped_vf_losses, response_mask, mode=loss_avg_mode) | |
| metrics = { | |
| "vf_clipfrac": VF.masked_mean((vf_loss1 < vf_loss2).float(), response_mask).detach().item(), | |
| "vpred_mean": VF.masked_mean(vpreds, response_mask).detach().item(), | |
| } | |
| return vf_loss, metrics | |
| def compute_kl( | |
| log_probs: torch.FloatTensor, | |
| ref_log_probs: torch.FloatTensor, | |
| kl_penalty: Literal["kl", "abs", "mse", "low_var_kl", "full"], | |
| ) -> torch.Tensor: | |
| """Compute KL divergence given log_probs and ref_log_probs. | |
| Adapted from https://github.com/huggingface/trl/blob/v0.11.0/trl/trainer/ppo_trainer.py#L1150 | |
| Args: | |
| log_probs: torch.Tensor | |
| ref_log_probs: torch.Tensor | |
| kl_penalty: str ("kl", "abs", "mse", "low_var_kl", "full") | |
| Returns: | |
| kl_div: torch.Tensor | |
| """ | |
| log_probs, ref_log_probs = log_probs.float(), ref_log_probs.float() | |
| if kl_penalty == "kl": | |
| return log_probs - ref_log_probs | |
| if kl_penalty == "abs": | |
| return (log_probs - ref_log_probs).abs() | |
| if kl_penalty == "mse": | |
| return 0.5 * (log_probs - ref_log_probs).square() | |
| # J. Schulman. Approximating kl divergence, 2020. | |
| # URL http://joschu.net/blog/kl-approx.html | |
| if kl_penalty == "low_var_kl": | |
| # For numerical stability | |
| kl = (ref_log_probs - log_probs).clamp(-20.0, 20.0) | |
| kld = (kl.exp() - kl - 1).contiguous() | |
| return torch.clamp(kld, min=-10.0, max=10.0) | |
| if kl_penalty == "full": | |
| return F.kl_div(ref_log_probs, log_probs, log_target=True, reduction="none").sum(-1) | |
| raise NotImplementedError(f"Unknown KL penalty: {kl_penalty}.") | |