# SPDX-FileCopyrightText: © 2025 Tenstorrent AI ULC # SPDX-License-Identifier: Apache-2.0 """On-device Gemma-3 text encoder + embeddings connectors for LTX-2, following the tt_dit encoder_pair convention (cf. ``encoders/t5/encoder_pair.py``).""" from __future__ import annotations import glob import os import time from typing import Callable import torch from huggingface_hub import snapshot_download from loguru import logger from safetensors import safe_open from safetensors.torch import load_file from transformers import AutoTokenizer import ttnn from ...parallel.manager import CCLManager from ...utils import cache as cache_module from ...utils.mochi import get_rot_transformation_mat from ...utils.tensor import bf16_tensor from ...utils.tracing import traced_function from .embeddings_connector import EmbeddingsConnector from .feature_extractor import GemmaFeatureExtractor from .model_gemma import GemmaConfig, GemmaEncoder GEMMA_SEQUENCE_LENGTH = 1024 VIDEO_EMBED_DIM = 4096 AUDIO_EMBED_DIM = 2048 _CONNECTOR_PREFIXES = ( "text_embedding_projection.video_aggregate_embed.", "text_embedding_projection.audio_aggregate_embed.", "model.diffusion_model.video_embeddings_connector.", "model.diffusion_model.audio_embeddings_connector.", ) def _resolve_gemma_dir(gemma: str) -> str: """Resolve a Gemma reference to a local directory: a local dir is returned as-is, a HuggingFace repo id is snapshot-downloaded (cached). The original reference — not this resolved snapshot dir, whose basename is an opaque revision hash — names the cache.""" if os.path.isdir(gemma): return gemma logger.info(f"Resolving HuggingFace Gemma repo {gemma} (auto-download if missing)") return snapshot_download(repo_id=gemma) # --- source state dicts (lazy: only read on a cache miss) ------------------- def _gemma_state_dict(gemma_path: str) -> dict[str, torch.Tensor]: """All Gemma encoder weights, from the HF safetensors shards.""" weight_files = sorted(glob.glob(f"{gemma_path}/model-*.safetensors")) or sorted( glob.glob(f"{gemma_path}/*.safetensors") ) sd: dict[str, torch.Tensor] = {} for f in weight_files: sd.update(load_file(f)) return sd def _read_connector_checkpoint(checkpoint_name: str) -> dict[str, torch.Tensor]: """The connector + aggregate_embed tensors from the LTX checkpoint.""" state: dict[str, torch.Tensor] = {} with safe_open(checkpoint_name, "pt") as f: for k in f.keys(): if k.startswith(_CONNECTOR_PREFIXES): state[k] = f.get_tensor(k) return state def _feature_extractor_state_dict(ckpt, *, mode: str, gemma_hidden_size: int, gemma_num_layers: int) -> dict: """video/audio aggregate_embed weights, permuted D-major→layer-major to match the on-device layer-major concat (see GemmaFeatureExtractor._weight_to_layer_major).""" sd = {} for axis in ("video", "audio") if mode == "av" else ("video",): prefix = f"text_embedding_projection.{axis}_aggregate_embed." for k, v in ckpt.items(): if k.startswith(prefix): sub = k[len(prefix) :] if sub == "weight": v = GemmaFeatureExtractor._weight_to_layer_major(v, gemma_hidden_size, gemma_num_layers) sd[f"{axis}_aggregate_embed.{sub}"] = v return sd def _connector_state_dict(ckpt, axis: str, num_blocks: int) -> dict: """One connector's transformer blocks + norm, dropping blocks beyond ``num_blocks``.""" prefix = f"model.diffusion_model.{axis}_embeddings_connector." sd = {} for k, v in ckpt.items(): if not k.startswith(prefix): continue sub = k[len(prefix) :] if sub.startswith("transformer_1d_blocks.") and int(sub.split(".")[1]) >= num_blocks: continue sd[sub] = v return sd class GemmaTokenizerEncoderPair: """Tokenizer + on-device Gemma-3 encoder + embeddings connectors for LTX-2. ``encode(prompts)`` returns ``[(video_embeds, audio_embeds), ...]``. Gemma weights load from ``gemma_path``; connector/feature-extractor weights from the LTX ``checkpoint_name``. """ def __init__( self, gemma_path: str | None, *, mesh_device: ttnn.MeshDevice, ccl_manager: CCLManager, parallel_config, checkpoint_name: str | None = None, mode: str = "av", dynamic_load: bool = False, sequence_length: int = GEMMA_SEQUENCE_LENGTH, video_dim: int = VIDEO_EMBED_DIM, audio_dim: int = AUDIO_EMBED_DIM, num_layers: int = 48, hidden_layer_index: int = -1, ) -> None: # Name the cache by the caller's reference (a repo id / dir basenames cleanly), # mirroring T5; resolve it to a local dir for tokenizer + weight loading. self.gemma_ref = gemma_path self.gemma_path = _resolve_gemma_dir(gemma_path) if gemma_path else None self.mesh_device = mesh_device self.ccl_manager = ccl_manager self.parallel_config = parallel_config self.checkpoint_name = checkpoint_name self.mode = mode self.dynamic_load = dynamic_load self._num_layers = num_layers self._hidden_layer_index = hidden_layer_index self._sequence_length = sequence_length self._video_dim = video_dim self._audio_dim = audio_dim self.gemma_encoder = None self.tokenizer = None self.feature_extractor = None self.video_connector = None self.audio_connector = None self._coresident_peers: list = [] self._cached_trans_mat = None # Trace the whole-encode graph only when the encoder stays resident. Under dynamic_load it # is evicted after each encode, so every encode would be a fresh (cold) capture — slower # than untraced — and the trace never amortizes. Resident (e.g. 4x8) captures once and # replays warm. self._encoder_trace = not dynamic_load # A capture taken after this one reuses the memory freed from its capture-time activations, # and replaying the encode then writes over whatever now owns that region. Consumers that # capture traces of their own after construction call ``defer_trace_capture`` and reopen the # gate once they are done, which makes the encode the last capture taken. self._trace_gate_open = True # Dims the pipeline warmup needs before the encoder is built. @property def sequence_length(self) -> int: return self._sequence_length @property def video_dim(self) -> int: return self._video_dim @property def audio_dim(self) -> int: return self._audio_dim def defer_trace_capture(self) -> None: """Hold the encode trace back until ``open_trace_gate`` (see ``_trace_gate_open``).""" self._trace_gate_open = False def open_trace_gate(self) -> None: """Allow the next encode to capture/replay its trace (see ``_trace_gate_open``).""" self._trace_gate_open = True def register_coresident_peers(self, peers: list) -> None: """Store the DiT/VAE peers the encoder modules must not be L1-coresident with. Exclusions are wired at each module's first build, so peers must be registered before the first ``ensure_loaded``/``load_*``.""" self._coresident_peers = list(peers) def _register_exclusions(self, module) -> None: """Bidirectionally exclude a freshly-built encoder module against the stored peers, before its first load — so the encoder load auto-evicts the DiT (and the later DiT reload auto-evicts the encoder). No-op unless dynamic_load.""" if not self.dynamic_load or not self._coresident_peers: return module.register_coresident_exclusions(*self._coresident_peers) for p in self._coresident_peers: p.register_coresident_exclusions(module) def is_loaded(self) -> bool: return self.gemma_encoder is not None and self.gemma_encoder.is_loaded() def ensure_loaded(self, connector_state: dict | Callable[[], dict] | None = None) -> None: """Build/reload the encoder + connector weights (evicted by dynamic_load).""" if self.is_loaded(): return self.load_gemma_encoder() self.load_embeddings_connectors( connector_state if connector_state is not None else (lambda: _read_connector_checkpoint(self.checkpoint_name)) ) def load_gemma_encoder(self, gemma_path: str | None = None) -> None: """Load the TTNN Gemma-3 encoder. Built once and reused across reloads; weights reload from the shared cache (no re-tilizing 12B params). The CCLManager is supplied by the pipeline (shared with the connectors), so reloads never rebuild it.""" gemma_path = gemma_path or self.gemma_path if self.gemma_encoder is None: config = GemmaConfig( num_hidden_layers=self._num_layers, hidden_layer_index=self._hidden_layer_index, max_position_embeddings=self._sequence_length, ) self.gemma_encoder = GemmaEncoder(config, self.mesh_device, self.ccl_manager, self.parallel_config) self._register_exclusions(self.gemma_encoder) # Left-padding matches the reference FeatureExtractorV2: [PAD..PAD, BOS, real]; # padded hidden states are zeroed via attention_mask on both sides. self.tokenizer = AutoTokenizer.from_pretrained(gemma_path) t0 = time.time() cache_module.load_model( self.gemma_encoder, model_name=os.path.basename(os.path.normpath(self.gemma_ref)), subfolder="text_encoder", parallel_config=self.parallel_config, mesh_shape=tuple(self.mesh_device.shape), mesh_device=self.mesh_device, get_torch_state_dict=lambda: _gemma_state_dict(gemma_path), ) logger.info(f"Loaded TTNN Gemma encoder ({self._num_layers}L) in {time.time()-t0:.0f}s") def load_embeddings_connectors( self, checkpoint_state: dict[str, torch.Tensor] | Callable[[], dict[str, torch.Tensor]], *, gemma_hidden_size: int = 3840, gemma_num_layers: int = 49, # embedding layer + 48 decoder layers video_num_blocks: int = 8, audio_num_blocks: int = 8, num_heads: int = 32, ) -> None: """Load the feature extractor + video/audio connectors from the LTX checkpoint through the shared cache. Modules are built once and reused across reloads; the CCLManager is supplied by the pipeline (shared with the Gemma encoder). ``checkpoint_state`` is a dict or a zero-arg callable; resolved once and only on a cache miss, so an all-cache-hit reload reads neither the checkpoint nor the tilizer. """ ckpt = _memoize(checkpoint_state) ckpt_name = ( os.path.basename(self.checkpoint_name).removesuffix(".safetensors") if self.checkpoint_name else "ltx-connectors" ) if self.feature_extractor is None: self.feature_extractor = GemmaFeatureExtractor( input_dim=gemma_hidden_size * gemma_num_layers, embedding_dim=gemma_hidden_size, video_dim=self._video_dim, audio_dim=self._audio_dim if self.mode == "av" else None, mesh_device=self.mesh_device, ccl_manager=self.ccl_manager, parallel_config=self.parallel_config, ) self._register_exclusions(self.feature_extractor) cache_module.load_model( self.feature_extractor, model_name=ckpt_name, subfolder="feature_extractor", parallel_config=self.parallel_config, mesh_shape=tuple(self.mesh_device.shape), mesh_device=self.mesh_device, get_torch_state_dict=lambda: _feature_extractor_state_dict( ckpt(), mode=self.mode, gemma_hidden_size=gemma_hidden_size, gemma_num_layers=gemma_num_layers ), ) self.video_connector = self._load_connector( "video", self._video_dim, video_num_blocks, num_heads, ckpt_name, ckpt ) self.audio_connector = ( self._load_connector("audio", self._audio_dim, audio_num_blocks, num_heads, ckpt_name, ckpt) if self.mode == "av" else None ) def _load_connector(self, axis, output_dim, num_blocks, num_heads, ckpt_name, ckpt) -> EmbeddingsConnector: connector = getattr(self, f"{axis}_connector") if connector is None: connector = EmbeddingsConnector( output_dim=output_dim, num_blocks=num_blocks, num_heads=num_heads, mesh_device=self.mesh_device, ccl_manager=self.ccl_manager, parallel_config=self.parallel_config, ) self._register_exclusions(connector) cache_module.load_model( connector, model_name=ckpt_name, subfolder=f"{axis}_connector", parallel_config=self.parallel_config, mesh_shape=tuple(self.mesh_device.shape), mesh_device=self.mesh_device, dtype="float32", get_torch_state_dict=lambda: _connector_state_dict(ckpt(), axis, num_blocks), ) logger.info(f"Loaded {axis} embeddings connector ({num_blocks} blocks, dim={output_dim})") return connector @traced_function(device=lambda self: self.mesh_device, clone_prep_inputs=False, prep_run=True) def _encode_device(self, tt_ids, tt_gemma_mask, fe_mask, src_idx, keep_mask): """Whole-encode device graph (gemma → feature extractor → connectors), captured as one ttnn trace and replayed per prompt. Per-prompt masks/indices are device inputs (built in encode); rope/trans_mat/registers are captured constants (prep_run allocates them before capture). Returns DEVICE (video, audio) embeds — tokenizer + final to_torch stay in encode.""" trans_mat = self._prepare_trans_mat() all_hidden = self.gemma_encoder(tt_ids, tt_attn_mask=tt_gemma_mask) # FeatureExtractorV2 consumes [embed, L0..L46, final_norm]; the encoder emits # [embed, L0..L47, final_norm], so drop index -2. hs_list = list(all_hidden[:-2]) + [all_hidden[-1]] video_feats, audio_feats = self.feature_extractor(hs_list, fe_mask) video = self.video_connector(video_feats, src_idx, keep_mask, trans_mat=trans_mat) audio = ( self.audio_connector(audio_feats, src_idx, keep_mask, trans_mat=trans_mat) if self.audio_connector is not None else None ) return video, audio def encode(self, prompts: list[str]) -> list[tuple[torch.Tensor, torch.Tensor | None]]: """Tokenize → traced whole-encode device graph → host embeds, one ``(video_embeds, audio_embeds)`` per prompt. Pure compute; the disk cache lives in the pipeline.""" assert self.gemma_encoder is not None, "Call ensure_loaded() first" results = [] for prompt in prompts: tokens = self.tokenizer( prompt, return_tensors="pt", padding="max_length", max_length=self._sequence_length, truncation=True ) tt_ids = ttnn.from_torch( tokens.input_ids, device=self.mesh_device, dtype=ttnn.uint32, layout=ttnn.ROW_MAJOR_LAYOUT ) seq = tt_ids.shape[-1] # Per-prompt device inputs, built on host and copied into the trace on replay. tt_gemma_mask = self.gemma_encoder.build_attn_mask(tokens.attention_mask, seq) fe_mask = self.feature_extractor.build_mask(tokens.attention_mask) # src_idx/keep_mask are dim-independent → shared by both connectors (build once). src_idx, keep_mask = self.video_connector.build_indices(tokens.attention_mask, seq) video_dev, audio_dev = self._encode_device( tt_ids, tt_gemma_mask, fe_mask, src_idx, keep_mask, traced=self._encoder_trace and self._trace_gate_open ) video_embeds = ttnn.to_torch(ttnn.get_device_tensors(video_dev)[0]).float() audio_embeds = ( ttnn.to_torch(ttnn.get_device_tensors(audio_dev)[0]).float() if audio_dev is not None else None ) results.append((video_embeds, audio_embeds)) return results def _prepare_trans_mat(self) -> ttnn.Tensor: """Cached per-tile rotation matrix for rotary_embedding_llama.""" if self._cached_trans_mat is None: self._cached_trans_mat = bf16_tensor(get_rot_transformation_mat(), device=self.mesh_device) return self._cached_trans_mat def _memoize(state: dict | Callable[[], dict]) -> Callable[[], dict]: """Wrap a dict-or-callable as a zero-arg callable that resolves at most once.""" cached: dict = {} def get() -> dict: if "sd" not in cached: cached["sd"] = state() if callable(state) else state return cached["sd"] return get