| name | chord-on-policy-off-policy-harmonization |
| title | CHORD: Harmonizing SFT and RL via Dynamic Weighting |
| version | 0.0.2 |
| engine | skillxiv-v0.0.2-claude-opus-4.6 |
| license | MIT |
| url | https://arxiv.org/abs/2508.11408 |
| keywords | ["reinforcement-learning","supervised-fine-tuning","dynamic-weighting","policy-learning","llm-training"] |
| description | Harmonize supervised fine-tuning and reinforcement learning through dynamic weighting, balancing expert imitation and on-policy exploration to prevent response pattern disruption. |
CHORD: Harmonizing SFT and RL via Dynamic Weighting
Core Concept
CHORD unifies Supervised Fine-Tuning (SFT) and Reinforcement Learning (RL) through a dual-control mechanism that maintains learned response patterns while enabling exploratory on-policy learning. Rather than treating SFT as a separate preliminary stage, CHORD integrates it as a dynamically weighted auxiliary objective within RL. A global coefficient guides the overall transition from imitation to exploration, while token-wise weighting enables fine-grained learning from expert data while suppressing off-policy interference.
Architecture Overview
- Global Weighting Coefficient: Interpolates between SFT (expert imitation) and RL (on-policy exploration)
- Token-Wise Weighting Function: Fine-grained control at token level, reducing off-policy interference
- Unified On/Off-Policy Framework: Treats SFT as off-policy learning and RL as on-policy within same formulation
- Expert Data Preservation: Maintains quality of established response patterns during RL training
- Adaptive Transition: Smooth interpolation preventing catastrophic forgetting
Implementation Steps
1. Define the Unified On/Off-Policy Formulation
Establish the mathematical framework combining SFT and RL:
from typing import Tuple
import torch
import torch.nn.functional as F
def unified_policy_objective(
model_logits: torch.Tensor,
expert_logits: torch.Tensor,
rl_rewards: torch.Tensor,
global_alpha: float = 0.5,
token_weights: torch.Tensor = None
) -> Tuple[torch.Tensor, dict]:
"""
Compute unified CHORD objective combining SFT and RL.
Loss = alpha * SFT_loss + (1 - alpha) * RL_loss
Token weights modulate per-token contribution.
"""
batch_size, seq_len, vocab_size = model_logits.shape
sft_loss = F.cross_entropy(
model_logits.view(-1, vocab_size),
expert_logits.argmax(-1).view(-1),
reduction='none'
).view(batch_size, seq_len)
model_probs = F.softmax(model_logits, dim=-1)
expert_probs = F.softmax(expert_logits, dim=-1)
kl_loss = torch.sum(
expert_probs * (torch.log(expert_probs) - torch.log(model_probs)),
dim=-1
)
rl_loss = -rl_rewards + 0.1 * kl_loss
if token_weights :
token_weights = torch.ones_like(sft_loss)
weighted_sft = (sft_loss * token_weights).mean()
weighted_rl = (rl_loss * token_weights).mean()
total_loss = global_alpha * weighted_sft + ( - global_alpha) * weighted_rl
metrics = {
: weighted_sft.item(),
: weighted_rl.item(),
: total_loss.item(),
: global_alpha
}
total_loss, metrics
2. Implement Global Coefficient Scheduling
Create curriculum that transitions from imitation to exploration:
class GlobalCoefficientScheduler:
def __init__(
self,
initial_alpha: float = 1.0,
final_alpha: float = 0.2,
total_steps: int = 10000,
schedule_type: str = "linear"
):
self.initial_alpha = initial_alpha
self.final_alpha = final_alpha
self.total_steps = total_steps
self.schedule_type = schedule_type
self.current_step = 0
def get_alpha(self, step: int = None) -> float:
"""Compute global weighting coefficient for current step."""
if step is None:
step = self.current_step
progress = min(step / self.total_steps, 1.0)
if self.schedule_type == "linear":
alpha = self.initial_alpha - (self.initial_alpha - self.final_alpha) * progress
elif self.schedule_type == "cosine":
math
alpha = .final_alpha + * (.initial_alpha - .final_alpha) * \
( + math.cos(math.pi * progress))
.schedule_type == :
math
alpha = .final_alpha + (.initial_alpha - .final_alpha) * \
math.exp(- * progress)
alpha
() -> :
alpha = .get_alpha(.current_step)
.current_step +=
alpha
3. Implement Token-Wise Weighting Function
Compute fine-grained weights that suppress off-policy interference:
def compute_token_weights(
model_logits: torch.Tensor,
expert_logits: torch.Tensor,
rl_rewards: torch.Tensor,
weighting_strategy: str = "adaptive"
) -> torch.Tensor:
"""
Compute token-level weights balancing expert data learning and exploration.
Strategies:
- adaptive: Weight based on expert confidence and reward signal alignment
- entropy: Weight by model confidence (high confidence = high weight for RL)
- margin: Weight by prediction margin between expert and model
"""
batch_size, seq_len, vocab_size = model_logits.shape
if weighting_strategy == "adaptive":
expert_probs = F.softmax(expert_logits, dim=-1)
expert_entropy = -torch.sum(
expert_probs * torch.log(expert_probs + 1e-10),
dim=-1
)
expert_confidence = 1.0 - (expert_entropy / torch.log(torch.tensor(vocab_size)))
reward_magnitude = torch.abs(rl_rewards)
normalized_reward = (reward_magnitude - reward_magnitude.min()) / \
(reward_magnitude.max() - reward_magnitude.min() + 1e-10)
weights = expert_confidence * (1.0 - 0.5 * normalized_reward)
elif weighting_strategy == "entropy":
model_probs = F.softmax(model_logits, dim=-1)
model_entropy = -torch.sum(
model_probs * torch.log(model_probs + 1e-10),
dim=-
)
model_confidence = - (model_entropy / torch.log(torch.tensor(vocab_size)))
weights = - model_confidence
weighting_strategy == :
model_probs = F.softmax(model_logits, dim=-)
expert_probs = F.softmax(expert_logits, dim=-)
model_top_prob = model_probs.(dim=-)[]
expert_top_prob = expert_probs.(dim=-)[]
margin = expert_top_prob - model_top_prob
weights = torch.clamp(margin, , )
:
weights = torch.ones(batch_size, seq_len)
weights = torch.clamp(weights, , )
weights
4. Implement CHORD Training Loop
Integrate components into unified training procedure:
class CHORDTrainer:
def __init__(
self,
model: torch.nn.Module,
optimizer: torch.optim.Optimizer,
initial_alpha: float = 1.0,
final_alpha: float = 0.2,
total_steps: int = 10000,
weighting_strategy: str = "adaptive"
):
self.model = model
self.optimizer = optimizer
self.alpha_scheduler = GlobalCoefficientScheduler(
initial_alpha, final_alpha, total_steps
)
self.weighting_strategy = weighting_strategy
self.current_step = 0
def train_step(
self,
batch_inputs: torch.Tensor,
expert_outputs: torch.Tensor,
rl_rewards: torch.Tensor,
) -> dict:
"""Execute single CHORD training step."""
model_logits = self.model(batch_inputs)
expert_logits = self.get_expert_logits(expert_outputs)
alpha = self.alpha_scheduler.step()
token_weights = compute_token_weights(
model_logits,
expert_logits,
rl_rewards,
self.weighting_strategy
)
loss, metrics = unified_policy_objective(
model_logits,
expert_logits,
rl_rewards,
global_alpha=alpha,
token_weights=token_weights
)
.optimizer.zero_grad()
loss.backward()
torch.nn.utils.clip_grad_norm_(.model.parameters(), )
.optimizer.step()
metrics[] = .current_step
metrics[] = token_weights.mean().item()
.current_step +=
metrics
():
5. Validate Training Dynamics
Monitor prevention of catastrophic forgetting:
def evaluate_chord_training(
model: torch.nn.Module,
sft_validation_set,
rl_validation_set,
alpha: float
) -> dict:
"""
Evaluate CHORD training:
- SFT performance shouldn't degrade
- RL performance should improve
"""
model.eval()
with torch.no_grad():
sft_accuracy = evaluate_sft_accuracy(model, sft_validation_set)
rl_reward = evaluate_rl_reward(model, rl_validation_set)
return {
"sft_accuracy": sft_accuracy,
"rl_reward": rl_reward,
"alpha": alpha,
"balance_score": alpha * sft_accuracy + (1 - alpha) * rl_reward
}
Practical Guidance
When to Use CHORD
- Training LLMs where existing response patterns are valuable
- Combining expert demonstrations with reward signals
- Preventing catastrophic forgetting during RL training
- Gradual exploration without disrupting learned behaviors
- Hybrid supervised+reinforcement training scenarios
When NOT to Use
- Pure supervised learning without RL signals
- Pure RL where forgetting established patterns is acceptable
- Scenarios requiring immediate full on-policy training
- Tasks where expert data quality is inconsistent
Key Hyperparameters
- initial_alpha: 0.8-1.0 (start with strong SFT)
- final_alpha: 0.1-0.3 (end with strong RL)
- Transition Schedule: Linear or cosine annealing
- Token Weighting Strategy: "adaptive" recommended
- Training Duration: Typically 1-2 epochs with CHORD
Performance Expectations
- SFT Preservation: Maintains 95%+ of original expert performance
- RL Improvement: 5-15% gains on task rewards
- Training Stability: Reduced variance vs. switching SFT→RL
- Convergence Speed: Similar or slightly faster than separate stages
Reference
Researchers. (2024). On-Policy RL Meets Off-Policy Experts: Harmonizing SFT and RL. arXiv preprint arXiv:2508.11408.