dwehr's picture
Migrate action viewer to local Cosmos generation
9f818c5
Raw History Blame Contribute Delete
137 kB
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: OpenMDW-1.1
"""Functions for implementing sequence packing with flexible attention modes.
This module provides utilities for packing text and image sequences together
with support for different attention patterns (causal, full, noise).
Key Components:
---------------
1. Attention Mask Creation:
- create_sparse_mask(): Creates sparse masks for flex attention
- prepare_attention_mask_per_sample(): Creates dense attention masks
2. Position ID Generation:
- get_flattened_position_ids_extrapolate(): Extrapolation-based position encoding
- get_flattened_position_ids_interpolate(): Interpolation-based position encoding
3. Tokenizer Setup:
- add_special_tokens(): Adds image boundary tokens to tokenizer
4. Sequence Packing:
- pack_input_sequence(): Main function for packing text and image sequences
- Helper functions: _pack_text_tokens(), _pack_image_tokens(), _finalize_packed_data()
Sequence Format:
---------------
Each sample consists of alternating text and image sections:
[text_tokens] <eos> <vision_start> [image_tokens] <vision_end> ...
Attention Modes:
---------------
- 'causal': Standard causal/autoregressive attention for text
- 'full': Bidirectional attention for images
- 'noise': Special mode for noise conditioning
"""
import math
from collections.abc import Mapping, Sequence
from dataclasses import dataclass, field
from typing import Any, Dict, List, Tuple
import torch
from torch.nn.attention.flex_attention import and_masks, or_masks
from cosmos_framework.model.attention.checks import check_valid_tuple_or_element
from cosmos_framework.model.attention.varlen import generate_multi_dim_varlen_parameters
from cosmos_framework.utils import log
from cosmos_framework.model.vfm.mot.unified_3dmrope_utils import (
get_3d_mrope_ids_text_tokens,
get_3d_mrope_ids_vae_tokens,
)
from cosmos_framework.model.vfm.utils.data_and_condition import GenerationDataClean
from cosmos_framework.model.vfm.tokenizers.tokenization_qwen2 import Qwen2Tokenizer
MAX_CAUSAL_LEN_IMAGE_BATCH = 0
MAX_FULL_LEN_IMAGE_BATCH = 0
MAX_CAUSAL_LEN_VIDEO_BATCH = 0
MAX_FULL_LEN_VIDEO_BATCH = 0
# ============================================================================
# Attention mask creation
# ============================================================================
def create_sparse_mask(document_lens, split_lens, attn_modes, device):
"""Create a sparse attention mask combining multiple attention patterns.
Args:
document_lens: List of document lengths
split_lens: List of split lengths within documents
attn_modes: List of attention modes ('causal', 'full', 'noise') for each split
device: Device to place tensors on
Returns:
Combined mask using flex attention API
"""
# Build sequence ID tensors for tracking full/noise attention regions
full_and_noise_seq_ids = []
noise_seq_ids = []
for seq_idx, (length, attn_mode) in enumerate(zip(split_lens, attn_modes)):
# Assign sequence ID for full/noise regions, -1 for causal regions
seq_id = seq_idx if attn_mode in ["full", "noise"] else -1
full_and_noise_seq_ids.extend([seq_id] * length)
# Assign sequence ID only for noise regions
noise_seq_id = seq_idx if attn_mode == "noise" else -1
noise_seq_ids.extend([noise_seq_id] * length)
full_and_noise_seq_id = torch.tensor(full_and_noise_seq_ids, device=device) # [seq_len]
noise_seq_id = torch.tensor(noise_seq_ids, device=device) # [seq_len]
document_id = torch.cat([torch.full((l,), i) for i, l in enumerate(document_lens, start=1)]).to(device) # [seq_len]
# Define component mask functions
def causal_mask(b, h, q_idx, kv_idx):
"""Standard causal attention: query can only attend to prior keys."""
return q_idx >= kv_idx
def full_and_noise_mask(b, h, q_idx, kv_idx):
"""Allow attention within same full/noise sequence."""
return (full_and_noise_seq_id[q_idx] == full_and_noise_seq_id[kv_idx]) & (full_and_noise_seq_id[q_idx] >= 0)
def remove_noise_mask(b, h, q_idx, kv_idx):
"""Prevent attending to noise tokens from different sequences."""
return ~((noise_seq_id[kv_idx] >= 0) & (noise_seq_id[q_idx] != noise_seq_id[kv_idx]))
def sample_mask(b, h, q_idx, kv_idx):
"""Ensure attention stays within same document/sample."""
return document_id[q_idx] == document_id[kv_idx]
# Combine all masks: (causal OR full_and_noise) AND remove_noise AND sample
return and_masks(or_masks(causal_mask, full_and_noise_mask), remove_noise_mask, sample_mask)
def prepare_attention_mask_per_sample(split_lens, attn_modes, device="cpu"):
"""Prepare dense attention mask for a single sample with multiple splits.
Args:
split_lens: List of integers indicating length of each split within the sample
attn_modes: List of attention modes for each split ('causal', 'full', or 'noise')
device: Device to place the attention mask tensor on
Returns:
Attention mask tensor of shape (sample_len, sample_len) with -inf for masked positions
"""
sample_len = sum(split_lens)
attention_mask = torch.zeros((sample_len, sample_len), dtype=torch.bool, device=device) # [sample_len,sample_len]
# First pass: Set up basic attention patterns for each split
current_pos = 0
for split_len, attn_mode in zip(split_lens, attn_modes):
assert attn_mode in ["causal", "full", "noise"], f"Invalid attention mode: {attn_mode}"
split_start = current_pos
split_end = current_pos + split_len
if attn_mode == "causal":
# Causal: lower triangular within split + full attention to previous splits
attention_mask[split_start:split_end, split_start:split_end] = torch.ones(
(split_len, split_len), device=device
).tril() # [split_len,split_len]
attention_mask[split_start:split_end, :split_start] = 1
else: # "full" or "noise"
# Full attention within split and to previous splits
attention_mask[split_start:split_end, split_start:split_end] = torch.ones(
(split_len, split_len), device=device
) # [split_len,split_len]
attention_mask[split_start:split_end, :split_start] = 1
current_pos += split_len
# Second pass: Handle noise mode - mask out noise columns except within same split
current_pos = 0
for split_len, attn_mode in zip(split_lens, attn_modes):
if attn_mode == "noise":
split_start = current_pos
split_end = current_pos + split_len
# Zero out the entire column for noise tokens
attention_mask[:, split_start:split_end] = 0
# But allow self-attention within the noise split
attention_mask[split_start:split_end, split_start:split_end] = 1
current_pos += split_len
# Convert boolean mask to float with -inf for masked positions
attention_mask = torch.zeros_like(attention_mask, dtype=torch.float).masked_fill_(
~attention_mask, float("-inf")
) # [sample_len,sample_len]
return attention_mask
# ============================================================================
# Tokenizer utilities
# ============================================================================
def add_special_tokens(tokenizer):
"""Add image-related special tokens to tokenizer if not already present.
Args:
tokenizer: Tokenizer to add special tokens to
Returns:
Tuple of (modified tokenizer, dict of new token IDs)
"""
# Collect existing special tokens
existing_special_tokens = []
for key, value in tokenizer.special_tokens_map.items():
if isinstance(value, str):
existing_special_tokens.append(value)
elif isinstance(value, list):
existing_special_tokens.extend(value)
# Define image boundary tokens to add if missing
tokens_to_add = []
if "<|vision_start|>" not in existing_special_tokens:
tokens_to_add.append("<|vision_start|>")
if "<|vision_end|>" not in existing_special_tokens:
tokens_to_add.append("<|vision_end|>")
# Add new tokens to tokenizer vocabulary
if tokens_to_add:
tokenizer.add_tokens(tokens_to_add)
# Get token IDs for image boundary tokens
new_token_ids = {
"start_of_generation": tokenizer.convert_tokens_to_ids("<|vision_start|>"),
"end_of_generation": tokenizer.convert_tokens_to_ids("<|vision_end|>"),
}
return tokenizer, new_token_ids
# ============================================================================
# Data structures
# ============================================================================
@dataclass
class ModalityData:
"""Unified container for a single generation modality's data.
This dataclass serves dual purposes:
1. During packing: Acts as a builder, accumulating data in lists
2. After finalize(): Holds finalized tensors ready for model consumption
Attributes:
sequence_indexes: Indices in the packed sequence where this modality's tokens appear.
List during building, Tensor after finalize().
timesteps: Diffusion timesteps for each noised token.
List during building, Tensor after finalize().
mse_loss_indexes: Indices where MSE loss should be computed (noised tokens only).
List during building, Tensor after finalize().
token_shapes: Shape metadata for each sample's tokens.
For vision: list of (T, H, W) tuples.
For action: list of (T,) tuples.
tokens: The actual latent tokens. List during build, Tensor after finalize().
condition_mask: Mask indicating clean frames (1=clean, 0=noised). Only after finalize().
noisy_frame_indexes: Indices of noised frames. Constructed from condition_mask during
sequence packing to reduce GPU->CPU synchronization later. Only after finalize().
domain_id: Domain ID for multi-domain training. Only after finalize(). NOTE: only used for action modality.
raw_action_dim: Raw action dimension. Only after finalize(). NOTE: only used for action modality.
"""
# Core tracking (list during build, tensor after finalize)
sequence_indexes: list[int] | torch.Tensor = field(default_factory=list)
timesteps: list[float] | torch.Tensor = field(default_factory=list)
mse_loss_indexes: list[int] | torch.Tensor = field(default_factory=list)
# list[tuple[int,int,int]] for vision, list[tuple[int]] for action, list[tuple[int,int,int]] for sound
token_shapes: list = field(default_factory=list)
# Populated during finalization (from GenerationDataClean / noise path)
tokens: list[torch.Tensor] = field(default_factory=list)
condition_mask: list[torch.Tensor] = field(default_factory=list)
noisy_frame_indexes: list[torch.Tensor] = field(default_factory=list)
domain_id: list[torch.Tensor] = field(default_factory=list)
raw_action_dim: list[torch.Tensor | None] | None = field(default_factory=list)
def to_cuda(self) -> None:
"""Move all tensor fields to CUDA in-place."""
if isinstance(self.sequence_indexes, torch.Tensor):
self.sequence_indexes = self.sequence_indexes.cuda()
if isinstance(self.timesteps, torch.Tensor):
self.timesteps = self.timesteps.cuda()
if isinstance(self.mse_loss_indexes, torch.Tensor):
self.mse_loss_indexes = self.mse_loss_indexes.cuda()
self.tokens = [token.cuda() for token in self.tokens]
self.condition_mask = [cm.cuda() for cm in self.condition_mask]
self.noisy_frame_indexes = [ni.cuda() for ni in self.noisy_frame_indexes]
self.domain_id = [d.cuda() for d in self.domain_id]
# raw_action_dim is optional (e.g., when action-channel masking is disabled).
if self.raw_action_dim is not None:
self.raw_action_dim = [d.cuda() if d is not None else None for d in self.raw_action_dim]
@dataclass
class PackedSequence:
"""Unified sequence container - works as builder during packing and final output.
This dataclass replaces the old SequenceStatus + PackedSequence pattern:
- Build phase: Accumulate data using lists, modalities use ModalityData builders
- After finalize(): Ready for model consumption with tensors
Attributes:
# Sequence structure
sample_lens: Length of each sample in the packed sequence.
split_lens: Length of each split (text/vision/action sections).
attn_modes: Attention mode for each split ('causal', 'full').
is_image_batch: Whether this batch contains images (vs videos).
sequence_length: Total length of packed sequence. Computed during finalize().
# Build-time tracking (not used after finalize)
curr: Current position in the packed sequence during building.
# Text modality (list during build, tensor after finalize)
text_ids: All text token IDs (including special tokens).
text_indexes: Indices where text tokens appear in sequence.
position_ids: RoPE position IDs for all tokens.
# Loss computation - Cross Entropy (text)
label_ids: Label IDs for cross-entropy loss.
ce_loss_indexes: Indices for computing cross-entropy loss.
ce_loss_weights: Weights for cross-entropy loss.
# Generation modalities - named fields for type safety
vision: Vision modality data (images/videos). None if no vision in batch.
action: Action modality data (robotics). None if no actions in batch.
sound: Sound modality data (audio). None if no sound in batch.
"""
# Sequence structure
sample_lens: list[int] = field(default_factory=list)
split_lens: list[int] = field(default_factory=list)
attn_modes: list[str] = field(default_factory=list)
is_image_batch: bool = False
sequence_length: int = 0
# Build-time tracking (used during packing, not after finalize)
curr: int = 0
# Text modality (list during build, tensor after finalize)
text_ids: list[int] | torch.Tensor = field(default_factory=list)
text_indexes: list[int] | torch.Tensor = field(default_factory=list)
position_ids: list[int] | torch.Tensor = field(default_factory=list)
# Loss computation - Cross Entropy (text)
label_ids: list[int] | torch.Tensor | None = field(default_factory=list)
ce_loss_indexes: list[int] | torch.Tensor | None = field(default_factory=list)
ce_loss_weights: list[float] | torch.Tensor | None = field(default_factory=list)
# Build-time mRoPE tracking (used during packing, not after finalize)
# When _use_mrope=True, position_ids accumulates (3, N) tensors instead of ints,
# and finalize() produces a (3, total_seq_len) tensor instead of (total_seq_len,).
_use_mrope: bool = False
# Running temporal index for mRoPE position ID generation within a single sample.
# Reset to 0 at the start of each sample, then advanced by text and vision helpers
# as segments are packed. Action reuses the pre-vision snapshot (parallel temporal
# range) without advancing it. Float when FPS modulation is enabled.
# E.g. offset=0 -> text(4 tokens) -> offset=4 -> vision(3 frames) -> offset=7.
_mrope_temporal_offset: int | float = 0
_mrope_reset_spatial: bool = True
# Temporal causal: whether supertoken 0's action slot contains null tokens.
# True for all training calls and AR frame 0; False for AR frame N>0 (real actions).
# Used by three_way_attention to zero out V for null action tokens (inline when attention_meta.null_action_supertokens=True).
null_action_supertokens: bool = False
# Temporal causal: number of action tokens prefixing each vision supertoken.
# Equals temporal_compression_factor when actions are packed inline; 0 when
# action_gen=False or for non-temporal-causal layouts. Single source of truth
# for downstream attention/KV-cache code (per-supertoken layout is
# num_action_tokens_per_supertoken + H_p * W_p).
num_action_tokens_per_supertoken: int = 0
# Generation modalities - NAMED FIELDS for type safety
vision: ModalityData | None = None
action: ModalityData | None = None
sound: ModalityData | None = None
def finalize(
self,
gen_data_clean: GenerationDataClean,
) -> "PackedSequence":
"""Convert all lists to tensors and compute derived values.
Args:
gen_data_clean: GenerationDataClean for metadata (e.g., action domain IDs).
Returns:
New PackedSequence instance with tensors instead of lists.
"""
# Compute sequence length
sequence_length = sum(self.sample_lens)
sample_lens = self.sample_lens.copy()
split_lens = self.split_lens.copy()
attn_modes = self.attn_modes.copy()
# Prepare loss-related tensors (cross-entropy)
label_ids: torch.Tensor | None = None
ce_loss_indexes: torch.Tensor | None = None
ce_loss_weights: torch.Tensor | None = None
if self.label_ids and len(self.label_ids) > 0:
label_ids = torch.tensor(self.label_ids) # [N_ce_tokens]
ce_loss_indexes = torch.tensor(self.ce_loss_indexes) # [N_ce_tokens]
ce_loss_weights = torch.tensor(self.ce_loss_weights) # [N_ce_tokens]
# The condition_mask and noisy_frame_indexes are kept as lists to support variable shapes.
# Finalize vision modality
vision: ModalityData | None = None
if self.vision is not None and len(self.vision.sequence_indexes) > 0:
vision = ModalityData(
sequence_indexes=torch.tensor(self.vision.sequence_indexes, dtype=torch.long), # [N_vision_tokens]
timesteps=torch.tensor(self.vision.timesteps), # [N_vision_noisy_tokens]
mse_loss_indexes=torch.tensor(
self.vision.mse_loss_indexes, dtype=torch.long
), # [N_vision_noisy_tokens]
token_shapes=list(self.vision.token_shapes),
tokens=self.vision.tokens,
condition_mask=list(self.vision.condition_mask),
noisy_frame_indexes=list(self.vision.noisy_frame_indexes),
)
# Finalize action modality
action: ModalityData | None = None
if self.action is not None and len(self.action.sequence_indexes) > 0:
action = ModalityData(
sequence_indexes=torch.tensor(self.action.sequence_indexes, dtype=torch.long), # [N_action_tokens]
timesteps=torch.tensor(self.action.timesteps), # [N_action_noisy_tokens]
mse_loss_indexes=torch.tensor(
self.action.mse_loss_indexes, dtype=torch.long
), # [N_action_noisy_tokens]
token_shapes=list(self.action.token_shapes),
tokens=self.action.tokens,
condition_mask=list(self.action.condition_mask), # Keep as list to support variable shapes
noisy_frame_indexes=list(self.action.noisy_frame_indexes),
domain_id=(
gen_data_clean.action_domain_id
if gen_data_clean.action_domain_id is not None
else [torch.zeros(1, dtype=torch.long)] * len(self.action.token_shapes)
),
raw_action_dim=gen_data_clean.raw_action_dim,
)
# Finalize sound modality (placeholder for future)
sound: ModalityData | None = None
if self.sound is not None and len(self.sound.sequence_indexes) > 0:
sound = ModalityData(
sequence_indexes=torch.tensor(self.sound.sequence_indexes, dtype=torch.long), # [N_sound_tokens]
timesteps=torch.tensor(self.sound.timesteps), # [N_sound_noisy_tokens]
mse_loss_indexes=torch.tensor(self.sound.mse_loss_indexes, dtype=torch.long), # [N_sound_noisy_tokens]
token_shapes=list(self.sound.token_shapes),
tokens=self.sound.tokens,
condition_mask=list(self.sound.condition_mask),
noisy_frame_indexes=list(self.sound.noisy_frame_indexes),
)
# Finalize position IDs: 3D mRoPE (3, seq_len) or 1D RoPE (seq_len,)
if self._use_mrope and len(self.position_ids) > 0 and isinstance(self.position_ids[0], torch.Tensor):
mrope_tensors: list[torch.Tensor] = self.position_ids # type: ignore[assignment]
position_ids = torch.cat(mrope_tensors, dim=1) # [3,actual_seq_len]
else: # Original 1D RoPE from Bagel, where all the media tokens share the same 1D position ID
position_ids = torch.tensor(self.position_ids) # [seq_len]
return PackedSequence(
# Sequence structure
sequence_length=sequence_length,
sample_lens=sample_lens,
split_lens=split_lens,
attn_modes=attn_modes,
is_image_batch=gen_data_clean.is_image_batch,
# Text modality (converted to tensors)
text_ids=torch.tensor(self.text_ids, dtype=torch.long), # [N_text_tokens]
text_indexes=torch.tensor(self.text_indexes, dtype=torch.long), # [N_text_tokens]
position_ids=position_ids, # [seq_len] or [3,seq_len]
# Loss computation - Cross Entropy
label_ids=label_ids,
ce_loss_indexes=ce_loss_indexes,
ce_loss_weights=ce_loss_weights,
# Generation modalities
vision=vision,
action=action,
sound=sound,
# Temporal causal
null_action_supertokens=self.null_action_supertokens,
num_action_tokens_per_supertoken=self.num_action_tokens_per_supertoken,
)
def to_cuda(self) -> None:
"""Move all tensor fields to CUDA in-place."""
if isinstance(self.text_ids, torch.Tensor):
self.text_ids = self.text_ids.cuda()
if isinstance(self.text_indexes, torch.Tensor):
self.text_indexes = self.text_indexes.cuda()
if isinstance(self.position_ids, torch.Tensor):
self.position_ids = self.position_ids.cuda()
if isinstance(self.label_ids, torch.Tensor):
self.label_ids = self.label_ids.cuda()
if isinstance(self.ce_loss_indexes, torch.Tensor):
self.ce_loss_indexes = self.ce_loss_indexes.cuda()
if isinstance(self.ce_loss_weights, torch.Tensor):
self.ce_loss_weights = self.ce_loss_weights.cuda()
if self.vision is not None:
self.vision.to_cuda()
if self.action is not None:
self.action.to_cuda()
if self.sound is not None:
self.sound.to_cuda()
@dataclass
class SequencePlan:
"""Plan describing which modalities are present in a sample.
This dataclass tracks the presence of different modalities (text, vision, action)
and their conditioning configurations for a dataset sample. Unlike SequencePlan
which holds the actual tensor data, this class provides a lightweight summary
of what modalities exist and how they should be conditioned.
Attributes:
has_text: Whether text/caption tokens are present for this sample.
Used for text-conditioned generation (e.g., text-to-image/video).
has_vision: Whether vision input (image or video latents) is present.
Defaults to False.
condition_frame_indexes_vision: Indexes of latent vision frames that are clean/conditioning.
[] means all frames are noised/supervised.
All frames specified means all frames are clean (no MSE supervision).
For multi-item samples (e.g. image editing where each sample has multiple
separately-encoded images), this applies to each vision item individually.
The number of items per sample is tracked by
``GenerationDataClean.num_vision_items_per_sample``.
has_action: Whether action input is present for robotics/embodied AI tasks.
Defaults to False.
condition_frame_indexes_action: Indexes of action steps that are clean/conditioning.
[] means all steps are noised/supervised.
All steps specified means all steps are clean (no MSE supervision).
"""
# -- understanding (text conditioning) --
has_text: bool
# -- vision modality --
has_vision: bool = False
condition_frame_indexes_vision: list[int] = field(default_factory=list)
# If True, all vision items in this sample share the same temporal mRoPE grid
# (controlnet-style transfer: target frame i is spatio-temporally aligned with
# control frame i). Each item gets the same temporal_offset; spatial reset
# behavior is unchanged. Requires num_vision_items_per_sample > 1, equal latent_t,
# and equal fps across items. Default False preserves single-clip and
# image-editing semantics where items represent distinct time states.
share_vision_temporal_positions: bool = False
# -- action modality --
has_action: bool = False
condition_frame_indexes_action: list[int] = field(default_factory=list)
action_start_frame_offset: int = 1
# -- sound modality --
has_sound: bool = False
condition_frame_indexes_sound: list[int] = field(default_factory=list)
def as_dict(self) -> dict:
return {
"has_text": self.has_text,
"has_vision": self.has_vision,
"has_action": self.has_action,
"has_sound": self.has_sound,
"condition_frame_indexes_vision": self.condition_frame_indexes_vision,
"condition_frame_indexes_action": self.condition_frame_indexes_action,
"condition_frame_indexes_sound": self.condition_frame_indexes_sound,
"share_vision_temporal_positions": self.share_vision_temporal_positions,
}
# ============================================================================
# Helper functions for packing sequences
# ============================================================================
def compute_text_split_length(
num_caption_tokens: int,
special_tokens: Dict[str, int],
has_generation: bool = True,
) -> int:
"""Compute the total text split length without mutating any state.
This is the number of token positions occupied by the text split in a
packed sequence: caption tokens + optional BOS + EOS + optional BOV.
Args:
num_caption_tokens: Number of raw caption token IDs (before special tokens).
special_tokens: Dictionary of special token IDs (checked for ``"bos_token_id"``).
has_generation: Whether a start-of-generation (BOV) token follows text.
Returns:
Total text split length (positions consumed in the packed sequence).
"""
n = num_caption_tokens
if "bos_token_id" in special_tokens:
n += 1
n += 1 # EOS
if has_generation:
n += 1 # start-of-generation / BOV
return n
def _pack_text_tokens(
packed_seq: PackedSequence,
text_ids: List[int],
special_tokens: Dict[str, int],
curr_rope_id: int,
has_generation: bool,
use_float_positions: bool = False,
) -> Tuple[int, int, int]:
"""Pack text tokens into the sequence.
Args:
packed_seq: PackedSequence instance to accumulate data into.
text_ids: List of text token IDs (integers).
special_tokens: Dictionary of special token IDs.
curr_rope_id: Current RoPE position ID.
has_generation: Whether there's media/action after text.
use_float_positions: If True, generate float position IDs for 3D mRoPE
(for consistency with FPS-modulated vision tokens).
Returns:
Tuple of (updated curr_rope_id, split_length, sample_length).
"""
# Ensure we're in build mode (fields are lists, not tensors)
assert isinstance(packed_seq.text_ids, list), "PackedSequence must be in build mode"
assert isinstance(packed_seq.text_indexes, list)
assert isinstance(packed_seq.position_ids, list)
assert isinstance(packed_seq.label_ids, list)
assert isinstance(packed_seq.ce_loss_indexes, list)
assert isinstance(packed_seq.ce_loss_weights, list)
curr = packed_seq.curr
# Prepend BOS token if available
if "bos_token_id" in special_tokens:
shifted_text_ids = [special_tokens["bos_token_id"]] + text_ids
else:
shifted_text_ids = text_ids
split_len = 0
# Add text tokens to sequence
packed_seq.text_ids.extend(shifted_text_ids)
packed_seq.text_indexes.extend(range(curr, curr + len(shifted_text_ids)))
# Configure loss computation for text tokens
packed_seq.ce_loss_indexes.extend(range(curr, curr + len(shifted_text_ids)))
packed_seq.ce_loss_weights.extend([1.0] * len(shifted_text_ids))
packed_seq.label_ids.extend(text_ids[1:] + [special_tokens["eos_token_id"]])
curr += len(shifted_text_ids)
split_len += len(shifted_text_ids)
# Add EOS token
packed_seq.text_ids.append(special_tokens["eos_token_id"])
packed_seq.text_indexes.append(curr)
curr += 1
split_len += 1
# Add start-of-generation token, but only if there's media/action present.
if has_generation:
packed_seq.text_ids.append(special_tokens["start_of_generation"])
packed_seq.text_indexes.append(curr)
curr += 1
split_len += 1
# Sanity check -- compute_text_split_length() is called elsewhere.
assert split_len == compute_text_split_length(len(text_ids), special_tokens, has_generation)
# Update position IDs and attention mode for text split
if packed_seq._use_mrope:
text_mrope_ids, packed_seq._mrope_temporal_offset = get_3d_mrope_ids_text_tokens(
num_tokens=split_len,
temporal_offset=packed_seq._mrope_temporal_offset,
use_float_positions=use_float_positions,
) # text_mrope_ids: [3,split_len]
packed_seq.position_ids.append(text_mrope_ids)
else:
packed_seq.position_ids.extend(range(curr_rope_id, curr_rope_id + split_len))
packed_seq.attn_modes.append("causal")
packed_seq.split_lens.append(split_len)
packed_seq.curr = curr
return curr_rope_id + split_len, split_len, split_len
def _pack_vision_tokens(
packed_seq: PackedSequence,
input_vision_tokens: torch.Tensor,
condition_frame_indexes_vision: list[int],
input_timestep: float | torch.Tensor,
curr_rope_id: int,
latent_patch_size: int = 1,
vision_fps: float | None = None,
enable_fps_modulation: bool = False,
base_fps: float = 24.0,
temporal_compression_factor: int = 4,
) -> int:
"""Pack vision tokens into the sequence.
Args:
packed_seq: PackedSequence instance to accumulate data into.
input_vision_tokens: Vision latent tokens (C, T, H, W).
condition_frame_indexes_vision: Indexes of conditioning frames.
input_timestep: Diffusion timestep. Either a float (teacher_forcing/none — all frames
share the same sigma) or a Tensor(T_max,) (diffusion_forcing — per-frame sigma;
indexed as input_timestep[frame_idx] for each noisy frame).
curr_rope_id: Current RoPE position ID.
latent_patch_size: Patch size for latent patchification.
vision_fps: Frames per second of the video. Used when enable_fps_modulation=True.
enable_fps_modulation: If True, scale temporal position IDs based on video FPS.
base_fps: Base FPS for normalization (default 24.0).
temporal_compression_factor: VAE temporal compression factor (default 4).
Returns:
Vision split length.
"""
# Ensure we're in build mode
assert isinstance(packed_seq.position_ids, list), "PackedSequence must be in build mode"
curr = packed_seq.curr
vision_split_len = 0
# Initialize vision modality if not present.
if packed_seq.vision is None:
packed_seq.vision = ModalityData()
# Ensure vision modality is in build mode
assert isinstance(packed_seq.vision.sequence_indexes, list)
assert isinstance(packed_seq.vision.mse_loss_indexes, list)
assert isinstance(packed_seq.vision.timesteps, list)
assert isinstance(packed_seq.vision.tokens, list)
# Compute position IDs for image patches
_, _, latent_t, latent_h, latent_w = input_vision_tokens.shape
if latent_patch_size < 1:
raise ValueError(f"latent_patch_size must be >= 1, got {latent_patch_size}")
# Use ceil to support latent dims not divisible by patch size (padding handled in network)
patch_h = math.ceil(latent_h / latent_patch_size)
patch_w = math.ceil(latent_w / latent_patch_size)
packed_seq.vision.token_shapes.append((latent_t, patch_h, patch_w))
packed_seq.vision.tokens.append(input_vision_tokens)
# Add image token indexes and loss information
num_vision_tokens = latent_t * patch_h * patch_w
packed_seq.vision.sequence_indexes.extend(range(curr, curr + num_vision_tokens))
# Supervise vision tokens based on conditioning frames
condition_set = {idx for idx in condition_frame_indexes_vision if 0 <= idx < latent_t}
assert isinstance(packed_seq.vision.condition_mask, list)
vision_condition_mask = torch.zeros(
(latent_t, 1, 1), device=input_vision_tokens.device, dtype=input_vision_tokens.dtype
) # [T,1,1]
for frame_idx in condition_set:
vision_condition_mask[frame_idx, 0, 0] = 1.0
packed_seq.vision.condition_mask.append(vision_condition_mask)
vision_noisy_frame_indexes = torch.tensor(
[idx for idx in range(latent_t) if idx not in condition_set],
device=input_vision_tokens.device,
dtype=torch.long,
) # [N_noisy_frames]
assert isinstance(packed_seq.vision.noisy_frame_indexes, list)
packed_seq.vision.noisy_frame_indexes.append(vision_noisy_frame_indexes)
frame_token_stride = patch_h * patch_w
for frame_idx in range(latent_t):
if frame_idx in condition_set:
continue
frame_start = curr + frame_idx * frame_token_stride
frame_end = frame_start + frame_token_stride
packed_seq.vision.mse_loss_indexes.extend(range(frame_start, frame_end))
if isinstance(input_timestep, torch.Tensor):
frame_ts = input_timestep[frame_idx].item()
else:
frame_ts = input_timestep
packed_seq.vision.timesteps.extend([frame_ts] * frame_token_stride)
curr += num_vision_tokens
vision_split_len += num_vision_tokens
# Update position IDs for image split
if packed_seq._use_mrope:
# Determine FPS for this vision segment (None disables FPS modulation)
effective_fps = vision_fps if enable_fps_modulation else None
vision_mrope_ids, packed_seq._mrope_temporal_offset = get_3d_mrope_ids_vae_tokens(
grid_t=latent_t,
grid_h=patch_h,
grid_w=patch_w,
temporal_offset=packed_seq._mrope_temporal_offset,
reset_spatial_indices=packed_seq._mrope_reset_spatial,
fps=effective_fps,
base_fps=base_fps,
temporal_compression_factor=temporal_compression_factor,
) # vision_mrope_ids: [3,N_vision_tokens]
packed_seq.position_ids.append(vision_mrope_ids)
else:
# All image tokens share the same RoPE position ID
packed_seq.position_ids.extend([curr_rope_id] * vision_split_len)
packed_seq.curr = curr
return vision_split_len
def _pack_action_tokens(
packed_seq: PackedSequence,
input_action_tokens: torch.Tensor,
condition_frame_indexes_action: list[int],
input_timestep: float,
curr_rope_id: int,
action_temporal_offset: int | float = 0,
enable_fps_modulation: bool = False,
base_fps: float = 24.0,
action_fps: float | None = None,
base_temporal_compression_factor: int | None = None,
action_start_frame_offset: int = 1,
) -> int:
"""Pack action tokens into the sequence.
Args:
packed_seq: PackedSequence instance to accumulate data into.
input_action_tokens: Action latent tokens (T, D).
condition_frame_indexes_action: Indexes of conditioning action steps.
input_timestep: Diffusion timestep.
curr_rope_id: Current RoPE position ID.
action_temporal_offset: Temporal offset for action mRoPE IDs (typically
the vision start offset so action aligns temporally with vision).
enable_fps_modulation: If True, scale temporal position IDs based on FPS.
base_fps: Base FPS for normalization (default 24.0).
action_fps: Frames per second of the action data. Used when enable_fps_modulation=True.
base_temporal_compression_factor: Base temporal compression factor for FPS scaling.
Should be set to the vision temporal compression factor (e.g. 4) so that action
tokens advance at frame rate (4x finer) relative to vision latent frames.
Only affects behavior when FPS modulation is enabled.
action_start_frame_offset: Frame offset for aligning action[0] with the
corresponding vision frame. Default 1 aligns action[0] with vision frame 1.
Returns:
Number of action tokens added.
"""
# Ensure we're in build mode
assert isinstance(packed_seq.position_ids, list), "PackedSequence must be in build mode"
curr = packed_seq.curr
action_split_len = input_action_tokens.shape[0]
# Initialize action modality if not present
if packed_seq.action is None:
packed_seq.action = ModalityData()
# Ensure action modality is in build mode
assert isinstance(packed_seq.action.sequence_indexes, list)
assert isinstance(packed_seq.action.mse_loss_indexes, list)
assert isinstance(packed_seq.action.timesteps, list)
assert isinstance(packed_seq.action.tokens, list)
# Add token indexes and loss information
action_indexes = list(range(curr, curr + action_split_len))
packed_seq.action.sequence_indexes.extend(action_indexes)
packed_seq.action.token_shapes.append((action_split_len,))
packed_seq.action.tokens.append(input_action_tokens)
condition_set = {idx for idx in condition_frame_indexes_action if 0 <= idx < action_split_len}
assert isinstance(packed_seq.action.condition_mask, list)
action_condition_mask = torch.zeros(
(action_split_len, 1), device=input_action_tokens.device, dtype=input_action_tokens.dtype
) # [T_action,1]
for frame_idx in condition_set:
action_condition_mask[frame_idx, 0] = 1.0
packed_seq.action.condition_mask.append(action_condition_mask)
action_noisy_frame_indexes = torch.tensor(
[idx for idx in range(action_split_len) if idx not in condition_set],
device=input_action_tokens.device,
dtype=torch.long,
) # [N_noisy_action_frames]
assert isinstance(packed_seq.action.noisy_frame_indexes, list)
packed_seq.action.noisy_frame_indexes.append(action_noisy_frame_indexes)
frame_token_stride = 1 # Action has 1 token per frame (no spatial dimension)
for frame_idx in range(action_split_len):
if frame_idx in condition_set:
continue
frame_start = curr + frame_idx * frame_token_stride
frame_end = frame_start + frame_token_stride
packed_seq.action.mse_loss_indexes.extend(range(frame_start, frame_end))
packed_seq.action.timesteps.extend([input_timestep] * frame_token_stride)
# Update RoPE position IDs for action tokens.
if packed_seq._use_mrope:
# 3D mRoPE: action tokens use a 1x1 spatial grid with start_frame_offset=1
# so action[0] (null token) aligns with vision frame 1, not frame 0.
effective_fps = action_fps if enable_fps_modulation else None
action_mrope_ids, _ = get_3d_mrope_ids_vae_tokens(
grid_t=action_split_len,
grid_h=1,
grid_w=1,
temporal_offset=action_temporal_offset,
reset_spatial_indices=packed_seq._mrope_reset_spatial,
fps=effective_fps,
base_fps=base_fps,
temporal_compression_factor=1, # Action is at frame rate (no temporal compression)
base_temporal_compression_factor=base_temporal_compression_factor,
start_frame_offset=action_start_frame_offset, # Align action[0] with vision frame action_start_frame_offset
) # action_mrope_ids: [3,N_action_tokens]
packed_seq.position_ids.append(action_mrope_ids)
# Note: we don't update _mrope_temporal_offset here because action tokens
# share the temporal space with vision tokens (they run in parallel).
else:
# All action tokens share the SAME RoPE position as vision tokens (see docs/sequence_packing.md).
packed_seq.position_ids.extend([curr_rope_id] * action_split_len)
packed_seq.curr = curr + action_split_len
return action_split_len
def _pack_sound_tokens(
packed_seq: PackedSequence,
input_sound_tokens: torch.Tensor,
condition_frame_indexes_sound: list[int],
input_timestep: float,
curr_rope_id: int,
sound_temporal_offset: int | float = 0,
enable_fps_modulation: bool = False,
base_fps: float = 24.0,
sound_fps: float | None = None,
) -> int:
"""Pack sound/audio tokens into the sequence.
Sound latents have shape [C, T] where C is channels and T is temporal frames.
Sound tokens are added to the unified generation split to maintain FactoredSequencePack's
2-split invariant (causal + full).
Args:
packed_seq: PackedSequence instance to accumulate data into.
input_sound_tokens: Sound latent tokens (C, T).
condition_frame_indexes_sound: Indexes of conditioning frames.
[] means all frames are noised/supervised.
All frames specified means all frames are clean (no MSE supervision).
input_timestep: Diffusion timestep.
curr_rope_id: Current RoPE position ID.
sound_temporal_offset: Temporal offset for m-RoPE position IDs (aligned with vision start).
enable_fps_modulation: If True, scale temporal positions by FPS ratio.
base_fps: Base FPS for normalization (default 24.0).
sound_fps: Sound latent FPS (e.g., 25.0). Used for FPS-aware m-RoPE positions.
Returns:
Number of sound tokens added.
"""
# Ensure we're in build mode
assert isinstance(packed_seq.position_ids, list), "PackedSequence must be in build mode"
curr = packed_seq.curr
# Sound latent shape: [C, T] → T tokens
_, sound_split_len = input_sound_tokens.shape
# Initialize sound modality if not present
if packed_seq.sound is None:
packed_seq.sound = ModalityData()
# Ensure sound modality is in build mode
assert isinstance(packed_seq.sound.sequence_indexes, list)
assert isinstance(packed_seq.sound.mse_loss_indexes, list)
assert isinstance(packed_seq.sound.timesteps, list)
assert isinstance(packed_seq.sound.tokens, list)
# Add token indexes - sound uses (T, 1, 1) shape for compatibility with 3D RoPE
packed_seq.sound.token_shapes.append((sound_split_len, 1, 1))
packed_seq.sound.sequence_indexes.extend(range(curr, curr + sound_split_len))
packed_seq.sound.tokens.append(input_sound_tokens)
# Supervise sound tokens based on conditioning frames
condition_set = {idx for idx in condition_frame_indexes_sound if 0 <= idx < sound_split_len}
assert isinstance(packed_seq.sound.condition_mask, list)
# Condition mask: shape (T, 1) — 1 = clean/conditioning, 0 = noised/supervised
sound_condition_mask = torch.zeros(
(sound_split_len, 1), device=input_sound_tokens.device, dtype=input_sound_tokens.dtype
) # [T_sound,1]
for frame_idx in condition_set:
sound_condition_mask[frame_idx, 0] = 1.0
packed_seq.sound.condition_mask.append(sound_condition_mask)
sound_noisy_frame_indexes = torch.tensor(
[idx for idx in range(sound_split_len) if idx not in condition_set],
device=input_sound_tokens.device,
dtype=torch.long,
) # [N_noisy_sound_frames]
assert isinstance(packed_seq.sound.noisy_frame_indexes, list)
packed_seq.sound.noisy_frame_indexes.append(sound_noisy_frame_indexes)
# Add to MSE loss indexes and timesteps for non-conditioning frames
for frame_idx in range(sound_split_len):
if frame_idx in condition_set:
continue
# Sound has 1 token per frame (no spatial dimension)
frame_start = curr + frame_idx
frame_end = frame_start + 1
packed_seq.sound.mse_loss_indexes.extend(range(frame_start, frame_end))
packed_seq.sound.timesteps.extend([input_timestep])
# Update RoPE position IDs for sound tokens.
if packed_seq._use_mrope:
# 3D mRoPE: sound tokens use a 1x1 spatial grid, aligned with vision temporal positions.
# sound[0] aligns with vision frame 0 (start_frame_offset=0, unlike action which offsets by 1).
effective_fps = sound_fps if enable_fps_modulation else None
sound_mrope_ids, _ = get_3d_mrope_ids_vae_tokens(
grid_t=sound_split_len,
grid_h=1,
grid_w=1,
temporal_offset=sound_temporal_offset,
reset_spatial_indices=packed_seq._mrope_reset_spatial,
fps=effective_fps,
base_fps=base_fps,
temporal_compression_factor=1, # Sound latent is already at sound_latent_fps (no further compression)
start_frame_offset=0, # Sound[0] aligns with vision frame 0
) # sound_mrope_ids: [3,N_sound_tokens]
packed_seq.position_ids.append(sound_mrope_ids)
# Note: we don't update _mrope_temporal_offset here because sound tokens
# share the temporal space with vision tokens (they run in parallel).
else:
# All sound tokens share the SAME RoPE position as vision/action tokens (unified generation split).
packed_seq.position_ids.extend([curr_rope_id] * sound_split_len)
packed_seq.curr = curr + sound_split_len
return sound_split_len
def _pack_supertokens_temporal_causal(
packed_seq: "PackedSequence",
input_vision_tokens: torch.Tensor,
input_action_tokens: torch.Tensor | None,
condition_frame_indexes_vision: list[int],
input_timestep: float | torch.Tensor,
curr_rope_id: int,
latent_patch_size: int,
temporal_compression_factor: int,
action_dim: int,
vision_fps: float | None = None,
action_fps: float | None = None,
enable_fps_modulation: bool = False,
base_fps: float = 24.0,
pack_action_tokens: bool = True,
) -> tuple[int, bool]:
"""Pack vision and (optionally) action tokens in supertoken order for temporal causal attention.
Buffer layout per frame:
pack_action_tokens=True: [action_t (tcf), vision_t (H*W)] — supertoken size tcf + H*W
pack_action_tokens=False: [vision_t (H*W)] — supertoken size H*W
Use ``pack_action_tokens=False`` when ``config.action_gen=False``; the resulting
``num_action_tokens_per_supertoken=0`` is stamped on the pack and read by the
attention builder so NATTEN metadata stays in sync automatically.
mRoPE layout (with actions, unified_3d_mrope only):
- Null actions (frame 0): all tcf tokens at ``temporal_offset``.
- Real training actions (frames 1..T-1): ``start_frame_offset=1`` so the
last action in group i co-locates with vision frame i.
- AR real actions (single supertoken): ``start_frame_offset=0``.
- Interleaved per frame as cat([action_ids, vision_ids]).
``input_timestep`` is float (TF/none) or Tensor(T_max,) (DF, per-frame sigma).
Conditioning frames are excluded from mse_loss_indexes either way.
Returns (total_split_len, null_action_flag); null_action_flag is False when
pack_action_tokens=False.
"""
assert isinstance(packed_seq.position_ids, list), "PackedSequence must be in build mode"
_, _, latent_t, latent_h, latent_w = input_vision_tokens.shape
patch_h = math.ceil(latent_h / latent_patch_size)
patch_w = math.ceil(latent_w / latent_patch_size)
tcf = temporal_compression_factor
patches_per_frame = patch_h * patch_w
supertoken_len = tcf + patches_per_frame if pack_action_tokens else patches_per_frame # S
# Initialize modalities if needed
if packed_seq.vision is None:
packed_seq.vision = ModalityData()
if pack_action_tokens and packed_seq.action is None:
packed_seq.action = ModalityData()
assert isinstance(packed_seq.vision.sequence_indexes, list)
assert isinstance(packed_seq.vision.mse_loss_indexes, list)
assert isinstance(packed_seq.vision.timesteps, list)
assert isinstance(packed_seq.vision.tokens, list)
assert isinstance(packed_seq.vision.condition_mask, list)
if pack_action_tokens:
assert isinstance(packed_seq.action.sequence_indexes, list)
assert isinstance(packed_seq.action.mse_loss_indexes, list)
assert isinstance(packed_seq.action.timesteps, list)
assert isinstance(packed_seq.action.tokens, list)
assert isinstance(packed_seq.action.condition_mask, list)
device = input_vision_tokens.device
dtype = input_vision_tokens.dtype
null_action_flag: bool
if pack_action_tokens:
# Build all_action_tokens: shape (latent_t * tcf, action_dim)
#
# Cases:
# 1. Training with conditioning frame (latent_t > 1, real_actions < latent_t*tcf):
# Prepend tcf null tokens for frame 0, then real actions for frames 1..T-1.
# 2. KV-cache continuation (latent_t > 1, real_actions == latent_t*tcf): all supertokens
# carry real actions (no conditioning frame in-segment).
# 3. AR frame N>0 (latent_t == 1, action provided): real actions, no null prefix.
# 4. AR frame 0 / image2video (action is None): all null tokens.
if input_action_tokens is not None:
# input_action_tokens shape: (1, T*tcf, D) or (T*tcf, D) for training; (tcf, D) for AR frame N>0
if input_action_tokens.dim() == 3:
real_actions = input_action_tokens.squeeze(0) # [T*tcf,action_dim] or [N,action_dim]
else:
real_actions = input_action_tokens # [N,action_dim]
null_tokens = torch.zeros(tcf, action_dim, device=device, dtype=real_actions.dtype) # [tcf,action_dim]
if latent_t == 1:
# AR frame N>0: single supertoken with real actions, no null prefix
all_action_tokens = real_actions # [tcf,action_dim]
null_action_flag = False
elif real_actions.shape[0] == latent_t * tcf:
# All frames have real actions (e.g. KV-cache continuation segments)
all_action_tokens = real_actions
null_action_flag = False
else:
# Conditioning frame present: null for supertoken 0, real for 1..T-1
all_action_tokens = torch.cat([null_tokens, real_actions], dim=0) # [T*tcf,action_dim]
null_action_flag = True
else:
# AR frame 0 or image2video: all action tokens are null
all_action_tokens = torch.zeros(
latent_t * tcf, action_dim, device=device, dtype=dtype
) # [T*tcf,action_dim]
null_action_flag = True
else:
# pack_action_tokens=False: action tokens must not be supplied.
assert input_action_tokens is None, (
"pack_action_tokens=False requires input_action_tokens=None; got a non-None tensor."
)
null_action_flag = False
# Record vision token shapes and tokens
packed_seq.vision.token_shapes.append((latent_t, patch_h, patch_w))
packed_seq.vision.tokens.append(input_vision_tokens)
# Vision conditioning mask: (T, 1, 1)
condition_set_vision = {idx for idx in condition_frame_indexes_vision if 0 <= idx < latent_t}
vision_condition_mask = torch.zeros((latent_t, 1, 1), device=device, dtype=dtype) # [T,1,1]
for fidx in condition_set_vision:
vision_condition_mask[fidx, 0, 0] = 1.0
packed_seq.vision.condition_mask.append(vision_condition_mask)
vision_noisy_frame_indexes = torch.tensor(
[idx for idx in range(latent_t) if idx not in condition_set_vision],
device=device,
dtype=torch.long,
) # [N_noisy_frames]
packed_seq.vision.noisy_frame_indexes.append(vision_noisy_frame_indexes)
if pack_action_tokens:
# Action token shapes: latent_t * tcf total (including null tokens)
packed_seq.action.token_shapes.append((latent_t * tcf,))
packed_seq.action.tokens.append(all_action_tokens)
# Action conditioning mask: all action tokens are conditioning (not supervised)
# Null tokens are always conditioning; real actions are conditioning too (they are inputs)
action_condition_mask = torch.ones((latent_t * tcf, 1), device=device, dtype=dtype) # [T*tcf,1]
packed_seq.action.condition_mask.append(action_condition_mask)
# Pack in interleaved supertoken order: [action_t, vision_t] for each frame t
# (or just [vision_t] per frame when pack_action_tokens=False)
curr = packed_seq.curr
total_split_len = 0
# mRoPE: snapshot offset before this sample, compute IDs
if packed_seq._use_mrope:
temporal_offset = packed_seq._mrope_temporal_offset
effective_vision_fps = vision_fps if enable_fps_modulation else None
# AR frame N>=1 with action_gen=True (latent_t==1 and real actions supplied):
# shift both vision and action by start_frame_offset=1 so the last action in
# the group co-locates with vision frame N, mirroring training's layout.
# All other cases (training latent_t>1, AR action_gen=False, AR frame 0 null)
# keep start_frame_offset=0. The caller in pack_input_sequence_autoregressive
# seeds temporal_offset accordingly (N-1 frames back when this shift applies).
ar_with_real_actions = latent_t == 1 and pack_action_tokens and input_action_tokens is not None
vision_sfo = 1 if ar_with_real_actions else 0
vision_ids_flat, new_offset = get_3d_mrope_ids_vae_tokens(
grid_t=latent_t,
grid_h=patch_h,
grid_w=patch_w,
temporal_offset=temporal_offset,
reset_spatial_indices=packed_seq._mrope_reset_spatial,
fps=effective_vision_fps,
base_fps=base_fps,
temporal_compression_factor=tcf,
start_frame_offset=vision_sfo,
) # vision_ids_flat: [3,T*patch_h*patch_w]
if pack_action_tokens:
effective_action_fps = action_fps if enable_fps_modulation else None
# Action IDs: null for frame 0 (all tcf tokens share temporal_offset,
# co-located with vision frame 0), real for frames 1..T-1.
# Real tokens (training and AR) use start_frame_offset=1 so the last
# action in a group co-locates with vision frame i.
fps_active = effective_action_fps is not None
t_dtype = torch.float32 if fps_active else torch.long
t_offset = float(temporal_offset) if fps_active else int(temporal_offset)
null_t = torch.full((tcf,), t_offset, dtype=t_dtype) # [tcf]
null_hw = torch.zeros(tcf, dtype=t_dtype) # [tcf]
null_ids = torch.stack([null_t, null_hw, null_hw]) # [3,tcf]
def _real_action_ids(n_frames: int, start_frame_offset: int) -> torch.Tensor:
flat, _ = get_3d_mrope_ids_vae_tokens(
grid_t=n_frames * tcf,
grid_h=1,
grid_w=1,
temporal_offset=temporal_offset,
reset_spatial_indices=packed_seq._mrope_reset_spatial,
fps=effective_action_fps,
base_fps=base_fps,
temporal_compression_factor=1,
base_temporal_compression_factor=tcf,
start_frame_offset=start_frame_offset,
)
return flat.reshape(3, n_frames, tcf) # [3,n_frames,tcf]
if latent_t > 1 and input_action_tokens is not None:
if real_actions.shape[0] == latent_t * tcf:
# KV continuation: real action in every supertoken (including frame 0)
action_ids_3d = _real_action_ids(latent_t, start_frame_offset=0)
else:
# Training with conditioning frame: supertoken 0 = null, 1..T-1 = real
null_ids_3d = null_ids.reshape(3, 1, tcf) # [3,1,tcf]
real_ids_3d = _real_action_ids(latent_t - 1, start_frame_offset=1) # [3,T-1,tcf]
action_ids_3d = torch.cat([null_ids_3d, real_ids_3d], dim=1) # [3,T,tcf]
elif latent_t > 1:
# No action tensor (all-null layout): same ID structure as training w/ conditioning frame.
null_ids_3d = null_ids.reshape(3, 1, tcf) # [3,1,tcf]
real_ids_3d = _real_action_ids(latent_t - 1, start_frame_offset=1) # [3,T-1,tcf]
action_ids_3d = torch.cat([null_ids_3d, real_ids_3d], dim=1) # [3,T,tcf]
elif input_action_tokens is None:
# AR frame 0 / image2video: only null
action_ids_3d = null_ids.reshape(3, 1, tcf) # [3,1,tcf]
else:
# AR frame N>=1: single supertoken with real actions. start_frame_offset=1
# matches training (last action co-locates with vision frame N); caller
# seeds temporal_offset to (N-1) frame-strides back to compensate.
action_ids_3d = _real_action_ids(1, start_frame_offset=1) # [3,1,tcf]
# (3, T*H*W) → (3, T, H*W)
vision_ids_3d = vision_ids_flat.reshape(3, latent_t, patches_per_frame) # [3,T,patch_h*patch_w]
# Interleave per frame: (3, T, tcf+H*W) → (3, T*S)
interleaved_ids = torch.cat([action_ids_3d, vision_ids_3d], dim=2).reshape(
3, latent_t * supertoken_len
) # [3,T*S]
packed_seq.position_ids.append(interleaved_ids)
else:
# No action tokens: just vision IDs, already in (3, T*H*W) order.
packed_seq.position_ids.append(vision_ids_flat)
packed_seq._mrope_temporal_offset = new_offset
for frame_t in range(latent_t):
if pack_action_tokens:
# Pack action tokens for this frame (indexes only; tokens already stored in packed_seq.action.tokens)
action_indexes = list(range(curr, curr + tcf))
packed_seq.action.sequence_indexes.extend(action_indexes)
# Action tokens are never in MSE loss (always conditioning)
curr += tcf
total_split_len += tcf
if not packed_seq._use_mrope:
packed_seq.position_ids.extend([curr_rope_id] * tcf)
# Pack vision tokens for this frame
frame_indexes = list(range(curr, curr + patches_per_frame))
packed_seq.vision.sequence_indexes.extend(frame_indexes)
curr += patches_per_frame
total_split_len += patches_per_frame
if not packed_seq._use_mrope:
packed_seq.position_ids.extend([curr_rope_id] * patches_per_frame)
# Vision MSE loss: supervise non-conditioning frames
if frame_t not in condition_set_vision:
packed_seq.vision.mse_loss_indexes.extend(frame_indexes)
frame_ts = input_timestep[frame_t].item() if isinstance(input_timestep, torch.Tensor) else input_timestep
packed_seq.vision.timesteps.extend([frame_ts] * patches_per_frame)
packed_seq.curr = curr
return total_split_len, null_action_flag
# ============================================================================
# Main packing function
# ============================================================================
def pack_input_sequence(
sequence_plans: list[SequencePlan],
input_text_indexes: list[list[int]],
gen_data_clean: GenerationDataClean,
input_timesteps: torch.Tensor,
special_tokens: dict[str, int],
max_num_tokens: int | None = None,
latent_patch_size: int = 1,
skip_text_tokens: bool = False,
include_end_of_generation_token: bool = False,
position_embedding_type: str = "3d_rope",
unified_3d_mrope_reset_spatial_ids: bool = True,
unified_3d_mrope_temporal_modality_margin: int = 0,
enable_fps_modulation: bool = False,
base_fps: float = 24.0,
temporal_compression_factor: int = 4,
video_temporal_causal: bool = False,
action_dim: int = 32,
initial_mrope_temporal_offset: int | float = 0,
) -> PackedSequence:
"""
Pack a sequence of input strings and VAE latents into a packed tensor format.
Uses SequencePlan to determine which modalities are present for each sample,
and maintains separate indices for text, vision, action, and sound to handle variable modality presence.
Args:
sequence_plans: List of SequencePlan items describing which modalities are present.
input_text_indexes: List of text token ID sequences (only for samples where has_text=True).
gen_data_clean: GenerationDataClean containing vision, action, and sound tensors.
- x0_tokens_vision: Vision tensors for samples where has_vision=True
- x0_tokens_action: Action tensors for samples where has_action=True
- x0_tokens_sound: Sound tensors (list of [C, T]) for samples where has_sound=True
input_timesteps: Diffusion timesteps for each sample. Shape (B,) or (B, 1) for
teacher_forcing/none (all frames share the same sigma), or (B, T_max) for
diffusion_forcing (per-frame independent sigma). Entries are extracted per
sample as a float (numel==1) or Tensor(T_max,) for per-frame indexing.
special_tokens: Dictionary containing special token IDs (eos_token_id, start_of_generation, end_of_generation)
max_num_tokens: Maximum number of tokens in the packed sequence
latent_patch_size: Patch size used by the network to pack latents
skip_text_tokens: If True, skip packing text tokens
include_end_of_generation_token: If True, append end-of-generation token
position_embedding_type: Position embedding type for vision tokens:
- "3d_rope": Additive 3D RoPE embeddings + 1D position IDs for attention
- "flattened_sin_cos": Additive flattened sin/cos embeddings + 1D position IDs
- "unified_3d_mrope": No additive embedding + 3D position IDs for Qwen3VL-style mRoPE
unified_3d_mrope_reset_spatial_ids: If True (default), spatial (H, W) indices
start from 0 for each vision segment. If False, spatial indices are offset
by the temporal offset (Qwen2VL-style). Only used when position_embedding_type="unified_3d_mrope".
enable_fps_modulation: If True, scale temporal position IDs based on video FPS
to reflect real time. Requires fps_vision in gen_data_clean.
Uses the same flag as diffusion_expert_config.enable_fps_modulation.
base_fps: Base FPS for normalization (default 24.0).
Uses the same value as diffusion_expert_config.base_fps.
temporal_compression_factor: VAE temporal compression factor (default 4).
Obtained from the VAE tokenizer at runtime.
Returns:
PackedSequence containing all packed tensors and metadata. See PackedSequence for field details.
"""
del max_num_tokens
assert special_tokens is not None, "Special tokens must be provided"
assert isinstance(input_timesteps, torch.Tensor), "input_timesteps must be a tensor"
if input_timesteps.is_cuda:
raise ValueError("input_timesteps must be on CPU, not CUDA")
if isinstance(input_text_indexes, torch.Tensor):
raise ValueError("input_text_tokens must be a list, not a tensor")
# Initialize packed sequence (acts as builder during packing)
packed_seq = PackedSequence()
# Configure 3D mRoPE on the builder (enabled when position_embedding_type is unified_3d_mrope)
packed_seq._use_mrope = position_embedding_type == "unified_3d_mrope"
packed_seq._mrope_reset_spatial = unified_3d_mrope_reset_spatial_ids
# Maintain separate indices for each modality
idx_text = 0
idx_vision = 0
idx_action = 0
idx_sound = 0
null_action_flags: list[bool] = [] # collected from TC path; asserted consistent after the loop
# Validate: all samples must have text (causal split is always required for two-way attention).
# CFG dropout only drops text *content*, not the structural text split.
if not skip_text_tokens:
for plan in sequence_plans:
assert plan.has_text, "All sequence plans must have has_text=True when skip_text_tokens=False"
# Pack each sample based on its sequence plan
for sample_idx, sequence_plan in enumerate(sequence_plans):
curr_rope_id = 0
sample_len = 0
# mRoPE temporal offset resets per sample.
# initial_mrope_temporal_offset is non-zero only for AR inference (frame N seeds at N*tcf).
packed_seq._mrope_temporal_offset = initial_mrope_temporal_offset
_ts = input_timesteps[sample_idx]
input_timestep = _ts.item() if _ts.numel() == 1 else _ts # float (TF) or Tensor(T_max,) (DF)
# Pack text tokens if has_text=True and not skipped
if sequence_plan.has_text and not skip_text_tokens:
text_ids = input_text_indexes[idx_text]
idx_text += 1
has_generation_for_sample = sequence_plan.has_vision or sequence_plan.has_action or sequence_plan.has_sound
curr_rope_id, _, text_sample_len = _pack_text_tokens(
packed_seq,
text_ids,
special_tokens,
curr_rope_id,
has_generation=has_generation_for_sample,
use_float_positions=enable_fps_modulation,
)
sample_len += text_sample_len
# End of text modality, add an offset as the boundary between text and vision.
packed_seq._mrope_temporal_offset += unified_3d_mrope_temporal_modality_margin
# Save temporal offset before vision for action tokens (action uses same offset as vision start)
vision_start_temporal_offset = packed_seq._mrope_temporal_offset
# Pack vision (and optionally action) tokens
if video_temporal_causal and sequence_plan.has_vision:
# Temporal causal path: when sequence_plan.has_action=True, interleaved supertokens
# [action_t, vision_t]; when False, supertokens are just vision patches.
assert position_embedding_type == "unified_3d_mrope", (
"video_temporal_causal=True requires position_embedding_type='unified_3d_mrope'"
)
input_vision_tokens = gen_data_clean.x0_tokens_vision[idx_vision]
idx_vision += 1
vision_fps = None
if (
enable_fps_modulation
and gen_data_clean.fps_vision is not None
and idx_vision - 1 < len(gen_data_clean.fps_vision)
):
vision_fps = float(gen_data_clean.fps_vision[idx_vision - 1].item())
input_action_tokens_tc: torch.Tensor | None = None
action_fps_tc: float | None = None
if sequence_plan.has_action:
input_action_tokens_tc = gen_data_clean.x0_tokens_action[idx_action]
if (
enable_fps_modulation
and gen_data_clean.fps_action is not None
and idx_action < len(gen_data_clean.fps_action)
):
action_fps_tc = float(gen_data_clean.fps_action[idx_action].item())
idx_action += 1
supertoken_split_len, null_flag = _pack_supertokens_temporal_causal(
packed_seq=packed_seq,
input_vision_tokens=input_vision_tokens,
input_action_tokens=input_action_tokens_tc,
condition_frame_indexes_vision=sequence_plan.condition_frame_indexes_vision,
input_timestep=input_timestep,
curr_rope_id=curr_rope_id,
latent_patch_size=latent_patch_size,
temporal_compression_factor=temporal_compression_factor,
action_dim=action_dim,
vision_fps=vision_fps,
action_fps=action_fps_tc,
enable_fps_modulation=enable_fps_modulation,
base_fps=base_fps,
pack_action_tokens=sequence_plan.has_action,
)
null_action_flags.append(null_flag)
# We assume all samples in a batch share the same has_action layout, so
# stamp the supertoken layout constant directly here. This is the
# single source of truth read by downstream attention / KV-cache
# code (no recomputation in the network).
packed_seq.num_action_tokens_per_supertoken = temporal_compression_factor if sequence_plan.has_action else 0
sample_len += supertoken_split_len
vision_split_len = supertoken_split_len
action_split_len = 0 # Already absorbed into supertoken_split_len
else:
# Standard path: vision and action packed separately
if sequence_plan.has_vision:
# Determine how many vision items this sample owns.
# For multi-item samples (e.g. image editing), num_vision_items_per_sample
# records [2, 2, ...]; for standard T2I/T2V it is None (1 item per sample).
num_vis = (
gen_data_clean.num_vision_items_per_sample[sample_idx]
if gen_data_clean.num_vision_items_per_sample is not None
else 1
)
vision_split_len = 0
# Controlnet-style transfer: when set, all vision items share the same
# temporal mRoPE grid. We snapshot the offset before the loop and
# rewind to it before each item, so every item produces identical
# temporal IDs. Each _pack_vision_tokens call still advances the
# offset by latent_t internally; in shared-grid mode the post-loop
# offset equals snapshot + latent_t (single-clip semantics for
# downstream EOV / next-modality tokens).
shared_grid = sequence_plan.share_vision_temporal_positions and num_vis > 1
items_temporal_offset_snapshot = packed_seq._mrope_temporal_offset
shared_latent_t: int | None = None
shared_patch_h: int | None = None
shared_patch_w: int | None = None
# FPS is recorded per-sample (shape [B]); for multi-item samples
# (transfer / image-edit) every vision item in this sample shares
# the same conditioning FPS, so we read by sample_idx, not by the
# flat idx_vision counter (which would alias to a neighbor sample's
# fps and corrupt RoPE FPS modulation).
sample_vision_fps: float | None = None
if (
enable_fps_modulation
and gen_data_clean.fps_vision is not None
and sample_idx < len(gen_data_clean.fps_vision)
):
sample_vision_fps = float(gen_data_clean.fps_vision[sample_idx].item())
for item_idx in range(num_vis):
input_vision_tokens = gen_data_clean.x0_tokens_vision[idx_vision]
vision_fps = sample_vision_fps
idx_vision += 1
# Determine conditioning for this vision item.
# For multi-item mode: all items except the last are fully conditioned
# (all frames are clean); the last item uses the SequencePlan's
# condition_frame_indexes_vision (typically [] = fully generated).
if num_vis > 1 and item_idx < num_vis - 1:
# Conditioning item (e.g. source image): mark all frames as clean
latent_t = input_vision_tokens.shape[2]
item_condition_frames = list(range(latent_t))
else:
# Generation item (single-item mode or last item in multi-item)
item_condition_frames = sequence_plan.condition_frame_indexes_vision
if shared_grid:
item_latent_t = input_vision_tokens.shape[2]
item_latent_h = input_vision_tokens.shape[3]
item_latent_w = input_vision_tokens.shape[4]
if shared_latent_t is None:
shared_latent_t = item_latent_t
shared_patch_h = item_latent_h
shared_patch_w = item_latent_w
else:
assert item_latent_t == shared_latent_t, (
f"share_vision_temporal_positions requires equal latent_t across items, "
f"got item {item_idx} latent_t={item_latent_t} vs first={shared_latent_t}"
)
assert item_latent_h == shared_patch_h and item_latent_w == shared_patch_w, (
f"share_vision_temporal_positions requires equal spatial grid across items, "
f"got item {item_idx} (H,W)=({item_latent_h},{item_latent_w}) "
f"vs first=({shared_patch_h},{shared_patch_w})"
)
# Rewind so this item starts at the same temporal offset as item 0.
packed_seq._mrope_temporal_offset = items_temporal_offset_snapshot
item_split_len = _pack_vision_tokens(
packed_seq=packed_seq,
input_vision_tokens=input_vision_tokens,
condition_frame_indexes_vision=item_condition_frames,
input_timestep=input_timestep,
curr_rope_id=curr_rope_id,
latent_patch_size=latent_patch_size,
vision_fps=vision_fps,
enable_fps_modulation=enable_fps_modulation,
base_fps=base_fps,
temporal_compression_factor=temporal_compression_factor,
)
vision_split_len += item_split_len
sample_len += vision_split_len
else:
vision_split_len = 0
# Pack action tokens if has_action=True
if sequence_plan.has_action:
input_action_tokens = gen_data_clean.x0_tokens_action[idx_action]
# Get FPS for action (action may have its own FPS independent of vision)
action_fps: float | None = None
if (
enable_fps_modulation
and gen_data_clean.fps_action is not None
and idx_action < len(gen_data_clean.fps_action)
):
action_fps = float(gen_data_clean.fps_action[idx_action].item())
idx_action += 1
action_split_len = _pack_action_tokens(
packed_seq=packed_seq,
input_action_tokens=input_action_tokens,
condition_frame_indexes_action=sequence_plan.condition_frame_indexes_action,
input_timestep=input_timestep,
curr_rope_id=curr_rope_id,
action_temporal_offset=vision_start_temporal_offset,
enable_fps_modulation=enable_fps_modulation,
base_fps=base_fps,
action_fps=action_fps,
base_temporal_compression_factor=temporal_compression_factor,
action_start_frame_offset=sequence_plan.action_start_frame_offset,
)
sample_len += action_split_len
else:
action_split_len = 0
# Pack sound tokens if has_sound=True
if sequence_plan.has_sound:
input_sound_tokens = gen_data_clean.x0_tokens_sound[idx_sound]
# Get FPS for sound (from gen_data_clean, like vision and action)
sound_fps: float | None = None
if (
enable_fps_modulation
and gen_data_clean.fps_sound is not None
and idx_sound < len(gen_data_clean.fps_sound)
):
sound_fps = float(gen_data_clean.fps_sound[idx_sound].item())
idx_sound += 1
sound_split_len = _pack_sound_tokens(
packed_seq=packed_seq,
input_sound_tokens=input_sound_tokens,
condition_frame_indexes_sound=sequence_plan.condition_frame_indexes_sound,
input_timestep=input_timestep,
curr_rope_id=curr_rope_id,
sound_temporal_offset=vision_start_temporal_offset,
enable_fps_modulation=enable_fps_modulation,
base_fps=base_fps,
sound_fps=sound_fps,
)
sample_len += sound_split_len
else:
sound_split_len = 0
# Add end-of-generation token if needed
eov_len = 0
has_any_generation = sequence_plan.has_vision or sequence_plan.has_action or sequence_plan.has_sound
if include_end_of_generation_token and has_any_generation:
# Type narrowing: we're in build mode, fields are lists
assert isinstance(packed_seq.text_ids, list)
assert isinstance(packed_seq.text_indexes, list)
assert isinstance(packed_seq.position_ids, list)
packed_seq.text_ids.append(special_tokens["end_of_generation"])
packed_seq.text_indexes.append(packed_seq.curr)
# EOV position IDs: 3D mRoPE or 1D RoPE
if packed_seq._use_mrope:
# Use float dtype when FPS modulation is enabled for consistency
eov_dtype = torch.float32 if enable_fps_modulation else torch.long
eov_mrope_ids = torch.full((3, 1), packed_seq._mrope_temporal_offset, dtype=eov_dtype) # [3,1]
packed_seq.position_ids.append(eov_mrope_ids) # type: ignore[arg-type]
packed_seq._mrope_temporal_offset += 1
else:
packed_seq.position_ids.append(curr_rope_id) # type: ignore[arg-type]
packed_seq.curr += 1
eov_len = 1
sample_len += 1
combined_split_len = vision_split_len + action_split_len + sound_split_len + eov_len
packed_seq.attn_modes.append("full")
packed_seq.split_lens.append(combined_split_len)
packed_seq.sample_lens.append(sample_len)
# Assert consistent null_action_supertokens across all TC samples, then set once
if null_action_flags:
assert len(set(null_action_flags)) == 1, (
f"Inconsistent null_action_supertokens across samples: {null_action_flags}. "
"All samples in a batch must have the same structure (all training or all AR inference)."
)
packed_seq.null_action_supertokens = null_action_flags[0]
# Finalize and return packed data
return packed_seq.finalize(
gen_data_clean=gen_data_clean,
)
# ============================================================================
# SequencePack:Operations on packed sequences
# ============================================================================
"""
SequencePack is a dictionary-based container for packed sequences.
We provide two implementations:
JointSequencePack: Stores all sub-sequences for all-sequences in a single tensor.
It is more flexible but is less performant. In this implementation, understanding tokens
can be placed in either causal or full-attention sub-sequences.
FactoredSequencePack:
Stores causal/undersanding and full/generation sub-sequences as separate tensors.
It is less flexible but is more performant. In this implementation, understanding tokens
must be on the causal sub-sequence, and generation tokens must be in the full-attention sub-sequence.
NOTES:
- We are aiming to deprecate and remove JointSequencePack; keeping it available for backwards compatibility at the moment.
- The reason we're implementing them via dict instead of python classes is to make torch.compile + activation checkpointing to work.
is_sharded (bool):
This flag indicates whether the sequence pack contains global data or a local shard for Context Parallelism (CP).
- When True, tensors represent only the local slice (Global_Length / CP_World_Size).
- Padding and reconstruction logic is skipped in `from_joint`.
- Operations requiring global context (e.g., `get_all_seq`, position ID reconstruction) are not allowed when is_sharded is True.
"""
# "Fake" types for readability; everything is plain dict at runtime.
FactoredSequencePack = dict[str, Any]
JointSequencePack = dict[str, Any]
SequencePack = FactoredSequencePack | JointSequencePack
# ------------------------------------
# SequencePack: internal helpers
# ------------------------------------
def _find_non_causal_text_token_idx(
attn_modes: List[str], split_lens: List[int], und_token_indexes: List[int]
) -> List[int]:
"""
Find the indexes of the "und" tokens that are under the "full" mode.
This are indices into the full_only_seq.
"""
# Return indexes *into* full_only_seq, not into the original packed sequence.
# The order within full_only_seq is the concatenation of each "full" split in order.
out = []
full_offset = 0
packed_idx = 0
und_token_set = set(und_token_indexes)
for attn_mode, split_len in zip(attn_modes, split_lens):
if attn_mode == "full":
split_indices = range(packed_idx, packed_idx + split_len)
# For this "full" split, find the und tokens within this split, mapped local to full_only_seq offset
for local_idx, split_idx in enumerate(split_indices):
if split_idx in und_token_set:
out.append(full_offset + local_idx)
full_offset += split_len
packed_idx += split_len
return out
def _compute_mode_indices_and_offsets(
split_lens: torch.Tensor | List[int], attn_modes: List[str], mode: str, device: torch.device
) -> tuple[torch.Tensor, torch.Tensor]:
"""
Compute indices from a joint tensor that are in the given mode.
"""
indices = []
offsets = [0]
next_offset = 0
start = 0
if isinstance(split_lens, torch.Tensor):
split_lens = split_lens.tolist()
for i, (split_len, attn_mode) in enumerate(zip(split_lens, attn_modes)):
if attn_mode == mode:
indices.extend(range(start, start + split_len))
next_offset += split_len
offsets.append(next_offset)
start += split_len
return torch.tensor(indices, dtype=torch.int32, device=device), torch.tensor( # [N_mode_tokens], [N_mode_splits+1]
offsets, dtype=torch.int32, device=device
)
# Pad causal_seq and full_only_seq to have length 2048 if not already at that size
def _pad_to_N(N, x: torch.Tensor) -> torch.Tensor:
assert x.shape[0] <= N
padded = x.new_zeros((N, *x.shape[1:]))
padded[: x.shape[0]] = x
return padded
def _round_up_to_N(n: int, cp_world_size: int = 1, pad_for_cuda_graphs: bool = False) -> int:
if pad_for_cuda_graphs:
# Reduce recompilations / CUDA graph re-captures by bucketing lengths.
# <= 2K: 128, <= 4K: 256, <= 8K: 512, <= 16K: 1024, > 16K: 2048
if n <= 2048:
alignment = 128
elif n <= 4096:
alignment = 256
elif n <= 8192:
alignment = 512
elif n <= 16384:
alignment = 1024
else:
alignment = 2048
n = ((n + alignment - 1) // alignment) * alignment
# ensure it's divisible by cp_world_size
if cp_world_size > 1:
remainder = n % cp_world_size
if remainder != 0:
n += cp_world_size - remainder
return n
def _pad(
causal_seq: torch.Tensor, full_only_seq: torch.Tensor, max_causal_len: int, max_full_len: int
) -> tuple[torch.Tensor, torch.Tensor]:
causal_seq = _pad_to_N(max_causal_len, causal_seq)
full_only_seq = _pad_to_N(max_full_len, full_only_seq)
return causal_seq, full_only_seq
def _ensure_core_metadata(pack: SequencePack) -> None:
required = [
"sample_offsets",
"max_sample_len",
"max_causal_len",
"max_full_len",
"_causal_indices",
"_full_indices",
"_causal_seq_offsets",
"_full_only_seq_offsets",
"is_sharded",
]
for key in required:
if key not in pack:
raise KeyError(f"Missing required pack field: {key}")
def _init_sequence_pack(
sample_lens: List[int],
split_lens: List[int],
attn_modes: List[str],
device: torch.device,
) -> dict[str, Any]:
_max_sample_len = max(sample_lens)
_max_causal_len = max((split_lens[i] for i in range(len(split_lens)) if attn_modes[i] == "causal"), default=0)
_max_full_len = max((split_lens[i] for i in range(len(split_lens)) if attn_modes[i] == "full"), default=0)
sample_lens_cu = torch.tensor([0] + sample_lens, device=device, dtype=torch.int32) # [N_samples+1]
_sample_offsets = torch.cumsum(sample_lens_cu, dim=0, dtype=torch.int32) # [N_samples+1]
_causal_indices, _causal_seq_offsets = _compute_mode_indices_and_offsets(split_lens, attn_modes, "causal", device)
_full_indices, _full_only_seq_offsets = _compute_mode_indices_and_offsets(split_lens, attn_modes, "full", device)
return dict(
sample_offsets=_sample_offsets,
max_sample_len=_max_sample_len,
max_causal_len=_max_causal_len,
max_full_len=_max_full_len,
_causal_indices=_causal_indices,
_full_indices=_full_indices,
_causal_seq_offsets=_causal_seq_offsets,
_full_only_seq_offsets=_full_only_seq_offsets,
_num_causal_tokens=len(_causal_indices),
_num_full_tokens=len(_full_indices),
split_lens=split_lens,
attn_modes=attn_modes,
)
# ------------------------------------
# SequencePack constructors
# ------------------------------------
def _round_up_for_cuda_graphs_or_cp(
causal_seq: torch.Tensor,
full_only_seq: torch.Tensor,
need_causal: int,
need_full: int,
is_image_batch: bool,
pad_for_cuda_graphs: bool,
) -> tuple[torch.Tensor, torch.Tensor]:
"""Pad causal/full sequences to the required lengths, growing global bounds for CUDA graphs."""
if pad_for_cuda_graphs:
global \
MAX_CAUSAL_LEN_IMAGE_BATCH, \
MAX_FULL_LEN_IMAGE_BATCH, \
MAX_CAUSAL_LEN_VIDEO_BATCH, \
MAX_FULL_LEN_VIDEO_BATCH
if is_image_batch:
if need_causal > MAX_CAUSAL_LEN_IMAGE_BATCH:
MAX_CAUSAL_LEN_IMAGE_BATCH = need_causal
log.info(f"Growing MAX_CAUSAL_LEN_IMAGE_BATCH to {MAX_CAUSAL_LEN_IMAGE_BATCH}", rank0_only=False)
if need_full > MAX_FULL_LEN_IMAGE_BATCH:
MAX_FULL_LEN_IMAGE_BATCH = need_full
log.info(f"Growing MAX_FULL_LEN_IMAGE_BATCH to {MAX_FULL_LEN_IMAGE_BATCH}", rank0_only=False)
causal_seq, full_only_seq = _pad(
causal_seq,
full_only_seq,
max_causal_len=MAX_CAUSAL_LEN_IMAGE_BATCH,
max_full_len=MAX_FULL_LEN_IMAGE_BATCH,
)
else:
if need_causal > MAX_CAUSAL_LEN_VIDEO_BATCH:
MAX_CAUSAL_LEN_VIDEO_BATCH = need_causal
log.info(f"Growing MAX_CAUSAL_LEN_VIDEO_BATCH to {MAX_CAUSAL_LEN_VIDEO_BATCH}", rank0_only=False)
if need_full > MAX_FULL_LEN_VIDEO_BATCH:
MAX_FULL_LEN_VIDEO_BATCH = need_full
log.info(f"Growing MAX_FULL_LEN_VIDEO_BATCH to {MAX_FULL_LEN_VIDEO_BATCH}", rank0_only=False)
causal_seq, full_only_seq = _pad(
causal_seq,
full_only_seq,
max_causal_len=MAX_CAUSAL_LEN_VIDEO_BATCH,
max_full_len=MAX_FULL_LEN_VIDEO_BATCH,
)
elif need_causal != int(causal_seq.shape[0]) or need_full != int(full_only_seq.shape[0]):
causal_seq, full_only_seq = _pad(causal_seq, full_only_seq, need_causal, need_full)
return causal_seq, full_only_seq
def factored_from_joint_sequence(
packed_sequence: torch.Tensor,
attn_modes: List[str],
split_lens: List[int],
sample_lens: List[int],
packed_und_token_indexes: torch.Tensor,
packed_gen_token_indexes: torch.Tensor,
is_image_batch: bool = False,
cp_world_size: int = 1,
pad_for_cuda_graphs: bool = False,
) -> FactoredSequencePack:
"""
Create a factored sequence pack from a packed sequence and metadata.
NOTE: Some arguments seem redundant because they in principle support more flexible sequence setups.
This constructor checks that the required invariants for FactoredSequencePack are satisfied.
NOTE: This constructor checks that there are no "und" tokens under "full" mode, and no "gen" tokens under "causal" mode,
since this is a requirement for FactoredSequencePack.
Args:
packed_sequence (torch.Tensor): Tensor containing all tokens in the batch of sequences.
attn_modes (List[str]): List of attention modes. Must be alternating ["causal", "full", ... "causal", "full"]
split_lens (List[int]): Length of each subsequence. len(split_lens) == len(attn_modes)
sample_lens (List[int]): Length of each sequence. len(sample_lens) == number of samples.
packed_und_token_indexes (torch.Tensor): The indexes of the understanding tokens in the packed sequence.
packed_gen_token_indexes (torch.Tensor): The indexes of the generating tokens in the packed sequence.
"""
del packed_gen_token_indexes
non_causal_text_idxs = _find_non_causal_text_token_idx(attn_modes, split_lens, packed_und_token_indexes.tolist())
assert len(non_causal_text_idxs) == 0, "non_causal_text_idxs should be empty"
assert sum(sample_lens) == packed_sequence.shape[0], (
"sum(sample_lens) must be equal to the length of the packed sequence"
)
meta = _init_sequence_pack(sample_lens, split_lens, attn_modes, packed_sequence.device)
causal_seq = packed_sequence[meta["_causal_indices"]] # [N_causal_tokens,D]
full_only_seq = packed_sequence[meta["_full_indices"]] # [N_full_tokens,D]
need_causal = _round_up_to_N(int(causal_seq.shape[0]), cp_world_size, pad_for_cuda_graphs)
need_full = _round_up_to_N(int(full_only_seq.shape[0]), cp_world_size, pad_for_cuda_graphs)
causal_seq, full_only_seq = _round_up_for_cuda_graphs_or_cp(
causal_seq,
full_only_seq,
need_causal,
need_full,
is_image_batch,
pad_for_cuda_graphs,
)
pack: FactoredSequencePack = {
**meta,
"max_num_tokens": sum(sample_lens),
"causal_seq": causal_seq,
"full_only_seq": full_only_seq,
"is_sharded": False,
}
return pack
def _validate_single_dim_params(params: Mapping, layer_idx: int, num_dims: int | None) -> dict:
"""
Helper function to validate NATTEN parameters for a dimensionality profile.
Args:
params (Mapping): parameter dict with window_size/window_size_float and other params
layer_idx (int): layer index for error messages
num_dims (int | None): 1, 2, 3, or None (for single-profile format)
Returns:
dict: validated parameter dict with proper types
"""
if not isinstance(params, Mapping):
dim_str = f" ({num_dims}-D)" if num_dims else ""
raise ValueError(f"Parameters for layer {layer_idx}{dim_str} must be a dict or None, got {params=}.")
is_causal = False if "is_causal" not in params else params["is_causal"]
if "window_size_float" in params:
window_size_float = params["window_size_float"]
if (
not isinstance(window_size_float, Sequence)
or len(window_size_float) not in [1, 2, 3]
or any(not isinstance(x, float) for x in window_size_float)
):
raise ValueError(f"'window_size_float' must be a float tuple of size 1, 2, or 3, got {window_size_float=}")
window_size_float = tuple(k for k in window_size_float)
num_dims = len(window_size_float)
def check_stride_dilation(x):
if isinstance(x, float):
if 0.0 <= x <= 1.0:
return tuple(x for _ in range(num_dims))
elif (
isinstance(x, Sequence)
and len(x) == num_dims
and all(isinstance(y, float) and 0.0 <= y <= 1.0 for y in x)
):
return tuple(y for y in x)
else:
raise ValueError(f"Invalid natten float parameter: {x=}")
stride_float = 0.0 if "stride_float" not in params else params["stride_float"]
dilation_float = 0.0 if "dilation_float" not in params else params["dilation_float"]
stride_float = check_stride_dilation(stride_float)
dilation_float = check_stride_dilation(dilation_float)
is_causal = check_valid_tuple_or_element(
is_causal, num_dims=num_dims, typename=bool, raise_error=True, param_name="is_causal"
)
if any(x in params for x in ["window_size", "stride", "dilation"]):
raise ValueError(
f"Please either use _float parameters, or integer ones, and not mix the two. Got {params=}."
)
return {
"window_size_float": window_size_float,
"stride_float": stride_float,
"dilation_float": dilation_float,
"is_causal": is_causal,
}
elif "window_size" in params:
window_size = params["window_size"]
num_dims = len(window_size)
stride = 1 if "stride" not in params else params["stride"]
dilation = 1 if "dilation" not in params else params["dilation"]
if any("_float" in x for x in params.keys()):
raise ValueError(
f"Please either use _float parameters, or integer ones, and not mix the two. Got {params=}."
)
window_size = check_valid_tuple_or_element(
window_size, num_dims=num_dims, typename=int, raise_error=True, param_name="window_size"
)
stride = check_valid_tuple_or_element(
stride, num_dims=num_dims, typename=int, raise_error=True, param_name="stride"
)
dilation = check_valid_tuple_or_element(
dilation, num_dims=num_dims, typename=int, raise_error=True, param_name="dilation"
)
is_causal = check_valid_tuple_or_element(
is_causal, num_dims=num_dims, typename=bool, raise_error=True, param_name="is_causal"
)
return {"window_size": window_size, "stride": stride, "dilation": dilation, "is_causal": is_causal}
else:
raise ValueError(
"Sparse parameters for a layer must have key 'window_size' or 'window_size_float', "
f"got {params=} in layer index {layer_idx}."
)
def verify_natten_parameter_list(
natten_parameter_list: list | None,
num_layers: int,
) -> list | None:
"""
Converts list of NATTEN parameters into expected types, and assigns defaults to unset
parameters.
This needs to be done separately during model initialization, and not forward pass.
There are no torch operations in this function.
Args:
natten_parameter_list (list | None): list of NATTEN parameters. Must be either None, or a
list of mappings, one for each layer. Each list element must be either None,
representing no sparsity / masking (full dense attention), or a mapping of NATTEN
parameters.
Parameters can be specified directly with integer or float format:
- 'window_size_float' (required), 'stride_float', 'dilation_float'
- 'window_size' (required), 'stride', 'dilation'
Or, parameters can be specified for multiple dimensionality profiles in case of
mixed-training (i.e. image and video training) using keys "1d", "2d", "3d":
- Each key maps to either None (dense attention) or a parameter dict
Integer and float parameters cannot be used together in the same layer!
Additionally, you can specify 'is_causal'.
Examples:
```
# 50 percent sparsity along each dimension in a 2-D token layout
{'window_size_float': (0.5, 0.5)} # valid
# 50 percent sparsity along each dimension in a 2-D token layout
# Maximum dilation along first dimension, no dilation along second dimension
{'window_size_float': (0.5, 0.5), 'dilation_float': (1.0, 0.0)} # valid
# Fixed window size of 8x8, dilation of 2x1.
{'window_size': (8, 8), 'dilation': (2, 1)} # valid
# Multi-profile: different parameters for 2D (images) and 3D (videos)
{
"2d": {"window_size_float": (0.5, 0.5)},
"3d": {"window_size_float": (1.0, 0.5, 0.5)}
} # valid
# Multi-profile: 2D uses dense attention, 3D uses sparse
{
"2d": None,
"3d": {"window_size_float": (1.0, 0.5, 0.5)}
} # valid
# Invalid:
{'window_size_float': (0.5, 0.5), 'dilation': (2, 1)}
```
num_layers (int): number of layers in the model. Just used to verify list length.
Returns:
output_parameter_list (list | None): verified and type-checked NATTEN parameters, or None if
no parameters passed.
"""
if natten_parameter_list is not None:
parameter_list_out = []
if not isinstance(natten_parameter_list, Sequence):
raise ValueError(f"Argument 'natten_parameter_list' must be a list or None, got {natten_parameter_list=}.")
if len(natten_parameter_list) != num_layers:
raise ValueError(
"Number of elements in 'natten_parameter_list' must match number of layers "
f"in the model, got {num_layers=}, {len(natten_parameter_list)=}."
)
for i, layer_parameters in enumerate(natten_parameter_list):
if layer_parameters is None:
log.debug(f"Layer {i} will use DENSE attention.")
parameter_list_out.append(None)
continue
if not isinstance(layer_parameters, Mapping):
raise ValueError(
f"Sparse parameters for a layer must be a dict or None, got {layer_parameters=} in layer index {i}."
)
# Detect format: multi-profile if has keys "1d", "2d", or "3d"
dim_keys = {"1d", "2d", "3d"}
has_dim_keys = any(k in layer_parameters for k in dim_keys)
if has_dim_keys:
# Multi-profile format: validate each explicitly defined dimensionality profile
validated_multi_profile = {}
for dim_str, dim_int in [("1d", 1), ("2d", 2), ("3d", 3)]:
if dim_str in layer_parameters:
dim_params = layer_parameters[dim_str]
if dim_params is None:
validated_multi_profile[dim_int] = None
else:
validated_multi_profile[dim_int] = _validate_single_dim_params(dim_params, i, dim_int)
else:
# Single-profile format: validate and convert to multi-profile format
# Infer dimensionality from parameter tuple length
validated_params = _validate_single_dim_params(layer_parameters, i, None)
if "window_size_float" in validated_params:
num_dims = len(validated_params["window_size_float"])
else: # "window_size"
num_dims = len(validated_params["window_size"])
validated_multi_profile = {num_dims: validated_params}
# If all explicitly defined profiles are None, treat as fully dense layer
if all(v is None for v in validated_multi_profile.values()):
log.debug(f"Layer {i} will use DENSE attention (all profiles None).")
parameter_list_out.append(None)
else:
parameter_list_out.append(validated_multi_profile)
log.info(f"Layer {i} NATTEN parameters: {validated_multi_profile}")
return parameter_list_out
return None
def generate_natten_metadata(
token_shapes: list[tuple[int, int, int]],
head_dim: int,
num_layers: int,
device: torch.device,
dtype: torch.dtype,
requires_grad: bool,
natten_parameter_list: list | None = None,
) -> list | None:
"""
Generates list of metadata required by Variable-Sized (variable-length) operations in NATTEN.
Required when training with three_way attention and NATTEN (multi-dimensional / sparse
attention).
Args:
token_shapes (list[tuple]): list of integer tuples corresponding to the
post-tokenization/patchify token layout shapes in the packed sequence. Must strictly be
integer tuples with the same profile (all 1D, 2D, or 3D). 1s will be automatically
stripped (i.e. [(1, 8, 8), (1, 16, 16)] is interpreted as [(8, 8), (16, 16)]).
head_dim (int): Attention head dimension (used to select NATTEN kernel configurations).
num_layers (int): number of layers in the model. Just used to verify list length.
device (torch.device): PyTorch device for offset tensors (should match QKV device).
dtype (torch.dtype): Expected QKV dtype.
requires_grad (bool): Determines whether backprop is expected, and sets up metadata for
backward pass as well.
natten_parameter_list (list | None): list of NATTEN parameters. Must be either None, or a
list of mappings, one for each layer. Each list element must be either None,
representing no sparsity / masking (full dense attention), or a mapping of NATTEN
parameters in either integer or float format:
- 'window_size_float' (required), 'stride_float', 'dilation_float'
- 'window_size' (required), 'stride', 'dilation'
Integer and float parameters cannot be used together in the same layer!
Additionally, you can specify 'is_causal'.
Examples:
```
# 50 percent sparsity along each dimension in a 2-D token layout
{'window_size_float': (0.5, 0.5)} # valid
# 50 percent sparsity along each dimension in a 2-D token layout
# Maximum dilation along first dimension, no dilation along second dimension
{'window_size_float': (0.5, 0.5), 'dilation_float': (1.0, 0.0)} # valid
# Fixed window size of 8x8, dilation of 2x1.
{'window_size': (8, 8), 'dilation': (2, 1)} # valid
# Invalid:
{'window_size_float': (0.5, 0.5), 'dilation': (2, 1)}
```
Returns:
natten_metadata_list (list | None): list of NATTEN varlen metadata, or Nones (dense layers).
Each non-None element will be a dictionary containing final parameters, and varlen
metadata (offset and size tensors, max lengths).
NOTE: to avoid excessive recompilations in torch.compile, we must carefully index into
this list during model.forward, and ideally using the iteration counter from the loop
over layers (nn.ModuleList).
"""
if token_shapes is None or len(token_shapes) < 1:
raise ValueError("'token_shapes' is required for 'three_way' attention.")
natten_metadata = None
if natten_parameter_list is not None:
natten_metadata = []
if not isinstance(natten_parameter_list, list):
raise ValueError(f"Argument 'natten_parameter_list' must be a list or None, got {natten_parameter_list=}.")
if len(natten_parameter_list) != num_layers:
raise ValueError(
"Number of elements in 'natten_parameter_list' must match number of layers "
f"in the model, got {num_layers=}, {len(natten_parameter_list)=}."
)
# We need to filter out 1s from shapes
def filter_shape(shape: tuple) -> tuple:
return tuple(x for x in shape if x > 1)
# Infer token layout rank (dimensionality)
num_dims = max([len(filter_shape(token_shape)) for token_shape in token_shapes])
# Single pass: check if all layers support this dimensionality and if any need processing
needs_processing = False
for i, layer_parameters in enumerate(natten_parameter_list):
if layer_parameters is None:
continue
# Fail fast if this dimensionality is not defined
if num_dims not in layer_parameters:
raise ValueError(
f"Layer {i}: batch has {num_dims}D data but parameters are not defined for {num_dims}D. "
f"Defined dimensionalities: {sorted(layer_parameters.keys())}"
)
# Check if this layer needs processing for this dimensionality
if layer_parameters[num_dims] is not None:
needs_processing = True
# Early exit if all layers are dense for this dimensionality profile
if not needs_processing:
log.debug(f"All layers use DENSE attention for {num_dims}D data.")
return None
# We actually need to process, so validate and filter all shapes
token_layout_list = []
for shape in token_shapes:
assert isinstance(shape, tuple)
shape_filtered = filter_shape(shape)
assert len(shape_filtered) == num_dims, (
f"All data in batch must have same dimensionality, got {num_dims}D and {len(shape_filtered)}D"
)
token_layout_list.append(shape_filtered)
log.debug(f"Batch dimensionality: {num_dims}D, token_layout_list={token_layout_list}")
for i, layer_parameters in enumerate(natten_parameter_list):
if layer_parameters is None:
natten_metadata.append(None)
continue
# Get parameters for this dimensionality (already validated above)
dim_params = layer_parameters[num_dims]
if dim_params is None:
# Dense attention for this dimensionality
natten_metadata.append(None)
continue
# Use dim_params (parameters for this specific dimensionality)
window_size_list = []
stride_list = []
dilation_list = []
if "window_size_float" in dim_params:
window_size_float = dim_params["window_size_float"]
stride_float = dim_params["stride_float"]
dilation_float = dim_params["dilation_float"]
for token_layout in token_layout_list:
window_size_ = tuple(
min(x, max(2, int(k * float(x)))) for k, x in zip(window_size_float, token_layout)
)
stride_ = tuple(min(k, max(1, int(s * float(k)))) for s, k in zip(stride_float, window_size_))
max_dilation = tuple(x // k for k, x in zip(window_size_, token_layout))
dilation_ = tuple(min(m, max(1, int(d * float(m)))) for d, m in zip(dilation_float, max_dilation))
window_size_list.append(window_size_)
stride_list.append(stride_)
dilation_list.append(dilation_)
assert len(window_size_list) == len(stride_list) == len(dilation_list) == len(token_layout_list)
log.debug(f"Layer {i}: {window_size_list=}")
log.debug(f"Layer {i}: {stride_list=}")
log.debug(f"Layer {i}: {dilation_list=}")
elif "window_size" in dim_params:
window_size = dim_params["window_size"]
stride = dim_params["stride"]
dilation = dim_params["dilation"]
window_size_list = [window_size for _ in range(len(token_layout_list))]
stride_list = [stride for _ in range(len(token_layout_list))]
dilation_list = [dilation for _ in range(len(token_layout_list))]
else:
raise ValueError(
"Sparse parameters for a layer must have key 'window_size' or 'window_size_float', "
f"got {dim_params=} in layer index {i}."
)
is_causal = dim_params["is_causal"]
# Create varlen metadata for natten varlen/varsized ops
# full size, that's why constant window sizes aren't allowed.
natten_metadata.append(
generate_multi_dim_varlen_parameters(
token_layout_list=token_layout_list,
head_dim=head_dim,
device=device,
dtype=dtype,
requires_grad=requires_grad,
#
window_size_list=window_size_list,
stride_list=stride_list,
dilation_list=dilation_list,
#
is_causal=is_causal,
)
)
return natten_metadata
def generate_temporal_causal_natten_metadata(
vision_token_shapes: list[tuple[int, int, int]],
num_action_tokens_per_supertoken: int,
num_layers: int,
head_dim: int,
device: torch.device,
dtype: torch.dtype,
requires_grad: bool,
) -> list:
"""Generate per-layer varlen metadata for temporal causal attention on supertokens.
Each sample's generation tokens are laid out as T_i supertokens of size
S_i = num_action_tokens_per_supertoken + H_i*W_i. Metadata encodes
is_causal=(True, False): causal across T, full within S. All layers share
the same metadata (full window, no spatial sparsity).
Unlike generate_natten_metadata, this function does not apply filter_shape — (T, S) layouts
are passed directly even when T=1. NATTEN handles T=1 causal masking correctly (trivially
full attention within S).
Args:
vision_token_shapes: List of (T, H, W) per sample.
num_action_tokens_per_supertoken: Number of action tokens prefixing each
supertoken (0 when actions are not packed inline).
num_layers: Number of transformer layers.
head_dim: Attention head dimension.
device: Target device.
dtype: Target dtype.
requires_grad: Whether metadata tensors require gradient.
Returns:
List of length num_layers, each element the same NATTEN varlen metadata dict.
"""
# T=1: NATTEN requires kernel_size >= 2 and kernel_size <= token_layout, which are mutually
# exclusive when T=1. Fall back to full dense attention (None) — a single supertoken trivially
# attends to only itself, so temporal causality is already satisfied.
# Mixed T=1/T>1 batches are rejected: NATTEN can't mask T=1 samples, and falling back to dense
# attention for the whole batch would break temporal causality for the T>1 samples.
# Ensure min_frames >= 5 in the dataloader so that T_latent = 1 + (N-1)//tcf >= 2 always.
has_short = any(t < 2 for t, h, w in vision_token_shapes)
if has_short:
if not all(t < 2 for t, h, w in vision_token_shapes):
raise ValueError(
"Mixed T=1 and T>1 samples in causal training batch: NATTEN cannot apply "
"causal masking when any sample has T=1 (kernel_size constraint), and falling "
"back to dense attention would break temporal causality for T>1 samples. "
"Ensure all samples have T_latent >= 2 (set min_frames >= 5 in the dataloader)."
)
return [None] * num_layers
token_layout_list = [(t, num_action_tokens_per_supertoken + h * w) for t, h, w in vision_token_shapes]
metadata = generate_multi_dim_varlen_parameters(
token_layout_list=token_layout_list,
head_dim=head_dim,
device=device,
dtype=dtype,
requires_grad=requires_grad,
is_causal=(True, False),
)
return [metadata] * num_layers
def joint_from_joint_sequence(
packed_sequence: torch.Tensor,
attn_modes: List[str],
split_lens: List[int],
sample_lens: List[int],
packed_und_token_indexes: torch.Tensor,
packed_gen_token_indexes: torch.Tensor,
is_image_batch: bool = False,
cp_world_size: int = 1,
pad_for_cuda_graphs: bool = False,
) -> JointSequencePack:
f"""
Create a JointSequencePack from a packed sequence and metadata.
This is in order to support the legacy joint flex-attention implementation.
Differently from FactoredSequencePack, it has less strict requirements on the packed sequence.
Args:
packed_sequence (torch.Tensor): Tensor containing all tokens in the batch of sequences.
attn_modes (List[str]): List of attention modes. Supports any sequence of {"causal", "full", "noise"}
split_lens (List[int]): Length of each subsequence. len(split_lens) == len(attn_modes)
sample_lens (List[int]): Length of each sequence. In this mode, sequences may have different number of splits,
as opposed to FactoredSequencePack where each sequence has exactly two splits..
packed_und_token_indexes (torch.Tensor): The indexes of the understanding tokens in the packed sequence.
packed_gen_token_indexes (torch.Tensor): The indexes of the generating tokens in the packed sequence.
"""
assert sum(sample_lens) == packed_sequence.shape[0], (
"sum(sample_lens) must be equal to the length of the packed sequence"
)
meta = _init_sequence_pack(sample_lens, split_lens, attn_modes, packed_sequence.device)
pack: JointSequencePack = {
**meta,
"max_num_tokens": sum(sample_lens),
"packed_sequence": packed_sequence,
"packed_und_token_indexes": packed_und_token_indexes,
"packed_gen_token_indexes": packed_gen_token_indexes,
"is_sharded": False,
}
return pack
def zeros_like(orig: FactoredSequencePack | JointSequencePack, shape: Tuple[int, ...] | torch.Size | None = None):
"""
Create a new sequence pack with the same metadata as the original, but with all tokens set to zero.
Args:
orig (FactoredSequencePack | JointSequencePack): The original sequence pack to copy metadata from.
shape (Tuple[int, ...] | torch.Size | None): The shape of the new sequence pack. If None, the shape will be the same as the original.
"""
_ensure_core_metadata(orig)
if "packed_sequence" in orig:
if shape is None:
shape_ = orig["packed_sequence"].shape
else:
assert len(shape) >= 1 and shape[0] == -1
shape_ = (orig["packed_sequence"].shape[0],) + tuple(shape)[1:]
packed_sequence = torch.zeros(
shape_, device=orig["packed_sequence"].device, dtype=orig["packed_sequence"].dtype
) # [seq_len,D]
return from_joint(packed_sequence, orig)
else:
if shape is None:
shape_causal = orig["causal_seq"].shape
shape_full = orig["full_only_seq"].shape
else:
assert len(shape) >= 1 and shape[0] == -1
shape_causal = (orig["causal_seq"].shape[0],) + tuple(shape)[1:]
shape_full = (orig["full_only_seq"].shape[0],) + tuple(shape)[1:]
causal_seq = torch.zeros(
shape_causal, device=orig["causal_seq"].device, dtype=orig["causal_seq"].dtype
) # [N_causal_tokens,D]
full_only_seq = torch.zeros(
shape_full, device=orig["full_only_seq"].device, dtype=orig["full_only_seq"].dtype
) # [N_full_tokens,D]
return from_mode_splits(causal_seq, full_only_seq, orig)
def from_joint(packed_sequence: torch.Tensor, metadata_source: FactoredSequencePack | JointSequencePack):
"""
Create a new sequence pack from a packed sequence and another sequence pack with the same metadata.
Args:
packed_sequence (torch.Tensor): Tensor containing all tokens in the batch of sequences.
metadata_source (FactoredSequencePack | JointSequencePack): The metadata source to copy from.
"""
_ensure_core_metadata(metadata_source)
if "packed_sequence" in metadata_source:
out = dict(metadata_source)
out["packed_sequence"] = packed_sequence
return out
else:
if metadata_source["is_sharded"]:
# Use sharded sequences as is when is_sharded is True (used in Context Parallel)
causal_seq = packed_sequence[: len(metadata_source["causal_seq"])] # [N_causal_tokens,D]
full_only_seq = packed_sequence[len(metadata_source["causal_seq"]) :] # [N_full_tokens,D]
else:
causal_seq = packed_sequence[metadata_source["_causal_indices"]] # [N_causal_tokens,D]
full_only_seq = packed_sequence[metadata_source["_full_indices"]] # [N_full_tokens,D]
causal_seq, full_only_seq = _pad(
causal_seq,
full_only_seq,
max_causal_len=metadata_source["causal_seq"].shape[0],
max_full_len=metadata_source["full_only_seq"].shape[0],
)
return from_mode_splits(causal_seq, full_only_seq, metadata_source)
def from_mode_splits(
causal_seq: torch.Tensor,
full_only_seq: torch.Tensor,
orig: FactoredSequencePack | JointSequencePack,
is_sharded: bool | None = None,
):
"""
Create a new sequence pack from two mode splits.
Args:
causal_seq (torch.Tensor): The causal sequence.
full_only_seq (torch.Tensor): The full-only sequence.
orig (FactoredSequencePack | JointSequencePack): The metadata source to copy from.
is_sharded (bool | None): If True, create a local pack for context parallel.
If None, inherits from orig.
"""
_ensure_core_metadata(orig)
if is_sharded is None:
is_sharded = orig.get("is_sharded", False)
if "packed_sequence" in orig:
all_len = int(orig["_causal_indices"].shape[0] + orig["_full_indices"].shape[0])
packed_sequence = causal_seq.new_zeros((all_len, *causal_seq.shape[1:])) # [seq_len,D]
packed_sequence[orig["_causal_indices"]] = causal_seq
packed_sequence[orig["_full_indices"]] = full_only_seq
return from_joint(packed_sequence, orig)
else:
out = dict(orig)
out["causal_seq"] = causal_seq
out["full_only_seq"] = full_only_seq
out["is_sharded"] = is_sharded
return out
def from_und_gen_splits(und_seq: torch.Tensor, gen_seq: torch.Tensor, orig: FactoredSequencePack | JointSequencePack):
"""
Create a new sequence pack from two und/gen splits.
Args:
und_seq (torch.Tensor): The understanding sequence.
gen_seq (torch.Tensor): The generating sequence.
orig (FactoredSequencePack | JointSequencePack): The metadata source to copy from.
"""
# If we have a joint pack (single packed_sequence), place by und/gen indexes.
if "packed_sequence" in orig and "packed_und_token_indexes" in orig and "packed_gen_token_indexes" in orig:
all_len = int(und_seq.shape[0] + gen_seq.shape[0])
packed_sequence = und_seq.new_zeros((all_len, *und_seq.shape[1:])) # [seq_len,D]
packed_sequence[orig["packed_und_token_indexes"]] = und_seq
packed_sequence[orig["packed_gen_token_indexes"]] = gen_seq
return from_joint(packed_sequence, orig)
# Otherwise, treat und/gen as mode splits (und == causal; gen == full).
return from_mode_splits(und_seq, gen_seq, orig)
# ------------------------------------
# Getters and setters for SequencePack
# ------------------------------------
def get_und_seq(pack: SequencePack) -> torch.Tensor:
"""
Get all understanding tokens in a sequence pack in a single tensor.
Args:
pack (FactoredSequencePack | JointSequencePack): The sequence pack to get the understanding sequence from.
Returns:
torch.Tensor: All understanding tokens concatenated over all sequences in the batch.
"""
if "causal_seq" in pack:
return pack["causal_seq"]
if "packed_sequence" in pack and "packed_und_token_indexes" in pack:
return pack["packed_sequence"][pack["packed_und_token_indexes"]]
raise KeyError("Cannot derive und_seq from provided pack")
def set_und_seq(pack: SequencePack, value: torch.Tensor) -> None:
"""
Override the understanding tokens in a sequence pack.
The order of tokens passed in must correspond to the order of tokens returned by get_und_seq.
Args:
pack (FactoredSequencePack | JointSequencePack): The sequence pack to set the understanding sequence in.
value (torch.Tensor): The understanding sequence to set.
"""
if "packed_sequence" in pack and "packed_und_token_indexes" in pack:
pack["packed_sequence"][pack["packed_und_token_indexes"]] = value
elif "causal_seq" in pack:
pack["causal_seq"] = value
else:
raise KeyError("Cannot set und_seq from provided pack")
def get_gen_seq(pack: SequencePack) -> torch.Tensor:
"""
Get all generating tokens in a sequence pack in a single tensor.
Args:
pack (FactoredSequencePack | JointSequencePack): The sequence pack to get the generating sequence from.
Returns:
torch.Tensor: All generating tokens concatenated over all sequences in the batch.
"""
if "full_only_seq" in pack:
return pack["full_only_seq"]
if "packed_sequence" in pack and "packed_gen_token_indexes" in pack:
return pack["packed_sequence"][pack["packed_gen_token_indexes"]]
raise KeyError("Cannot derive gen_seq from provided pack")
def set_gen_seq(pack: SequencePack, value: torch.Tensor) -> None:
"""
Override the generating tokens in a sequence pack.
The order of tokens passed in must correspond to the order of tokens returned by get_gen_seq.
Args:
pack (FactoredSequencePack | JointSequencePack): The sequence pack to set the generating sequence in.
value (torch.Tensor): The generating sequence to set.
"""
if "packed_sequence" in pack and "packed_gen_token_indexes" in pack:
pack["packed_sequence"][pack["packed_gen_token_indexes"]] = value
elif "full_only_seq" in pack:
pack["full_only_seq"] = value
else:
raise KeyError("Cannot set gen_seq from provided pack")
def get_all_seq(pack: SequencePack) -> torch.Tensor:
"""
Get all tokens in a sequence pack in a single tensor.
Args:
pack (FactoredSequencePack | JointSequencePack): The sequence pack to get the all sequence from.
Returns:
torch.Tensor: All tokens concatenated over all sequences in the batch.
"""
if "all_seq" in pack:
return pack["all_seq"]
if "packed_sequence" in pack:
return pack["packed_sequence"]
if "causal_seq" in pack and "full_only_seq" in pack:
_ensure_core_metadata(pack)
if pack["is_sharded"]:
assert False, "get_all_seq is not supported in context parallel sharded mode"
else:
out = pack["causal_seq"].new_zeros(
int(pack["_causal_indices"].shape[0] + pack["_full_indices"].shape[0]), *pack["causal_seq"].shape[1:]
) # [seq_len,D]
if pack["causal_seq"].shape[0] > 0:
out[pack["_causal_indices"]] = pack["causal_seq"][: pack["_causal_indices"].shape[0]]
if pack["full_only_seq"].shape[0] > 0:
out[pack["_full_indices"]] = pack["full_only_seq"][: pack["_full_indices"].shape[0]]
return out
raise KeyError("Cannot derive all_seq from provided pack")
def set_all_seq(pack: SequencePack, value: torch.Tensor) -> None:
"""
Override the all tokens in a sequence pack.
The order of tokens passed in must correspond to the order of tokens returned by get_all_seq.
Args:
pack (FactoredSequencePack | JointSequencePack): The sequence pack to set the all sequence in.
value (torch.Tensor): The all sequence to set.
"""
if "packed_sequence" in pack:
pack["packed_sequence"] = value
elif "causal_seq" in pack and "full_only_seq" in pack:
_ensure_core_metadata(pack)
pack["causal_seq"][: pack["_causal_indices"].shape[0]] = value[pack["_causal_indices"]]
pack["full_only_seq"][: pack["_full_indices"].shape[0]] = value[pack["_full_indices"]]
else:
pack["all_seq"] = value
def get_causal_seq(pack: SequencePack) -> Tuple[torch.Tensor, torch.Tensor]:
"""
Get the causal sequence and its offsets in a sequence pack.
Args:
pack (FactoredSequencePack | JointSequencePack): The sequence pack to get the causal sequence from.
Returns:
Tuple[torch.Tensor, torch.Tensor]: The concatenated causal sub-sequences and the starting offset for each sub-sequence.
"""
_ensure_core_metadata(pack)
if "causal_seq" in pack:
return pack["causal_seq"], pack["_causal_seq_offsets"]
assert "packed_sequence" in pack
return pack["packed_sequence"][pack["_causal_indices"]], pack["_causal_seq_offsets"]
def get_full_only_seq(pack: SequencePack) -> Tuple[torch.Tensor, torch.Tensor]:
"""
Get the full-only sequence and its offsets in a sequence pack.
Args:
pack (FactoredSequencePack | JointSequencePack): The sequence pack to get the full-only sequence from.
Returns:
Tuple[torch.Tensor, torch.Tensor]: The concatenated full-only sub-sequences and the starting offset for each sub-sequence.
"""
_ensure_core_metadata(pack)
if "full_only_seq" in pack:
return pack["full_only_seq"], pack["_full_only_seq_offsets"]
assert "packed_sequence" in pack
return pack["packed_sequence"][pack["_full_indices"]], pack["_full_only_seq_offsets"]
def get_device_and_dtype(pack: SequencePack) -> Tuple[torch.device, torch.dtype]:
"""
Get the device and dtype of a sequence pack.
Args:
pack (FactoredSequencePack | JointSequencePack): The sequence pack to get the device and dtype from.
Returns:
Tuple[torch.device, torch.dtype]: The device and dtype of the sequence pack.
"""
if "packed_sequence" in pack:
return pack["packed_sequence"].device, pack["packed_sequence"].dtype
if "causal_seq" in pack and "full_only_seq" in pack:
return pack["causal_seq"].device, pack["causal_seq"].dtype
raise KeyError("Cannot derive device and dtype from provided pack")
def build_sequence_plans_from_data_batch(
data_batch: dict,
input_video_key,
input_image_key: str,
) -> list[SequencePlan]:
"""Build or retrieve sequence plans from a data batch dictionary.
This function extracts sequence plans from the data batch if they exist,
otherwise creates default SequencePlan objects for each sample
in the batch.
Args:
data_batch: Dictionary containing the data batch from the dataloader.
Expected keys include 'video' or other tensors to determine batch size.
If 'sequence_plan' key exists, those plans are returned directly.
Returns:
List of SequencePlan objects, one per sample in the batch.
"""
# For new modalities, please generate the sequence_plan in the dataset class!!!!
# If sequence_plan already exists in data_batch, return it
if "sequence_plan" in data_batch:
return data_batch["sequence_plan"]
assert "action" not in data_batch or data_batch["action"] is None, "Action data SHOULD have sequence_plans!"
assert "sound" not in data_batch or data_batch["sound"] is None, "Sound data SHOULD have sequence_plans!"
# Determine batch size from available tensors
batch_size = 0
for key in [input_video_key, input_image_key]:
if key in data_batch:
val = data_batch[key]
if isinstance(val, torch.Tensor):
batch_size = val.shape[0]
break
elif isinstance(val, list):
batch_size = len(val)
break
if batch_size == 0:
raise ValueError(
f"Cannot determine batch size from data_batch. Expected {input_video_key}, {input_image_key}, or similar key."
)
# Build default SequencePlan objects
return [
SequencePlan(
has_text=True, # Has text prompt!
has_vision=True,
condition_frame_indexes_vision=[], # No conditioning frames!
)
for _ in range(batch_size)
]
# ============================================================================
# Demo/Test function
# ============================================================================
def main():
"""Demonstrate sequence packing with sample text and images."""
# Initialize tokenizer and add special tokens
tokenizer = Qwen2Tokenizer.from_pretrained("Qwen/Qwen2.5-0.5B-Instruct")
tokenizer, _ = add_special_tokens(tokenizer)
# Define special tokens (Note: Qwen models don't have bos_token_id)
special_tokens = {
"eos_token_id": tokenizer.eos_token_id,
"start_of_generation": tokenizer.convert_tokens_to_ids("<|vision_start|>"),
"end_of_generation": tokenizer.convert_tokens_to_ids("<|vision_end|>"),
}
# Sample text inputs
input_strings = ["Hello world", "How are you?", "I am fine"]
# Tokenize input strings
input_text_tokens = [tokenizer.encode(text, add_special_tokens=False) for text in input_strings]
# Create sample images (in practice, these would be VAE latents)
input_images = torch.stack([torch.randn(3, 1, 64, 64) for _ in range(3)]) # [B, C, T, H, W] format
# Diffusion timesteps for each image
input_timesteps = torch.tensor([0.0, 0.5, 0.9])
# Create GenerationDataClean for images
gen_data_clean_images = GenerationDataClean(
batch_size=3,
is_image_batch=True,
raw_state_vision=input_images,
x0_tokens_vision=torch.randn(3, 16, 8, 8), # dummy tokenized latents
raw_state_action=None,
)
# Create SequencePlan for each sample (all have text and vision)
sequence_plans = [
SequencePlan(
has_text=True,
has_vision=True,
has_action=False,
condition_frame_indexes_vision=[],
condition_frame_indexes_action=[],
)
for _ in range(3)
]
# Pack sequences
packed_data = pack_input_sequence(
sequence_plans=sequence_plans,
input_text_indexes=input_text_tokens,
gen_data_clean=gen_data_clean_images,
input_timesteps=input_timesteps,
special_tokens=special_tokens,
include_end_of_generation_token=True,
)
# Display results (after finalize, fields are tensors)
print(f"Packed sequence length: {packed_data.sequence_length}")
assert isinstance(packed_data.text_ids, torch.Tensor)
print(f"Packed text IDs shape: {packed_data.text_ids.shape}")
if packed_data.vision:
assert isinstance(packed_data.vision.sequence_indexes, torch.Tensor)
print(f"VAE token indexes shape: {packed_data.vision.sequence_indexes.shape}")
print(f"Packed position_ids: {packed_data.position_ids}")
##################
## Video data
input_videos = torch.stack([torch.randn(3, 5, 64, 64) for _ in range(2)]) # [B, C, T, H, W] format
# Diffusion timesteps for each video
input_timesteps_video = torch.tensor([0.5, 0.9])
# Create GenerationDataClean for videos
gen_data_clean_videos = GenerationDataClean(
batch_size=2,
is_image_batch=False,
raw_state_vision=input_videos,
x0_tokens_vision=torch.randn(2, 16, 2, 8, 8), # dummy tokenized latents
raw_state_action=None,
)
# Create SequencePlan for video samples
sequence_plans_video = [
SequencePlan(
has_text=True,
has_vision=True,
has_action=False,
condition_frame_indexes_vision=[],
condition_frame_indexes_action=[],
)
for _ in range(2)
]
# Pack sequences
packed_data = pack_input_sequence(
sequence_plans=sequence_plans_video,
input_text_indexes=input_text_tokens[0:2],
gen_data_clean=gen_data_clean_videos,
input_timesteps=input_timesteps_video,
special_tokens=special_tokens,
include_end_of_generation_token=True,
)
# Display results (after finalize, fields are tensors)
print(f"Packed sequence length: {packed_data.sequence_length}")
assert isinstance(packed_data.text_ids, torch.Tensor)
print(f"Packed text IDs shape: {packed_data.text_ids.shape}")
if packed_data.vision:
assert isinstance(packed_data.vision.sequence_indexes, torch.Tensor)
print(f"VAE token indexes shape: {packed_data.vision.sequence_indexes.shape}")
print(f"Packed position_ids: {packed_data.position_ids}")
def get_und_position_ids(position_ids: torch.Tensor, meta: dict[str, Any]) -> torch.Tensor:
"""
Get the understanding position ids in a sequence pack.
Args:
position_ids (torch.Tensor): The position ids. Shape (seq_len,) for 1D RoPE
or (3, seq_len) for 3D mRoPE.
meta (dict[str, Any]): The metadata.
Returns:
torch.Tensor: The understanding position ids.
"""
assert not meta["is_sharded"], "get_und_position_ids is not supported in context parallel sharded mode"
if position_ids.dim() == 2:
# 3D mRoPE: position_ids is (3, seq_len)
return position_ids[:, meta["_causal_indices"]] # [3,N_causal_tokens]
return position_ids[meta["_causal_indices"]] # [N_causal_tokens]
def get_gen_position_ids(position_ids: torch.Tensor, meta: dict[str, Any]) -> torch.Tensor:
"""
Get the generating position ids in a sequence pack.
Args:
position_ids (torch.Tensor): The position ids. Shape (seq_len,) for 1D RoPE
or (3, seq_len) for 3D mRoPE.
meta (dict[str, Any]): The metadata.
Returns:
torch.Tensor: The generating position ids.
"""
assert not meta["is_sharded"], "get_gen_position_ids is not supported in context parallel sharded mode"
if position_ids.dim() == 2:
# 3D mRoPE: position_ids is (3, seq_len)
return position_ids[:, meta["_full_indices"]] # [3,N_full_tokens]
return position_ids[meta["_full_indices"]] # [N_full_tokens]
if __name__ == "__main__":
main()