Skip to content

Commit 197ffdf

Browse files
committed
Address PR comments: GLM-Image Port to FastVideo- Replaced diffusers FeedForward with FastVideo MLP in DiT.- Moved SDPAMetadata import to top of attention layer for cleaner code.- Removed unnecessary __call__ override from pipeline to use standard composition.- Consolidated CFG into a single batched pass with concatenated embeddings for performance.- Fixed attention mask compatibility issues with optimized FLASH_ATTN backend.- Robustly patched flash_attn unpad utility for variable version return values.
1 parent cf67618 commit 197ffdf

23 files changed

Lines changed: 1836 additions & 11 deletions
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
"""
3+
GLM-Image inference example using FastVideo.
4+
5+
This example demonstrates how to generate images using the GLM-Image model
6+
through FastVideo's pipeline infrastructure.
7+
8+
Usage:
9+
python examples/inference/basic/basic_glm_image.py
10+
"""
11+
12+
import os
13+
14+
# Set environment variables for single-GPU distributed setup
15+
os.environ.setdefault("MASTER_ADDR", "127.0.0.1")
16+
os.environ.setdefault("MASTER_PORT", "29501")
17+
os.environ.setdefault("RANK", "0")
18+
os.environ.setdefault("WORLD_SIZE", "1")
19+
20+
from fastvideo.pipelines.basic.glm_image import GlmImagePipeline
21+
from fastvideo.pipelines.pipeline_batch_info import ForwardBatch
22+
23+
OUTPUT_PATH = "image_output"
24+
25+
26+
27+
def main():
28+
# Load the GLM-Image pipeline
29+
pipe = GlmImagePipeline.from_pretrained(
30+
"zai-org/GLM-Image",
31+
num_gpus=1,
32+
trust_remote_code=True,
33+
)
34+
35+
# Create a batch with generation parameters
36+
prompt = (
37+
"A beautiful landscape photography with rolling hills, "
38+
"a winding river, and a vibrant sunset in the background. "
39+
"Warm golden light, photorealistic style."
40+
)
41+
42+
batch = ForwardBatch(
43+
data_type="image",
44+
prompt=prompt,
45+
negative_prompt="",
46+
height=1024,
47+
width=1024,
48+
num_inference_steps=50,
49+
guidance_scale=7.5,
50+
guidance_rescale=0.7,
51+
do_classifier_free_guidance=True,
52+
num_frames=1,
53+
seed=42,
54+
)
55+
56+
# Generate the image
57+
result = pipe.forward(batch, pipe.fastvideo_args)
58+
59+
# Output is in result.output as a tensor [B, C, T, H, W]
60+
print(f"Generated image tensor shape: {result.output.shape}")
61+
62+
# Save the output image
63+
import torch
64+
from torchvision.utils import save_image
65+
66+
os.makedirs(OUTPUT_PATH, exist_ok=True)
67+
# result.output is [B, C, 1, H, W] for images
68+
image_tensor = result.output.squeeze(2) # [B, C, H, W]
69+
save_path = os.path.join(OUTPUT_PATH, "output.png")
70+
save_image(image_tensor, save_path)
71+
print(f"Image saved to {save_path}")
72+
73+
74+
if __name__ == "__main__":
75+
main()

fastvideo/attention/layer.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
# SPDX-License-Identifier: Apache-2.0
22

3+
from typing import TYPE_CHECKING
4+
35
import torch
46
import torch.nn as nn
57

8+
from fastvideo.attention.backends.sdpa import SDPAMetadata
69
from fastvideo.attention.selector import backend_name_to_enum, get_attn_backend
710
from fastvideo.distributed.communication_op import (
811
sequence_model_parallel_all_gather, sequence_model_parallel_all_to_all_4D)
@@ -301,6 +304,7 @@ def forward(
301304
k: torch.Tensor,
302305
v: torch.Tensor,
303306
freqs_cis: tuple[torch.Tensor, torch.Tensor] | None = None,
307+
attention_mask: torch.Tensor | None = None,
304308
) -> torch.Tensor:
305309
"""
306310
Apply local attention between query, key and value tensors.
@@ -324,6 +328,12 @@ def forward(
324328
cos, sin = freqs_cis
325329
q = _apply_rotary_emb(q, cos, sin, is_neox_style=False)
326330
k = _apply_rotary_emb(k, cos, sin, is_neox_style=False)
331+
332+
if attention_mask is not None:
333+
if ctx_attn_metadata is None:
334+
ctx_attn_metadata = SDPAMetadata(current_timestep=0, attn_mask=attention_mask)
335+
else:
336+
ctx_attn_metadata.attn_mask = attention_mask
327337

328338
output = self.attn_impl.forward(q, k, v, ctx_attn_metadata)
329339
return output

fastvideo/attention/utils/flash_attn_no_pad.py

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,15 @@ def flash_attn_no_pad(qkv,
2929
seqlen = qkv.shape[1]
3030
nheads = qkv.shape[-2]
3131
x = rearrange(qkv, "b s three h d -> b s (three h d)")
32-
x_unpad, indices, cu_seqlens, max_s, used_seqlens_in_batch = unpad_input(
33-
x, key_padding_mask)
32+
33+
# Handle version differences in flash_attn unpad_input
34+
unpad_output = unpad_input(x, key_padding_mask)
35+
if len(unpad_output) == 5:
36+
x_unpad, indices, cu_seqlens, max_s, used_seqlens_in_batch = unpad_output
37+
else:
38+
# Older versions return 4 values or different order?
39+
# Typically: x_unpad, indices, cu_seqlens, max_s
40+
x_unpad, indices, cu_seqlens, max_s = unpad_output[:4]
3441

3542
x_unpad = rearrange(x_unpad,
3643
"nnz (three h d) -> nnz three h d",
@@ -69,12 +76,29 @@ def flash_attn_no_pad_v3(qkv,
6976
batch_size, seqlen, _, nheads, head_dim = qkv.shape
7077
query, key, value = qkv.unbind(dim=2)
7178

72-
query_unpad, indices, cu_seqlens_q, max_seqlen_q, _ = unpad_input(
79+
# Handle version differences in flash_attn unpad_input for query
80+
unpad_output_q = unpad_input(
7381
rearrange(query, "b s h d -> b s (h d)"), key_padding_mask)
74-
key_unpad, _, cu_seqlens_k, _, _ = unpad_input(
82+
if len(unpad_output_q) >= 4:
83+
query_unpad, indices, cu_seqlens_q, max_seqlen_q = unpad_output_q[:4]
84+
else:
85+
raise ValueError(f"Unexpected unpad_input output length: {len(unpad_output_q)}")
86+
87+
# Handle version differences for key
88+
unpad_output_k = unpad_input(
7589
rearrange(key, "b s h d -> b s (h d)"), key_padding_mask)
76-
value_unpad, _, _, _, _ = unpad_input(
90+
if len(unpad_output_k) >= 3:
91+
key_unpad, _, cu_seqlens_k = unpad_output_k[:3]
92+
else:
93+
raise ValueError(f"Unexpected unpad_input output length: {len(unpad_output_k)}")
94+
95+
# Handle version differences for value
96+
unpad_output_v = unpad_input(
7797
rearrange(value, "b s h d -> b s (h d)"), key_padding_mask)
98+
if len(unpad_output_v) >= 1:
99+
value_unpad = unpad_output_v[0]
100+
else:
101+
raise ValueError(f"Unexpected unpad_input output length: {len(unpad_output_v)}")
78102

79103
query_unpad = rearrange(query_unpad, "nnz (h d) -> nnz h d", h=nheads)
80104
key_unpad = rearrange(key_unpad, "nnz (h d) -> nnz h d", h=nheads)

fastvideo/configs/models/base.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,7 @@ def update_model_arch(self, source_model_dict: dict[str, Any]) -> None:
5151
for key, value in source_model_dict.items():
5252
if key in valid_fields:
5353
setattr(arch_config, key, value)
54-
else:
55-
raise AttributeError(
56-
f"{type(arch_config).__name__} has no field '{key}'")
54+
# Skip unknown fields - HF configs often have extra fields we don't need
5755

5856
if hasattr(arch_config, "__post_init__"):
5957
arch_config.__post_init__()

fastvideo/configs/models/dits/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from fastvideo.configs.models.dits.cosmos import CosmosVideoConfig
22
from fastvideo.configs.models.dits.cosmos2_5 import Cosmos25VideoConfig
3+
from fastvideo.configs.models.dits.glm_image import GlmImageDiTConfig
34
from fastvideo.configs.models.dits.hunyuanvideo import HunyuanVideoConfig
45
from fastvideo.configs.models.dits.hunyuanvideo15 import HunyuanVideo15Config
56
from fastvideo.configs.models.dits.longcat import LongCatVideoConfig
@@ -9,5 +10,5 @@
910
__all__ = [
1011
"HunyuanVideoConfig", "HunyuanVideo15Config", "WanVideoConfig",
1112
"StepVideoConfig", "CosmosVideoConfig", "Cosmos25VideoConfig",
12-
"LongCatVideoConfig"
13+
"LongCatVideoConfig", "GlmImageDiTConfig"
1314
]
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
"""GLM-Image DiT configuration.
3+
4+
GLM-Image uses a 7B diffusion transformer (DiT) decoder that expands tokens
5+
from the autoregressive vision-language encoder into high-resolution images.
6+
"""
7+
from dataclasses import dataclass, field
8+
9+
from fastvideo.configs.models.dits.base import DiTArchConfig, DiTConfig
10+
11+
12+
def is_blocks(n: str, m) -> bool:
13+
return "transformer_blocks" in n and str.isdigit(n.split(".")[-1])
14+
15+
16+
@dataclass
17+
class GlmImageDiTArchConfig(DiTArchConfig):
18+
"""Architecture config for GlmImageTransformer2DModel."""
19+
20+
_fsdp_shard_conditions: list = field(default_factory=lambda: [is_blocks])
21+
22+
# GLM-Image DiT settings (7B model)
23+
# hidden_size = num_attention_heads * attention_head_dim = 64 * 64 = 4096
24+
hidden_size: int = 4096
25+
num_attention_heads: int = 32
26+
attention_head_dim: int = 128
27+
in_channels: int = 16
28+
out_channels: int = 16
29+
num_layers: int = 30
30+
31+
# Text and condition dims
32+
text_embed_dim: int = 1472
33+
time_embed_dim: int = 512
34+
condition_dim: int = 256
35+
36+
# VQ settings for AR tokens
37+
prior_vq_quantizer_codebook_size: int = 16384
38+
39+
# Patch embedding
40+
patch_size: int = 2
41+
42+
# Positional embedding max resolution
43+
max_height: int = 2048
44+
max_width: int = 2048
45+
46+
# QK normalization
47+
qk_norm: str = "layer_norm"
48+
eps: float = 1e-5
49+
50+
# LoRA exclusions
51+
exclude_lora_layers: list[str] = field(default_factory=lambda: ["image_projector", "glyph_projector", "prior_token_embedding"])
52+
53+
# Param name mappings for weight loading (HF -> custom)
54+
param_names_mapping: dict = field(
55+
default_factory=lambda: {
56+
# Projectors (mapped from FeedForward net.0.proj -> fc_in, net.2 -> fc_out)
57+
r"^image_projector\.net\.0\.proj\.(.*)$": r"image_projector.fc_in.\1",
58+
r"^image_projector\.net\.2\.(.*)$": r"image_projector.fc_out.\1",
59+
r"^glyph_projector\.net\.0\.proj\.(.*)$": r"glyph_projector.fc_in.\1",
60+
r"^glyph_projector\.net\.2\.(.*)$": r"glyph_projector.fc_out.\1",
61+
r"^prior_projector\.net\.0\.proj\.(.*)$": r"prior_projector.fc_in.\1",
62+
r"^prior_projector\.net\.2\.(.*)$": r"prior_projector.fc_out.\1",
63+
64+
r"^prior_token_embedding\.(.*)$": r"prior_token_embedding.\1",
65+
66+
# Transformer blocks
67+
r"^transformer_blocks\.(\d+)\.norm1\.(.*)$": r"transformer_blocks.\1.norm1.\2",
68+
r"^transformer_blocks\.(\d+)\.attn1\.to_q\.(.*)$": r"transformer_blocks.\1.attn1.to_q.\2",
69+
r"^transformer_blocks\.(\d+)\.attn1\.to_k\.(.*)$": r"transformer_blocks.\1.attn1.to_k.\2",
70+
r"^transformer_blocks\.(\d+)\.attn1\.to_v\.(.*)$": r"transformer_blocks.\1.attn1.to_v.\2",
71+
r"^transformer_blocks\.(\d+)\.attn1\.to_out\.0\.(.*)$": r"transformer_blocks.\1.attn1.to_out.0.\2",
72+
73+
# FeedForward in blocks (net.0.proj -> fc_in, net.2 -> fc_out)
74+
r"^transformer_blocks\.(\d+)\.ff\.net\.0\.proj\.(.*)$": r"transformer_blocks.\1.ff.fc_in.\2",
75+
r"^transformer_blocks\.(\d+)\.ff\.net\.2\.(.*)$": r"transformer_blocks.\1.ff.fc_out.\2",
76+
77+
# Output
78+
r"^norm_out\.(.*)$": r"norm_out.\1",
79+
r"^proj_out\.(.*)$": r"proj_out.\1",
80+
})
81+
82+
reverse_param_names_mapping: dict = field(default_factory=dict)
83+
lora_param_names_mapping: dict = field(default_factory=dict)
84+
85+
def __post_init__(self):
86+
super().__post_init__()
87+
self.num_channels_latents = self.out_channels
88+
89+
90+
@dataclass
91+
class GlmImageDiTConfig(DiTConfig):
92+
"""Configuration for GLM-Image DiT model."""
93+
94+
arch_config: DiTArchConfig = field(default_factory=GlmImageDiTArchConfig)
95+
prefix: str = "GlmImage"

fastvideo/configs/models/vaes/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from fastvideo.configs.models.vaes.cosmosvae import CosmosVAEConfig
2+
from fastvideo.configs.models.vaes.glm_image import GlmImageVAEConfig
23
from fastvideo.configs.models.vaes.hunyuanvae import HunyuanVAEConfig
34
from fastvideo.configs.models.vaes.hunyuan15vae import Hunyuan15VAEConfig
45
from fastvideo.configs.models.vaes.stepvideovae import StepVideoVAEConfig
@@ -10,4 +11,5 @@
1011
"StepVideoVAEConfig",
1112
"CosmosVAEConfig",
1213
"Hunyuan15VAEConfig",
14+
"GlmImageVAEConfig",
1315
]
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
"""GLM-Image VAE configuration.
3+
4+
GLM-Image uses an AutoencoderKL for encoding/decoding images to/from latent space.
5+
This is an image-only VAE (no temporal compression).
6+
"""
7+
from dataclasses import dataclass, field
8+
9+
from fastvideo.configs.models.vaes.base import VAEArchConfig, VAEConfig
10+
11+
12+
@dataclass
13+
class GlmImageVAEArchConfig(VAEArchConfig):
14+
"""Architecture config for GLM-Image VAE (AutoencoderKL)."""
15+
16+
# Standard KL-VAE parameters
17+
in_channels: int = 3
18+
out_channels: int = 3
19+
latent_channels: int = 16
20+
21+
# Encoder/decoder architecture
22+
block_out_channels: tuple[int, ...] = (128, 256, 512, 512)
23+
layers_per_block: int = 2
24+
norm_num_groups: int = 32
25+
26+
# Scaling factor for latents (standard for SD-style VAEs)
27+
scaling_factor: float = 0.18215
28+
29+
# Image-only VAE: no temporal compression
30+
temporal_compression_ratio: int = 1
31+
spatial_compression_ratio: int = 8
32+
33+
34+
@dataclass
35+
class GlmImageVAEConfig(VAEConfig):
36+
"""Configuration for GLM-Image VAE."""
37+
38+
arch_config: GlmImageVAEArchConfig = field(default_factory=GlmImageVAEArchConfig)
39+
40+
# Tiling settings for high-resolution images
41+
use_tiling: bool = True
42+
use_temporal_tiling: bool = False # Image model, no temporal dimension
43+
use_parallel_tiling: bool = False
44+
45+
# Tile dimensions for memory efficiency
46+
tile_sample_min_height: int = 512
47+
tile_sample_min_width: int = 512
48+
tile_sample_stride_height: int = 384
49+
tile_sample_stride_width: int = 384
50+
51+
# For image models, we need both encoder and decoder
52+
load_encoder: bool = True
53+
load_decoder: bool = True

fastvideo/configs/pipelines/base.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@ class PipelineConfig:
7373
...] = field(default_factory=lambda:
7474
(postprocess_text, ))
7575

76+
# Post-decoding hook for custom processing after VAE decoding
77+
# This allows pipelines to apply custom transformations to decoded images/videos
78+
post_decoding: Callable[[torch.Tensor], torch.Tensor] | None = None
79+
7680
# StepVideo specific parameters
7781
pos_magic: str | None = None
7882
neg_magic: str | None = None

0 commit comments

Comments
 (0)