Source code for protomotions.agents.common.transformer
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
"""Transformer architecture for sequential modeling.
This module implements transformer-based networks for processing temporal information
in reinforcement learning. Used primarily in motion tracking and MaskedMimic agents
for handling sequential observations.
Key Classes:
- Transformer: Main transformer model with positional encoding
- PositionalEncoding: Sinusoidal positional encodings for sequence position
Key Features:
- Multi-head self-attention for temporal dependencies
- Multiple input heads with different encoders
- Positional encoding for sequence awareness
- Flexible output heads (single or multi-headed)
"""
import torch
from torch import nn
from tensordict import TensorDict
from protomotions.agents.base_agent.model import ProtoMotionsTensorDictModule
from protomotions.agents.utils.training import get_activation_func
from protomotions.agents.common.config import TransformerConfig
[docs]
class Transformer(ProtoMotionsTensorDictModule):
"""Transformer network for sequential observation processing.
Processes multi-modal sequential inputs through separate encoders, combines them
into a sequence of tokens, and applies transformer layers for temporal modeling.
Used in motion tracking agents to process future reference poses.
Args:
config: Transformer configuration specifying architecture parameters.
Attributes:
input_models: Dictionary of input encoders for different observation types.
sequence_pos_encoder: Positional encoding layer.
seqTransEncoder: Stack of transformer encoder layers.
in_keys: List of input keys collected from all input models.
out_keys: List containing output key.
Example:
>>> config = TransformerConfig()
>>> model = Transformer(config)
>>> output_td = model(tensordict)
"""
[docs]
def __init__(self, config: TransformerConfig):
super().__init__()
self.config = config
# Set TensorDict keys
self.in_keys = self.config.in_keys
self.out_keys = self.config.out_keys
self.output_activation = None
if self.config.output_activation is not None:
self.output_activation = get_activation_func(self.config.output_activation)
# Extract all input tokens that aren't masks.
token_input_keys = []
mask_keys = (
[value for value in self.config.input_and_mask_mapping.values()]
if self.config.input_and_mask_mapping
else []
)
for in_key in self.in_keys:
if in_key not in mask_keys:
token_input_keys.append(in_key)
self._token_input_keys = token_input_keys
# Transformer layers
seqTransEncoderLayer = nn.TransformerEncoderLayer(
d_model=self.config.latent_dim,
nhead=self.config.num_heads,
dim_feedforward=self.config.ff_size,
dropout=self.config.dropout,
activation=get_activation_func(
self.config.activation, return_type="functional"
),
batch_first=True,
)
self.seqTransEncoder = nn.TransformerEncoder(
seqTransEncoderLayer, num_layers=self.config.num_layers
)
[docs]
def forward(
self,
tensordict: TensorDict,
log_internals: bool = False,
) -> TensorDict:
"""Forward pass through transformer.
Args:
tensordict: TensorDict containing all input observations.
log_internals: Accepted for the common TensorDict-module contract.
Returns:
TensorDict with transformer output added at self.out_keys[0].
"""
all_tokens = []
for in_key in self._token_input_keys:
if tensordict[in_key].dim() == 2:
all_tokens.append(tensordict[in_key].unsqueeze(1))
else:
all_tokens.append(tensordict[in_key])
all_tokens = torch.cat(all_tokens, dim=1)
all_masks = []
for in_key in self._token_input_keys:
if (
self.config.input_and_mask_mapping
and in_key in self.config.input_and_mask_mapping
):
mask_key = self.config.input_and_mask_mapping[in_key]
token = tensordict[in_key]
token_seq_len = 1 if token.dim() == 2 else token.shape[1]
# Our mask is 1 for valid and 0 for invalid
# The transformer expects the mask to be 0 for valid and 1 for invalid
raw_mask = tensordict[mask_key]
if raw_mask.dim() == 1:
mask = raw_mask.logical_not().unsqueeze(1)
else:
mask = raw_mask.logical_not()
# Reduce mask when it has more entries than the token sequence
# e.g. per-object mask (E, O) for a single-token encoding (E, 1, D)
if mask.shape[1] > token_seq_len:
# Token is invalid only if ALL mask entries are invalid
# (inverted: any(original_valid) -> token valid -> inverted_mask=False)
# mask is inverted (True=invalid), so: all(inverted)=True -> invalid
mask = mask.all(dim=-1, keepdim=True).expand(-1, token_seq_len)
elif mask.shape[1] < token_seq_len:
mask = mask.expand(-1, token_seq_len)
all_masks.append(mask)
else:
if tensordict[in_key].dim() == 2:
all_masks.append(
torch.zeros(
tensordict.batch_size[0],
1,
dtype=torch.bool,
device=tensordict[in_key].device,
)
)
else:
all_masks.append(
torch.zeros(
tensordict.batch_size[0],
tensordict[in_key].shape[1],
dtype=torch.bool,
device=tensordict[in_key].device,
)
)
all_masks = torch.cat(all_masks, dim=1)
output = self.seqTransEncoder(
all_tokens, src_key_padding_mask=all_masks
) # [batch, seq_len, features]
output = output[:, 0, :] # [batch, features] - take first token
if self.output_activation is not None:
output = self.output_activation(output)
tensordict[self.out_keys[0]] = output
return tensordict