Source code for protomotions.agents.common.latent
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
"""Shared latent-output keys and latent-specific loss helpers."""
import torch
from protomotions.agents.common.autoregressive import (
prior_constrained_sampling_log_probs,
sampling_log_probs,
)
LATENT_KEY = "latent"
LATENT_LOGITS_KEY = "latent_logits"
LATENT_LOGPROB_KEY = "latent_logprob"
LATENT_MU_KEY = "latent_mu"
LATENT_LOGVAR_KEY = "latent_logvar"
VAE_LATENT_KEY = "vae_latent"
VAE_NOISE_KEY = "vae_noise"
PRIVILEGED_LATENT_KEY = "privileged_latent"
PRIVILEGED_LATENT_MU_KEY = "privileged_latent_mu"
PRIVILEGED_LATENT_LOGVAR_KEY = "privileged_latent_logvar"
TARGET_LATENT_KEY = "target_latent"
TARGET_LATENT_LOGITS_KEY = "target_latent_logits"
TARGET_LATENT_MU_KEY = "target_latent_mu"
TARGET_LATENT_LOGVAR_KEY = "target_latent_logvar"
[docs]
def compute_discrete_latent_ppo_loss(
*,
logits: torch.Tensor,
selected: torch.Tensor,
old_neglogp: torch.Tensor,
advantages: torch.Tensor,
e_clip: float,
entropy_coef: float = 0.0,
temperature: float = 1.0,
top_p: float = 1.0,
prior_logits: torch.Tensor = None,
log_prefix: str = "actor",
):
"""Compute PPO loss under the same distribution used during token rollout."""
if prior_logits is None:
log_probs = sampling_log_probs(logits, p=top_p, temperature=temperature)
else:
log_probs = prior_constrained_sampling_log_probs(
logits,
prior_logits,
p=top_p,
temperature=temperature,
)
logprob = log_probs.gather(-1, selected.unsqueeze(-1)).squeeze(-1)
old_logprob = -old_neglogp
logprob_sum = logprob.sum(dim=-1)
old_logprob_sum = old_logprob.sum(dim=-1)
ratio = torch.exp(logprob_sum - old_logprob_sum)
unclipped = advantages * ratio
clipped = advantages * torch.clamp(ratio, 1.0 - e_clip, 1.0 + e_clip)
ppo_loss = -torch.min(unclipped, clipped).mean()
probs = log_probs.exp()
finite_log_probs = torch.where(
torch.isfinite(log_probs),
log_probs,
torch.zeros_like(log_probs),
)
entropy = -(probs * finite_log_probs).sum(dim=-1).mean()
loss = ppo_loss - entropy_coef * entropy
with torch.no_grad():
kl = (old_logprob_sum - logprob_sum).mean()
clip_frac = (torch.abs(ratio - 1.0) > e_clip).float().mean()
metrics = {
f"{log_prefix}/ppo_loss": ppo_loss.detach(),
f"{log_prefix}/entropy": entropy.detach(),
f"{log_prefix}/ratio": ratio.mean().detach(),
f"{log_prefix}/clip_frac": clip_frac.detach(),
f"{log_prefix}/kl": kl.detach(),
f"{log_prefix}/loss": loss.detach(),
}
return loss, metrics