readVDOpromt2 / pid /_src /tokenizers /scale_rae_vae.py
victor's picture
victor HF Staff
Cache examples and simplify frontend
e2ddf3f verified
Raw
History Blame Contribute Delete
20.8 kB
# Scale-RAE Representation AutoEncoder tokenizer.
#
# Pairs the frozen SigLIP-2 So400M (`google/siglip2-so400m-patch14-224`) encoder
# with the Scale-RAE paper's ViT-XL decoder
# (https://huggingface.co/nyu-visionx/Scale-RAE-Qwen1.5B_DiT2.4B). This mirrors
# the existing DINOv2-based RAE tokenizer (`dinov2_vae.py`) but for the SigLIP-2
# feature space the Scale-RAE T2I diffusion model targets.
#
# Shape contract (16-multiple I/O matching dinov2_vae conventions; the latent
# grid is Scale-RAE's native 16×16 / 256-token / patch-14 layout):
# Input : (B, 3, 1, H, W) pixel range [-1, 1] (H = W = 256)
# Latent : (B, 1152, 1, 16, 16) (virtual 16x compression)
# Output : (B, 3, 1, 256, 256) pixel range [-1, 1] — only when decoder weights
# are supplied; otherwise decode() returns zeros.
#
# Scale-RAE was natively trained at 224 input → 16×16 token grid (patch 14).
# We bridge the 14-multiple internal grid with the pipeline's 16-multiple
# interface using two bicubic interpolations at the pixel boundary, so both
# encoder and decoder run IN DISTRIBUTION on the 16×16 / 224-pixel grid:
#
# encode: 256 (16×16) → bicubic ↓ → 224 (14×16) → SigLIP-2 patch=14 → 16×16 grid
# decode: 16×16 grid → SigLIP-2 decoder patch=14 → 224 (14×16) → bicubic ↑ → 256
#
# Internal pipeline per encode():
# 1. [-1,1] -> [0,1]
# 2. bicubic ↓ resize H,W -> 224,224 (with antialias=True)
# 3. SigLIP normalize using the encoder's image_mean/std (typically 0.5/0.5)
# 4. SigLIP-2 vision_tower forward -> (B, 256, 1152) last-hidden tokens
# 5. F.layer_norm(.., (1152,), eps=1e-6) — affine-free layer norm matches the
# Scale-RAE feature space the decoder + diffusion DiT were trained on
# 6. reshape (B, 256, 1152) -> (B, 1152, 16, 16)
#
# Internal pipeline per decode():
# 1. (B, 1152, 16, 16) -> (B, 256, 1152)
# 2. prepend zero CLS -> (B, 257, 1152)
# 3. GeneralDecoder forward (drop_cls_token=True) -> (B, 256, 14*14*3)
# 4. unpatchify with 16×16 grid -> (B, 3, 224, 224) in SigLIP-normalized space
# 5. denormalize via image_std/mean -> [0, 1]
# 6. bicubic ↑ resize 224 -> 256 (the "14→16 multiple bridge")
# 7. map to [-1, 1]
#
# Architectural knobs (mirroring inference.py:siglip2-so400m-web73m):
# pretrained_path = "google/siglip2-so400m-patch14-224"
# resize_target = 224 (encoder internal, 14-multiple)
# native_spatial_resolution = 256 (interface, 16-multiple)
# decoder_out_pixel_size = 224 (decoder raw output, 14-multiple)
# final_pixel_size = 256 (after 14→16 multiple bridge)
# pretrained_decoder_path = checkpoints/scale_rae/decoder/siglip2_sop14_i224_web73M_ganw3_decXL.pt
# decoder_config_path = checkpoints/scale_rae/decoder/XL_decoder_config.json
import json
import math
import os
from contextlib import nullcontext
from typing import Optional
import torch
import torch.nn as nn
import torch.nn.functional as F
from pid._ext.imaginaire.lazy_config import LazyCall as L
from pid._ext.imaginaire.lazy_config import LazyDict
from pid._ext.imaginaire.utils import log
from pid._ext.imaginaire.utils.distributed import get_rank, rank0_first, sync_model_states
from pid._src.tokenizers.interface import VideoTokenizerInterface
from pid._src.tokenizers.scale_rae_decoder import GeneralDecoder
__all__ = [
"ScaleRAEEncoder",
"ScaleRAEVAE",
"ScaleRAEVAEInterface",
"ScaleRAEConfig",
]
# ===========================================================================
# Layer 1 — SigLIP-2 encoder + (optional) Scale-RAE ViT-XL decoder
# ===========================================================================
class ScaleRAEEncoder(nn.Module):
"""Frozen SigLIP-2 So400M encoder (+ optional frozen Scale-RAE decoder).
Args:
pretrained_path: HF repo id or local directory
(default ``google/siglip2-so400m-patch14-224``).
pretrained_decoder_path: path to the Scale-RAE ViT-XL decoder weights
(e.g. ``siglip2_sop14_i224_web73M_ganw3_decXL.pt``). If None the
decoder stays None and decode() is a no-op.
decoder_config_path: path to the decoder JSON config (e.g.
``XL_decoder_config.json``). Required when ``pretrained_decoder_path``
is given.
decoder_num_patches: token grid size the decoder was trained with
(16*16 = 256 for SigLIP-2 So400M @ 224).
"""
def __init__(
self,
pretrained_path: str = "google/siglip2-so400m-patch14-224",
pretrained_decoder_path: Optional[str] = None,
decoder_config_path: Optional[str] = None,
decoder_num_patches: int = 256,
):
super().__init__()
os.environ.setdefault("HF_HUB_DISABLE_SYMLINKS_WARNING", "1")
os.environ.setdefault("HF_HUB_DISABLE_PROGRESS_BARS", "1")
from transformers import AutoImageProcessor, AutoModel
from transformers.models.vit_mae.configuration_vit_mae import ViTMAEConfig
try:
full_model = AutoModel.from_pretrained(pretrained_path, local_files_only=True)
except (OSError, ValueError, AttributeError):
full_model = AutoModel.from_pretrained(pretrained_path)
# SigLIP wraps a vision_model + text_model; we only need the vision tower.
vision_tower = getattr(full_model, "vision_model", full_model)
vision_tower.requires_grad_(False)
vision_tower.eval()
# Bypass the SigLIP-2 post_layernorm: the Scale-RAE decoder was trained
# against the *pre*-final-LN hidden state (with an affine-free LN
# applied externally — see Scale-RAE's `SigLIPEncoderForDebugging`,
# multimodal_decoder/__init__.py:80-93). On transformers 4.57+
# `Siglip2VisionModel.forward` no longer surfaces hidden_states even
# when `output_hidden_states=True` is passed, so the only way to reach
# the pre-LN tensor is to neutralize `post_layernorm` itself.
if hasattr(vision_tower, "post_layernorm"):
vision_tower.post_layernorm = nn.Identity()
self.encoder = vision_tower
# patch14 + 224 input -> 16x16 = 256 tokens; hidden_size = 1152.
vis_cfg = getattr(full_model.config, "vision_config", full_model.config)
self.patch_size = int(vis_cfg.patch_size)
self.hidden_size = int(vis_cfg.hidden_size)
# SigLIP image mean/std (0.5/0.5 for siglip2-so400m). Read from the
# processor to avoid hard-coding. Non-persistent — not saved to ckpt.
try:
proc = AutoImageProcessor.from_pretrained(pretrained_path, local_files_only=True)
except (OSError, ValueError, AttributeError):
proc = AutoImageProcessor.from_pretrained(pretrained_path)
mean = torch.tensor(proc.image_mean, dtype=torch.float32).view(1, 3, 1, 1)
std = torch.tensor(proc.image_std, dtype=torch.float32).view(1, 3, 1, 1)
self.register_buffer("siglip_mean", mean, persistent=False)
self.register_buffer("siglip_std", std, persistent=False)
# Optional Scale-RAE decoder.
self.decoder: Optional[GeneralDecoder] = None
if pretrained_decoder_path is not None:
if decoder_config_path is None:
raise ValueError("decoder_config_path is required when pretrained_decoder_path is set")
with open(decoder_config_path) as f:
cfg_dict = json.load(f)
# Match Scale-RAE/multimodal_decoder/__init__.py: hidden_size is the
# encoder's vision hidden dim (1152 for SigLIP-2 So400M).
cfg_dict["hidden_size"] = self.hidden_size
cfg = ViTMAEConfig(**cfg_dict)
decoder = GeneralDecoder(cfg, num_patches=decoder_num_patches)
log.info(f"Loading Scale-RAE decoder weights from {pretrained_decoder_path}")
state_dict = torch.load(pretrained_decoder_path, map_location="cpu")
missing, unexpected = decoder.load_state_dict(state_dict, strict=False)
if missing:
log.warning(f"Scale-RAE decoder missing keys: {missing}")
if unexpected:
log.warning(f"Scale-RAE decoder unexpected keys: {unexpected}")
decoder.requires_grad_(False)
decoder.eval()
self.decoder = decoder
def forward(self, x: torch.Tensor) -> torch.Tensor:
"""x: (B, 3, H, W) already SigLIP-normalized.
Returns the pre-final-LN patch tokens (B, N_patches, hidden_size).
We neutralize `vision_model.post_layernorm` at __init__ time, so
``out.last_hidden_state`` IS the pre-LN tensor that Scale-RAE's
decoder was trained against. The ScaleRAEVAE wrapper then applies
an affine-free `F.layer_norm` to match `SigLIPEncoderForDebugging`.
"""
out = self.encoder(x)
return out.last_hidden_state
# ===========================================================================
# Layer 2 — ScaleRAEVAE wrapper (dtype/AMP + resize + preprocess + decode)
# ===========================================================================
_DECODE_WARNED = {"v": False}
@rank0_first
def _build_scale_rae_encoder(
pretrained_path: str,
pretrained_decoder_path: Optional[str],
decoder_config_path: Optional[str],
decoder_num_patches: int,
) -> ScaleRAEEncoder:
log.info(
f"Loading Scale-RAE encoder from {pretrained_path} (decoder={pretrained_decoder_path}) on rank {get_rank()}"
)
return ScaleRAEEncoder(
pretrained_path=pretrained_path,
pretrained_decoder_path=pretrained_decoder_path,
decoder_config_path=decoder_config_path,
decoder_num_patches=decoder_num_patches,
)
class ScaleRAEVAE:
"""Dtype/AMP wrapper around ScaleRAEEncoder + Scale-RAE decoder.
All tensors are 4D (B, C, H, W).
encode(images): (B,3,H,W) in [-1,1] → (B, 1152, H/spatial_compression, W/spatial_compression)
decode(zs) : (B, 1152, h, w) → (B,3,H_out,W_out) in [-1,1] when a
decoder is loaded, otherwise a zeros tensor.
"""
def __init__(
self,
pretrained_path: str = "google/siglip2-so400m-patch14-224",
resize_target: int = 224,
spatial_compression_factor: int = 16,
dtype: torch.dtype = torch.bfloat16,
device: str = "cuda",
is_amp: bool = False,
pretrained_decoder_path: Optional[str] = None,
decoder_config_path: Optional[str] = None,
decoder_num_patches: int = 256,
decoder_out_pixel_size: int = 224,
final_pixel_size: int = 256,
):
self.dtype = dtype
self.device = device
self.resize_target = resize_target
self.spatial_compression_factor = spatial_compression_factor
self.decoder_out_pixel_size = decoder_out_pixel_size
self.final_pixel_size = final_pixel_size
self.model = _build_scale_rae_encoder(
pretrained_path=pretrained_path,
pretrained_decoder_path=pretrained_decoder_path,
decoder_config_path=decoder_config_path,
decoder_num_patches=decoder_num_patches,
)
self.model = self.model.to(device).eval().requires_grad_(False)
sync_model_states(self.model)
self.has_decoder = self.model.decoder is not None
self.is_amp = is_amp
if not is_amp:
self.model = self.model.to(dtype=dtype)
self.context = nullcontext()
else:
self.context = torch.amp.autocast("cuda", dtype=dtype)
def count_param(self):
return sum(p.numel() for p in self.model.parameters())
@torch.no_grad()
def encode(self, images: torch.Tensor) -> torch.Tensor:
"""images: (B, 3, H, W) in [-1, 1].
Returns (B, 1152, H/spatial_compression, W/spatial_compression).
"""
assert images.ndim == 4, f"ScaleRAEVAE.encode expects 4D input, got {images.shape}"
in_dtype = images.dtype
B, _, H, W = images.shape
x = (images + 1.0) / 2.0 # [-1,1] → [0,1]
if H != self.resize_target or W != self.resize_target:
# Downsample (e.g. 256 → 224 = 16 × patch_size 14). antialias=True
# adds the recommended low-pass filter for bicubic downsampling
# (PyTorch ≥ 1.11) — avoids moire when shrinking to a non-integer
# ratio. The 16→14 multiple bridge on the encoder side.
x = F.interpolate(
x.to(torch.float32),
size=(self.resize_target, self.resize_target),
mode="bicubic",
align_corners=False,
antialias=True,
)
mean = self.model.siglip_mean.to(x.device, x.dtype)
std = self.model.siglip_std.to(x.device, x.dtype)
x = (x - mean) / std
with self.context:
if not self.is_amp:
x = x.to(self.dtype)
tokens = self.model(x) # (B, N_patches, hidden_size)
# Affine-free layer norm — matches Scale-RAE feature space.
tokens = F.layer_norm(tokens.float(), (self.model.hidden_size,), weight=None, bias=None, eps=1e-6)
N, C = tokens.shape[1], tokens.shape[2]
h = int(math.isqrt(N))
assert h * h == N, f"SigLIP-2 patch count {N} is not a perfect square"
z = tokens.transpose(1, 2).contiguous().view(B, C, h, h)
return z.to(in_dtype)
@torch.no_grad()
def decode(self, zs: torch.Tensor) -> torch.Tensor:
"""zs: (B, 1152, h, w). Returns (B, 3, final_pixel_size, final_pixel_size) in [-1, 1].
If the decoder was not loaded, returns zeros of the expected shape.
"""
if not self.has_decoder:
if not _DECODE_WARNED["v"]:
log.warning("ScaleRAEVAE.decode is a no-op stub; no decoder weights loaded.")
_DECODE_WARNED["v"] = True
B, _, h, w = zs.shape
H = h * self.spatial_compression_factor
W = w * self.spatial_compression_factor
return torch.zeros((B, 3, H, W), device=zs.device, dtype=zs.dtype)
in_dtype = zs.dtype
B, C, h, w = zs.shape
intermediate = self.decoder_out_pixel_size
# (B, C, h, w) → (B, h*w, C); prepend zero CLS to (B, h*w+1, C).
tokens = zs.reshape(B, C, h * w).transpose(1, 2)
cls = torch.zeros((B, 1, C), device=zs.device, dtype=zs.dtype)
tokens = torch.cat([cls, tokens], dim=1)
with self.context:
if not self.is_amp:
tokens = tokens.to(self.dtype)
decoder_out = self.model.decoder(tokens, drop_cls_token=True)
logits = decoder_out.logits # (B, 256, p*p*3)
pixels = self.model.decoder.unpatchify(
logits,
original_image_size=(intermediate, intermediate),
) # (B, 3, intermediate, intermediate) in SigLIP-normalized space
# De-SigLIP-normalize to [0,1].
pixels = pixels.to(torch.float32)
mean = self.model.siglip_mean.to(pixels.device, pixels.dtype)
std = self.model.siglip_std.to(pixels.device, pixels.dtype)
pixels = (pixels * std + mean).clamp(0.0, 1.0)
# 14-multiple → 16-multiple bridge: bicubic upsample (e.g. 224 → 256).
# antialias is irrelevant for upsampling (no aliasing introduced).
if pixels.shape[-1] != self.final_pixel_size:
pixels = F.interpolate(
pixels,
size=(self.final_pixel_size, self.final_pixel_size),
mode="bicubic",
align_corners=False,
).clamp(0.0, 1.0)
# [0,1] → [-1,1].
pixels = pixels * 2.0 - 1.0
return pixels.to(in_dtype)
# ===========================================================================
# Layer 3 — ScaleRAEVAEInterface(VideoTokenizerInterface)
# ===========================================================================
class ScaleRAEVAEInterface(VideoTokenizerInterface):
"""Pipeline-compatible Scale-RAE: SigLIP-2 So400M encoder + ViT-XL decoder.
Image-only (T=1). Default I/O at 256×256 (16-multiple, virtual 16x
compression to a 16×16 token grid) — unified with dinov2_vae and the
pixel-diffusion pipeline. Internally bridges Scale-RAE's native
14-multiple resolution (224 = 16×14) with two bicubic interpolations:
encode-side downsample (256→224, antialias) and decode-side upsample
(224→256). Both encoder and decoder run IN DISTRIBUTION on the pretrained
16×16 token grid.
"""
def __init__(
self,
chunk_duration: int = 1,
pretrained_path: str = "google/siglip2-so400m-patch14-224",
resize_target: int = 224,
native_spatial_resolution: int = 256,
pretrained_decoder_path: Optional[str] = None,
decoder_config_path: Optional[str] = None,
decoder_num_patches: int = 256,
decoder_out_pixel_size: int = 224,
final_pixel_size: int = 256,
**kwargs,
):
del kwargs # absorb LazyDict metadata (e.g. "name")
self.model = ScaleRAEVAE(
pretrained_path=pretrained_path,
resize_target=resize_target,
spatial_compression_factor=16,
dtype=torch.bfloat16,
is_amp=False,
pretrained_decoder_path=pretrained_decoder_path,
decoder_config_path=decoder_config_path,
decoder_num_patches=decoder_num_patches,
decoder_out_pixel_size=decoder_out_pixel_size,
final_pixel_size=final_pixel_size,
)
self.chunk_duration = chunk_duration
self._spatial_resolution = native_spatial_resolution
@property
def dtype(self):
return self.model.dtype
def reset_dtype(self):
pass
def encode(self, state: torch.Tensor) -> torch.Tensor:
if state.ndim == 5:
assert state.shape[2] == 1, f"Image-only VAE requires T=1, got T={state.shape[2]}"
x = state.squeeze(2)
else:
x = state
latent = self.model.encode(x) # (B, 1152, h, w)
return latent.unsqueeze(2) # (B, 1152, 1, h, w)
def decode(self, latent: torch.Tensor) -> torch.Tensor:
if latent.ndim == 5:
assert latent.shape[2] == 1, f"Image-only VAE requires T=1, got T={latent.shape[2]}"
z = latent.squeeze(2)
else:
z = latent
recon = self.model.decode(z) # (B, 3, H, W)
return recon.unsqueeze(2) # (B, 3, 1, H, W)
def get_latent_num_frames(self, num_pixel_frames: int) -> int:
return num_pixel_frames
def get_pixel_num_frames(self, num_latent_frames: int) -> int:
return num_latent_frames
@property
def spatial_compression_factor(self):
return 16
@property
def temporal_compression_factor(self):
return 1
@property
def pixel_chunk_duration(self):
return self.chunk_duration
@property
def latent_chunk_duration(self):
return self.chunk_duration
@property
def latent_ch(self):
return 1152
@property
def spatial_resolution(self):
return self._spatial_resolution
@property
def name(self):
return "scale_rae_tokenizer"
# ===========================================================================
# Layer 4 — LazyDict config
# ===========================================================================
# Encoder-only configuration (no decoder, e.g. for diffusion training that only
# needs the latent target and decodes at evaluation time). 256 input → 16×16
# latent grid at 1152-D (16-multiple I/O, virtual 16x spatial compression).
ScaleRAEEncoderOnlyConfig: LazyDict = L(ScaleRAEVAEInterface)(
name="scale_rae_encoder_only_tokenizer",
pretrained_path="google/siglip2-so400m-patch14-224",
resize_target=224,
native_spatial_resolution=256,
)
# Full Scale-RAE at 256×256 I/O (SigLIP-2 So400M encoder + ViT-XL decoder,
# web73M). Both encoder and decoder run IN DISTRIBUTION on Scale-RAE's
# pretrained 16×16 / 224-pixel grid; bicubic interpolations bridge to the
# pipeline's 16-multiple 256-pixel boundary at I/O.
ScaleRAEConfig: LazyDict = L(ScaleRAEVAEInterface)(
name="scale_rae_tokenizer",
pretrained_path="google/siglip2-so400m-patch14-224",
resize_target=224,
native_spatial_resolution=256,
pretrained_decoder_path="checkpoints/scale_rae/decoder/siglip2_sop14_i224_web73M_ganw3_decXL.pt",
decoder_config_path="checkpoints/scale_rae/decoder/XL_decoder_config.json",
decoder_num_patches=256,
decoder_out_pixel_size=224,
final_pixel_size=256,
)