RotEx / flux /modules /moe.py
datnguyentien204's picture
Upload RotEx inference code, demos, documentation, and pretrained checkpoints
40440d6 verified
Raw History Blame Contribute Delete
5.9 kB
"""
Mixture-of-Experts (MoE) module for time-based style control.
Implements sparse expert selection based on time intervals.
Author: https://github.com/datnguyen-tien204
"""
import torch
import torch.nn as nn
import torch.nn.functional as F
from typing import Tuple, Optional, List
import math
class Expert(nn.Module):
def __init__(self, input_dim: int, hidden_dim: int, output_dim: int):
super().__init__()
self.network = nn.Sequential(
nn.Linear(input_dim, hidden_dim),
nn.SiLU(),
nn.Linear(hidden_dim, hidden_dim),
nn.SiLU(),
nn.Linear(hidden_dim, output_dim)
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
return self.network(x)
class TimeBasedGatingNetwork(nn.Module):
def __init__(self, num_experts: int, input_dim: int = 2, temperature: float = 0.2,
custom_centers: Optional[List[float]] = None,
custom_widths: Optional[List[float]] = None):
super().__init__()
self.num_experts = num_experts
self.temperature = temperature
if custom_centers is not None:
centers_rad = [c * (2 * math.pi / 24.0) for c in custom_centers]
self.register_buffer('expert_centers', torch.tensor(centers_rad))
else:
centers = [i * (2 * math.pi / num_experts) for i in range(num_experts)]
self.register_buffer('expert_centers', torch.tensor(centers))
if custom_widths is not None:
widths_rad = [(w * (2 * math.pi / 24.0)) / 2.0 for w in custom_widths]
init_log_widths = torch.log(torch.tensor(widths_rad) + 1e-5)
self.expert_log_widths = nn.Parameter(init_log_widths)
else:
self.expert_log_widths = nn.Parameter(torch.zeros(num_experts))
self.min_width = 0.1
def forward(self, phi: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
phi = phi.view(-1, 1)
centers = self.expert_centers.view(1, -1)
diff = torch.remainder(phi - centers + math.pi, 2 * math.pi) - math.pi
widths = F.softplus(self.expert_log_widths).view(1, -1) + self.min_width
logits = -diff ** 2 / (2 * widths ** 2)
weights = F.softmax(logits / self.temperature, dim=-1)
return weights, logits
class MixtureOfExperts(nn.Module):
def __init__(self, input_dim, hidden_dim, output_dim, num_experts, k=2, temperature=0.2, noise_std=0.1,
custom_centers=None, custom_widths=None):
super().__init__()
self.num_experts = num_experts
self.k = k
self.noise_std = noise_std
self.experts = nn.ModuleList([
Expert(input_dim, hidden_dim, output_dim) for _ in range(num_experts)
])
self.gate = TimeBasedGatingNetwork(
num_experts, input_dim=2, temperature=temperature,
custom_centers=custom_centers, custom_widths=custom_widths
)
self.register_buffer('load_balance_loss_coef', torch.tensor(0.01))
def _add_noise(self, logits):
if self.training and self.noise_std > 0:
noise = torch.randn_like(logits) * self.noise_std
return logits + noise
return logits
def _compute_load_balance_loss(self, weights):
mean_weights = weights.mean(dim=0)
target = torch.ones_like(mean_weights) / self.num_experts
loss = F.mse_loss(mean_weights, target)
return loss * self.load_balance_loss_coef
def forward(self, x, phi, return_aux=False):
is_sequence = x.dim() == 3
if is_sequence:
B, L, C = x.shape
x = x.reshape(B * L, C)
phi = phi.unsqueeze(1).expand(B, L).reshape(B * L)
weights, gate_logits = self.gate(phi)
if self.training:
gate_logits = self._add_noise(gate_logits)
weights = F.softmax(gate_logits / self.gate.temperature, dim=-1)
if self.k < self.num_experts:
top_k_weights, top_k_indices = torch.topk(weights, self.k, dim=-1)
top_k_weights = top_k_weights / top_k_weights.sum(dim=-1, keepdim=True)
sparse_weights = torch.zeros_like(weights)
sparse_weights.scatter_(1, top_k_indices, top_k_weights)
weights = sparse_weights
expert_outputs = torch.stack([e(x) for e in self.experts], dim=1)
output = torch.einsum('be,beo->bo', weights, expert_outputs)
if is_sequence:
output = output.reshape(B, L, -1)
weights = weights.reshape(B, L, -1).mean(dim=1)
aux = None
if return_aux:
aux = {'load_balance_loss': self._compute_load_balance_loss(weights)}
return output, aux
class MoEStyleAdapter(nn.Module):
def __init__(self, style_dim, hidden_dim, num_experts, custom_centers=None, custom_widths=None):
super().__init__()
self.moe = MixtureOfExperts(
style_dim, hidden_dim, style_dim, num_experts, k=2, temperature=0.2, noise_std=0.1,
custom_centers=custom_centers, custom_widths=custom_widths
)
def forward(self, style, phi, return_aux=False):
adapted, aux = self.moe(style, phi, return_aux=return_aux)
return adapted + style, aux
class MoEFeatureProcessor(nn.Module):
def __init__(self, feature_dim, num_experts, expand_ratio=4, custom_centers=None, custom_widths=None):
super().__init__()
self.moe = MixtureOfExperts(
feature_dim, feature_dim * expand_ratio, feature_dim, num_experts, k=2, temperature=0.2, noise_std=0.1,
custom_centers=custom_centers, custom_widths=custom_widths
)
self.norm = nn.LayerNorm(feature_dim)
def forward(self, x, phi, return_aux=False):
normed = self.norm(x)
processed, aux = self.moe(normed, phi, return_aux=return_aux)
return x + processed, aux