From bd94ec32125885f740fe91d439378c94fe63506c Mon Sep 17 00:00:00 2001 From: N!no Date: Tue, 30 Jun 2026 15:57:48 -0400 Subject: [PATCH 1/4] add kl --- comlrl/trainers/actor_critic/ac_base.py | 20 +++ comlrl/trainers/actor_critic/iac.py | 112 ++++++++++++- comlrl/trainers/actor_critic/maac.py | 106 +++++++++++-- comlrl/trainers/reinforce/magrpo.py | 109 ++++++++++++- comlrl/utils/__init__.py | 12 ++ comlrl/utils/reference_kl.py | 118 ++++++++++++++ tests/test_reference_kl.py | 200 ++++++++++++++++++++++++ 7 files changed, 661 insertions(+), 16 deletions(-) create mode 100644 comlrl/utils/reference_kl.py create mode 100644 tests/test_reference_kl.py diff --git a/comlrl/trainers/actor_critic/ac_base.py b/comlrl/trainers/actor_critic/ac_base.py index 1ae6523..9ff3dce 100644 --- a/comlrl/trainers/actor_critic/ac_base.py +++ b/comlrl/trainers/actor_critic/ac_base.py @@ -184,6 +184,26 @@ def _summarize_rollout_metrics(self, rollouts: List[Any]) -> Dict[str, float]: if target_vals.numel() > 0 and torch.isfinite(target_vals).all(): metrics["value_target_mean"] = float(target_vals.mean().item()) + reference_kls = [ + float(sample.metadata.get("reference_kl", 0.0)) + for sample in rollouts + if "reference_kl" in sample.metadata + ] + if reference_kls: + vals = torch.tensor(reference_kls, dtype=torch.float32) + if torch.isfinite(vals).all(): + metrics["reference_kl_mean"] = float(vals.mean().item()) + + reference_penalties = [ + float(sample.metadata.get("reference_kl_penalty", 0.0)) + for sample in rollouts + if "reference_kl_penalty" in sample.metadata + ] + if reference_penalties: + vals = torch.tensor(reference_penalties, dtype=torch.float32) + if torch.isfinite(vals).all(): + metrics["reference_kl_penalty_mean"] = float(vals.mean().item()) + return metrics def _iter_dataloader(self, dataloader, epoch: int, total_epochs: int): diff --git a/comlrl/trainers/actor_critic/iac.py b/comlrl/trainers/actor_critic/iac.py index 8ed72ea..8ecec3b 100644 --- a/comlrl/trainers/actor_critic/iac.py +++ b/comlrl/trainers/actor_critic/iac.py @@ -14,6 +14,13 @@ from comlrl.utils.distributed import local_context, unwrap_model from comlrl.utils.formatters import build_formatters from comlrl.utils.model_loading import resolve_model_sources +from comlrl.utils.reference_kl import ( + clone_reference_models, + reference_kl_coef, + reference_kl_enabled, + reference_kl_for_sequence, + validate_reference_kl_config, +) from comlrl.utils.reward_utils import call_reward_function, normalize_reward_lengths from comlrl.utils.tokenizer_utils import apply_tokenizer_specials, resolve_tokenizers from .ac_base import ActorCriticTrainerBase @@ -58,6 +65,8 @@ class IACConfig: eval_batch_size: int = 1 early_termination_threshold: Optional[float] = -0.2 logging_steps: int = 1 + reference_kl_enabled: bool = False + reference_kl_coef: float = 0.1 def __post_init__(self) -> None: if self.rollout_buffer_size < 1: @@ -103,6 +112,7 @@ def __post_init__(self) -> None: "use_separate_critic=True." ) self.parallel_training = mode + validate_reference_kl_config(self, self.num_agents) @dataclass @@ -308,14 +318,25 @@ def __init__( else: self.critics = [] + self.reference_models: List[Any] = [] + if reference_kl_enabled(self.args): + self.reference_models = clone_reference_models( + self.agents, + devices=self.agent_devices, + ) + if self.tokenizers and len(self.tokenizers) == len(self.agents): for idx, tok in enumerate(self.tokenizers): models = [self.agents[idx]] if idx < len(self.critics): models.append(self.critics[idx]) + if idx < len(self.reference_models): + models.append(self.reference_models[idx]) apply_tokenizer_specials(tok, models) else: - apply_tokenizer_specials(self.tokenizer, [*self.agents, *self.critics]) + apply_tokenizer_specials( + self.tokenizer, [*self.agents, *self.critics, *self.reference_models] + ) self.agent_optimizers = [] self.critic_optimizers = [] @@ -390,6 +411,8 @@ def _init_wandb(self) -> None: "max_new_tokens": self.args.max_new_tokens, "use_separate_critic": self.args.use_separate_critic, "critic_type": self.args.critic_type, + "reference_kl_enabled": reference_kl_enabled(self.args), + "reference_kl_coef": reference_kl_coef(self.args), } sections = ( @@ -549,6 +572,14 @@ def _generate_rollout( output_values=False, ) logprobs.append(lp.squeeze(0)) + reference_kls = self._reference_kl_values( + agent_idx, + agent_model, + sequences, + full_attention_mask, + prompt_len, + response_lens, + ) return { "prompt": prompt, @@ -557,11 +588,58 @@ def _generate_rollout( "attention_mask": full_attention_mask, "response_lens": response_lens, "logprobs": logprobs, + "reference_kls": reference_kls, "values": value, "completions": completion_texts, "char_lengths": [len(txt) for txt in completion_texts], } + def _reference_kl_values( + self, + agent_idx: int, + policy_model: CausalLMWithValueHead, + sequences: torch.Tensor, + attention_mask: torch.Tensor, + prompt_len: int, + response_lens: Sequence[int], + ) -> List[float]: + if not reference_kl_enabled(self.args): + return [] + if not getattr(self, "reference_models", None): + raise RuntimeError( + "Reference KL is enabled but reference models are missing." + ) + reference_model = self.reference_models[agent_idx] + values: List[float] = [] + for seq, attn, response_len in zip(sequences, attention_mask, response_lens): + kl_value = reference_kl_for_sequence( + policy_model, + reference_model, + seq.unsqueeze(0), + attn.unsqueeze(0), + prompt_len, + int(response_len), + ) + values.append(float(kl_value.view(-1)[0].item())) + return values + + def _kl_shaped_reward( + self, reward: float, data: Dict[str, Any], index: int + ) -> Tuple[float, Dict[str, float]]: + if not reference_kl_enabled(self.args): + return reward, {} + reference_kl = 0.0 + penalty = 0.0 + raw_kls = data.get("reference_kls") or [] + if index < len(raw_kls): + reference_kl = float(raw_kls[index]) + penalty = reference_kl_coef(self.args) * reference_kl + return reward - penalty, { + "reference_kl": reference_kl, + "reference_kl_penalty": penalty, + "environment_reward": reward, + } + def _expand_rewards(self, rewards: List[float], num_ret: int) -> List[List[float]]: num_agents = self.args.num_agents if len(rewards) == 1: @@ -628,8 +706,10 @@ def _generate_agent(agent_idx: int) -> Dict[str, Any]: value = data["values"][i] reward = float(rewards_matrix[agent_idx][i]) reward_cpu = torch.tensor([reward], dtype=torch.float32) + shaped_reward, kl_meta = self._kl_shaped_reward(reward, data, i) + shaped_reward_cpu = torch.tensor([shaped_reward], dtype=torch.float32) value_cpu = value.detach().cpu() - returns_cpu = reward_cpu.clone() + returns_cpu = shaped_reward_cpu.clone() advantage_cpu = returns_cpu - value_cpu.to(dtype=returns_cpu.dtype) logprob_cpu = logprob.detach().cpu() @@ -652,6 +732,7 @@ def _generate_agent(agent_idx: int) -> Dict[str, Any]: advantage=advantage_cpu, metadata={ "char_length": data["char_lengths"][i], + **kl_meta, "value_target": returns_cpu, }, ) @@ -743,6 +824,8 @@ def _generate_agent_turn(agent_idx: int) -> Dict[str, Any]: value = data["values"][0] reward_val = float(rewards_matrix[agent_idx][0]) reward_cpu = torch.tensor([reward_val], dtype=torch.float32) + shaped_reward, kl_meta = self._kl_shaped_reward(reward_val, data, 0) + shaped_reward_cpu = torch.tensor([shaped_reward], dtype=torch.float32) value_cpu = value.detach().cpu() logprob_cpu = logprob.detach().cpu() @@ -758,12 +841,13 @@ def _generate_agent_turn(agent_idx: int) -> Dict[str, Any]: old_logprob=logprob_cpu, old_value=value_cpu, reward=reward_cpu, - returns=reward_cpu.clone(), + returns=shaped_reward_cpu.clone(), advantage=torch.zeros_like(reward_cpu), normalized_advantage=None, metadata={ "char_length": data["char_lengths"][0], "turn_idx": turn_idx, + **kl_meta, }, ) rollouts.append(sample) @@ -780,7 +864,7 @@ def _generate_agent_turn(agent_idx: int) -> Dict[str, Any]: for agent_idx in range(self.args.num_agents): traj = per_agent_samples[agent_idx] for t, sample in enumerate(traj): - r = float(sample.reward.view(-1)[0].item()) + r = float(sample.returns.view(-1)[0].item()) if t < len(traj) - 1: next_v = float(traj[t + 1].old_value.view(-1)[0].item()) target = r + gamma * next_v @@ -792,7 +876,7 @@ def _generate_agent_turn(agent_idx: int) -> Dict[str, Any]: for agent_idx in range(self.args.num_agents): future = 0.0 for sample in reversed(per_agent_samples[agent_idx]): - immediate = float(sample.reward.view(-1)[0].item()) + immediate = float(sample.returns.view(-1)[0].item()) future = immediate + gamma * future sample.returns = torch.tensor([future], dtype=torch.float32) sample.advantage = torch.zeros_like(sample.returns) @@ -1085,6 +1169,24 @@ def _update( metrics["reward_mean"].append(rewards.mean().item()) if returns_raw.numel() > 0 and torch.isfinite(returns_raw).all(): metrics["expected_return"].append(returns_raw.mean().item()) + reference_kls = [ + float(sample.metadata.get("reference_kl", 0.0)) + for sample in rollouts + if "reference_kl" in sample.metadata + ] + if reference_kls: + vals = torch.tensor(reference_kls, dtype=torch.float32) + if torch.isfinite(vals).all(): + metrics["reference_kl_mean"].append(vals.mean().item()) + reference_penalties = [ + float(sample.metadata.get("reference_kl_penalty", 0.0)) + for sample in rollouts + if "reference_kl_penalty" in sample.metadata + ] + if reference_penalties: + vals = torch.tensor(reference_penalties, dtype=torch.float32) + if torch.isfinite(vals).all(): + metrics["reference_kl_penalty_mean"].append(vals.mean().item()) if self.metrics_callback is not None: extra = self.metrics_callback(rollouts) diff --git a/comlrl/trainers/actor_critic/maac.py b/comlrl/trainers/actor_critic/maac.py index 57b4099..3cc2442 100644 --- a/comlrl/trainers/actor_critic/maac.py +++ b/comlrl/trainers/actor_critic/maac.py @@ -15,6 +15,13 @@ from comlrl.utils.distributed import local_context, unwrap_model from comlrl.utils.formatters import build_formatters from comlrl.utils.model_loading import resolve_model_sources +from comlrl.utils.reference_kl import ( + clone_reference_models, + reference_kl_coef, + reference_kl_enabled, + reference_kl_for_sequence, + validate_reference_kl_config, +) from comlrl.utils.reward_utils import call_reward_function, normalize_reward_lengths from comlrl.utils.tokenizer_utils import apply_tokenizer_specials, resolve_tokenizers from .ac_base import ActorCriticTrainerBase @@ -54,6 +61,8 @@ class MAACConfig: eval_num_samples: int = 4 eval_batch_size: int = 1 logging_steps: int = 1 + reference_kl_enabled: bool = False + reference_kl_coef: float = 0.1 def __post_init__(self) -> None: if self.rollout_buffer_size < 1: @@ -96,6 +105,7 @@ def __post_init__(self) -> None: "parallel_training='mp' requires explicit critic_devices." ) self.parallel_training = mode + validate_reference_kl_config(self, self.num_agents) class MAACTrainer(ActorCriticTrainerBase): @@ -272,12 +282,24 @@ def __init__( critic_model_instance.to(self.critic_device) self.critics: List[CausalLMWithValueHead] = [critic_model_instance] + self.reference_models: List[Any] = [] + if reference_kl_enabled(self.args): + self.reference_models = clone_reference_models( + self.agents, + devices=self.agent_devices, + ) + if self.tokenizers and len(self.tokenizers) == len(self.agents): for idx, tok in enumerate(self.tokenizers): - apply_tokenizer_specials(tok, [self.agents[idx]]) + models = [self.agents[idx]] + if idx < len(self.reference_models): + models.append(self.reference_models[idx]) + apply_tokenizer_specials(tok, models) apply_tokenizer_specials(self.tokenizers[0], [self.critics[0]]) else: - apply_tokenizer_specials(self.tokenizer, [*self.agents, self.critics[0]]) + apply_tokenizer_specials( + self.tokenizer, [*self.agents, self.critics[0], *self.reference_models] + ) self.formatters = build_formatters(formatters, self.args.num_agents) try: @@ -340,6 +362,8 @@ def _init_wandb(self) -> None: "max_new_tokens": self.args.max_new_tokens, "num_generations": self.args.num_generations, "critic_type": self.args.critic_type, + "reference_kl_enabled": reference_kl_enabled(self.args), + "reference_kl_coef": reference_kl_coef(self.args), } sections = ( @@ -490,16 +514,72 @@ def _generate(self, agent_model, prompt: str, agent_idx: int) -> Dict[str, Any]: completion_texts.append( tokenizer.decode(seq[:resp_len], skip_special_tokens=True) ) + full_attention_mask = torch.ones_like(sequences, device=agent_device) + reference_kls = self._reference_kl_values( + agent_idx, + agent_model, + sequences, + full_attention_mask, + prompt_len, + response_lens, + ) return { "prompt": prompt, "prompt_len": prompt_len, "sequences": sequences, - "attention_mask": torch.ones_like(sequences, device=agent_device), + "attention_mask": full_attention_mask, "response_lens": response_lens, + "reference_kls": reference_kls, "completions": completion_texts, } + def _reference_kl_values( + self, + agent_idx: int, + policy_model: CausalLMWithValueHead, + sequences: torch.Tensor, + attention_mask: torch.Tensor, + prompt_len: int, + response_lens: Sequence[int], + ) -> List[float]: + if not reference_kl_enabled(self.args): + return [] + if not getattr(self, "reference_models", None): + raise RuntimeError( + "Reference KL is enabled but reference models are missing." + ) + reference_model = self.reference_models[agent_idx] + values: List[float] = [] + for seq, attn, response_len in zip(sequences, attention_mask, response_lens): + kl_value = reference_kl_for_sequence( + policy_model, + reference_model, + seq.unsqueeze(0), + attn.unsqueeze(0), + prompt_len, + int(response_len), + ) + values.append(float(kl_value.view(-1)[0].item())) + return values + + def _kl_shaped_reward( + self, reward: float, data: Dict[str, Any], index: int + ) -> Tuple[float, Dict[str, float]]: + if not reference_kl_enabled(self.args): + return reward, {} + reference_kl = 0.0 + penalty = 0.0 + raw_kls = data.get("reference_kls") or [] + if index < len(raw_kls): + reference_kl = float(raw_kls[index]) + penalty = reference_kl_coef(self.args) * reference_kl + return reward - penalty, { + "reference_kl": reference_kl, + "reference_kl_penalty": penalty, + "environment_reward": reward, + } + def _collect_rollouts(self, item: Dict[str, Any]) -> List[RolloutSample]: num_turns = max(1, int(getattr(self.args, "num_turns", 1))) if num_turns > 1: @@ -522,6 +602,7 @@ def _generate_agent(agent_idx: int) -> Dict[str, Any]: "sequences": gen["sequences"], "attention_mask": gen["attention_mask"], "response_lens": gen["response_lens"], + "reference_kls": gen["reference_kls"], "completion_texts": gen["completions"], } @@ -583,6 +664,7 @@ def _generate_agent(agent_idx: int) -> Dict[str, Any]: attn = data["attention_mask"][i] resp_len = data["response_lens"][i] reward = float(rewards_matrix[agent_idx][i]) + shaped_reward, kl_meta = self._kl_shaped_reward(reward, data, i) logprob, _ = self._policy_eval( self.agents[agent_idx], @@ -603,6 +685,7 @@ def _generate_agent(agent_idx: int) -> Dict[str, Any]: joint_len = int(critic_pack["prompt_len"]) value = critic_pack["value"].detach().cpu() reward_cpu = torch.tensor([reward], dtype=torch.float32) + shaped_reward_cpu = torch.tensor([shaped_reward], dtype=torch.float32) logprob_cpu = logprob.detach().cpu() rollouts.append( RolloutSample( @@ -619,7 +702,7 @@ def _generate_agent(agent_idx: int) -> Dict[str, Any]: old_logprob=logprob_cpu, old_value=value, reward=reward_cpu, - returns=reward_cpu.clone(), + returns=shaped_reward_cpu.clone(), advantage=torch.zeros_like(reward_cpu), normalized_advantage=None, metadata={ @@ -627,13 +710,14 @@ def _generate_agent(agent_idx: int) -> Dict[str, Any]: "joint_attention_mask": joint_mask.detach().cpu(), "joint_prompt_len": joint_len, "turn_idx": 0, - "adv_target": reward_cpu, + "adv_target": shaped_reward_cpu, + **kl_meta, }, ) ) for sample in rollouts: - r = float(sample.reward.view(-1)[0].item()) + r = float(sample.returns.view(-1)[0].item()) sample.metadata["value_target"] = torch.tensor([r]).cpu() if self.metrics_callback is not None: @@ -703,6 +787,7 @@ def _generate_agent_turn(agent_idx: int) -> Dict[str, Any]: "sequences": gen["sequences"], "attention_mask": gen["attention_mask"], "response_lens": gen["response_lens"], + "reference_kls": gen["reference_kls"], "completion_texts": gen["completions"], } @@ -744,6 +829,8 @@ def _generate_agent_turn(agent_idx: int) -> Dict[str, Any]: resp_len = data["response_lens"][0] reward_val = float(rewards_matrix[agent_idx][0]) reward_cpu = torch.tensor([reward_val], dtype=torch.float32) + shaped_reward, kl_meta = self._kl_shaped_reward(reward_val, data, 0) + shaped_reward_cpu = torch.tensor([shaped_reward], dtype=torch.float32) logprob, _ = self._policy_eval( self.agents[agent_idx], @@ -768,7 +855,7 @@ def _generate_agent_turn(agent_idx: int) -> Dict[str, Any]: old_logprob=logprob_cpu, old_value=value, reward=reward_cpu, - returns=reward_cpu.clone(), + returns=shaped_reward_cpu.clone(), advantage=torch.zeros_like(reward_cpu), normalized_advantage=None, metadata={ @@ -776,6 +863,7 @@ def _generate_agent_turn(agent_idx: int) -> Dict[str, Any]: "joint_attention_mask": joint_mask.detach().cpu(), "joint_prompt_len": joint_len, "turn_idx": turn_idx, + **kl_meta, }, ) rollouts.append(sample) @@ -792,7 +880,7 @@ def _generate_agent_turn(agent_idx: int) -> Dict[str, Any]: for agent_idx in range(self.args.num_agents): traj = per_agent_samples[agent_idx] for t, sample in enumerate(traj): - r = float(sample.reward.view(-1)[0].item()) + r = float(sample.returns.view(-1)[0].item()) if t < len(traj) - 1: next_v = float(traj[t + 1].old_value.view(-1)[0].item()) target = r + gamma * next_v @@ -804,7 +892,7 @@ def _generate_agent_turn(agent_idx: int) -> Dict[str, Any]: for agent_idx in range(self.args.num_agents): future = 0.0 for sample in reversed(per_agent_samples[agent_idx]): - immediate = float(sample.reward.view(-1)[0].item()) + immediate = float(sample.returns.view(-1)[0].item()) future = immediate + gamma * future sample.returns = torch.tensor([future], dtype=torch.float32) sample.advantage = torch.zeros_like(sample.returns) diff --git a/comlrl/trainers/reinforce/magrpo.py b/comlrl/trainers/reinforce/magrpo.py index e27f200..db4e516 100644 --- a/comlrl/trainers/reinforce/magrpo.py +++ b/comlrl/trainers/reinforce/magrpo.py @@ -21,6 +21,13 @@ ) from comlrl.utils.formatters import build_formatters from comlrl.utils.model_loading import infer_model_name, resolve_model_sources +from comlrl.utils.reference_kl import ( + clone_reference_models, + reference_kl_coef, + reference_kl_enabled, + reference_kl_for_sequence, + validate_reference_kl_config, +) from comlrl.utils.reward_utils import call_reward_function from comlrl.utils.tokenizer_utils import ( apply_tokenizer_specials, @@ -61,6 +68,8 @@ class MAGRPOConfig: train_batch_size: Optional[int] = None advantage_normalization: bool = True advantage_mode: str = "mean" + reference_kl_enabled: bool = False + reference_kl_coef: float = 0.1 def __post_init__(self) -> None: if self.num_train_epochs < 1: @@ -95,6 +104,7 @@ def __post_init__(self) -> None: if mode == "mp" and self.agent_devices is None: raise ValueError("parallel_training='mp' requires explicit agent_devices.") self.parallel_training = mode + validate_reference_kl_config(self, self.num_agents) @dataclass @@ -262,6 +272,18 @@ def __init__( for tok in self.tokenizers: tok.add_special_tokens(special_tokens) + self.reference_models: List[Any] = [] + if reference_kl_enabled(self.args): + self.reference_models = clone_reference_models( + self.agents, + devices=self.agent_devices, + ) + if self.tokenizers and len(self.tokenizers) == len(self.reference_models): + for idx, tok in enumerate(self.tokenizers): + apply_tokenizer_specials(tok, [self.reference_models[idx]]) + else: + apply_tokenizer_specials(self.tokenizer, self.reference_models) + # Allow single-agent as a special case (GRPO) if self.num_agents < 1: raise ValueError("num_agents must be >= 1") @@ -393,6 +415,8 @@ def _init_wandb(self): "algorithm": self.algorithm_name, "advantage_mode": self.advantage_mode, "advantage_normalization": self.args.advantage_normalization, + "reference_kl_enabled": reference_kl_enabled(self.args), + "reference_kl_coef": reference_kl_coef(self.args), "agent_learning_rate": self.args.agent_learning_rate, "num_train_epochs": self.args.num_train_epochs, "num_generations": self.args.num_generations, @@ -1245,15 +1269,23 @@ def _generate_completions( batch_completions = [] batch_completion_tokens = [] + batch_response_lens = [] end_idx = min(num_return_sequences, total_sequences) for s in range(end_idx): completion_tokens = completion_input_ids[s, prompt_len:] + pad_positions = (completion_tokens == tokenizer.pad_token_id).nonzero() + response_len = ( + pad_positions[0].item() + if pad_positions.shape[0] > 0 + else completion_tokens.shape[0] + ) batch_completion_tokens.append(completion_tokens) + batch_response_lens.append(int(response_len)) completion_text = tokenizer.decode( - completion_tokens, skip_special_tokens=True + completion_tokens[:response_len], skip_special_tokens=True ) batch_completions.append(completion_text) @@ -1270,6 +1302,14 @@ def _generate_completions( logits = ( generation_output.scores if hasattr(generation_output, "scores") else [] ) + reference_kls = self._reference_kl_values( + agent_idx, + agent_module, + completion_input_ids[:end_idx], + torch.ones_like(completion_input_ids[:end_idx], device=device), + prompt_len, + batch_response_lens, + ) return { "prompts": prompts, @@ -1279,6 +1319,8 @@ def _generate_completions( "completions": completions, "completion_input_ids": completion_tokens_list, "completion_attention_mask": completion_attention_masks, + "response_lens": batch_response_lens, + "reference_kls": reference_kls, "logits": logits, } @@ -1419,8 +1461,56 @@ def _pack_completions_for_buffer( return { "prompt_input_ids": prompt_ids, "completion_input_ids": packed_completion_ids, + "reference_kls": list(completions_data.get("reference_kls") or []), } + def _reference_kl_values( + self, + agent_idx: int, + policy_model: Any, + sequences: torch.Tensor, + attention_mask: torch.Tensor, + prompt_len: int, + response_lens: Sequence[int], + ) -> List[float]: + if not reference_kl_enabled(self.args): + return [] + if not getattr(self, "reference_models", None): + raise RuntimeError( + "Reference KL is enabled but reference models are missing." + ) + reference_model = self.reference_models[agent_idx] + values: List[float] = [] + for seq, attn, response_len in zip(sequences, attention_mask, response_lens): + kl_value = reference_kl_for_sequence( + policy_model, + reference_model, + seq.unsqueeze(0), + attn.unsqueeze(0), + prompt_len, + int(response_len), + ) + values.append(float(kl_value.view(-1)[0].item())) + return values + + def _apply_reference_kl_to_returns( + self, returns_tensor: torch.Tensor, completions_data: Dict[str, Any] + ) -> torch.Tensor: + if not reference_kl_enabled(self.args): + return returns_tensor + raw_kls = list(completions_data.get("reference_kls") or []) + if not raw_kls: + return returns_tensor + kls = torch.zeros_like(returns_tensor) + usable = min(len(raw_kls), returns_tensor.numel()) + if usable > 0: + kls[:usable] = torch.tensor( + raw_kls[:usable], + dtype=returns_tensor.dtype, + device=returns_tensor.device, + ) + return returns_tensor - reference_kl_coef(self.args) * kls + def _should_log_train(self, step: int) -> bool: interval = int(getattr(self.args, "logging_steps", 1)) if interval <= 1: @@ -1458,6 +1548,18 @@ def _process_buffer( batch_log[prefix + "expected_return"] = float( np.mean([s.node_mean_return for s in samples]) ) + reference_kls = [ + float(kl) + for sample in samples + for kl in sample.completions_data.get("reference_kls", []) + ] + if reference_kls: + batch_log[prefix + "reference_kl_mean"] = float( + np.mean(reference_kls) + ) + batch_log[prefix + "reference_kl_penalty_mean"] = float( + reference_kl_coef(self.args) * np.mean(reference_kls) + ) step = max(s.node_env_step for s in samples) log_entries.append( { @@ -1560,7 +1662,10 @@ def _compute_loss_with_gradients(self, agent, completions_data, returns): # Convert returns to tensor returns_tensor = torch.tensor(returns, dtype=torch.float, device=device) - advantages = self._compute_advantages(returns_tensor) + effective_returns = self._apply_reference_kl_to_returns( + returns_tensor, completions_data + ) + advantages = self._compute_advantages(effective_returns) if self.args.advantage_normalization and advantages.numel() > 1: mean = advantages.mean() std = advantages.std(unbiased=False).clamp(min=1e-6) diff --git a/comlrl/utils/__init__.py b/comlrl/utils/__init__.py index bca4c0d..0799794 100644 --- a/comlrl/utils/__init__.py +++ b/comlrl/utils/__init__.py @@ -1,5 +1,12 @@ from .formatters import build_formatters from .model_loading import infer_model_name, resolve_model_sources +from .reference_kl import ( + clone_reference_models, + reference_kl_coef, + reference_kl_enabled, + reference_kl_for_sequence, + validate_reference_kl_config, +) from .reward_processor import RewardProcessors from .reward_utils import call_reward_function, normalize_reward_lengths from .distributed import ( @@ -27,6 +34,11 @@ "resolve_tokenizers", "infer_model_name", "resolve_model_sources", + "clone_reference_models", + "reference_kl_coef", + "reference_kl_enabled", + "reference_kl_for_sequence", + "validate_reference_kl_config", "RewardProcessors", "call_reward_function", "normalize_reward_lengths", diff --git a/comlrl/utils/reference_kl.py b/comlrl/utils/reference_kl.py new file mode 100644 index 0000000..d720724 --- /dev/null +++ b/comlrl/utils/reference_kl.py @@ -0,0 +1,118 @@ +from __future__ import annotations + +import copy +from typing import Any, List, Sequence + +import torch +import torch.nn.functional as F + +from comlrl.utils.distributed import unwrap_model + + +def reference_kl_enabled(args: Any) -> bool: + return bool(getattr(args, "reference_kl_enabled", False)) + + +def reference_kl_coef(args: Any) -> float: + return float(getattr(args, "reference_kl_coef", 0.1)) + + +def validate_reference_kl_config(args: Any, expected_count: int) -> None: + del expected_count + coef = reference_kl_coef(args) + if coef < 0: + raise ValueError("reference_kl_coef must be >= 0.") + + +def clone_reference_models( + policy_models: Sequence[Any], + *, + devices: Sequence[torch.device], +) -> List[Any]: + references: List[Any] = [] + for idx, policy_model in enumerate(policy_models): + reference_model = copy.deepcopy(policy_model) + reference_model.to(devices[idx]) + reference_model.eval() + for param in reference_model.parameters(): + param.requires_grad = False + references.append(reference_model) + return references + + +def response_token_logprobs( + model: Any, + sequences: torch.Tensor, + attention_mask: torch.Tensor, + prompt_len: int, + response_len: int, +) -> torch.Tensor: + module = unwrap_model(model) + try: + outputs = module( + input_ids=sequences, + attention_mask=attention_mask, + output_values=False, + ) + except TypeError: + outputs = module( + input_ids=sequences, + attention_mask=attention_mask, + return_dict=True, + ) + logits = outputs.logits + shifted_logits = logits[:, :-1, :] + shifted_targets = sequences[:, 1:] + log_probs = F.log_softmax(shifted_logits, dim=-1) + token_log_probs = log_probs.gather( + dim=-1, index=shifted_targets.unsqueeze(-1) + ).squeeze(-1) + start_index = max(int(prompt_len) - 1, 0) + end_index = start_index + int(response_len) + return token_log_probs[:, start_index:end_index] + + +def reference_kl_for_sequence( + policy_model: Any, + reference_model: Any, + sequences: torch.Tensor, + attention_mask: torch.Tensor, + prompt_len: int, + response_len: int, +) -> torch.Tensor: + """ + Return a non-negative sampled KL estimate for generated response tokens. + + Uses Schulman's k3 estimator per token: + exp(log p_ref - log p_policy) - (log p_ref - log p_policy) - 1. + """ + policy_module = unwrap_model(policy_model) + reference_module = unwrap_model(reference_model) + policy_device = next(policy_module.parameters()).device + reference_device = next(reference_module.parameters()).device + policy_seq = sequences.to(policy_device) + policy_mask = attention_mask.to(policy_device) + reference_seq = sequences.to(reference_device) + reference_mask = attention_mask.to(reference_device) + policy_training = bool(policy_module.training) + reference_training = bool(reference_module.training) + policy_module.eval() + reference_module.eval() + try: + with torch.no_grad(): + policy_logps = response_token_logprobs( + policy_model, policy_seq, policy_mask, prompt_len, response_len + ).to(reference_device) + reference_logps = response_token_logprobs( + reference_model, + reference_seq, + reference_mask, + prompt_len, + response_len, + ) + log_ratio_ref_policy = reference_logps - policy_logps + token_kl = torch.exp(log_ratio_ref_policy) - log_ratio_ref_policy - 1.0 + finally: + policy_module.train(policy_training) + reference_module.train(reference_training) + return token_kl.sum(dim=-1).detach().cpu() diff --git a/tests/test_reference_kl.py b/tests/test_reference_kl.py new file mode 100644 index 0000000..d4ef7e3 --- /dev/null +++ b/tests/test_reference_kl.py @@ -0,0 +1,200 @@ +from types import SimpleNamespace + +import pytest +import torch +from transformers import GPT2Config, GPT2LMHeadModel + +from comlrl.trainers.actor_critic import IACTrainer +from comlrl.trainers.actor_critic.iac import IACConfig +from comlrl.trainers.actor_critic.maac import MAACConfig, MAACTrainer +from comlrl.trainers.reinforce import MAGRPOTrainer +from comlrl.trainers.reinforce.magrpo import MAGRPOConfig +from comlrl.utils.reference_kl import reference_kl_for_sequence + + +def _tiny_model(vocab_size: int = 32) -> GPT2LMHeadModel: + cfg = GPT2Config( + vocab_size=vocab_size, + n_positions=32, + n_ctx=32, + n_embd=16, + n_layer=1, + n_head=1, + ) + return GPT2LMHeadModel(cfg) + + +def _dummy_tokenizer(): + return SimpleNamespace( + pad_token="", + eos_token="", + pad_token_id=0, + eos_token_id=1, + ) + + +def _reward_func(*_args, **_kwargs): + return [0.0] + + +def test_reference_kl_config_defaults_off(): + magrpo = MAGRPOConfig() + iac = IACConfig() + maac = MAACConfig() + + assert magrpo.reference_kl_enabled is False + assert iac.reference_kl_enabled is False + assert maac.reference_kl_enabled is False + + +@pytest.mark.parametrize("config_cls", [MAGRPOConfig, IACConfig, MAACConfig]) +def test_reference_kl_rejects_negative_coef(config_cls): + with pytest.raises(ValueError, match="reference_kl_coef"): + config_cls(reference_kl_coef=-0.1) + + +def test_reference_kl_enabled_uses_self_reference_by_default(): + cfg = MAGRPOConfig( + num_agents=1, + num_turns=1, + num_generations=2, + agent_devices="cpu", + reference_kl_enabled=True, + ) + trainer = MAGRPOTrainer( + agents=[_tiny_model()], + tokenizer=_dummy_tokenizer(), + reward_func=_reward_func, + args=cfg, + ) + + assert len(trainer.reference_models) == 1 + assert trainer.reference_models[0] is not trainer.agents[0] + assert all(not p.requires_grad for p in trainer.reference_models[0].parameters()) + + +def test_reference_kl_for_identical_model_is_zero(): + model = _tiny_model() + sequences = torch.tensor([[2, 3, 4, 5]], dtype=torch.long) + attention_mask = torch.ones_like(sequences) + + kl = reference_kl_for_sequence( + model, + model, + sequences, + attention_mask, + prompt_len=2, + response_len=2, + ) + + assert torch.allclose(kl, torch.zeros_like(kl), atol=1e-6) + + +def test_reference_kl_disabled_actor_critic_shaping_is_noop(): + iac = IACTrainer( + agents=[_tiny_model()], + tokenizer=_dummy_tokenizer(), + reward_func=_reward_func, + args=IACConfig( + num_agents=1, + num_turns=1, + use_separate_critic=False, + agent_devices="cpu", + critic_devices="cpu", + ), + ) + shaped_reward, metadata = iac._kl_shaped_reward(1.0, {"reference_kls": [0.4]}, 0) + assert shaped_reward == pytest.approx(1.0) + assert metadata == {} + + maac = MAACTrainer( + agents=[_tiny_model()], + critics=[_tiny_model()], + tokenizer=_dummy_tokenizer(), + reward_func=_reward_func, + args=MAACConfig( + num_agents=1, + num_turns=1, + agent_devices="cpu", + critic_devices="cpu", + ), + ) + shaped_reward, metadata = maac._kl_shaped_reward(1.0, {"reference_kls": [0.4]}, 0) + assert shaped_reward == pytest.approx(1.0) + assert metadata == {} + + +def test_magrpo_applies_reference_kl_to_returns(): + args = MAGRPOConfig( + num_agents=1, + num_turns=1, + num_generations=2, + agent_devices="cpu", + reference_kl_enabled=True, + reference_kl_coef=0.5, + ) + trainer = MAGRPOTrainer( + agents=[_tiny_model()], + tokenizer=_dummy_tokenizer(), + reward_func=_reward_func, + args=args, + ) + + returns = torch.tensor([1.0, 2.0]) + adjusted = trainer._apply_reference_kl_to_returns( + returns, {"reference_kls": [0.2, 0.4]} + ) + + assert torch.allclose(adjusted, torch.tensor([0.9, 1.8])) + + +def test_iac_uses_reference_kl_shaped_reward(): + args = IACConfig( + num_agents=1, + num_turns=1, + use_separate_critic=False, + agent_devices="cpu", + critic_devices="cpu", + reference_kl_enabled=True, + reference_kl_coef=0.25, + ) + trainer = IACTrainer( + agents=[_tiny_model()], + tokenizer=_dummy_tokenizer(), + reward_func=_reward_func, + args=args, + ) + + shaped_reward, metadata = trainer._kl_shaped_reward( + 1.0, {"reference_kls": [0.4]}, 0 + ) + + assert shaped_reward == pytest.approx(0.9) + assert metadata["reference_kl"] == pytest.approx(0.4) + assert metadata["reference_kl_penalty"] == pytest.approx(0.1) + + +def test_maac_uses_reference_kl_shaped_reward(): + args = MAACConfig( + num_agents=1, + num_turns=1, + agent_devices="cpu", + critic_devices="cpu", + reference_kl_enabled=True, + reference_kl_coef=0.25, + ) + trainer = MAACTrainer( + agents=[_tiny_model()], + critics=[_tiny_model()], + tokenizer=_dummy_tokenizer(), + reward_func=_reward_func, + args=args, + ) + + shaped_reward, metadata = trainer._kl_shaped_reward( + 1.0, {"reference_kls": [0.4]}, 0 + ) + + assert shaped_reward == pytest.approx(0.9) + assert metadata["reference_kl"] == pytest.approx(0.4) + assert metadata["reference_kl_penalty"] == pytest.approx(0.1) From 86d1bb9012ee31f0f1eaeaa034a305ac4ad0edb5 Mon Sep 17 00:00:00 2001 From: N!no Date: Tue, 30 Jun 2026 17:30:30 -0400 Subject: [PATCH 2/4] allow ref on separate devices --- comlrl/trainers/actor_critic/iac.py | 11 +++++++++- comlrl/trainers/actor_critic/maac.py | 11 +++++++++- comlrl/trainers/reinforce/magrpo.py | 11 +++++++++- comlrl/utils/__init__.py | 2 ++ comlrl/utils/reference_kl.py | 24 +++++++++++++++++++++- tests/test_reference_kl.py | 30 +++++++++++++++++++++++++++- 6 files changed, 84 insertions(+), 5 deletions(-) diff --git a/comlrl/trainers/actor_critic/iac.py b/comlrl/trainers/actor_critic/iac.py index 8ecec3b..bc3b1a2 100644 --- a/comlrl/trainers/actor_critic/iac.py +++ b/comlrl/trainers/actor_critic/iac.py @@ -19,6 +19,7 @@ reference_kl_coef, reference_kl_enabled, reference_kl_for_sequence, + resolve_reference_devices, validate_reference_kl_config, ) from comlrl.utils.reward_utils import call_reward_function, normalize_reward_lengths @@ -67,6 +68,7 @@ class IACConfig: logging_steps: int = 1 reference_kl_enabled: bool = False reference_kl_coef: float = 0.1 + reference_devices: Optional[Union[str, Sequence[str]]] = None def __post_init__(self) -> None: if self.rollout_buffer_size < 1: @@ -319,10 +321,16 @@ def __init__( self.critics = [] self.reference_models: List[Any] = [] + self.reference_devices: List[torch.device] = [] if reference_kl_enabled(self.args): + self.reference_devices = resolve_reference_devices( + self.args, + self.agent_devices, + self.args.num_agents, + ) self.reference_models = clone_reference_models( self.agents, - devices=self.agent_devices, + devices=self.reference_devices, ) if self.tokenizers and len(self.tokenizers) == len(self.agents): @@ -413,6 +421,7 @@ def _init_wandb(self) -> None: "critic_type": self.args.critic_type, "reference_kl_enabled": reference_kl_enabled(self.args), "reference_kl_coef": reference_kl_coef(self.args), + "reference_devices": getattr(self.args, "reference_devices", None), } sections = ( diff --git a/comlrl/trainers/actor_critic/maac.py b/comlrl/trainers/actor_critic/maac.py index 3cc2442..27ce77c 100644 --- a/comlrl/trainers/actor_critic/maac.py +++ b/comlrl/trainers/actor_critic/maac.py @@ -20,6 +20,7 @@ reference_kl_coef, reference_kl_enabled, reference_kl_for_sequence, + resolve_reference_devices, validate_reference_kl_config, ) from comlrl.utils.reward_utils import call_reward_function, normalize_reward_lengths @@ -63,6 +64,7 @@ class MAACConfig: logging_steps: int = 1 reference_kl_enabled: bool = False reference_kl_coef: float = 0.1 + reference_devices: Optional[Union[str, Sequence[str]]] = None def __post_init__(self) -> None: if self.rollout_buffer_size < 1: @@ -283,10 +285,16 @@ def __init__( self.critics: List[CausalLMWithValueHead] = [critic_model_instance] self.reference_models: List[Any] = [] + self.reference_devices: List[torch.device] = [] if reference_kl_enabled(self.args): + self.reference_devices = resolve_reference_devices( + self.args, + self.agent_devices, + self.args.num_agents, + ) self.reference_models = clone_reference_models( self.agents, - devices=self.agent_devices, + devices=self.reference_devices, ) if self.tokenizers and len(self.tokenizers) == len(self.agents): @@ -364,6 +372,7 @@ def _init_wandb(self) -> None: "critic_type": self.args.critic_type, "reference_kl_enabled": reference_kl_enabled(self.args), "reference_kl_coef": reference_kl_coef(self.args), + "reference_devices": getattr(self.args, "reference_devices", None), } sections = ( diff --git a/comlrl/trainers/reinforce/magrpo.py b/comlrl/trainers/reinforce/magrpo.py index db4e516..573585b 100644 --- a/comlrl/trainers/reinforce/magrpo.py +++ b/comlrl/trainers/reinforce/magrpo.py @@ -26,6 +26,7 @@ reference_kl_coef, reference_kl_enabled, reference_kl_for_sequence, + resolve_reference_devices, validate_reference_kl_config, ) from comlrl.utils.reward_utils import call_reward_function @@ -70,6 +71,7 @@ class MAGRPOConfig: advantage_mode: str = "mean" reference_kl_enabled: bool = False reference_kl_coef: float = 0.1 + reference_devices: Optional[Union[str, Sequence[str]]] = None def __post_init__(self) -> None: if self.num_train_epochs < 1: @@ -273,10 +275,16 @@ def __init__( tok.add_special_tokens(special_tokens) self.reference_models: List[Any] = [] + self.reference_devices: List[torch.device] = [] if reference_kl_enabled(self.args): + self.reference_devices = resolve_reference_devices( + self.args, + self.agent_devices, + self.num_agents, + ) self.reference_models = clone_reference_models( self.agents, - devices=self.agent_devices, + devices=self.reference_devices, ) if self.tokenizers and len(self.tokenizers) == len(self.reference_models): for idx, tok in enumerate(self.tokenizers): @@ -417,6 +425,7 @@ def _init_wandb(self): "advantage_normalization": self.args.advantage_normalization, "reference_kl_enabled": reference_kl_enabled(self.args), "reference_kl_coef": reference_kl_coef(self.args), + "reference_devices": getattr(self.args, "reference_devices", None), "agent_learning_rate": self.args.agent_learning_rate, "num_train_epochs": self.args.num_train_epochs, "num_generations": self.args.num_generations, diff --git a/comlrl/utils/__init__.py b/comlrl/utils/__init__.py index 0799794..c6482b0 100644 --- a/comlrl/utils/__init__.py +++ b/comlrl/utils/__init__.py @@ -5,6 +5,7 @@ reference_kl_coef, reference_kl_enabled, reference_kl_for_sequence, + resolve_reference_devices, validate_reference_kl_config, ) from .reward_processor import RewardProcessors @@ -38,6 +39,7 @@ "reference_kl_coef", "reference_kl_enabled", "reference_kl_for_sequence", + "resolve_reference_devices", "validate_reference_kl_config", "RewardProcessors", "call_reward_function", diff --git a/comlrl/utils/reference_kl.py b/comlrl/utils/reference_kl.py index d720724..c305456 100644 --- a/comlrl/utils/reference_kl.py +++ b/comlrl/utils/reference_kl.py @@ -6,6 +6,7 @@ import torch import torch.nn.functional as F +from comlrl.schedulers import DeviceScheduler from comlrl.utils.distributed import unwrap_model @@ -18,10 +19,31 @@ def reference_kl_coef(args: Any) -> float: def validate_reference_kl_config(args: Any, expected_count: int) -> None: - del expected_count coef = reference_kl_coef(args) if coef < 0: raise ValueError("reference_kl_coef must be >= 0.") + reference_devices = getattr(args, "reference_devices", None) + if reference_kl_enabled(args) and reference_devices is not None: + DeviceScheduler.resolve_devices( + reference_devices, + expected_count, + kind="reference_devices", + ) + + +def resolve_reference_devices( + args: Any, + fallback_devices: Sequence[torch.device], + expected_count: int, +) -> List[torch.device]: + reference_devices = getattr(args, "reference_devices", None) + if reference_devices is None: + return list(fallback_devices) + return DeviceScheduler.resolve_devices( + reference_devices, + expected_count, + kind="reference_devices", + ) def clone_reference_models( diff --git a/tests/test_reference_kl.py b/tests/test_reference_kl.py index d4ef7e3..e681ea5 100644 --- a/tests/test_reference_kl.py +++ b/tests/test_reference_kl.py @@ -9,7 +9,10 @@ from comlrl.trainers.actor_critic.maac import MAACConfig, MAACTrainer from comlrl.trainers.reinforce import MAGRPOTrainer from comlrl.trainers.reinforce.magrpo import MAGRPOConfig -from comlrl.utils.reference_kl import reference_kl_for_sequence +from comlrl.utils.reference_kl import ( + reference_kl_for_sequence, + resolve_reference_devices, +) def _tiny_model(vocab_size: int = 32) -> GPT2LMHeadModel: @@ -45,6 +48,9 @@ def test_reference_kl_config_defaults_off(): assert magrpo.reference_kl_enabled is False assert iac.reference_kl_enabled is False assert maac.reference_kl_enabled is False + assert magrpo.reference_devices is None + assert iac.reference_devices is None + assert maac.reference_devices is None @pytest.mark.parametrize("config_cls", [MAGRPOConfig, IACConfig, MAACConfig]) @@ -53,6 +59,28 @@ def test_reference_kl_rejects_negative_coef(config_cls): config_cls(reference_kl_coef=-0.1) +@pytest.mark.parametrize("config_cls", [MAGRPOConfig, IACConfig, MAACConfig]) +def test_reference_kl_rejects_mismatched_reference_devices(config_cls): + kwargs = { + "num_agents": 2, + "reference_kl_enabled": True, + "reference_devices": ["cpu", "cpu", "cpu"], + } + if config_cls is MAGRPOConfig: + kwargs["num_generations"] = 2 + with pytest.raises(ValueError, match="reference_devices length"): + config_cls(**kwargs) + + +def test_reference_devices_can_be_resolved_separately_from_agents(): + args = SimpleNamespace(reference_devices=["cuda:1"]) + fallback = [torch.device("cuda:0")] + + devices = resolve_reference_devices(args, fallback, expected_count=1) + + assert devices == [torch.device("cuda:1")] + + def test_reference_kl_enabled_uses_self_reference_by_default(): cfg = MAGRPOConfig( num_agents=1, From d99bde96ccfd00a9afb65db823f377c7d38b5668 Mon Sep 17 00:00:00 2001 From: N!no Date: Tue, 30 Jun 2026 18:10:15 -0400 Subject: [PATCH 3/4] allow ref on different device --- comlrl/trainers/actor_critic/iac.py | 24 ++++++++++++++------- comlrl/trainers/actor_critic/maac.py | 24 ++++++++++++++------- comlrl/trainers/reinforce/magrpo.py | 32 +++++++++++++++++----------- comlrl/utils/__init__.py | 2 ++ comlrl/utils/reference_kl.py | 30 +++++++++++++++++++++----- tests/test_reference_kl.py | 24 +++++++++++++++++++++ 6 files changed, 103 insertions(+), 33 deletions(-) diff --git a/comlrl/trainers/actor_critic/iac.py b/comlrl/trainers/actor_critic/iac.py index bc3b1a2..5e32b04 100644 --- a/comlrl/trainers/actor_critic/iac.py +++ b/comlrl/trainers/actor_critic/iac.py @@ -16,6 +16,7 @@ from comlrl.utils.model_loading import resolve_model_sources from comlrl.utils.reference_kl import ( clone_reference_models, + load_reference_models_from_sources, reference_kl_coef, reference_kl_enabled, reference_kl_for_sequence, @@ -242,6 +243,9 @@ def __init__( expected_count=self.args.num_agents, model_label="agent_model", ) + actor_model_kwargs = self._filter_model_kwargs( + self.model_config.get("model_kwargs", {}) + ) for idx, actor_source in enumerate(actor_sources): if actor_source is None: raise ValueError("A policy model identifier or instance is required.") @@ -256,12 +260,9 @@ def __init__( attach_value_head=attach_value, ) else: - model_kwargs = self._filter_model_kwargs( - self.model_config.get("model_kwargs", {}) - ) try: base_model = AutoModelForCausalLM.from_pretrained( - actor_source, **model_kwargs + actor_source, **actor_model_kwargs ) except (OSError, ValueError) as exc: raise ValueError( @@ -328,10 +329,17 @@ def __init__( self.agent_devices, self.args.num_agents, ) - self.reference_models = clone_reference_models( - self.agents, - devices=self.reference_devices, - ) + if actor_sources and all(isinstance(src, str) for src in actor_sources): + self.reference_models = load_reference_models_from_sources( + actor_sources, + devices=self.reference_devices, + model_kwargs=actor_model_kwargs, + ) + else: + self.reference_models = clone_reference_models( + self.agents, + devices=self.reference_devices, + ) if self.tokenizers and len(self.tokenizers) == len(self.agents): for idx, tok in enumerate(self.tokenizers): diff --git a/comlrl/trainers/actor_critic/maac.py b/comlrl/trainers/actor_critic/maac.py index 27ce77c..43d5eed 100644 --- a/comlrl/trainers/actor_critic/maac.py +++ b/comlrl/trainers/actor_critic/maac.py @@ -17,6 +17,7 @@ from comlrl.utils.model_loading import resolve_model_sources from comlrl.utils.reference_kl import ( clone_reference_models, + load_reference_models_from_sources, reference_kl_coef, reference_kl_enabled, reference_kl_for_sequence, @@ -210,6 +211,9 @@ def __init__( expected_count=self.args.num_agents, model_label="agent_model", ) + actor_model_kwargs = self._filter_model_kwargs( + self.model_config.get("model_kwargs", {}) + ) for idx, actor_source in enumerate(actor_sources): if actor_source is None: raise ValueError("agent_model must be provided for MAAC.") @@ -223,12 +227,9 @@ def __init__( value_head_hidden_dim=None, ) else: - model_kwargs = self._filter_model_kwargs( - self.model_config.get("model_kwargs", {}) - ) try: base = AutoModelForCausalLM.from_pretrained( - actor_source, **model_kwargs + actor_source, **actor_model_kwargs ) except (OSError, ValueError) as exc: raise ValueError( @@ -292,10 +293,17 @@ def __init__( self.agent_devices, self.args.num_agents, ) - self.reference_models = clone_reference_models( - self.agents, - devices=self.reference_devices, - ) + if actor_sources and all(isinstance(src, str) for src in actor_sources): + self.reference_models = load_reference_models_from_sources( + actor_sources, + devices=self.reference_devices, + model_kwargs=actor_model_kwargs, + ) + else: + self.reference_models = clone_reference_models( + self.agents, + devices=self.reference_devices, + ) if self.tokenizers and len(self.tokenizers) == len(self.agents): for idx, tok in enumerate(self.tokenizers): diff --git a/comlrl/trainers/reinforce/magrpo.py b/comlrl/trainers/reinforce/magrpo.py index 573585b..1c91ce7 100644 --- a/comlrl/trainers/reinforce/magrpo.py +++ b/comlrl/trainers/reinforce/magrpo.py @@ -23,6 +23,7 @@ from comlrl.utils.model_loading import infer_model_name, resolve_model_sources from comlrl.utils.reference_kl import ( clone_reference_models, + load_reference_models_from_sources, reference_kl_coef, reference_kl_enabled, reference_kl_for_sequence, @@ -240,17 +241,17 @@ def __init__( self.agent_devices = [single_device] * self.num_agents self.device = self.agent_devices[0] self.dist_env = local_context(self.device) + model_kwargs = {} + torch_dtype = None + if isinstance(self.model_config, dict): + torch_dtype = self.model_config.get("torch_dtype") or self.model_config.get( + "dtype" + ) + if torch_dtype is not None: + model_kwargs["torch_dtype"] = torch_dtype if actor_sources and all(isinstance(src, str) for src in actor_sources): from transformers import AutoModelForCausalLM - model_kwargs = {} - torch_dtype = None - if isinstance(self.model_config, dict): - torch_dtype = self.model_config.get( - "torch_dtype" - ) or self.model_config.get("dtype") - if torch_dtype is not None: - model_kwargs["torch_dtype"] = torch_dtype self.agents = [ AutoModelForCausalLM.from_pretrained(name, **model_kwargs) for name in actor_sources @@ -282,10 +283,17 @@ def __init__( self.agent_devices, self.num_agents, ) - self.reference_models = clone_reference_models( - self.agents, - devices=self.reference_devices, - ) + if actor_sources and all(isinstance(src, str) for src in actor_sources): + self.reference_models = load_reference_models_from_sources( + actor_sources, + devices=self.reference_devices, + model_kwargs=model_kwargs, + ) + else: + self.reference_models = clone_reference_models( + self.agents, + devices=self.reference_devices, + ) if self.tokenizers and len(self.tokenizers) == len(self.reference_models): for idx, tok in enumerate(self.tokenizers): apply_tokenizer_specials(tok, [self.reference_models[idx]]) diff --git a/comlrl/utils/__init__.py b/comlrl/utils/__init__.py index c6482b0..0d78c03 100644 --- a/comlrl/utils/__init__.py +++ b/comlrl/utils/__init__.py @@ -2,6 +2,7 @@ from .model_loading import infer_model_name, resolve_model_sources from .reference_kl import ( clone_reference_models, + load_reference_models_from_sources, reference_kl_coef, reference_kl_enabled, reference_kl_for_sequence, @@ -36,6 +37,7 @@ "infer_model_name", "resolve_model_sources", "clone_reference_models", + "load_reference_models_from_sources", "reference_kl_coef", "reference_kl_enabled", "reference_kl_for_sequence", diff --git a/comlrl/utils/reference_kl.py b/comlrl/utils/reference_kl.py index c305456..8f946dd 100644 --- a/comlrl/utils/reference_kl.py +++ b/comlrl/utils/reference_kl.py @@ -1,10 +1,11 @@ from __future__ import annotations import copy -from typing import Any, List, Sequence +from typing import Any, Mapping, List, Sequence import torch import torch.nn.functional as F +from transformers import AutoModelForCausalLM from comlrl.schedulers import DeviceScheduler from comlrl.utils.distributed import unwrap_model @@ -55,13 +56,32 @@ def clone_reference_models( for idx, policy_model in enumerate(policy_models): reference_model = copy.deepcopy(policy_model) reference_model.to(devices[idx]) - reference_model.eval() - for param in reference_model.parameters(): - param.requires_grad = False - references.append(reference_model) + references.append(_freeze_reference_model(reference_model)) return references +def load_reference_models_from_sources( + model_sources: Sequence[str], + *, + devices: Sequence[torch.device], + model_kwargs: Mapping[str, Any] | None = None, +) -> List[Any]: + references: List[Any] = [] + kwargs = dict(model_kwargs or {}) + for idx, model_source in enumerate(model_sources): + reference_model = AutoModelForCausalLM.from_pretrained(model_source, **kwargs) + reference_model.to(devices[idx]) + references.append(_freeze_reference_model(reference_model)) + return references + + +def _freeze_reference_model(model: Any) -> Any: + model.eval() + for param in model.parameters(): + param.requires_grad = False + return model + + def response_token_logprobs( model: Any, sequences: torch.Tensor, diff --git a/tests/test_reference_kl.py b/tests/test_reference_kl.py index e681ea5..b57f17e 100644 --- a/tests/test_reference_kl.py +++ b/tests/test_reference_kl.py @@ -101,6 +101,30 @@ def test_reference_kl_enabled_uses_self_reference_by_default(): assert all(not p.requires_grad for p in trainer.reference_models[0].parameters()) +def test_reference_model_loads_from_actor_source(tmp_path): + model_dir = tmp_path / "tiny-model" + _tiny_model().save_pretrained(model_dir) + + trainer = IACTrainer( + agent_model=str(model_dir), + tokenizer=_dummy_tokenizer(), + reward_func=_reward_func, + args=IACConfig( + num_agents=1, + num_turns=1, + use_separate_critic=False, + agent_devices="cpu", + reference_kl_enabled=True, + reference_devices="cpu", + ), + ) + + assert len(trainer.reference_models) == 1 + assert isinstance(trainer.reference_models[0], GPT2LMHeadModel) + assert trainer.reference_models[0] is not trainer.agents[0] + assert all(not p.requires_grad for p in trainer.reference_models[0].parameters()) + + def test_reference_kl_for_identical_model_is_zero(): model = _tiny_model() sequences = torch.tensor([[2, 3, 4, 5]], dtype=torch.long) From dda56615775046c0a00180a1ae4400bd4ef023dd Mon Sep 17 00:00:00 2001 From: N!no Date: Tue, 30 Jun 2026 18:18:24 -0400 Subject: [PATCH 4/4] ud --- README.md | 2 +- docs/assets/sponsors.jpg | Bin 169245 -> 0 bytes docs/assets/sponsors.png | Bin 0 -> 922635 bytes 3 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 docs/assets/sponsors.jpg create mode 100644 docs/assets/sponsors.png diff --git a/README.md b/README.md index ea30c61..01f8f99 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,7 @@ For new contributors, please see [contributing guidelines](./CONTRIBUTING.md) on CoMLRL was developed using substantial computational resources. Its growth has been made possible by the generous support of the following organizations and institutions. -

+

We welcome computational sponsorship to support the continued development of CoMLRL. If you are interested in supporting this project, please contact us. Email diff --git a/docs/assets/sponsors.jpg b/docs/assets/sponsors.jpg deleted file mode 100644 index 9f4be2b9930614186103037d8ca25c2d6a6d79a6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 169245 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