Source code for protomotions.agents.common.autoencoder
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
"""Shared encoder-bottleneck-decoder TensorDict modules.
This file intentionally stays narrow. ``AutoEncoder`` is useful for models
that really are encoder -> bottleneck -> decoder, such as FSQ trackers and
small reconstruction-style students. Autoregressive GPC priors are not
autoencoders and should use their own ``BaseModel`` implementation instead.
"""
from typing import Dict
import torch
from tensordict import TensorDict
from tensordict.nn import TensorDictModuleBase
from protomotions.agents.base_agent.model import BaseModel
from protomotions.agents.common.common import MODULE_INTERNALS_KEY
from protomotions.agents.common.autoencoder.config import AutoEncoderConfig
from protomotions.utils.hydra_replacement import get_class
[docs]
class AutoEncoder(BaseModel):
"""Generic encoder-bottleneck-decoder module.
Subclasses customize only the bottleneck behavior. For example, an FSQ
tracker quantizes the encoder latent before decoding. Models with a
different shape, such as causal token priors, should not inherit this class.
"""
supports_log_internals = False
config: AutoEncoderConfig
[docs]
def __init__(self, config: AutoEncoderConfig):
super().__init__(config)
self.config = config
encoder_class = get_class(config.encoder._target_)
decoder_class = get_class(config.decoder._target_)
self.encoder: TensorDictModuleBase = encoder_class(config=config.encoder)
self.decoder: TensorDictModuleBase = decoder_class(config=config.decoder)
self.encoder_out_keys = list(config.encoder_out_keys or self.encoder.out_keys)
self.decoder_out_keys = list(config.decoder_out_keys or self.decoder.out_keys)
self.latent_key = config.latent_key
latent_keys = set(self.encoder_out_keys + [self.latent_key])
self.in_keys = list(
dict.fromkeys(
list(self.encoder.in_keys)
+ [key for key in self.decoder.in_keys if key not in latent_keys]
)
)
self.out_keys = list(self.decoder_out_keys)
[docs]
def bottleneck(
self,
latent: torch.Tensor,
tensordict: TensorDict,
) -> torch.Tensor:
return latent
[docs]
def internal_logs(
self,
latent: torch.Tensor,
tensordict: TensorDict,
) -> Dict[str, torch.Tensor]:
return {}
[docs]
def predict_latent(self, tensordict: TensorDict) -> torch.Tensor:
tensordict = self.encoder(tensordict)
encoder_out = tensordict[self.encoder_out_keys[0]]
return self.bottleneck(encoder_out, tensordict)
[docs]
def decode(
self,
tensordict: TensorDict,
latent: torch.Tensor,
) -> TensorDict:
tensordict[self.latent_key] = latent
return self.decoder(tensordict)
[docs]
def forward(
self,
tensordict: TensorDict,
log_internals: bool = False,
) -> TensorDict:
latent = self.predict_latent(tensordict)
tensordict = self.decode(tensordict, latent)
if log_internals:
logs = self.internal_logs(latent.detach(), tensordict)
if logs:
tensordict[MODULE_INTERNALS_KEY] = TensorDict(
logs,
batch_size=tensordict.batch_size,
)
return tensordict