MOSS-Image-Base / pipeline_moss_image.py
Phospheneser's picture yhzx233's picture
init
5f40f07
Raw History Blame Contribute Delete
25.1 kB
"""MOSSImage modeling and text-to-image pipeline (no Motor/TorchTitan imports).
Dependencies: torch>=2.9, diffusers>=0.38, transformers>=5.3, safetensors,
accelerate, Pillow. Copy this file anywhere and import it normally.
import torch
# Import MOSSImagePipeline from this file for component-level loading.
pipe = MOSSImagePipeline.from_components_pretrained(
transformer_path="exported_transformer", # save_pretrained directory
text_encoder_path="Qwen/Qwen3.5-9B",
vae_path="black-forest-labs/FLUX.2-klein-4B",
).to("cuda")
pipe("A cat", generator=torch.Generator("cuda").manual_seed(42)).images[0].save("cat.png")
Default guidance uses CFG 3 and skips block 22 only in the negative branch
(zero-based main DiT block indices; two forwards per sampling step):
pipe("A cat", guidance_scale=3.0, uncond_skip_layers=[22])
pipe("A cat", uncond_skip_layers=[]) # disable negative-branch skipping
Optional additional positive-branch SLG adds a third forward:
pipe("A cat", slg_layers=[24], slg_scale=3.0)
The additional SLG layer/scale above illustrates the API, not a quality-tuned setting.
For a consolidated training state dict, create MOSSImageTransformer and
call load_training_state_dict(state), then save_pretrained("exported_transformer").
Select raw or EMA weights explicitly before calling it; distributed checkpoint
shards/optimizer states are not model state dicts. Defaults match the finalized dense MOSSImage model,
not MoE, CFG-distilled, video, or reference-image variants. Prompt strings are
already-rendered captions (dataset-specific quality/domain tags are not added).
First-release defaults: bf16 DiT/text-encoder weights, fp32 RoPE frequencies
and phase/sin/cos calculation, then RoPE cast to the video activation dtype.
Initial Gaussian noise is sampled directly in fp32, including with bf16 weights.
CFG and Euler latent updates use fp32; VAE decoding defaults to fp32. Scheduler
shift defaults to 5.0. Exact numerical parity with the original fp32-master
inference pipeline is not a release requirement.
"""
from __future__ import annotations
import math
from pathlib import Path
import torch
from torch import nn
from torch.nn import functional as F
from diffusers import AutoencoderKLFlux2, DiffusionPipeline, FlowMatchEulerDiscreteScheduler
from diffusers.schedulers.scheduling_utils import SchedulerMixin # Required by custom-component discovery.
from diffusers.configuration_utils import ConfigMixin, register_to_config
from diffusers.image_processor import VaeImageProcessor
from diffusers.models.modeling_utils import ModelMixin
from diffusers.models.modeling_outputs import Transformer2DModelOutput
from diffusers.pipelines.pipeline_utils import ImagePipelineOutput
from diffusers.utils.torch_utils import randn_tensor
from transformers import AutoModel, AutoTokenizer
class _RMSNorm(nn.Module):
def __init__(self, dim):
super().__init__()
self.weight = nn.Parameter(torch.ones(dim))
def forward(self, x):
y = x.float() * torch.rsqrt(x.float().square().mean(-1, keepdim=True) + 1e-6)
return y.to(x.dtype) * self.weight
def _situ(hidden, gate):
return (4.0 * torch.tanh(gate / 4.0) * torch.sigmoid(gate)) * (25.0 * torch.tanh(hidden / 25.0))
def _validate_skip_layers(layers, depth, name='skip_layers'):
layers = () if layers is None else tuple(layers)
if any(type(index) is not int or not 0 <= index < depth for index in layers):
raise ValueError(f'{name} must contain integer block indices in [0, {depth}).')
return frozenset(layers)
def _rotate(x, rope):
sin, cos = rope.chunk(2, -1)
sin = torch.cat((sin, sin), -1).unsqueeze(-2)
cos = torch.cat((cos, cos), -1).unsqueeze(-2)
a, b = x.chunk(2, -1)
return x * cos + torch.cat((-b, a), -1) * sin
def _attention(q, k, v, rope, heads):
shape = (*q.shape[:2], heads, q.shape[-1] // heads)
q = _rotate(q.reshape(shape), rope).transpose(1, 2)
k = _rotate(k.reshape(shape), rope).transpose(1, 2)
v = v.reshape(shape).transpose(1, 2)
return F.scaled_dot_product_attention(q.to(v.dtype), k.to(v.dtype), v).transpose(1, 2).flatten(2)
class _TextBlock(nn.Module):
def __init__(self, dim, ffn_dim, heads):
super().__init__()
self.heads = heads
self.norm1, self.norm2 = _RMSNorm(dim), _RMSNorm(dim)
self.q_norm, self.k_norm = _RMSNorm(dim // heads), _RMSNorm(dim // heads)
self.qkv = nn.Linear(dim, 3 * dim)
self.proj = nn.Linear(dim, dim)
self.ffn_gate, self.ffn_up = nn.Linear(dim, ffn_dim), nn.Linear(dim, ffn_dim)
self.ffn_down = nn.Linear(ffn_dim, dim)
def forward(self, x, rope):
q, k, v = self.qkv(self.norm1(x)).chunk(3, -1)
q = self.q_norm(q.unflatten(-1, (self.heads, -1))).flatten(2)
k = self.k_norm(k.unflatten(-1, (self.heads, -1))).flatten(2)
x = x + self.proj(_attention(q, k, v, rope, self.heads))
y = self.norm2(x)
return x + self.ffn_down(_situ(self.ffn_up(y), self.ffn_gate(y)))
class _TextProjection(nn.Module):
def __init__(self, text_dim, dim, ffn_dim, heads):
super().__init__()
self.proj = nn.Linear(text_dim, dim)
self.blocks = nn.ModuleList([_TextBlock(dim, ffn_dim, heads) for _ in range(2)])
def forward(self, x, rope):
x = self.proj(x.to(self.proj.weight.dtype))
for block in self.blocks:
x = block(x, rope)
return x
class _Block(nn.Module):
def __init__(self, dim, ffn_dim, heads):
super().__init__()
self.heads = heads
self.norm1, self.norm2 = _RMSNorm(dim), _RMSNorm(dim)
self.q_norm, self.k_norm = _RMSNorm(dim // heads), _RMSNorm(dim // heads)
self.proj = nn.ModuleDict({
name: nn.Linear(dim, dim, bias=False) for name in ('q', 'k', 'v', 'o')
})
self.proj['ffn_up_gate'] = nn.Linear(dim, 2 * ffn_dim, bias=False)
self.proj['ffn_down'] = nn.Linear(ffn_dim, dim, bias=False)
def forward(self, x, rope, modulation):
shift_a, scale_a, gate_a, shift_f, scale_f, gate_f = modulation.unbind(1)
y = self.norm1(x) * (1 + scale_a[:, None]) + shift_a[:, None]
dtype = self.proj['q'].weight.dtype
y = y.to(dtype)
q = self.q_norm(self.proj['q'](y).unflatten(-1, (self.heads, -1))).flatten(2)
k = self.k_norm(self.proj['k'](y).unflatten(-1, (self.heads, -1))).flatten(2)
x = x + self.proj['o'](_attention(q, k, self.proj['v'](y), rope, self.heads)) * gate_a[:, None]
y = self.norm2(x) * (1 + scale_f[:, None]) + shift_f[:, None]
hidden, gate = self.proj['ffn_up_gate'](y.to(dtype)).chunk(2, -1)
return x + self.proj['ffn_down'](_situ(hidden, gate)) * gate_f[:, None]
class _RoPE(nn.Module):
def __init__(self, head_dim, base):
super().__init__()
self.num_bands = head_dim // 8
self.base = base
self.register_buffer('bands', self._default_bands(torch.device('cpu')))
def _default_bands(self, device):
# Match training's CPU fp32 construction, including under meta init.
exponent = torch.arange(self.num_bands, device='cpu', dtype=torch.float32) / self.num_bands
return (1 / (self.base ** exponent)).to(device)
def _apply(self, fn, recurse=True):
super()._apply(fn, recurse=recurse)
# .float() cannot recover frequencies already rounded by .to(bf16).
self.bands = self._default_bands(self.bands.device)
return self
def _load_from_state_dict(self, *args, **kwargs):
super()._load_from_state_dict(*args, **kwargs)
# Frequencies are derived constants; retain the key for checkpoint compatibility.
self.bands = self._default_bands(self.bands.device)
def forward(self, height, width, text_length, device):
# Diffusers/Accelerate can install tensors directly, bypassing load hooks.
if self.bands.dtype != torch.float32:
self.bands = self._default_bands(device)
# Coordinates: (r, f, h, w); sequence order is image followed by text.
h, w = torch.meshgrid(torch.arange(height, device=device), torch.arange(width, device=device), indexing='ij')
image = torch.stack((torch.zeros_like(h), torch.full_like(h, text_length), h, w), -1).flatten(0, 1).float()
image[:, 2] -= (height - 1) / 2
image[:, 3] -= (width - 1) / 2
text = torch.zeros(text_length, 4, device=device, dtype=torch.float32)
text[:, 1] = torch.arange(text_length, device=device)
phase = torch.cat((image, text), 0).unsqueeze(-1) * self.bands
return torch.cat((phase.sin(), phase.cos()), -2).flatten(-2).unsqueeze(0)
class MOSSImageTransformer(ModelMixin, ConfigMixin):
"""MOSSImage DiT with training-compatible state-dict names and SDPA attention.
Each sample is evaluated at its true text length, including text refinement;
padded tokens cannot influence either text or joint attention.
"""
@register_to_config
def __init__(self, dim=3072, ffn_dim=8192, num_heads=24, num_layers=42,
text_dim=4096, freq_dim=256, latent_channels=128, rope_base=10000.0):
super().__init__()
if dim % num_heads or (dim // num_heads) % 8 or freq_dim % 2:
raise ValueError('Require dim divisible by heads, head_dim divisible by 8, and even freq_dim.')
self.video_embedder = nn.Linear(latent_channels, dim)
self.text_embedder = _TextProjection(text_dim, dim, ffn_dim, num_heads)
self.rope = _RoPE(dim // num_heads, rope_base)
self.time_embedding = nn.Sequential(nn.Linear(freq_dim, dim), nn.SiLU(), nn.Linear(dim, dim))
self.shared_block_modulation = nn.Linear(dim, 6 * dim)
self.layers = nn.ModuleList([_Block(dim, ffn_dim, num_heads) for _ in range(num_layers)])
self.final_norm_video = _RMSNorm(dim)
self.final_linear_video = nn.Linear(dim, latent_channels, bias=False)
self.video_head_modulation = nn.Linear(dim, 2 * dim)
def load_training_state_dict(self, state_dict, *, prefix=''):
"""Load a consolidated raw/EMA model mapping, strictly; no silent key drops."""
if prefix:
if any(not key.startswith(prefix) for key in state_dict):
raise ValueError('All keys must start with the explicit prefix.')
state_dict = {key[len(prefix):]: value for key, value in state_dict.items()}
return self.load_state_dict(state_dict, strict=True)
def forward(self, hidden_states, timestep, encoder_hidden_states,
encoder_attention_mask=None, return_dict=True, skip_layers=None):
skip_layers = _validate_skip_layers(skip_layers, len(self.layers))
if hidden_states.ndim != 4 or hidden_states.shape[1] != self.config.latent_channels:
raise ValueError('hidden_states must be [B, latent_channels, H, W].')
batch, _, height, width = hidden_states.shape
if encoder_hidden_states.ndim != 3 or encoder_hidden_states.shape[0] != batch:
raise ValueError('encoder_hidden_states must be [B, S, text_dim].')
mask = encoder_attention_mask
if mask is None:
mask = torch.ones(encoder_hidden_states.shape[:2], dtype=torch.bool, device=hidden_states.device)
if mask.shape != encoder_hidden_states.shape[:2] or not torch.all((mask == 0) | (mask == 1)):
raise ValueError('encoder_attention_mask must be a binary [B, S] mask.')
times = torch.as_tensor(timestep, device=hidden_states.device, dtype=torch.float32).reshape(-1)
times = times.expand(batch)
# Training computes the sinusoid in float64 and feeds float32 into its MLP.
half = self.config.freq_dim // 2
freq = 10000 ** (-torch.arange(half, device=times.device, dtype=torch.float64) / half)
phase = times.double()[:, None] * freq
embedding = torch.cat((phase.cos(), phase.sin()), -1).float()
time = self.time_embedding(embedding.to(self.time_embedding[0].weight.dtype)).float()
mod = self.shared_block_modulation(time.to(self.shared_block_modulation.weight.dtype)).view(batch, 6, -1)
head = self.video_head_modulation(time.to(self.video_head_modulation.weight.dtype)).view(batch, 2, -1)
outputs = []
for index in range(batch):
context = encoder_hidden_states[index:index + 1, mask[index].bool()]
if context.shape[1] == 0:
raise ValueError('Each sample needs at least one valid text token.')
video = hidden_states[index:index + 1].flatten(2).transpose(1, 2)
video = self.video_embedder(video.to(self.video_embedder.weight.dtype).contiguous())
# Match the reference activation dtype under fp32 weights + autocast.
rope = self.rope(height, width, context.shape[1], hidden_states.device).to(video.dtype)
text = self.text_embedder(context, rope[:, height * width:])
x = torch.cat((video, text), 1)
for layer_index, layer in enumerate(self.layers):
if layer_index not in skip_layers:
x = layer(x, rope, mod[index:index + 1])
x = self.final_norm_video(x[:, :height * width].to(self.final_norm_video.weight.dtype))
x = x * (1 + head[index:index + 1, 1, None]) + head[index:index + 1, 0, None]
x = self.final_linear_video(x.to(self.final_linear_video.weight.dtype))
outputs.append(x.transpose(1, 2).reshape(1, -1, height, width))
sample = torch.cat(outputs)
return Transformer2DModelOutput(sample=sample) if return_dict else (sample,)
class MOSSImageScheduler(FlowMatchEulerDiscreteScheduler):
"""Euler integration using the training project's V2, nearest-grid schedule."""
@register_to_config
def __init__(self, num_train_timesteps=1000, shift=5.0, training_shift=1.0):
super().__init__(num_train_timesteps=num_train_timesteps, shift=1.0)
self.register_to_config(shift=shift, training_shift=training_shift)
def set_timesteps(self, num_inference_steps=50, device=None):
if num_inference_steps < 1 or self.config.shift <= 0 or self.config.training_shift <= 0:
raise ValueError('Steps and shifts must be positive.')
total = self.config.num_train_timesteps
def grid(count, shift):
lower = shift / (total + shift - 1)
values = torch.linspace(1, lower, count)
return shift * values / (1 + (shift - 1) * values)
sigmas = grid(num_inference_steps, self.config.shift)
training = grid(total, self.config.training_shift).flip(0)
upper = torch.searchsorted(training, sigmas).clamp(max=total - 1)
lower = (upper - 1).clamp(min=0)
nearest = torch.where((sigmas - training[lower]).abs() <= (training[upper] - sigmas).abs(), lower, upper)
sigmas = torch.unique_consecutive(training[nearest])
# Our sigmas have already been shifted and snapped. Avoid a second shift.
self._shift = 1.0
super().set_timesteps(device=device, sigmas=sigmas.tolist())
class MOSSImagePipeline(DiffusionPipeline):
model_cpu_offload_seq = 'text_encoder->transformer->vae'
_optional_components = ['text_encoder', 'tokenizer', 'vae']
def __init__(self, transformer: MOSSImageTransformer,
scheduler: MOSSImageScheduler, text_encoder=None, tokenizer=None, vae=None):
super().__init__()
self.register_modules(transformer=transformer, scheduler=scheduler,
text_encoder=text_encoder, tokenizer=tokenizer, vae=vae)
self.image_processor = VaeImageProcessor(vae_scale_factor=16)
@classmethod
def from_components_pretrained(cls, transformer_path, text_encoder_path, vae_path,
torch_dtype=torch.bfloat16, vae_dtype=torch.float32,
local_files_only=False):
common = dict(local_files_only=local_files_only)
transformer = MOSSImageTransformer.from_pretrained(transformer_path, torch_dtype=torch_dtype, **common)
tokenizer = AutoTokenizer.from_pretrained(text_encoder_path, **common)
text_encoder = AutoModel.from_pretrained(text_encoder_path, dtype=torch_dtype, **common).eval()
subfolder = None if (Path(vae_path) / 'config.json').is_file() else 'vae'
vae = AutoencoderKLFlux2.from_pretrained(vae_path, subfolder=subfolder, torch_dtype=vae_dtype, **common).eval()
return cls(transformer.eval(), MOSSImageScheduler(), text_encoder, tokenizer, vae)
@torch.no_grad()
def encode_prompt(self, prompt, max_sequence_length=4096):
if self.text_encoder is None or self.tokenizer is None:
raise ValueError('Text encoder/tokenizer required for string prompts.')
prompts = [prompt] if isinstance(prompt, str) else list(prompt)
if not prompts or not all(isinstance(p, str) for p in prompts):
raise ValueError('prompt must be a string or a nonempty list of strings.')
rows = []
# Unpadded individual forwards also avoid padding effects in Qwen GDN.
for value in prompts:
formatted = self.tokenizer.apply_chat_template(
[{'role': 'user', 'content': value}], tokenize=False,
add_generation_prompt=True, enable_thinking=True)
inputs = self.tokenizer(formatted, return_tensors='pt', truncation=True,
max_length=max_sequence_length).to(self._execution_device)
output = self.text_encoder(**inputs, use_cache=False, return_dict=True)
rows.append(output.last_hidden_state[0])
length = max(row.shape[0] for row in rows)
embeds = rows[0].new_zeros(len(rows), length, rows[0].shape[-1])
mask = torch.zeros(len(rows), length, device=embeds.device, dtype=torch.bool)
for index, row in enumerate(rows):
embeds[index, :row.shape[0]] = row
mask[index, :row.shape[0]] = True
return embeds, mask
@torch.no_grad()
def __call__(self, prompt=None, *, height=1024, width=1024, num_inference_steps=50,
guidance_scale=3.0, negative_prompt='', num_images_per_prompt=1,
generator=None, latents=None, prompt_embeds=None, prompt_attention_mask=None,
negative_prompt_embeds=None, negative_prompt_attention_mask=None,
max_sequence_length=4096, output_type='pil', return_dict=True,
uncond_skip_layers=(22,), slg_layers=None, slg_scale=0.0,
slg_start=0.0, slg_stop=1.0):
"""Generate images with optional full-block skipping (zero-based indices).
uncond_skip_layers defaults to (22,) and replaces CFG's negative-prompt
prediction with a skipped-block prediction, retaining two forwards per
step. Pass None or an empty sequence to disable skipping. slg_layers
adds a skipped positive-prompt forward and the fp32 correction
slg_scale * (cond - cond_skip) to CFG. With guidance_scale=1, active
SLG uses cond_skip + slg_scale * (cond - cond_skip), matching Diffusers'
default SkipLayerGuidance formulation, with two forwards and no negative
branch. Both options can combine. slg_scale=1 then gives normal cond.
Both uncond skipping and SLG are active where
slg_start <= step_index / num_steps < slg_stop;
the interval follows denoising order, from initial noise to final image.
Outside it, inference uses normal CFG without skipping.
slg_scale=0 or empty slg_layers disables the additional forward.
"""
uncond_skip_layers = _validate_skip_layers(
uncond_skip_layers, len(self.transformer.layers), 'uncond_skip_layers')
slg_layers = _validate_skip_layers(slg_layers, len(self.transformer.layers), 'slg_layers')
if not math.isfinite(slg_scale) or slg_scale < 0:
raise ValueError('slg_scale must be finite and nonnegative.')
if not 0 <= slg_start <= slg_stop <= 1:
raise ValueError('Require 0 <= slg_start <= slg_stop <= 1.')
if height <= 0 or width <= 0 or height % 16 or width % 16:
raise ValueError('height and width must be positive multiples of 16.')
if output_type not in {'pil', 'np', 'pt', 'latent'}:
raise ValueError('output_type must be pil, np, pt or latent.')
if num_images_per_prompt < 1 or not math.isfinite(guidance_scale) or guidance_scale < 0:
raise ValueError('Invalid image count or guidance scale.')
if not 1 <= max_sequence_length <= 4096:
raise ValueError('max_sequence_length must be between 1 and 4096.')
if (prompt is None) == (prompt_embeds is None):
raise ValueError('Provide exactly one of prompt and prompt_embeds.')
device = self._execution_device
if prompt_embeds is None:
prompt_embeds, prompt_attention_mask = self.encode_prompt(prompt, max_sequence_length)
batch = prompt_embeds.shape[0]
def prepare(embeds, mask):
if embeds.ndim != 3 or embeds.shape[0] != batch:
raise ValueError('Prompt embeddings must be [B, S, C] with matching batch size.')
if mask is None:
mask = torch.ones(embeds.shape[:2], dtype=torch.bool, device=embeds.device)
return (embeds.to(device).repeat_interleave(num_images_per_prompt, 0),
mask.to(device).repeat_interleave(num_images_per_prompt, 0))
positive, positive_mask = prepare(prompt_embeds, prompt_attention_mask)
do_cfg = guidance_scale != 1.0
if do_cfg:
if negative_prompt_embeds is None:
negatives = [negative_prompt] * batch if isinstance(negative_prompt, str) else negative_prompt
if len(negatives) != batch:
raise ValueError('negative_prompt must match prompt batch size.')
negative_prompt_embeds, negative_prompt_attention_mask = self.encode_prompt(negatives, max_sequence_length)
negative, negative_mask = prepare(negative_prompt_embeds, negative_prompt_attention_mask)
shape = (batch * num_images_per_prompt, self.transformer.config.latent_channels, height // 16, width // 16)
if latents is None:
# Sample in fp32: promoting bf16 noise afterward cannot restore its precision.
latents = randn_tensor(shape, generator=generator, device=device, dtype=torch.float32)
elif tuple(latents.shape) != shape:
raise ValueError(f'latents must have shape {shape}.')
else:
latents = latents.to(device=device, dtype=torch.float32)
self.scheduler.set_timesteps(num_inference_steps, device=device)
num_steps = len(self.scheduler.timesteps)
for step_index, timestep in enumerate(self.progress_bar(self.scheduler.timesteps)):
skip_active = slg_start <= step_index / num_steps < slg_stop
# CFG must combine fp32 predictions, even when the DiT runs in bf16.
# Converting only the combined velocity would preserve rounding errors.
prediction = self.transformer(latents, timestep, positive, positive_mask).sample.float()
conditional = prediction
if do_cfg:
skip_kwargs = {'skip_layers': uncond_skip_layers} if uncond_skip_layers and skip_active else {}
uncond = self.transformer(latents, timestep, negative, negative_mask, **skip_kwargs).sample.float()
prediction = uncond + guidance_scale * (prediction - uncond)
if slg_layers and slg_scale > 0 and skip_active:
skipped = self.transformer(
latents, timestep, positive, positive_mask, skip_layers=slg_layers).sample.float()
if not do_cfg:
prediction = skipped
prediction = prediction + slg_scale * (conditional - skipped)
latents = self.scheduler.step(prediction, timestep, latents, return_dict=False)[0]
if output_type == 'latent':
images = latents
else:
if self.vae is None:
raise ValueError('A FLUX.2 VAE is required for decoded output.')
z = latents.to(dtype=self.vae.dtype)
bn = self.vae.bn
eps = self.vae.config.batch_norm_eps
z = z * (bn.running_var + eps).sqrt().to(z).view(1, -1, 1, 1) + bn.running_mean.to(z).view(1, -1, 1, 1)
b, c, h, w = z.shape
z = z.reshape(b, c // 4, 2, 2, h, w).permute(0, 1, 4, 2, 5, 3).reshape(b, c // 4, 2 * h, 2 * w)
images = self.vae.decode(z, return_dict=False)[0].clamp(-1, 1)
images = self.image_processor.postprocess(images, output_type=output_type)
self.maybe_free_model_hooks()
return ImagePipelineOutput(images=images) if return_dict else (images,)