feat: add online ppo with value-model critic and gae advantages

- register online_ppo train type backed by PPOStrategy: token-level clipped surrogate over GAE advantages plus masked value regression against rollout-pinned returns, with explained-variance metrics
- fold the reference-KL penalty (k3 estimator) into per-token rewards before GAE and pin advantages/returns on RolloutResult so replayed gradient steps optimize fixed targets
- add self-contained ValueModel critic with a zero-initialized value head and backbone warm-started from policy weights; AutoRegressiveLM stays untouched and trunk parity is pinned by tests
- step the critic's own optimizer outside the policy-version lock with the same max_grad_norm clipping as the policy
- persist critic state as value_model.pt/value_optimizer.pt checkpoint extras; resume restores it, fails loudly when missing, and the train.sh completeness check requires the extras for online_ppo configs
- extract shared rollout sequence/logprob helpers from GRPO (behavior unchanged) and add ppo_gamma/ppo_gae_lambda/ppo_vf_coef CLI options
This commit is contained in:
2026-09-05 01:59:50 +08:00
parent 816b96a58a
commit 350e4a1849
17 changed files with 1390 additions and 72 deletions
+55
View File
@@ -0,0 +1,55 @@
"""Value (critic) model for actor-critic RL training."""
from typing import Dict, Optional
import torch.nn as nn
from torch import Tensor
from astrai.config.model_config import AutoRegressiveLMConfig
from astrai.model.automodel import ModelFactory
from astrai.model.components.linear import Linear
from astrai.model.transformer import AutoRegressiveLM, process_attention_mask
@ModelFactory.register("value_model")
class ValueModel(AutoRegressiveLM):
"""Critic scoring each state with a scalar instead of vocab logits.
Inherits the ``AutoRegressiveLM`` components so a policy checkpoint can
warm-start the critic backbone (``load_state_dict(..., strict=False)``);
only ``value_head`` keeps its fresh initialization. The inherited
``lm_head`` parameters stay dormant — the forward below never projects
through them — so checkpoints round-trip with stable keys. The trunk
pass mirrors ``AutoRegressiveLM.forward`` for training-style input;
``tests/trainer/test_ppo_strategy.py`` pins the two to identical
hidden states.
"""
def __init__(self, config: AutoRegressiveLMConfig):
super().__init__(config)
self.value_head = Linear(config.hidden_size, 1, bias=True)
# Zero head so training starts from V(s) == 0 and the first GAE
# advantages are driven purely by rewards.
nn.init.zeros_(self.value_head.weight)
nn.init.zeros_(self.value_head.bias)
def forward(
self,
input_ids: Tensor,
input_mask: Optional[Tensor] = None,
position_ids: Optional[Tensor] = None,
) -> Dict[str, Tensor]:
if input_ids.ndim != 2:
raise ValueError("critic input_ids must be [batch, seq_len]")
x = self.embed_tokens(input_ids)
rotary_emb = self.rotary_embedding(x, position_ids)
attn_mask = process_attention_mask(input_mask)
use_sdpa_causal_mask = attn_mask is None
for layer in self.layers:
x = layer(x, rotary_emb, attn_mask, None, use_sdpa_causal_mask, None)[
"hidden_states"
]
hidden_states = self.norm(x)
values = self.value_head(hidden_states).squeeze(-1)
return {"values": values}