svae-fresnel-128 / modeling_patchsvae.py
AbstractPhil's picture
Update modeling_patchsvae.py
a3d42fb verified
Raw
History Blame Contribute Delete
11.4 kB
"""PatchSVAE model for HuggingFace AutoModel.
Usage:
from transformers import AutoConfig, AutoModel
config = AutoConfig.from_pretrained("AbstractPhil/svae-fresnel-128", trust_remote_code=True)
model = AutoModel.from_pretrained("AbstractPhil/svae-fresnel-128", trust_remote_code=True)
# Full reconstruction
output = model(images)
recon = output["recon"] # (B, 3, 128, 128)
latent = output["latent"] # (B, 16, 8, 8) omega tokens
# Encode to omega tokens
omega = model.encode(images) # (B, 16, 8, 8)
# Full SVD decomposition
svd = model.encode_full(images) # dict with U, S, Vt, M per patch
"""
import math
import torch
import torch.nn as nn
import torch.nn.functional as F
from typing import Optional, Dict, Union
from transformers import PreTrainedModel
from .configuration_patchsvae import PatchSVAEConfig
# ── SVD Backend (self-contained, no external deps required) ──────
try:
from geolip_core.linalg.eigh import FLEigh, _FL_MAX_N
_HAS_FL = True
except ImportError:
_HAS_FL = False
def _gram_eigh_svd(A):
"""Thin SVD via Gram + eigh in fp64."""
orig_dtype = A.dtype
with torch.amp.autocast('cuda', enabled=False):
A_d = A.double()
G = torch.bmm(A_d.transpose(1, 2), A_d)
eigenvalues, V = torch.linalg.eigh(G)
eigenvalues = eigenvalues.flip(-1)
V = V.flip(-1)
S = torch.sqrt(eigenvalues.clamp(min=1e-24))
U = torch.bmm(A_d, V) / S.unsqueeze(1).clamp(min=1e-16)
Vh = V.transpose(-2, -1).contiguous()
return U.to(orig_dtype), S.to(orig_dtype), Vh.to(orig_dtype)
def _svd_fp64(A):
"""Auto-dispatch: FL eigh for N<=12, Gram eigh otherwise."""
B, M, N = A.shape
if _HAS_FL and N <= _FL_MAX_N and A.is_cuda:
orig_dtype = A.dtype
with torch.amp.autocast('cuda', enabled=False):
A_d = A.double()
G = torch.bmm(A_d.transpose(1, 2), A_d)
eigenvalues, V = FLEigh()(G.float())
eigenvalues = eigenvalues.double().flip(-1)
V = V.double().flip(-1)
S = torch.sqrt(eigenvalues.clamp(min=1e-24))
U = torch.bmm(A_d, V) / S.unsqueeze(1).clamp(min=1e-16)
Vh = V.transpose(-2, -1).contiguous()
return U.to(orig_dtype), S.to(orig_dtype), Vh.to(orig_dtype)
else:
return _gram_eigh_svd(A)
# ── Patch Utilities ──────────────────────────────────────────────
def _extract_patches(images, patch_size):
B, C, H, W = images.shape
gh, gw = H // patch_size, W // patch_size
x = images.reshape(B, C, gh, patch_size, gw, patch_size)
x = x.permute(0, 2, 4, 1, 3, 5)
return x.reshape(B, gh * gw, C * patch_size * patch_size), gh, gw
def _stitch_patches(patches, gh, gw, patch_size):
B = patches.shape[0]
x = patches.reshape(B, gh, gw, 3, patch_size, patch_size)
x = x.permute(0, 3, 1, 4, 2, 5)
return x.reshape(B, 3, gh * patch_size, gw * patch_size)
# ── Components ───────────────────────────────────────────────────
class _BoundarySmooth(nn.Module):
def __init__(self, channels=3, mid=16):
super().__init__()
self.net = nn.Sequential(
nn.Conv2d(channels, mid, 3, padding=1),
nn.GELU(),
nn.Conv2d(mid, channels, 3, padding=1),
)
nn.init.zeros_(self.net[-1].weight)
nn.init.zeros_(self.net[-1].bias)
def forward(self, x):
return x + self.net(x)
class _SpectralCrossAttention(nn.Module):
def __init__(self, D, n_heads=4, max_alpha=0.2, alpha_init=-2.0):
super().__init__()
self.n_heads = n_heads
self.head_dim = D // n_heads
self.max_alpha = max_alpha
assert D % n_heads == 0
self.qkv = nn.Linear(D, 3 * D)
self.out_proj = nn.Linear(D, D)
self.norm = nn.LayerNorm(D)
self.scale = self.head_dim ** -0.5
self.alpha_logits = nn.Parameter(torch.full((D,), alpha_init))
@property
def alpha(self):
return self.max_alpha * torch.sigmoid(self.alpha_logits)
def forward(self, S):
B, N, D = S.shape
S_normed = self.norm(S)
qkv = self.qkv(S_normed).reshape(B, N, 3, self.n_heads, self.head_dim)
qkv = qkv.permute(2, 0, 3, 1, 4)
q, k, v = qkv[0], qkv[1], qkv[2]
attn = (q @ k.transpose(-2, -1)) * self.scale
attn = attn.softmax(dim=-1)
out = (attn @ v).transpose(1, 2).reshape(B, N, D)
gate = torch.tanh(self.out_proj(out))
return S * (1.0 + self.alpha.unsqueeze(0).unsqueeze(0) * gate)
# ── Model ────────────────────────────────────────────────────────
class PatchSVAEModel(PreTrainedModel):
"""Patch-based SVD Autoencoder — The Fresnel Geometric Compression Lens.
Decomposes images into patches, encodes each to a sphere-normalized
matrix, performs SVD, coordinates spectra via cross-attention,
and reconstructs with 99.993% fidelity.
The spectral vectors S form omega tokens: modality-agnostic,
geometrically structured, universal representations.
"""
config_class = PatchSVAEConfig
_tied_weights_keys = []
def __init__(self, config: PatchSVAEConfig):
super().__init__(config)
V = config.matrix_v
D = config.D
hidden = config.hidden
depth = config.depth
ps = config.patch_size
patch_dim = 3 * ps * ps
mat_dim = V * D
# Encoder
self.enc_in = nn.Linear(patch_dim, hidden)
self.enc_blocks = nn.ModuleList([
nn.Sequential(nn.LayerNorm(hidden), nn.Linear(hidden, hidden),
nn.GELU(), nn.Linear(hidden, hidden))
for _ in range(depth)
])
self.enc_out = nn.Linear(hidden, mat_dim)
nn.init.orthogonal_(self.enc_out.weight)
# Decoder
self.dec_in = nn.Linear(mat_dim, hidden)
self.dec_blocks = nn.ModuleList([
nn.Sequential(nn.LayerNorm(hidden), nn.Linear(hidden, hidden),
nn.GELU(), nn.Linear(hidden, hidden))
for _ in range(depth)
])
self.dec_out = nn.Linear(hidden, patch_dim)
# Cross-attention
self.cross_attn = nn.ModuleList([
_SpectralCrossAttention(D, n_heads=min(4, D),
max_alpha=config.max_alpha,
alpha_init=config.alpha_init)
for _ in range(config.n_cross_layers)
])
# Boundary smoothing
self.boundary_smooth = _BoundarySmooth(channels=3, mid=16)
self.post_init()
def _encode_patches_to_svd(self, patches):
B, N, _ = patches.shape
V, D = self.config.matrix_v, self.config.D
flat = patches.reshape(B * N, -1)
h = F.gelu(self.enc_in(flat))
for block in self.enc_blocks:
h = h + block(h)
M = self.enc_out(h).reshape(B * N, V, D)
M = F.normalize(M, dim=-1)
U, S, Vt = _svd_fp64(M)
U = U.reshape(B, N, V, D)
S = S.reshape(B, N, D)
Vt = Vt.reshape(B, N, D, D)
M = M.reshape(B, N, V, D)
S_coord = S
for layer in self.cross_attn:
S_coord = layer(S_coord)
return {"U": U, "S_orig": S, "S": S_coord, "Vt": Vt, "M": M}
def _decode_from_svd(self, U, S, Vt):
B, N, V, D = U.shape
U_flat = U.reshape(B * N, V, D)
S_flat = S.reshape(B * N, D)
Vt_flat = Vt.reshape(B * N, D, D)
M_hat = torch.bmm(U_flat * S_flat.unsqueeze(1), Vt_flat)
h = F.gelu(self.dec_in(M_hat.reshape(B * N, -1)))
for block in self.dec_blocks:
h = h + block(h)
return self.dec_out(h).reshape(B, N, -1)
def encode(self, pixel_values: torch.Tensor) -> torch.Tensor:
"""Encode images to omega tokens (spatial latent).
Args:
pixel_values: (B, 3, H, W) normalized images
Returns:
(B, D, gh, gw) spectral latent — omega tokens
For 128×128: (B, 16, 8, 8) = 1024 values, 48:1 compression
"""
ps = self.config.patch_size
patches, gh, gw = _extract_patches(pixel_values, ps)
svd = self._encode_patches_to_svd(patches)
S = svd["S"] # (B, N, D)
return S.permute(0, 2, 1).reshape(S.shape[0], self.config.D, gh, gw)
def encode_full(self, pixel_values: torch.Tensor) -> Dict:
"""Encode to full SVD decomposition per patch.
Returns dict with U, S_orig, S, Vt, M, gh, gw.
"""
ps = self.config.patch_size
patches, gh, gw = _extract_patches(pixel_values, ps)
svd = self._encode_patches_to_svd(patches)
svd["gh"] = gh
svd["gw"] = gw
return svd
def decode(self, latent: torch.Tensor,
U: Optional[torch.Tensor] = None,
Vt: Optional[torch.Tensor] = None) -> torch.Tensor:
"""Decode from omega tokens to images.
Args:
latent: (B, D, gh, gw) spectral latent
U: optional (B, N, V, D) for lossless reconstruction
Vt: optional (B, N, D, D) for lossless reconstruction
Returns:
(B, 3, H, W) reconstructed image
"""
B, D, gh, gw = latent.shape
N = gh * gw
S = latent.reshape(B, D, N).permute(0, 2, 1)
if U is None or Vt is None:
V = self.config.matrix_v
U = torch.eye(V, D, device=latent.device, dtype=latent.dtype)
U = U.unsqueeze(0).unsqueeze(0).expand(B, N, -1, -1)
Vt = torch.eye(D, device=latent.device, dtype=latent.dtype)
Vt = Vt.unsqueeze(0).unsqueeze(0).expand(B, N, -1, -1)
decoded = self._decode_from_svd(U, S, Vt)
recon = _stitch_patches(decoded, gh, gw, self.config.patch_size)
return self.boundary_smooth(recon)
def forward(
self,
pixel_values: torch.Tensor,
**kwargs,
) -> Dict[str, torch.Tensor]:
"""Full encode → SVD → coordinate → decode pipeline.
Args:
pixel_values: (B, 3, H, W) normalized images
Returns:
dict with "recon", "latent", "svd" keys
"""
ps = self.config.patch_size
patches, gh, gw = _extract_patches(pixel_values, ps)
svd = self._encode_patches_to_svd(patches)
decoded = self._decode_from_svd(svd["U"], svd["S"], svd["Vt"])
recon = _stitch_patches(decoded, gh, gw, ps)
recon = self.boundary_smooth(recon)
S = svd["S"]
latent = S.permute(0, 2, 1).reshape(S.shape[0], self.config.D, gh, gw)
return {"recon": recon, "latent": latent, "svd": svd}
@staticmethod
def effective_rank(S):
p = S / (S.sum(-1, keepdim=True) + 1e-8)
p = p.clamp(min=1e-8)
return (-(p * p.log()).sum(-1)).exp()
# Register for AutoClass — this is what makes AutoModel.from_pretrained work
PatchSVAEConfig.register_for_auto_class()
PatchSVAEModel.register_for_auto_class("AutoModel")