Source code for kimodo.model.twostage_denoiser

# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
"""Two-stage transformer denoiser: root stage then body stage for motion diffusion."""

import contextlib
from typing import Optional

import torch
from torch import nn

from .backbone import TransformerEncoderBlock
from .loading import load_checkpoint_state_dict


[docs] class TwostageDenoiser(nn.Module): """Two-stage denoiser: first predicts global root features, then body features conditioned on local root."""
[docs] def __init__( self, motion_rep, motion_mask_mode, ckpt_path: Optional[str] = None, **kwargs, ): """Build root and body transformer blocks; optionally load checkpoint from ckpt_path.""" super().__init__() self.motion_rep = motion_rep self.motion_mask_mode = motion_mask_mode # it should be a dual motion_rep # and be global by default # global motion_rep as inpnut input_dim = motion_rep.motion_rep_dim will_concatenate = motion_mask_mode == "concat" # stage 1: root only root_input_dim = input_dim * 2 if will_concatenate else input_dim root_output_dim = motion_rep.global_root_dim self.root_model = TransformerEncoderBlock( input_dim=root_input_dim, output_dim=root_output_dim, skeleton=self.motion_rep.skeleton, **kwargs, ) # replace the global root by the local root local_motion_rep_dim = input_dim - motion_rep.global_root_dim + motion_rep.local_root_dim # stage 2: local body body_input_dim = local_motion_rep_dim + ( input_dim if will_concatenate else 0 ) # body stage always takes in local root info for motion (but still the global mask) body_output_dim = input_dim - motion_rep.global_root_dim self.body_model = TransformerEncoderBlock( input_dim=body_input_dim, output_dim=body_output_dim, skeleton=self.motion_rep.skeleton, **kwargs, ) if ckpt_path: self.load_ckpt(ckpt_path)
[docs] def load_ckpt(self, ckpt_path: str) -> None: """Load checkpoint from path; state dict keys are stripped of 'denoiser.backbone.' prefix.""" state_dict = load_checkpoint_state_dict(ckpt_path) state_dict = {key.replace("denoiser.backbone.", ""): val for key, val in state_dict.items()} self.load_state_dict(state_dict)
[docs] def forward( self, x: torch.Tensor, x_pad_mask: torch.Tensor, text_feat: torch.Tensor, text_feat_pad_mask: torch.Tensor, timesteps: torch.Tensor, first_heading_angle: Optional[torch.Tensor] = None, motion_mask: Optional[torch.Tensor] = None, observed_motion: Optional[torch.Tensor] = None, ) -> torch.Tensor: """ Args: x (torch.Tensor): [B, T, dim_motion] current noisy motion x_pad_mask (torch.Tensor): [B, T] attention mask, positions with True are allowed to attend, False are not text_feat (torch.Tensor): [B, max_text_len, llm_dim] embedded text prompts text_feat_pad_mask (torch.Tensor): [B, max_text_len] attention mask, positions with True are allowed to attend, False are not timesteps (torch.Tensor): [B,] current denoising step motion_mask observed_motion Returns: torch.Tensor: same size as input x """ if self.motion_mask_mode == "concat": if motion_mask is None or observed_motion is None: motion_mask = torch.zeros_like(x) observed_motion = torch.zeros_like(x) x = x * (1 - motion_mask) + observed_motion * motion_mask x_extended = torch.cat([x, motion_mask], axis=-1) else: x_extended = x # Stage 1: predict root motion in global root_motion_pred = self.root_model( x_extended, x_pad_mask, text_feat, text_feat_pad_mask, timesteps, first_heading_angle, ) # [B, T, 5] # Maybe pass this as argument instead of recomputing it lengths = x_pad_mask.sum(-1) # Convert root pred to local rep # At test-time want to allow gradient through for guidance convert_ctx = torch.no_grad() if self.training else contextlib.nullcontext() with convert_ctx: root_motion_local = self.motion_rep.global_root_to_local_root( root_motion_pred, normalized=True, lengths=lengths, ) if self.training: root_motion_local = root_motion_local.detach() # concatenate the predicted local root with the body motion body_x = x[..., self.motion_rep.body_slice] x_new = torch.cat([root_motion_local, body_x], axis=-1) if self.motion_mask_mode == "concat": x_new_extended = torch.cat([x_new, motion_mask], axis=-1) else: x_new_extended = x_new # Stage 2: predict local body motion based on local root predicted_body = self.body_model( x_new_extended, x_pad_mask, text_feat, text_feat_pad_mask, timesteps, first_heading_angle, ) # concatenate the predicted local body with the predicted root output = torch.cat([root_motion_pred, predicted_body], axis=-1) return output