Video-ORA-9B / code /verl /trainer /orarl_config.py
lyhisme's picture
Add OraRL training and evaluation code
53c10a4 verified
Raw
History Blame Contribute Delete
7.91 kB
# Copyright 2024 Bytedance Ltd. and/or its affiliates
# Copyright 2026 OraRL contributors
#
# 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.
"""Validation and serialization for the GRPO/OraRL algorithm surface.
The trainer accepts exactly two recipes. Every OraRL option is checked here,
before any worker is created, so an inconsistent batch layout or a missing
oracle builder fails immediately instead of half-way through the first step.
"""
from __future__ import annotations
import math
from dataclasses import asdict
from typing import Any
SUPPORTED_ALGORITHMS = frozenset({"grpo", "orarl"})
_ORACLE_CONFIG_PREFIXES = (
"oracle_",
"directional_gain",
"detached_oracle_",
"selection_",
"post_selection_",
)
def algorithm_name(config: Any) -> str:
"""Return the normalized algorithm name."""
return str(getattr(config.algorithm, "name", "grpo")).strip().lower()
def is_orarl(config: Any) -> bool:
return algorithm_name(config) == "orarl"
def validate_algorithm(config: Any) -> None:
"""Validate GRPO/OraRL options before workers are initialized."""
name = algorithm_name(config)
if name not in SUPPORTED_ALGORITHMS:
choices = ", ".join(sorted(SUPPORTED_ALGORITHMS))
raise ValueError(f"algorithm.name must be one of {{{choices}}}, got {name!r}.")
if name == "grpo":
return
algo = config.algorithm
estimator = getattr(algo.adv_estimator, "value", algo.adv_estimator)
if estimator != "grpo":
raise ValueError("OraRL requires algorithm.adv_estimator=grpo.")
if bool(getattr(algo, "scale_rewards", True)):
raise ValueError("OraRL requires raw advantages: algorithm.scale_rewards=false.")
if not bool(algo.oracle_injection):
raise ValueError("OraRL requires algorithm.oracle_injection=true.")
if str(algo.oracle_injection_mode) != "append":
raise ValueError("OraRL requires algorithm.oracle_injection_mode=append.")
if not str(algo.oracle_builder or "").strip():
raise ValueError("OraRL requires algorithm.oracle_builder.")
if int(config.worker.rollout.n) <= 1:
raise ValueError("OraRL requires worker.rollout.n > 1.")
if bool(getattr(config.worker.rollout, "calculate_log_probs", False)):
raise ValueError(
"OraRL oracle rows require worker.rollout.calculate_log_probs=false so "
"old log probabilities are recomputed for their actual tokens."
)
prune_ratio = float(algo.selection_prune_ratio)
if not math.isfinite(prune_ratio) or not 0.0 < prune_ratio < 1.0:
raise ValueError("OraRL selection_prune_ratio must be in (0, 1).")
n_rollouts = int(config.worker.rollout.n)
keep_rows = max(1, int(n_rollouts * (1.0 - prune_ratio)))
if not bool(algo.selection_keep_oracle):
raise ValueError("OraRL requires the oracle rollout to be retained.")
oracle_slot = 1
positive = int(algo.selection_positive_quota)
negative = int(algo.selection_negative_quota)
if positive < 0 or negative < 0:
raise ValueError("OraRL selection quotas must be non-negative.")
if positive + negative + oracle_slot != keep_rows:
raise ValueError(
"OraRL selection quotas must fill the keep budget: "
f"positive({positive}) + negative({negative}) + "
f"oracle({oracle_slot}) != keep_rows({keep_rows})."
)
if not bool(algo.selection_strict_sign_balance):
raise ValueError("OraRL requires strict sign-balanced selection.")
nodes = int(config.trainer.nnodes)
gpus_per_node = int(config.trainer.n_gpus_per_node)
rollout_batch_size = int(config.data.rollout_batch_size)
if nodes <= 0 or gpus_per_node <= 0 or rollout_batch_size <= 0:
raise ValueError(
"OraRL requires positive trainer node/GPU counts and rollout_batch_size."
)
world_size = nodes * gpus_per_node
selected_batch = rollout_batch_size * keep_rows
if selected_batch % world_size != 0:
raise ValueError(
"OraRL selected batch must divide the actor world size: "
f"{selected_batch} rows for world_size={world_size}."
)
# The oracle is appended as an extra row, so the pre-selection batch is
# rollout_batch_size*(n+1) and seqlen-balanced partitioning needs it to
# shard evenly.
preselection_batch = rollout_batch_size * (n_rollouts + 1)
if preselection_batch % world_size != 0:
raise ValueError(
"OraRL append-oracle batch must divide the actor world size: "
f"{preselection_batch} rows for world_size={world_size}."
)
if not bool(algo.directional_gain):
raise ValueError("OraRL requires algorithm.directional_gain=true.")
if not bool(algo.directional_gain_positive_only):
raise ValueError("OraRL requires positive-only directional gain.")
if not bool(algo.directional_gain_recenter):
raise ValueError("OraRL requires post-gain policy re-centering.")
gain_gamma = float(algo.directional_gain_gamma)
if not math.isfinite(gain_gamma) or gain_gamma <= 0.0:
raise ValueError("OraRL directional_gain_gamma must be finite and positive.")
if not bool(algo.detached_oracle_advantage):
raise ValueError("OraRL requires a detached oracle advantage.")
detached_scale = float(algo.detached_oracle_advantage_scale)
if not math.isfinite(detached_scale) or detached_scale <= 0.0:
raise ValueError(
"OraRL detached_oracle_advantage_scale must be finite and positive."
)
gap_beta = float(algo.oracle_reward_gate_beta)
if not math.isfinite(gap_beta) or gap_beta <= 0.0:
raise ValueError("OraRL oracle_reward_gate_beta must be finite and positive.")
if bool(algo.detached_oracle_use_directional_gain):
raise ValueError(
"The released OraRL recipe keeps directional gain policy-only; "
"set detached_oracle_use_directional_gain=false."
)
match_ratio = float(algo.detached_oracle_match_best_ratio)
match_min = float(algo.detached_oracle_match_best_min)
match_max = float(algo.detached_oracle_match_best_max)
if (
not all(math.isfinite(value) for value in (match_ratio, match_min, match_max))
or match_ratio <= 0.0
or match_min < 0.0
or match_max < match_min
):
raise ValueError(
"OraRL oracle cap requires a finite positive ratio and "
"finite 0 <= min <= max."
)
if not bool(algo.post_selection_recenter):
raise ValueError("OraRL requires post-selection re-centering.")
if not bool(algo.post_selection_rms_match):
raise ValueError("OraRL requires post-selection RMS matching.")
rms_min = float(algo.post_selection_rms_min_scale)
if not 0.0 <= rms_min <= 1.0:
raise ValueError("OraRL post_selection_rms_min_scale must be in [0, 1].")
def public_config_dict(config: Any) -> dict[str, Any]:
"""Serialize the config, dropping options the selected recipe never reads."""
value = asdict(config)
if algorithm_name(config) != "grpo":
return value
algorithm = value.get("algorithm", {})
for key in tuple(algorithm):
if key.startswith(_ORACLE_CONFIG_PREFIXES):
algorithm.pop(key, None)
value.get("worker", {}).get("actor", {}).pop("selection_prune_ratio", None)
return value