Text Generation
Transformers
Safetensors
qwen3
llama-factory
full
Generated from Trainer
conversational
text-generation-inference
Instructions to use ayh015/myLightningOPD with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use ayh015/myLightningOPD with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-generation", model="ayh015/myLightningOPD") messages = [ {"role": "user", "content": "Who are you?"}, ] pipe(messages)# Load model directly from transformers import AutoTokenizer, AutoModelForCausalLM tokenizer = AutoTokenizer.from_pretrained("ayh015/myLightningOPD") model = AutoModelForCausalLM.from_pretrained("ayh015/myLightningOPD", device_map="auto") messages = [ {"role": "user", "content": "Who are you?"}, ] inputs = tokenizer.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(tokenizer.decode(outputs[0][inputs["input_ids"].shape[-1]:])) - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use ayh015/myLightningOPD with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "ayh015/myLightningOPD" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "ayh015/myLightningOPD", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }'Use Docker
docker model run hf.co/ayh015/myLightningOPD
- SGLang
How to use ayh015/myLightningOPD 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 "ayh015/myLightningOPD" \ --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": "ayh015/myLightningOPD", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }'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 "ayh015/myLightningOPD" \ --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": "ayh015/myLightningOPD", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }' - Docker Model Runner
How to use ayh015/myLightningOPD with Docker Model Runner:
docker model run hf.co/ayh015/myLightningOPD
| # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | |
| # SPDX-License-Identifier: Apache-2.0 | |
| # Adapt from https://github.com/OpenRLHF/OpenRLHF/blob/10c733694ed9fbb78a0a2ff6a05efc7401584d46/openrlhf/models/utils.py | |
| # and https://github.com/OpenRLHF/OpenRLHF/blob/10c733694ed9fbb78a0a2ff6a05efc7401584d46/openrlhf/trainer/ppo_utils/experience_maker.py | |
| from argparse import Namespace | |
| import torch | |
| import torch.distributed as dist | |
| import torch.nn.functional as F | |
| def compute_approx_kl( | |
| log_probs: torch.Tensor, | |
| log_probs_base: torch.Tensor, | |
| kl_loss_type: str, | |
| importance_ratio: torch.Tensor | None = None, | |
| ) -> torch.Tensor: | |
| """ | |
| Compute the approximate KL divergence between two distributions. | |
| Schulman blog: http://joschu.net/blog/kl-approx.html | |
| Args: | |
| log_probs: Log probabilities of the new distribution. | |
| log_probs_base: Log probabilities of the base distribution. | |
| kl_loss_type: Type of KL estimator (k1, k2, k3, low_var_kl). | |
| importance_ratio: Optional IS ratio (π_θ/π_old) for unbiased KL estimation. | |
| """ | |
| log_ratio = log_probs.float() - log_probs_base.float() | |
| if kl_loss_type == "k1": | |
| kl = log_ratio | |
| elif kl_loss_type == "k2": | |
| kl = log_ratio**2 / 2.0 | |
| elif kl_loss_type in ["k3", "low_var_kl"]: | |
| # The non negative kl approximation in | |
| # http://joschu.net/blog/kl-approx.html | |
| # Besides non negative, it is also unbiased and have lower variance. | |
| log_ratio = -log_ratio | |
| kl = log_ratio.exp() - 1 - log_ratio | |
| else: | |
| raise ValueError(f"Unknown kl_loss_type: {kl_loss_type}") | |
| # Apply IS ratio for unbiased KL estimation (DeepSeek-V3.2) | |
| if importance_ratio is not None: | |
| kl = importance_ratio * kl | |
| # Clamp only for low_var_kl for numerical stability | |
| if kl_loss_type == "low_var_kl": | |
| kl = torch.clamp(kl, min=-10, max=10) | |
| return kl | |
| def compute_opsm_mask( | |
| args: Namespace, | |
| full_log_probs: list[torch.Tensor], | |
| full_old_log_probs: list[torch.Tensor], | |
| advantages: list[torch.Tensor], | |
| loss_masks: list[torch.Tensor], | |
| ) -> tuple[torch.Tensor, torch.Tensor]: | |
| """Compute Off-Policy Sequence Masking (OPSM) mask. | |
| Args: | |
| args: Configuration containing `opsm_delta` threshold. | |
| full_log_probs: Current policy log-probs per sample. | |
| full_old_log_probs: Old policy log-probs per sample. | |
| advantages: Advantage values per sample. | |
| loss_masks: Loss masks per sample. | |
| Returns: | |
| Tuple of `(opsm_mask, opsm_clipfrac)` where `opsm_mask` is a | |
| concatenated tensor of per-token masks and | |
| `opsm_clipfrac` is the count of masked sequences. | |
| """ | |
| opsm_mask_list = [] | |
| device = advantages[0].device | |
| opsm_clipfrac = torch.tensor(0.0, device=device) | |
| for full_log_prob, full_old_log_prob, advantage, loss_mask in zip( | |
| full_log_probs, full_old_log_probs, advantages, loss_masks, strict=False | |
| ): | |
| # Calculate sequence-level KL | |
| seq_kl = ((full_old_log_prob - full_log_prob) * loss_mask).sum() / torch.clamp_min(loss_mask.sum(), 1) | |
| # Create mask: 0 if (advantage < 0 and seq_kl > delta), else 1 | |
| mask = ((advantage < 0) & (seq_kl > args.opsm_delta)).float() | |
| opsm_clipfrac += mask.sum() / torch.clamp_min(loss_mask.sum(), 1) | |
| opsm_mask_list.append(1 - mask) | |
| opsm_mask = torch.cat(opsm_mask_list, dim=0) | |
| return opsm_mask, opsm_clipfrac | |
| def compute_gspo_kl( | |
| full_log_probs: list[torch.Tensor], | |
| full_old_log_probs: list[torch.Tensor], | |
| local_log_probs: list[torch.Tensor], | |
| loss_masks: list[torch.Tensor], | |
| ) -> torch.Tensor: | |
| """Compute GSPO-style per-sequence KL divergence. | |
| Args: | |
| full_log_probs: Current policy log-probs per sample (full or CP-local). | |
| full_old_log_probs: Old policy log-probs per sample (full or CP-local). | |
| local_log_probs: Local (CP-local) log-probs for expansion shape reference. | |
| loss_masks: Loss masks per sample. | |
| Returns: | |
| Concatenated tensor of per-token KL values where each token in a | |
| sequence has the same KL value (the sequence-level KL). | |
| """ | |
| # Compute sequence-level KL and expand to per-token | |
| ppo_kl = [ | |
| ((old_logprob - log_prob) * loss_mask).sum() / torch.clamp_min(loss_mask.sum(), 1) | |
| for log_prob, old_logprob, loss_mask in zip(full_log_probs, full_old_log_probs, loss_masks, strict=False) | |
| ] | |
| ppo_kl = [kl.expand_as(log_prob) for kl, log_prob in zip(ppo_kl, local_log_probs, strict=False)] | |
| ppo_kl = torch.cat(ppo_kl, dim=0) | |
| return ppo_kl | |
| def compute_policy_loss( | |
| ppo_kl: torch.Tensor, | |
| advantages: torch.Tensor, | |
| eps_clip: float, | |
| eps_clip_high: float, | |
| eps_clip_c: float | None = None, | |
| ): | |
| ratio = (-ppo_kl).exp() | |
| pg_losses1 = -ratio * advantages | |
| pg_losses2 = -ratio.clamp(1 - eps_clip, 1 + eps_clip_high) * advantages | |
| clip_pg_losses1 = torch.maximum(pg_losses1, pg_losses2) | |
| clipfrac = torch.gt(pg_losses2, pg_losses1).float() | |
| if eps_clip_c is not None: | |
| assert ( | |
| eps_clip_c > 1.0 | |
| ), f"The lower bound of the clip_ratio_c for dual-clip PPO should be greater than 1.0, but get the value: {eps_clip_c}." | |
| pg_losses3 = -eps_clip_c * advantages | |
| clip_pg_losses2 = torch.min(pg_losses3, clip_pg_losses1) | |
| pg_losses = torch.where(advantages < 0, clip_pg_losses2, clip_pg_losses1) | |
| else: | |
| pg_losses = clip_pg_losses1 | |
| return pg_losses, clipfrac | |
| def compute_log_probs(logits: torch.Tensor, tokens: torch.Tensor, process_group: dist.ProcessGroup | None): | |
| from megatron.core.fusions.fused_cross_entropy import fused_vocab_parallel_cross_entropy | |
| # convert to [seq_len, batch_size, vocab_size] as expected by fused_vocab_parallel_cross_entropy | |
| logits = logits.unsqueeze(1) | |
| tokens = tokens.unsqueeze(1) | |
| return -fused_vocab_parallel_cross_entropy(logits, tokens, process_group) | |
| # from https://github.com/volcengine/verl/blob/0bdf7f469854815177e73dcfe9e420836c952e6e/verl/utils/megatron/tensor_parallel.py#L99 | |
| class _VocabParallelEntropy(torch.autograd.Function): | |
| def forward(ctx, vocab_parallel_logits: torch.Tensor, process_group: dist.ProcessGroup) -> torch.Tensor: | |
| def mul_reduce(a, b): | |
| return (a * b).sum(dim=-1, keepdim=True) | |
| logits_max = vocab_parallel_logits.max(dim=-1, keepdim=True).values | |
| dist.all_reduce(logits_max, op=dist.ReduceOp.MAX, group=process_group) | |
| normalized_vocab_parallel_logits = vocab_parallel_logits - logits_max | |
| normalized_exp_logits = normalized_vocab_parallel_logits.exp_() | |
| normalized_sum_exp_logits = normalized_exp_logits.sum(dim=-1, keepdim=True) | |
| dist.all_reduce(normalized_sum_exp_logits, group=process_group) | |
| softmax_logits = normalized_exp_logits.div_(normalized_sum_exp_logits) | |
| sum_softmax_times_logits = mul_reduce(softmax_logits, vocab_parallel_logits) | |
| dist.all_reduce(sum_softmax_times_logits, group=process_group) | |
| entropy = logits_max + normalized_sum_exp_logits.log() - sum_softmax_times_logits | |
| ctx.save_for_backward(vocab_parallel_logits, softmax_logits, sum_softmax_times_logits) | |
| return entropy.squeeze(dim=-1) | |
| def backward(ctx, grad_output: torch.Tensor) -> torch.Tensor: | |
| vocab_parallel_logits, softmax_logits, sum_softmax_times_logits = ctx.saved_tensors | |
| # reuse softmax_logits as grad | |
| vocab_parallel_logits.sub_(sum_softmax_times_logits) | |
| softmax_logits.mul_(vocab_parallel_logits) | |
| softmax_logits.mul_(grad_output.unsqueeze(dim=-1)) | |
| # recover vocab_parallel_logits | |
| vocab_parallel_logits.add_(sum_softmax_times_logits) | |
| softmax_logits.mul_(-1) | |
| return softmax_logits, None | |
| def compute_entropy_from_logits(logits: torch.Tensor, process_group) -> torch.Tensor: | |
| return _VocabParallelEntropy.apply(logits, process_group) | |
| def get_grpo_returns( | |
| rewards: torch.Tensor, | |
| kl: list[torch.Tensor], | |
| ): | |
| returns = [] | |
| for i in range(len(rewards)): | |
| returns.append(torch.ones_like(kl[i]) * rewards[i]) | |
| return returns | |
| def get_reinforce_plus_plus_returns( | |
| rewards: torch.Tensor, | |
| kl: list[torch.Tensor], | |
| loss_masks: list[torch.Tensor], | |
| response_lengths: list[int], | |
| total_lengths: list[int], | |
| kl_coef: float, | |
| gamma: float, | |
| ) -> list[torch.Tensor]: | |
| """ | |
| Calculates discounted returns for REINFORCE++ (https://arxiv.org/pdf/2501.03262) | |
| Args: | |
| rewards (Tensor): A tensor of scalar rewards for each sequence. | |
| kl (List[Tensor]): List of per-token KL divergence tensors for sequence chunks. | |
| loss_masks (List[Tensor]): List of response-only loss masks for each full sequence. | |
| response_lengths (List[int]): The full length of each response sequence. | |
| total_lengths (List[int]): The full length of each sequence (prompt + response). | |
| kl_coef (float): Coefficient for the KL penalty. | |
| gamma (float): The discount factor. | |
| Returns: | |
| List[torch.Tensor]: A list of return (G_t) tensors for the | |
| local sequence chunks owned by the current GPU rank. | |
| """ | |
| from megatron.core import mpu | |
| cp_size = mpu.get_context_parallel_world_size() | |
| final_returns_chunks = [] | |
| for i in range(len(rewards)): | |
| local_kl_chunk = kl[i] | |
| total_len, response_len = total_lengths[i], response_lengths[i] | |
| if cp_size > 1: | |
| # Step 1,2:Gather all chunks and token_offsets from all ranks and reconstruct the full response tensor by splitting and placing each part | |
| from slime.backends.megatron_utils.cp_utils import all_gather_with_cp | |
| full_kl_response = all_gather_with_cp(local_kl_chunk, total_len, response_len) | |
| else: | |
| full_kl_response = local_kl_chunk | |
| # Step 3: Compute returns on full response kl tensor. | |
| token_level_rewards = -kl_coef * full_kl_response | |
| full_mask = loss_masks[i] | |
| assert full_mask.sum().item() > 0, f"Sequence at index {i} is fully masked." | |
| last_idx = full_mask.nonzero(as_tuple=True)[0][-1] | |
| token_level_rewards[last_idx] += rewards[i] | |
| returns_for_seq = torch.zeros_like(token_level_rewards) | |
| running_return = 0.0 | |
| for t in reversed(range(token_level_rewards.size(0))): | |
| # G_t = r_t + gamma * G_{t+1} | |
| running_return = token_level_rewards[t] + gamma * running_return | |
| returns_for_seq[t] = running_return | |
| # Step 4: Pick up the results corresponding to our local chunk's parts. | |
| if cp_size > 1: | |
| from slime.backends.megatron_utils.cp_utils import slice_log_prob_with_cp | |
| local_returns_chunk = slice_log_prob_with_cp(returns_for_seq, total_len, response_len) | |
| else: | |
| local_returns_chunk = returns_for_seq | |
| final_returns_chunks.append(local_returns_chunk) | |
| return final_returns_chunks | |
| def get_reinforce_plus_plus_baseline_advantages( | |
| rewards: torch.Tensor, | |
| kl: list[torch.Tensor], | |
| loss_masks: list[torch.Tensor], | |
| kl_coef: float, | |
| ) -> list[torch.Tensor]: | |
| """ | |
| Calculates the unwhitened advantages for the REINFORCE++-baseline algorithm. | |
| Broadcasting the scalar (reward - group_baseline) to each token. | |
| Args: | |
| rewards (Tensor): A tensor of scalar rewards, where the group-wise | |
| baseline has already been subtracted. | |
| kl (list[Tensor]): A list of per-token KL divergence tensors. Used to | |
| get the shape for broadcasting. | |
| loss_masks (list[Tensor]): A list of per-token loss masks. | |
| kl_coef (float): Coefficient for the KL penalty. | |
| Returns: | |
| list[Tensor]: A list of tensors containing the unwhitened advantages. | |
| """ | |
| # Broadcast to get unwhitened advantages | |
| unwhitened_advantages = [ | |
| torch.ones_like(kl_tensor) * reward_val - kl_coef * kl_tensor | |
| for kl_tensor, reward_val in zip(kl, rewards, strict=False) | |
| ] | |
| return unwhitened_advantages | |
| def get_advantages_and_returns( | |
| total_len: int, | |
| response_len: int, | |
| values: torch.Tensor, | |
| rewards: torch.Tensor, | |
| gamma: float, | |
| lambd: float, | |
| ) -> tuple[torch.Tensor, torch.Tensor]: | |
| """Function that computes advantages and returns from rewards and values. | |
| Calculated as in the original PPO paper: https://arxiv.org/abs/1707.06347 | |
| Note that rewards may include a KL divergence loss term. | |
| Advantages looks like this: | |
| Adv1 = R1 + γ * λ * R2 + γ^2 * λ^2 * R3 + ... | |
| - V1 + γ * (1 - λ) V2 + γ^2 * λ * (1 - λ) V3 + ... | |
| Returns looks like this: | |
| Ret1 = R1 + γ * λ * R2 + γ^2 * λ^2 * R3 + ... | |
| + γ * (1 - λ) V2 + γ^2 * λ * (1 - λ) V3 + ... | |
| Input: | |
| - values: Tensor of shape (response_size,) | |
| - rewards: Tensor of shape (response_size,) | |
| Output: | |
| - advantages: Tensor of shape (response_size,) | |
| - returns: Tensor of shape (response_size,) | |
| """ | |
| from megatron.core import mpu | |
| cp_size = mpu.get_context_parallel_world_size() | |
| if cp_size > 1: | |
| from slime.backends.megatron_utils.cp_utils import all_gather_with_cp | |
| full_rewards = all_gather_with_cp(rewards, total_len, response_len) | |
| full_values = all_gather_with_cp(values, total_len, response_len) | |
| else: | |
| full_rewards = rewards | |
| full_values = values | |
| lastgaelam = 0 | |
| advantages_reversed = [] | |
| for t in reversed(range(response_len)): | |
| nextvalues = full_values[t + 1] if t < response_len - 1 else 0.0 | |
| delta = full_rewards[t] + gamma * nextvalues - full_values[t] | |
| lastgaelam = delta + gamma * lambd * lastgaelam | |
| advantages_reversed.append(lastgaelam) | |
| full_advantages = torch.tensor(advantages_reversed[::-1], dtype=full_values.dtype, device=full_values.device) | |
| full_returns = full_advantages + full_values | |
| if cp_size > 1: | |
| from slime.backends.megatron_utils.cp_utils import slice_log_prob_with_cp | |
| advantages = slice_log_prob_with_cp(full_advantages, total_len, response_len) | |
| returns = slice_log_prob_with_cp(full_returns, total_len, response_len) | |
| else: | |
| advantages = full_advantages | |
| returns = full_returns | |
| return advantages.detach(), returns | |
| def get_advantages_and_returns_batch( | |
| total_lengths, | |
| response_lengths, | |
| values_list, | |
| rewards_list, | |
| gamma, | |
| lambd, | |
| chunked: bool = True, | |
| ): | |
| """ | |
| Batched GAE with CP support. | |
| Input: | |
| total_lengths: list[int], each sample's total_len | |
| response_lengths: list[int], each sample's response_len | |
| values_list: list[Tensor], each shape = [resp_len_i] | |
| rewards_list: list[Tensor], same shape | |
| Output: | |
| advantages_list: list[Tensor], each shape = [resp_len_i] | |
| returns_list: list[Tensor], same shape | |
| """ | |
| from megatron.core import mpu | |
| with torch.no_grad(): | |
| B = len(response_lengths) | |
| assert B == len(values_list) | |
| assert B == len(rewards_list) | |
| cp_size = mpu.get_context_parallel_world_size() | |
| device = values_list[0].device | |
| dtype = values_list[0].dtype | |
| if cp_size > 1: | |
| from slime.backends.megatron_utils.cp_utils import all_gather_with_cp | |
| full_values_list = [] | |
| full_rewards_list = [] | |
| for total_len, resp_len, v, r in zip( | |
| total_lengths, response_lengths, values_list, rewards_list, strict=False | |
| ): | |
| full_v = all_gather_with_cp(v, total_len, resp_len) | |
| full_r = all_gather_with_cp(r, total_len, resp_len) | |
| full_values_list.append(full_v) | |
| full_rewards_list.append(full_r) | |
| # full_values_list[i].shape = [total_len_i] | |
| else: | |
| full_values_list = values_list | |
| full_rewards_list = rewards_list | |
| # pad to max_len for batched GAE | |
| max_len = max(response_lengths) | |
| full_values = torch.zeros(B, max_len, device=device, dtype=dtype) | |
| full_rewards = torch.zeros(B, max_len, device=device, dtype=dtype) | |
| for i in range(B): | |
| L = response_lengths[i] | |
| full_values[i, :L] = full_values_list[i][:L] | |
| full_rewards[i, :L] = full_rewards_list[i][:L] | |
| if not chunked: | |
| full_advantages, full_returns = vanilla_gae( | |
| rewards=full_rewards, | |
| values=full_values, | |
| gamma=gamma, | |
| lambd=lambd, | |
| ) | |
| else: | |
| full_advantages, full_returns = chunked_gae( | |
| rewards=full_rewards, | |
| values=full_values, | |
| gamma=gamma, | |
| lambd=lambd, | |
| ) | |
| advantages_list = [] | |
| returns_list = [] | |
| if cp_size > 1: | |
| from slime.backends.megatron_utils.cp_utils import slice_log_prob_with_cp | |
| for total_len, resp_len, adv_row, ret_row in zip( | |
| total_lengths, | |
| response_lengths, | |
| full_advantages, | |
| full_returns, | |
| strict=False, | |
| ): | |
| adv_full = adv_row # shape = [resp_len_i padded to max_len] | |
| ret_full = ret_row | |
| adv_sliced = slice_log_prob_with_cp(adv_full[:resp_len], total_len, resp_len) | |
| ret_sliced = slice_log_prob_with_cp(ret_full[:resp_len], total_len, resp_len) | |
| advantages_list.append(adv_sliced) | |
| returns_list.append(ret_sliced) | |
| else: | |
| for i in range(B): | |
| L = response_lengths[i] | |
| advantages_list.append(full_advantages[i, :L]) | |
| returns_list.append(full_returns[i, :L]) | |
| return advantages_list, returns_list | |
| def vanilla_gae( | |
| rewards: torch.Tensor, | |
| values: torch.Tensor, | |
| gamma: float, | |
| lambd: float, | |
| ): | |
| B, T = rewards.shape | |
| device = rewards.device | |
| dtype = rewards.dtype | |
| lastgaelam = torch.zeros(B, device=device, dtype=dtype) | |
| adv_rev = [] | |
| for t in reversed(range(T)): | |
| next_value = values[:, t + 1] if t < T - 1 else 0.0 | |
| delta = rewards[:, t] + gamma * next_value - values[:, t] | |
| lastgaelam = delta + gamma * lambd * lastgaelam | |
| adv_rev.append(lastgaelam) | |
| full_advantages = torch.stack(adv_rev[::-1], dim=1) # [B, max_len] | |
| full_returns = full_advantages + values # [B, max_len] | |
| return full_advantages, full_returns | |
| def chunked_gae( | |
| rewards: torch.Tensor, | |
| values: torch.Tensor, | |
| gamma: float, | |
| lambd: float, | |
| chunk_size: int = 128, | |
| ): | |
| """ | |
| Compute Generalized Advantage Estimation (GAE) using a FlashLinearAttention- | |
| inspired algorithm: parallel prefix scan within chunks and recurrent state | |
| propagation across chunks. | |
| This reduces the sequential dependency length from O(T) to O(T / chunk_size), | |
| while keeping chunk computations fully parallelizable (O(C^2) per chunk). | |
| Args: | |
| rewards (Tensor): [B, T] reward sequence. | |
| values (Tensor): [B, T] value predictions. The next-value of the final | |
| step is assumed to be zero (standard PPO convention). | |
| gamma (float): discount factor. | |
| lam (float): GAE lambda. | |
| chunk_size (int): sequence chunk length for parallel scan. | |
| Returns: | |
| advantages (Tensor): [B, T] computed advantages. | |
| returns (Tensor): [B, T] advantages + values. | |
| """ | |
| # ------------------------------------------------------------------------- | |
| # Validate inputs | |
| # ------------------------------------------------------------------------- | |
| assert rewards.ndim == 2 and values.ndim == 2 | |
| B, T = rewards.shape | |
| assert values.shape == (B, T) | |
| device = rewards.device | |
| dtype = rewards.dtype | |
| # ------------------------------------------------------------------------- | |
| # Build δ_t = r_t + γ * V_{t+1} - V_t with V_{T} = 0 | |
| # ------------------------------------------------------------------------- | |
| next_values = torch.cat( | |
| [values[:, 1:], torch.zeros(B, 1, device=device, dtype=dtype)], | |
| dim=1, | |
| ) | |
| deltas = rewards + gamma * next_values - values | |
| # Reformulate backward GAE as a forward scan on the reversed sequence: | |
| # S[i] = Δ[i] + w * S[i - 1], w = γλ | |
| w = gamma * lambd | |
| deltas_rev = torch.flip(deltas, dims=[1]) # [B, T] | |
| # ------------------------------------------------------------------------- | |
| # Pad to a multiple of chunk_size | |
| # ------------------------------------------------------------------------- | |
| if T % chunk_size != 0: | |
| pad = chunk_size - (T % chunk_size) | |
| deltas_rev = F.pad(deltas_rev, (0, pad)) | |
| else: | |
| pad = 0 | |
| B, T_pad = deltas_rev.shape | |
| n_chunks = T_pad // chunk_size | |
| deltas_chunks = deltas_rev.view(B, n_chunks, chunk_size) | |
| # ------------------------------------------------------------------------- | |
| # Construct the intra-chunk parallel scan kernel M | |
| # | |
| # For a chunk Δ[0..C-1], we want: | |
| # S_local[t] = sum_{k=0..t} w^(t-k) * Δ[k] | |
| # | |
| # This is implemented as: | |
| # S_local = Δ @ M | |
| # | |
| # where: | |
| # M[i, j] = w^(j - i) if j >= i | |
| # 0 otherwise | |
| # ------------------------------------------------------------------------- | |
| idx = torch.arange(chunk_size, device=device) | |
| row = idx[:, None] | |
| col = idx[None, :] | |
| diff = col - row | |
| M = torch.zeros(chunk_size, chunk_size, device=device, dtype=dtype) | |
| mask = diff >= 0 | |
| if w == 0.0: | |
| M[mask & (diff == 0)] = 1.0 | |
| else: | |
| M[mask] = w ** diff[mask].to(dtype) | |
| # pow_vec[t] = w^(t+1), used to inject the recurrent state s_prev | |
| if w == 0.0: | |
| pow_vec = torch.zeros(chunk_size, device=device, dtype=dtype) | |
| else: | |
| pow_vec = w ** torch.arange(1, chunk_size + 1, device=device, dtype=dtype) | |
| # ------------------------------------------------------------------------- | |
| # Parallel compute local chunk results (assuming initial state = 0) | |
| # ------------------------------------------------------------------------- | |
| deltas_flat = deltas_chunks.reshape(B * n_chunks, chunk_size) | |
| S_local_flat = deltas_flat @ M | |
| S_local_chunks = S_local_flat.view(B, n_chunks, chunk_size) | |
| # Effective length of each chunk (the last chunk may be padded) | |
| lengths = [chunk_size] * n_chunks | |
| if pad > 0: | |
| lengths[-1] = chunk_size - pad | |
| # ------------------------------------------------------------------------- | |
| # Recurrent propagation between chunks | |
| # | |
| # Each chunk contributes: | |
| # S_global[t] = S_local[t] + w^(t+1) * s_prev | |
| # | |
| # And updates: | |
| # s_prev = S_global[last_t] | |
| # ------------------------------------------------------------------------- | |
| S_rev = deltas_rev.new_zeros(B, T_pad) | |
| s_prev = torch.zeros(B, device=device, dtype=dtype) | |
| for c in range(n_chunks): | |
| Lc = lengths[c] | |
| start = c * chunk_size | |
| end = start + Lc | |
| S_local = S_local_chunks[:, c, :Lc] | |
| S_global = S_local + s_prev.unsqueeze(1) * pow_vec[:Lc] | |
| S_rev[:, start:end] = S_global | |
| s_prev = S_global[:, -1] # state for next chunk | |
| # Remove padding and flip back to original time order | |
| if pad > 0: | |
| S_rev = S_rev[:, :T] | |
| advantages = torch.flip(S_rev, dims=[1]) | |
| returns = advantages + values | |
| return advantages, returns | |
| def calculate_log_probs_and_entropy(logits, tokens, tp_group, with_entropy: bool = False, chunk_size: int = -1): | |
| logits = logits.contiguous() | |
| # TODO: not sure why we need to clone the logits here. | |
| # Without the clone, the backward will trigger inplace edit error. | |
| # It seems that the function with tp will modify the logits inplace. | |
| entropy = None | |
| if logits.size(0) != 0: | |
| if chunk_size > 0: | |
| num_chunks = (logits.size(0) - 1) // chunk_size + 1 | |
| tokens_chunks = tokens.chunk(num_chunks, dim=0) | |
| logits_chunks = logits.chunk(num_chunks, dim=0) | |
| log_probs = [] | |
| for tokens_chunk, logits_chunk in zip(tokens_chunks, logits_chunks, strict=True): | |
| log_prob = compute_log_probs(logits_chunk.clone(), tokens_chunk, tp_group) | |
| log_probs.append(log_prob) | |
| log_prob = torch.cat(log_probs, dim=0) | |
| if with_entropy: | |
| entropys = [] | |
| for _, logits_chunk in zip(tokens_chunks, logits_chunks, strict=True): | |
| entropy = compute_entropy_from_logits(logits_chunk.clone(), tp_group) | |
| entropys.append(entropy) | |
| entropy = torch.cat(entropys, dim=0) | |
| else: | |
| log_prob = compute_log_probs(logits.clone(), tokens, tp_group) | |
| if with_entropy: | |
| entropy = compute_entropy_from_logits(logits.clone(), tp_group) | |
| else: | |
| log_prob = logits.new_zeros((0,)) | |
| if with_entropy: | |
| entropy = logits.new_zeros((0,)) | |
| return log_prob, entropy | |
| def vanilla_tis_function( | |
| args, | |
| *, | |
| pg_loss: torch.Tensor, | |
| train_log_probs: list[torch.Tensor], | |
| rollout_log_probs: list[torch.Tensor], | |
| loss_masks: list[torch.Tensor], | |
| **kwargs, | |
| ) -> tuple[torch.Tensor, list[torch.Tensor], dict[str, torch.Tensor]]: | |
| """Apply TIS off-policy correction using importance sampling. | |
| Parameters: | |
| args: Arguments containing TIS settings. | |
| pg_loss: Policy gradient loss tensor of shape [total_seq_len - 1]. | |
| train_log_probs: List of tensors containing training log-probabilities | |
| for each sequence. | |
| rollout_log_probs: List of tensors containing rollout log-probabilities | |
| for each sequence. | |
| loss_masks: List of tensors containing loss masks for each sequence. | |
| """ | |
| rollout_log_probs = torch.cat(rollout_log_probs, dim=0) | |
| old_log_probs = torch.cat(train_log_probs, dim=0) | |
| tis = torch.exp(old_log_probs - rollout_log_probs) | |
| tis_abs = (tis - 1).abs() | |
| tis_clip_low = args.tis_clip_low if args.tis_clip_low is not None else 0.1 | |
| tis_clip_high = args.tis_clip if args.tis_clip is not None else 2.0 | |
| tis_weights = torch.clamp(tis, min=tis_clip_low, max=tis_clip_high) | |
| tis_clipfrac = (tis_weights != tis).float() | |
| metrics = { | |
| "tis": tis.clone().detach(), | |
| "tis_clipfrac": tis_clipfrac.clone().detach(), | |
| "tis_abs": tis_abs.clone().detach(), | |
| } | |
| pg_loss = pg_loss * tis_weights | |
| return pg_loss, loss_masks, metrics | |