Skip to content

Commit d922ab2

Browse files
SolitaryThinkerGnav3852macthecadillac
authored
[model] Flux2 Klein Port (#1349)
Co-authored-by: Gnav3852 <63612880+Gnav3852@users.noreply.github.com> Co-authored-by: Mac Lee <macthecadillac@gmail.com>
1 parent 9ea77d3 commit d922ab2

61 files changed

Lines changed: 8821 additions & 48 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ env
3434
*.log
3535
weights/
3636
logs/
37+
official_weights/
38+
converted_weights/
3739

3840
# SSIM test outputs
3941
fastvideo/tests/ssim/generated_videos/
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
"""Run full Flux2 text-to-image generation through FastVideo.
3+
4+
User story:
5+
"I have a local or HF Diffusers-format full Flux2 checkpoint and want a
6+
minimal text-to-image generation command that uses embedded guidance."
7+
"""
8+
import argparse
9+
import os
10+
from pathlib import Path
11+
12+
from fastvideo import VideoGenerator
13+
from fastvideo.api import (
14+
ComponentConfig,
15+
EngineConfig,
16+
GenerationRequest,
17+
GeneratorConfig,
18+
OffloadConfig,
19+
OutputConfig,
20+
ParallelismConfig,
21+
PipelineSelection,
22+
SamplingConfig,
23+
)
24+
25+
26+
def parse_args() -> argparse.Namespace:
27+
parser = argparse.ArgumentParser(description="Run full Flux2 text-to-image generation.")
28+
parser.add_argument(
29+
"--model-path",
30+
default="black-forest-labs/FLUX.2-dev",
31+
help="HF id or local diffusers-format full Flux2 weights directory.",
32+
)
33+
parser.add_argument(
34+
"--output",
35+
default="outputs/flux2/flux2.png",
36+
help="Output PNG path.",
37+
)
38+
parser.add_argument(
39+
"--prompt",
40+
default="a photo of a banana on a wooden table, studio lighting",
41+
help="Text prompt.",
42+
)
43+
parser.add_argument("--height", type=int, default=1024)
44+
parser.add_argument("--width", type=int, default=1024)
45+
parser.add_argument("--steps", type=int, default=50)
46+
parser.add_argument("--guidance-scale", type=float, default=4.0)
47+
parser.add_argument("--max-sequence-length", type=int, default=None)
48+
parser.add_argument("--seed", type=int, default=0)
49+
parser.add_argument("--num-gpus", type=int, default=1)
50+
parser.add_argument("--tp-size", type=int, default=None)
51+
parser.add_argument("--sp-size", type=int, default=None)
52+
parser.add_argument(
53+
"--backend",
54+
default=None,
55+
help="Set FASTVIDEO_ATTENTION_BACKEND, for example TORCH_SDPA.",
56+
)
57+
return parser.parse_args()
58+
59+
60+
def main() -> None:
61+
args = parse_args()
62+
if args.backend:
63+
os.environ["FASTVIDEO_ATTENTION_BACKEND"] = args.backend
64+
65+
output = Path(args.output)
66+
output.parent.mkdir(parents=True, exist_ok=True)
67+
tp_size = args.tp_size if args.tp_size is not None else (
68+
args.num_gpus if args.num_gpus > 1 else 1
69+
)
70+
sp_size = args.sp_size if args.sp_size is not None else (
71+
1 if args.num_gpus > 1 else args.num_gpus
72+
)
73+
74+
generator_config = GeneratorConfig(
75+
model_path=args.model_path,
76+
engine=EngineConfig(
77+
num_gpus=args.num_gpus,
78+
parallelism=ParallelismConfig(tp_size=tp_size, sp_size=sp_size),
79+
use_fsdp_inference=False,
80+
offload=OffloadConfig(
81+
dit=False,
82+
vae=True,
83+
text_encoder=True,
84+
pin_cpu_memory=False,
85+
),
86+
),
87+
pipeline=PipelineSelection(
88+
workload_type="t2i",
89+
components=ComponentConfig(override_pipeline_cls_name="Flux2Pipeline"),
90+
),
91+
)
92+
93+
generator = VideoGenerator.from_config(generator_config)
94+
try:
95+
sampling = SamplingConfig(
96+
height=args.height,
97+
width=args.width,
98+
num_frames=1,
99+
fps=1,
100+
num_inference_steps=args.steps,
101+
guidance_scale=args.guidance_scale,
102+
seed=args.seed,
103+
)
104+
extensions = {}
105+
if args.max_sequence_length is not None:
106+
extensions["max_sequence_length"] = args.max_sequence_length
107+
108+
request = GenerationRequest(
109+
prompt=args.prompt,
110+
sampling=sampling,
111+
output=OutputConfig(
112+
output_path=str(output),
113+
save_video=True,
114+
return_frames=False,
115+
),
116+
extensions=extensions,
117+
)
118+
generator.generate(request)
119+
finally:
120+
generator.shutdown()
121+
122+
123+
if __name__ == "__main__":
124+
main()
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
"""Run Flux2 Klein text-to-image generation through FastVideo.
3+
4+
User story:
5+
"I need a short local smoke for the Flux2 Klein checkpoint before wiring it
6+
into an image workflow. Use the model's distilled four-step defaults and
7+
write a single PNG so I can compare the output against the reference."
8+
"""
9+
10+
import argparse
11+
import os
12+
13+
from fastvideo import VideoGenerator
14+
from fastvideo.api import (
15+
EngineConfig,
16+
GenerationRequest,
17+
GeneratorConfig,
18+
OffloadConfig,
19+
OutputConfig,
20+
PipelineSelection,
21+
SamplingConfig,
22+
)
23+
24+
25+
DEFAULT_PROMPT = "a brushed steel espresso machine on a marble counter, morning window light"
26+
27+
28+
def parse_args() -> argparse.Namespace:
29+
parser = argparse.ArgumentParser(description="Run Flux2 Klein text-to-image generation.")
30+
parser.add_argument(
31+
"--model-path",
32+
default="black-forest-labs/FLUX.2-klein-4B",
33+
help="HF id or local diffusers-format Flux2 Klein weights directory.",
34+
)
35+
parser.add_argument(
36+
"--output-path",
37+
default="outputs/flux2/flux2_klein.png",
38+
help="PNG output path or output directory.",
39+
)
40+
parser.add_argument("--prompt", default=DEFAULT_PROMPT, help="Prompt text.")
41+
parser.add_argument("--seed", type=int, default=0, help="Generation seed.")
42+
parser.add_argument("--height", type=int, default=1024, help="Output image height.")
43+
parser.add_argument("--width", type=int, default=1024, help="Output image width.")
44+
parser.add_argument("--steps", type=int, default=4, help="Number of denoising steps.")
45+
parser.add_argument("--num-gpus", type=int, default=1, help="Number of GPUs to use.")
46+
parser.add_argument(
47+
"--backend",
48+
default=None,
49+
help="Set FASTVIDEO_ATTENTION_BACKEND, for example TORCH_SDPA.",
50+
)
51+
return parser.parse_args()
52+
53+
54+
def main() -> None:
55+
args = parse_args()
56+
if args.backend:
57+
os.environ["FASTVIDEO_ATTENTION_BACKEND"] = args.backend
58+
59+
generator_config = GeneratorConfig(
60+
model_path=args.model_path,
61+
engine=EngineConfig(
62+
num_gpus=args.num_gpus,
63+
use_fsdp_inference=False,
64+
offload=OffloadConfig(
65+
dit=False,
66+
vae=True,
67+
text_encoder=True,
68+
pin_cpu_memory=False,
69+
),
70+
),
71+
pipeline=PipelineSelection(workload_type="t2i"),
72+
)
73+
74+
generator = VideoGenerator.from_config(generator_config)
75+
try:
76+
request = GenerationRequest(
77+
prompt=args.prompt,
78+
sampling=SamplingConfig(
79+
height=args.height,
80+
width=args.width,
81+
num_frames=1,
82+
fps=1,
83+
num_inference_steps=args.steps,
84+
guidance_scale=1.0,
85+
seed=args.seed,
86+
),
87+
output=OutputConfig(
88+
output_path=args.output_path,
89+
save_video=True,
90+
),
91+
)
92+
generator.generate(request)
93+
finally:
94+
generator.shutdown()
95+
96+
97+
if __name__ == "__main__":
98+
main()

fastvideo/api/sampling_param.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ class SamplingParam:
2929
# Video inputs
3030
video_path: str | None = None
3131

32+
# Optional pre-generated diffusion latents. Used by parity/debug harnesses
33+
# and advanced callers that need deterministic latent reuse.
34+
latents: Any | None = None
35+
3236
# Action control inputs (Matrix-Game)
3337
mouse_cond: Any | None = None # Shape: (B, T, 2)
3438
keyboard_cond: Any | None = None # Shape: (B, T, K)
@@ -64,6 +68,7 @@ class SamplingParam:
6468
# Text inputs
6569
prompt: str | list[str] | None = None
6670
negative_prompt: str = "Bright tones, overexposed, static, blurred details, subtitles, style, works, paintings, images, static, overall gray, worst quality, low quality, JPEG compression residue, ugly, incomplete, extra fingers, poorly drawn hands, poorly drawn faces, deformed, disfigured, misshapen limbs, fused fingers, still picture, messy background, three legs, many people in the background, walking backwards"
71+
max_sequence_length: int | None = None
6772
prompt_path: str | None = None
6873
output_path: str = "outputs/"
6974
output_video_name: str | None = None

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.flux_2 import Flux2Config
34
from fastvideo.configs.models.dits.hunyuangamecraft import HunyuanGameCraftConfig
45
from fastvideo.configs.models.dits.hunyuanvideo import HunyuanVideoConfig
56
from fastvideo.configs.models.dits.hunyuanvideo15 import HunyuanVideo15Config
@@ -14,5 +15,5 @@
1415
__all__ = [
1516
"HunyuanVideoConfig", "HunyuanVideo15Config", "HunyuanGameCraftConfig", "WanVideoConfig", "CosmosVideoConfig",
1617
"Cosmos25VideoConfig", "LongCatVideoConfig", "LTX2VideoConfig", "HYWorldConfig", "Kandinsky5VideoConfig",
17-
"MagiHumanVideoConfig", "StableAudioConfig"
18+
"MagiHumanVideoConfig", "StableAudioConfig", "Flux2Config"
1819
]

fastvideo/configs/models/dits/base.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ class DiTArchConfig(ArchConfig):
1414
param_names_mapping: dict = field(default_factory=dict)
1515
reverse_param_names_mapping: dict = field(default_factory=dict)
1616
lora_param_names_mapping: dict = field(default_factory=dict)
17+
# When True, the denoising stage casts text/prompt embeddings to the DiT's
18+
# working dtype before the diffusion loop. Flux2 requires this (BFL casts ctx
19+
# to bf16 before denoising); models with fp32 text encoders (Wan, Hunyuan15,
20+
# SD3.5) leave it False to preserve full-precision embeddings.
21+
cast_prompt_embeds_to_dit_dtype: bool = False
1722
_supported_attention_backends: tuple[AttentionBackendEnum,
1823
...] = (AttentionBackendEnum.SAGE_ATTN, AttentionBackendEnum.FLASH_ATTN,
1924
AttentionBackendEnum.TORCH_SDPA,
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# Copied and adapted from: https://github.com/sglang-ai/sglang
3+
from dataclasses import dataclass, field
4+
5+
from fastvideo.configs.models.dits.base import DiTArchConfig, DiTConfig
6+
from fastvideo.logger import init_logger
7+
8+
logger = init_logger(__name__)
9+
10+
11+
@dataclass
12+
class Flux2ArchConfig(DiTArchConfig):
13+
"""Architecture configuration for Flux2 transformer model."""
14+
15+
cast_prompt_embeds_to_dit_dtype: bool = True
16+
17+
# Flux2-specific architecture parameters
18+
patch_size: int = 1
19+
in_channels: int = 64
20+
out_channels: int | None = None
21+
num_layers: int = 19 # Number of double-stream transformer blocks
22+
num_single_layers: int = 38 # Number of single-stream transformer blocks
23+
attention_head_dim: int = 128
24+
num_attention_heads: int = 24
25+
joint_attention_dim: int = 4096 # Dimension for text encoder output
26+
timestep_guidance_channels: int = 256 # Dimension for timestep embedding
27+
mlp_ratio: float = 3.0
28+
axes_dims_rope: tuple[int, ...] = (32, 32, 32, 32) # RoPE dimensions per axis (match diffusers Flux2)
29+
rope_theta: int = 2000 # Base frequency for RoPE (match diffusers Flux2)
30+
eps: float = 1e-6
31+
guidance_embeds: bool = True # Whether to use guidance embeddings
32+
# When True, compute SwiGLU in fp32 inside ``ff_context`` only (bf16 noise mitigation).
33+
ff_context_swiglu_fp32: bool = False
34+
35+
# Parameter name mapping for loading HuggingFace checkpoints
36+
param_names_mapping: dict = field(default_factory=lambda: {
37+
r"transformer\.(\w*)\.(.*)$": r"\1.\2",
38+
})
39+
40+
def __post_init__(self) -> None:
41+
super().__post_init__()
42+
self.out_channels = self.out_channels or self.in_channels
43+
self.hidden_size = self.num_attention_heads * self.attention_head_dim
44+
self.num_channels_latents = self.out_channels
45+
46+
def update_from_weight_keys(self, all_keys: set[str]) -> None:
47+
"""Infer num_layers and num_single_layers from checkpoint weight keys so the model is built with the same number of blocks as the weights."""
48+
if not all_keys:
49+
return
50+
num_layers = 0
51+
num_single_layers = 0
52+
for k in all_keys:
53+
if "single_transformer_blocks." not in k and "transformer_blocks." in k:
54+
parts = k.split("transformer_blocks.")[-1].split(".")
55+
if parts[0].isdigit():
56+
num_layers = max(num_layers, int(parts[0]) + 1)
57+
if "single_transformer_blocks." in k:
58+
parts = k.split("single_transformer_blocks.")[-1].split(".")
59+
if parts[0].isdigit():
60+
num_single_layers = max(num_single_layers, int(parts[0]) + 1)
61+
if num_layers > 0:
62+
self.num_layers = num_layers
63+
logger.info("Inferred num_layers=%s from checkpoint keys", num_layers)
64+
if num_single_layers > 0:
65+
self.num_single_layers = num_single_layers
66+
logger.info("Inferred num_single_layers=%s from checkpoint keys", num_single_layers)
67+
if num_layers > 0 or num_single_layers > 0:
68+
self.__post_init__()
69+
70+
71+
@dataclass
72+
class Flux2Config(DiTConfig):
73+
"""Configuration for Flux2 transformer model."""
74+
75+
arch_config: DiTArchConfig = field(default_factory=Flux2ArchConfig)
76+
77+
prefix: str = "Flux"

fastvideo/configs/models/encoders/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
from fastvideo.configs.models.encoders.siglip import SiglipVisionConfig
88
from fastvideo.configs.models.encoders.reason1 import Reason1ArchConfig, Reason1Config
99
from fastvideo.configs.models.encoders.gemma import LTX2GemmaConfig
10+
from fastvideo.configs.models.encoders.mistral3 import Mistral3TextConfig
11+
from fastvideo.configs.models.encoders.qwen3 import Qwen3TextConfig
1012
from fastvideo.configs.models.encoders.stable_audio_conditioner import (StableAudioConditionerArchConfig,
1113
StableAudioConditionerConfig)
1214
from fastvideo.configs.models.encoders.t5gemma import T5GemmaEncoderConfig
@@ -15,5 +17,5 @@
1517
"EncoderConfig", "TextEncoderConfig", "ImageEncoderConfig", "BaseEncoderOutput", "CLIPTextConfig",
1618
"CLIPVisionConfig", "WAN2_1ControlCLIPVisionConfig", "LlamaConfig", "T5Config", "T5LargeConfig", "Qwen2_5_VLConfig",
1719
"Reason1ArchConfig", "Reason1Config", "LTX2GemmaConfig", "SiglipVisionConfig", "StableAudioConditionerArchConfig",
18-
"StableAudioConditionerConfig", "T5GemmaEncoderConfig"
20+
"StableAudioConditionerConfig", "T5GemmaEncoderConfig", "Qwen3TextConfig", "Mistral3TextConfig"
1921
]

fastvideo/configs/models/encoders/base.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ class TextEncoderArchConfig(EncoderArchConfig):
3636
default_factory=list) # mapping from huggingface weight names to custom names
3737
tokenizer_kwargs: dict[str, Any] = field(default_factory=dict)
3838
_fsdp_shard_conditions: list = field(default_factory=lambda: [])
39+
# When True, the tokenizer loader prefers AutoProcessor over AutoTokenizer
40+
# for encoders whose tokenizer dir ships a processor_config.json (e.g. Flux2
41+
# full's Mistral3 multimodal processor). Default False keeps every existing
42+
# encoder on the historical AutoTokenizer path.
43+
require_processor: bool = False
3944

4045
def __post_init__(self) -> None:
4146
self.tokenizer_kwargs = {

0 commit comments

Comments
 (0)