This commit is contained in:
Jaret Burkett committed 2026-08-27 11:53:08 -06:00
1 parent e8d9cf6d35
commit 8db198ec0a
51 files changed
+985 -1273

No files matched your search

@@ -12,6 +12,8 @@ from optimum.quanto import freeze
from toolkit.basic import flush from toolkit.basic import flush
from toolkit.util.quantize import quantize, get_qtype from toolkit.util.quantize import quantize, get_qtype
from toolkit.models.v2.text_encoders.qwen3_vl import patch_qwen_vl_patch_embed
from .BaseCaptioner import BaseCaptioner from .BaseCaptioner import BaseCaptioner
import transformers import transformers
import logging import logging
@@ -19,29 +21,6 @@ import traceback
import warnings import warnings
def patch_qwen_vl_patch_embed(model):
"""Qwen-VL's vision patch_embed is a Conv3d whose kernel == stride, i.e. a plain
linear projection of each flattened patch. bf16 Conv3d has no fast cuDNN kernel and
falls back to a slow, GPU-underutilizing path. Swap it for the equivalent F.linear
(a GEMM). The weight is read lazily so this survives later .to(device)/dtype moves.
Returns the number of patch_embed modules patched."""
patched = 0
for module in model.modules():
proj = getattr(module, "proj", None)
if isinstance(proj, torch.nn.Conv3d) and tuple(proj.kernel_size) == tuple(
proj.stride
):
def fast_forward(hidden_states, _proj=proj):
w = _proj.weight.reshape(_proj.weight.shape[0], -1)
x = hidden_states.view(-1, w.shape[1]).to(w.dtype)
return F.linear(x, w, _proj.bias)
module.forward = fast_forward
patched += 1
return patched
# transformers.logging.set_verbosity_error() # transformers.logging.set_verbosity_error()
warnings.filterwarnings("ignore") warnings.filterwarnings("ignore")
logging.disable(logging.WARNING) logging.disable(logging.WARNING)
@@ -30,6 +30,7 @@ from toolkit.advanced_prompt_embeds import AdvancedPromptEmbeds
from toolkit.basic import flush from toolkit.basic import flush
from toolkit.config_modules import GenerateImageConfig, ModelConfig from toolkit.config_modules import GenerateImageConfig, ModelConfig
from toolkit.models.base_model import BaseModel from toolkit.models.base_model import BaseModel
from toolkit.models.v2.text_encoders.qwen3_vl import patch_qwen_vl_patch_embed
from toolkit.samplers.custom_flowmatch_sampler import ( from toolkit.samplers.custom_flowmatch_sampler import (
CustomFlowMatchEulerDiscreteScheduler, CustomFlowMatchEulerDiscreteScheduler,
) )
@@ -71,33 +72,6 @@ SYSTEM_PROMPT_T2I = (
HF_TOKEN = os.getenv("HF_TOKEN", None) HF_TOKEN = os.getenv("HF_TOKEN", None)
def patch_qwen_vl_patch_embed(model) -> int:
"""Swap Qwen-VL's vision ``patch_embed`` Conv3d for the equivalent ``F.linear``.
Qwen-VL's patch_embed is a Conv3d whose kernel == stride, i.e. just a linear
projection of each flattened patch. bf16 Conv3d has no fast cuDNN kernel and
falls back to a slow path that effectively locks up image caching for the edit
(TI2I) model. The weight is read lazily so this survives later ``.to()`` moves.
Returns the number of patch_embed modules patched. (Vendored from
extensions_built_in/captioner/Qwen3VLCaptioner.py.)
"""
patched = 0
for module in model.modules():
proj = getattr(module, "proj", None)
if isinstance(proj, torch.nn.Conv3d) and tuple(proj.kernel_size) == tuple(
proj.stride
):
def fast_forward(hidden_states, _proj=proj):
w = _proj.weight.reshape(_proj.weight.shape[0], -1)
x = hidden_states.view(-1, w.shape[1]).to(w.dtype)
return F.linear(x, w, _proj.bias)
module.forward = fast_forward
patched += 1
return patched
class BooguImageModel(BaseModel): class BooguImageModel(BaseModel):
arch = "boogu_image" arch = "boogu_image"
# Default HF repo when model.name_or_path is unset (overridden by the edit model). # Default HF repo when model.name_or_path is unset (overridden by the edit model).
@@ -167,8 +141,8 @@ class BooguImageModel(BaseModel):
# deserialize -- use the bf16 repo and let ai-toolkit quantize if wanted. # deserialize -- use the bf16 repo and let ai-toolkit quantize if wanted.
self.print_and_status_update("Loading transformer") self.print_and_status_update("Loading transformer")
try: try:
transformer = BooguImageTransformer2DModel.from_pretrained( transformer = BooguImageTransformer2DModel.load_model(
base, subfolder="transformer", torch_dtype=dtype, token=HF_TOKEN base, dtype=dtype, token=HF_TOKEN
) )
except OSError as e: except OSError as e:
raise OSError( raise OSError(
@@ -21,6 +21,8 @@ from typing import Any, Dict, List, Optional, Tuple, Union
import torch import torch
import torch.nn as nn import torch.nn as nn
from diffusers.configuration_utils import ConfigMixin, register_to_config from diffusers.configuration_utils import ConfigMixin, register_to_config
from toolkit.models.v2._mixin import OstrisModelMixin
from diffusers.loaders import PeftAdapterMixin from diffusers.loaders import PeftAdapterMixin
from diffusers.loaders.single_file_model import FromOriginalModelMixin from diffusers.loaders.single_file_model import FromOriginalModelMixin
from diffusers.models.attention_processor import Attention from diffusers.models.attention_processor import Attention
@@ -490,10 +492,17 @@ class BooguImageDoubleStreamTransformerBlock(nn.Module):
class BooguImageTransformer2DModel( class BooguImageTransformer2DModel(
ModelMixin, ConfigMixin, PeftAdapterMixin, FromOriginalModelMixin ModelMixin, ConfigMixin, PeftAdapterMixin, FromOriginalModelMixin, OstrisModelMixin
): ):
"""Boogu-Image transformer with mixed double-stream -> single-stream topology.""" """Boogu-Image transformer with mixed double-stream -> single-stream topology."""
aitk_subfolder = "transformer"
@classmethod
def get_transformer_block_names(cls):
return ["double_stream_layers", "single_stream_layers"]
_supports_gradient_checkpointing = True _supports_gradient_checkpointing = True
_no_split_modules = [ _no_split_modules = [
"BooguImageTransformerBlock", "BooguImageTransformerBlock",
@@ -5,8 +5,9 @@ import torch
from toolkit.config_modules import GenerateImageConfig, ModelConfig from toolkit.config_modules import GenerateImageConfig, ModelConfig
from PIL import Image from PIL import Image
from toolkit.models.base_model import BaseModel from toolkit.models.base_model import BaseModel
from toolkit.models.v2.text_encoders.t5 import T5TextEncoder
from toolkit.models.v2.vae.autoencoder_kl import KLVAE
from toolkit.basic import flush from toolkit.basic import flush
from diffusers import AutoencoderKL
# from toolkit.pixel_shuffle_encoder import AutoencoderPixelMixer # from toolkit.pixel_shuffle_encoder import AutoencoderPixelMixer
from toolkit.prompt_utils import PromptEmbeds from toolkit.prompt_utils import PromptEmbeds
from toolkit.samplers.custom_flowmatch_sampler import CustomFlowMatchEulerDiscreteScheduler from toolkit.samplers.custom_flowmatch_sampler import CustomFlowMatchEulerDiscreteScheduler
@@ -14,7 +15,6 @@ from toolkit.dequantize import patch_dequantization_on_save
from toolkit.accelerator import unwrap_model from toolkit.accelerator import unwrap_model
from optimum.quanto import freeze, QTensor from optimum.quanto import freeze, QTensor
from toolkit.util.quantize import quantize, get_qtype from toolkit.util.quantize import quantize, get_qtype
from transformers import T5TokenizerFast, T5EncoderModel, CLIPTextModel, CLIPTokenizer
from .pipeline import ChromaPipeline, prepare_latent_image_ids from .pipeline import ChromaPipeline, prepare_latent_image_ids
from einops import rearrange, repeat from einops import rearrange, repeat
import random import random
@@ -148,37 +148,17 @@ class ChromaModel(BaseModel):
self.print_and_status_update("Loading transformer") self.print_and_status_update("Loading transformer")
chroma_state_dict = load_file(model_path, 'cpu') if model_path.endswith(".safetensors"):
transformer = Chroma.load_model(model_path, dtype=dtype)
# determine number of double and single blocks else:
double_blocks = 0 transformer = Chroma.load_from_state_dict(load_file(model_path, "cpu"), dtype)
single_blocks = 0
for key in chroma_state_dict.keys():
if "double_blocks" in key:
block_num = int(key.split(".")[1]) + 1
if block_num > double_blocks:
double_blocks = block_num
elif "single_blocks" in key:
block_num = int(key.split(".")[1]) + 1
if block_num > single_blocks:
single_blocks = block_num
print(f"Double Blocks: {double_blocks}")
print(f"Single Blocks: {single_blocks}")
chroma_params.depth = double_blocks
chroma_params.depth_single_blocks = single_blocks
transformer = Chroma(chroma_params)
# add dtype, not sure why it doesnt have it # add dtype, not sure why it doesnt have it
transformer.dtype = dtype transformer.dtype = dtype
# load the state dict into the model
transformer.load_state_dict(chroma_state_dict)
transformer.to(self.quantize_device, dtype=dtype) transformer.to(self.quantize_device, dtype=dtype)
transformer.config = FakeConfig() transformer.config = FakeConfig()
transformer.config.num_layers = double_blocks transformer.config.num_layers = transformer.params.depth
transformer.config.num_single_layers = single_blocks transformer.config.num_single_layers = transformer.params.depth_single_blocks
if self.model_config.quantize: if self.model_config.quantize:
# patch the state dict method # patch the state dict method
@@ -195,21 +175,9 @@ class ChromaModel(BaseModel):
flush() flush()
self.print_and_status_update("Loading T5") self.print_and_status_update("Loading T5")
tokenizer_2 = T5TokenizerFast.from_pretrained( tokenizer_2 = T5TextEncoder.load_tokenizer(extras_path)
extras_path, subfolder="tokenizer_2", torch_dtype=dtype text_encoder_2 = T5TextEncoder.load_model(extras_path, dtype=dtype)
) self.prepare_text_encoder(text_encoder_2, dtype=dtype)
text_encoder_2 = T5EncoderModel.from_pretrained(
extras_path, subfolder="text_encoder_2", torch_dtype=dtype
)
text_encoder_2.to(self.device_torch, dtype=dtype)
flush()
if self.model_config.quantize_te:
self.print_and_status_update("Quantizing T5")
quantize(text_encoder_2, weights=get_qtype(
self.model_config.qtype))
freeze(text_encoder_2)
flush()
# self.print_and_status_update("Loading CLIP") # self.print_and_status_update("Loading CLIP")
text_encoder = FakeCLIP() text_encoder = FakeCLIP()
@@ -219,12 +187,7 @@ class ChromaModel(BaseModel):
self.noise_scheduler = ChromaModel.get_train_scheduler() self.noise_scheduler = ChromaModel.get_train_scheduler()
self.print_and_status_update("Loading VAE") self.print_and_status_update("Loading VAE")
vae = AutoencoderKL.from_pretrained( vae = KLVAE.load_model(extras_path, dtype=dtype, device=self.device_torch)
extras_path,
subfolder="vae",
torch_dtype=dtype
)
vae = vae.to(self.device_torch, dtype=dtype)
self.print_and_status_update("Making pipe") self.print_and_status_update("Making pipe")
@@ -5,8 +5,8 @@ import torch
from toolkit.config_modules import GenerateImageConfig, ModelConfig from toolkit.config_modules import GenerateImageConfig, ModelConfig
from PIL import Image from PIL import Image
from toolkit.models.base_model import BaseModel from toolkit.models.base_model import BaseModel
from toolkit.models.v2.text_encoders.t5 import T5TextEncoder
from toolkit.basic import flush from toolkit.basic import flush
from diffusers import AutoencoderKL
# from toolkit.pixel_shuffle_encoder import AutoencoderPixelMixer # from toolkit.pixel_shuffle_encoder import AutoencoderPixelMixer
from toolkit.prompt_utils import PromptEmbeds from toolkit.prompt_utils import PromptEmbeds
from toolkit.samplers.custom_flowmatch_sampler import CustomFlowMatchEulerDiscreteScheduler from toolkit.samplers.custom_flowmatch_sampler import CustomFlowMatchEulerDiscreteScheduler
@@ -14,7 +14,6 @@ from toolkit.dequantize import patch_dequantization_on_save
from toolkit.accelerator import unwrap_model from toolkit.accelerator import unwrap_model
from optimum.quanto import freeze, QTensor from optimum.quanto import freeze, QTensor
from toolkit.util.quantize import quantize, get_qtype from toolkit.util.quantize import quantize, get_qtype
from transformers import T5TokenizerFast, T5EncoderModel, CLIPTextModel, CLIPTokenizer
from .pipeline import ChromaPipeline, prepare_latent_image_ids from .pipeline import ChromaPipeline, prepare_latent_image_ids
from einops import rearrange, repeat from einops import rearrange, repeat
import random import random
@@ -152,38 +151,16 @@ class ChromaRadianceModel(BaseModel):
if model_path.endswith('.pth') or model_path.endswith('.pt'): if model_path.endswith('.pth') or model_path.endswith('.pt'):
chroma_state_dict = torch.load(model_path, map_location='cpu', weights_only=True) chroma_state_dict = torch.load(model_path, map_location='cpu', weights_only=True)
transformer = Chroma.load_from_state_dict(chroma_state_dict, dtype)
else: else:
chroma_state_dict = load_file(model_path, 'cpu') transformer = Chroma.load_model(model_path, dtype=dtype)
# determine number of double and single blocks
double_blocks = 0
single_blocks = 0
for key in chroma_state_dict.keys():
if "double_blocks" in key:
block_num = int(key.split(".")[1]) + 1
if block_num > double_blocks:
double_blocks = block_num
elif "single_blocks" in key:
block_num = int(key.split(".")[1]) + 1
if block_num > single_blocks:
single_blocks = block_num
print(f"Double Blocks: {double_blocks}")
print(f"Single Blocks: {single_blocks}")
chroma_params.depth = double_blocks
chroma_params.depth_single_blocks = single_blocks
transformer = Chroma(chroma_params)
# add dtype, not sure why it doesnt have it # add dtype, not sure why it doesnt have it
transformer.dtype = dtype transformer.dtype = dtype
# load the state dict into the model
transformer.load_state_dict(chroma_state_dict)
transformer.to(self.quantize_device, dtype=dtype) transformer.to(self.quantize_device, dtype=dtype)
transformer.config = FakeConfig() transformer.config = FakeConfig()
transformer.config.num_layers = double_blocks transformer.config.num_layers = transformer.params.depth
transformer.config.num_single_layers = single_blocks transformer.config.num_single_layers = transformer.params.depth_single_blocks
if self.model_config.quantize: if self.model_config.quantize:
# patch the state dict method # patch the state dict method
@@ -200,21 +177,9 @@ class ChromaRadianceModel(BaseModel):
flush() flush()
self.print_and_status_update("Loading T5") self.print_and_status_update("Loading T5")
tokenizer_2 = T5TokenizerFast.from_pretrained( tokenizer_2 = T5TextEncoder.load_tokenizer(extras_path)
extras_path, subfolder="tokenizer_2", torch_dtype=dtype text_encoder_2 = T5TextEncoder.load_model(extras_path, dtype=dtype)
) self.prepare_text_encoder(text_encoder_2, dtype=dtype)
text_encoder_2 = T5EncoderModel.from_pretrained(
extras_path, subfolder="text_encoder_2", torch_dtype=dtype
)
text_encoder_2.to(self.device_torch, dtype=dtype)
flush()
if self.model_config.quantize_te:
self.print_and_status_update("Quantizing T5")
quantize(text_encoder_2, weights=get_qtype(
self.model_config.qtype))
freeze(text_encoder_2)
flush()
# self.print_and_status_update("Loading CLIP") # self.print_and_status_update("Loading CLIP")
text_encoder = FakeCLIP() text_encoder = FakeCLIP()
@@ -1,4 +1,6 @@
from dataclasses import dataclass from dataclasses import dataclass, replace
from toolkit.models.v2._mixin import OstrisModelMixin
import torch import torch
from torch import Tensor, nn from torch import Tensor, nn
@@ -86,11 +88,40 @@ def modify_mask_to_attend_padding(mask, max_seq_length, num_extra_padding=8):
return modified_mask return modified_mask
class Chroma(nn.Module): class Chroma(nn.Module, OstrisModelMixin):
""" """
Transformer model for flow matching on sequences. Transformer model for flow matching on sequences.
""" """
@classmethod
def aitk_config_from_state_dict(cls, state_dict):
# block counts come from the checkpoint's key indices
double_blocks = 0
single_blocks = 0
for key in state_dict.keys():
if "double_blocks" in key:
block_num = int(key.split(".")[1]) + 1
if block_num > double_blocks:
double_blocks = block_num
elif "single_blocks" in key:
block_num = int(key.split(".")[1]) + 1
if block_num > single_blocks:
single_blocks = block_num
print(f"Double Blocks: {double_blocks}")
print(f"Single Blocks: {single_blocks}")
return replace(
chroma_params, depth=double_blocks, depth_single_blocks=single_blocks
)
@classmethod
def aitk_from_config(cls, config):
with torch.device("meta"):
return cls(config)
@classmethod
def get_transformer_block_names(cls):
return ["double_blocks", "single_blocks"]
def __init__(self, params: ChromaParams): def __init__(self, params: ChromaParams):
super().__init__() super().__init__()
self.params = params self.params = params
@@ -1,4 +1,6 @@
from dataclasses import dataclass from dataclasses import dataclass, replace
from toolkit.models.v2._mixin import OstrisModelMixin
import torch import torch
from torch import Tensor, nn from torch import Tensor, nn
@@ -100,11 +102,40 @@ def modify_mask_to_attend_padding(mask, max_seq_length, num_extra_padding=8):
return modified_mask return modified_mask
class Chroma(nn.Module): class Chroma(nn.Module, OstrisModelMixin):
""" """
Transformer model for flow matching on sequences. Transformer model for flow matching on sequences.
""" """
@classmethod
def aitk_config_from_state_dict(cls, state_dict):
# block counts come from the checkpoint's key indices
double_blocks = 0
single_blocks = 0
for key in state_dict.keys():
if "double_blocks" in key:
block_num = int(key.split(".")[1]) + 1
if block_num > double_blocks:
double_blocks = block_num
elif "single_blocks" in key:
block_num = int(key.split(".")[1]) + 1
if block_num > single_blocks:
single_blocks = block_num
print(f"Double Blocks: {double_blocks}")
print(f"Single Blocks: {single_blocks}")
return replace(
chroma_params, depth=double_blocks, depth_single_blocks=single_blocks
)
@classmethod
def aitk_from_config(cls, config):
with torch.device("meta"):
return cls(config)
@classmethod
def get_transformer_block_names(cls):
return ["double_blocks", "single_blocks"]
def __init__(self, params: ChromaParams): def __init__(self, params: ChromaParams):
super().__init__() super().__init__()
self.params = params self.params = params
@@ -79,20 +79,14 @@ class ErnieImageModel(BaseModel):
self.print_and_status_update("Loading transformer") self.print_and_status_update("Loading transformer")
transformer_path = model_path if os.path.exists(model_path):
transformer_subfolder = "transformer"
if os.path.exists(transformer_path):
transformer_subfolder = None
transformer_path = os.path.join(transformer_path, "transformer")
# check if the path is a full checkpoint. # check if the path is a full checkpoint.
te_folder_path = os.path.join(model_path, "text_encoder") te_folder_path = os.path.join(model_path, "text_encoder")
# if we have the te, this folder is a full checkpoint, use it as the base # if we have the te, this folder is a full checkpoint, use it as the base
if os.path.exists(te_folder_path): if os.path.exists(te_folder_path):
base_model_path = model_path base_model_path = model_path
transformer = ErnieImageTransformer2DModel.from_pretrained( transformer = ErnieImageTransformer2DModel.load_model(model_path, dtype=dtype)
transformer_path, subfolder=transformer_subfolder, torch_dtype=dtype
)
if self.model_config.quantize: if self.model_config.quantize:
self.print_and_status_update("Quantizing Transformer") self.print_and_status_update("Quantizing Transformer")
@@ -33,6 +33,8 @@ from diffusers.models.attention_dispatch import dispatch_attention_fn
from diffusers.models.attention_processor import Attention from diffusers.models.attention_processor import Attention
from diffusers.models.embeddings import TimestepEmbedding, Timesteps from diffusers.models.embeddings import TimestepEmbedding, Timesteps
from diffusers.models.modeling_utils import ModelMixin from diffusers.models.modeling_utils import ModelMixin
from toolkit.models.v2._mixin import OstrisModelMixin
from diffusers.models.normalization import RMSNorm from diffusers.models.normalization import RMSNorm
@@ -288,8 +290,14 @@ class ErnieImageAdaLNContinuous(nn.Module):
return x return x
class ErnieImageTransformer2DModel(ModelMixin, ConfigMixin): class ErnieImageTransformer2DModel(ModelMixin, ConfigMixin, OstrisModelMixin):
_supports_gradient_checkpointing = True _supports_gradient_checkpointing = True
aitk_subfolder = "transformer"
@classmethod
def get_transformer_block_names(cls):
return ["layers"]
@register_to_config @register_to_config
def __init__( def __init__(
@@ -6,15 +6,15 @@ import yaml
from toolkit.config_modules import GenerateImageConfig, ModelConfig from toolkit.config_modules import GenerateImageConfig, ModelConfig
from PIL import Image from PIL import Image
from toolkit.models.base_model import BaseModel from toolkit.models.base_model import BaseModel
from toolkit.models.v2.text_encoders.t5 import T5TextEncoder
from toolkit.models.v2.vae.autoencoder_kl import KLVAE
from toolkit.basic import flush from toolkit.basic import flush
from diffusers import AutoencoderKL
from toolkit.prompt_utils import PromptEmbeds from toolkit.prompt_utils import PromptEmbeds
from toolkit.samplers.custom_flowmatch_sampler import CustomFlowMatchEulerDiscreteScheduler from toolkit.samplers.custom_flowmatch_sampler import CustomFlowMatchEulerDiscreteScheduler
from toolkit.dequantize import patch_dequantization_on_save from toolkit.dequantize import patch_dequantization_on_save
from toolkit.accelerator import unwrap_model from toolkit.accelerator import unwrap_model
from optimum.quanto import freeze, QTensor from optimum.quanto import freeze, QTensor
from toolkit.util.quantize import quantize, get_qtype from toolkit.util.quantize import quantize, get_qtype
from transformers import T5TokenizerFast, T5EncoderModel
from .src import FLitePipeline, DiT from .src import FLitePipeline, DiT
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -74,11 +74,7 @@ class FLiteModel(BaseModel):
self.print_and_status_update("Loading transformer") self.print_and_status_update("Loading transformer")
transformer = DiT.from_pretrained( transformer = DiT.load_model(model_path, dtype=dtype)
model_path,
subfolder="dit_model",
torch_dtype=dtype,
)
transformer.to(self.quantize_device, dtype=dtype) transformer.to(self.quantize_device, dtype=dtype)
@@ -97,31 +93,16 @@ class FLiteModel(BaseModel):
flush() flush()
self.print_and_status_update("Loading T5") self.print_and_status_update("Loading T5")
tokenizer = T5TokenizerFast.from_pretrained( tokenizer = T5TextEncoder.load_tokenizer(extras_path, subfolder="tokenizer")
extras_path, subfolder="tokenizer", torch_dtype=dtype text_encoder = T5TextEncoder.load_model(
extras_path, dtype=dtype, subfolder="text_encoder"
) )
text_encoder = T5EncoderModel.from_pretrained( self.prepare_text_encoder(text_encoder, dtype=dtype)
extras_path, subfolder="text_encoder", torch_dtype=dtype
)
text_encoder.to(self.device_torch, dtype=dtype)
flush()
if self.model_config.quantize_te:
self.print_and_status_update("Quantizing T5")
quantize(text_encoder, weights=get_qtype(
self.model_config.qtype))
freeze(text_encoder)
flush()
self.noise_scheduler = FLiteModel.get_train_scheduler() self.noise_scheduler = FLiteModel.get_train_scheduler()
self.print_and_status_update("Loading VAE") self.print_and_status_update("Loading VAE")
vae = AutoencoderKL.from_pretrained( vae = KLVAE.load_model(extras_path, dtype=dtype, device=self.device_torch)
extras_path,
subfolder="vae",
torch_dtype=dtype
)
vae = vae.to(self.device_torch, dtype=dtype)
self.print_and_status_update("Making pipe") self.print_and_status_update("Making pipe")
@@ -7,6 +7,8 @@ import torch.nn.functional as F
from diffusers.configuration_utils import ConfigMixin, register_to_config from diffusers.configuration_utils import ConfigMixin, register_to_config
from diffusers.loaders import FromOriginalModelMixin, PeftAdapterMixin from diffusers.loaders import FromOriginalModelMixin, PeftAdapterMixin
from diffusers.models.modeling_utils import ModelMixin from diffusers.models.modeling_utils import ModelMixin
from toolkit.models.v2._mixin import OstrisModelMixin
from diffusers.utils.accelerate_utils import apply_forward_hook from diffusers.utils.accelerate_utils import apply_forward_hook
from einops import rearrange from einops import rearrange
from peft import get_peft_model_state_dict, set_peft_model_state_dict from peft import get_peft_model_state_dict, set_peft_model_state_dict
@@ -302,7 +304,13 @@ def apply_rotary_emb(x, cos, sin):
return torch.cat([y1, y2], 3).to(dtype=orig_dtype) return torch.cat([y1, y2], 3).to(dtype=orig_dtype)
class DiT(ModelMixin, ConfigMixin, FromOriginalModelMixin, PeftAdapterMixin): # type: ignore[misc] class DiT(ModelMixin, ConfigMixin, FromOriginalModelMixin, PeftAdapterMixin, OstrisModelMixin): # type: ignore[misc]
aitk_subfolder = "dit_model"
@classmethod
def get_transformer_block_names(cls):
return ["blocks"]
_supports_gradient_checkpointing = True _supports_gradient_checkpointing = True
@register_to_config @register_to_config
@@ -21,7 +21,11 @@ from toolkit.util.quantize import quantize, get_qtype, quantize_model
from transformers import AutoProcessor, Mistral3ForConditionalGeneration from transformers import AutoProcessor, Mistral3ForConditionalGeneration
from .src.model import Flux2, Flux2Params from .src.model import Flux2, Flux2Params
from .src.pipeline import Flux2Pipeline from .src.pipeline import Flux2Pipeline
from .src.autoencoder import AutoEncoder, AutoEncoderParams, AutoEncoderSmallDecoderParams from toolkit.models.v2.vae.flux2_kl import (
AutoEncoder,
AutoEncoderParams,
AutoEncoderSmallDecoderParams,
)
from safetensors.torch import load_file, save_file from safetensors.torch import load_file, save_file
from PIL import Image from PIL import Image
import torch.nn.functional as F import torch.nn.functional as F
@@ -1,435 +0,0 @@
from dataclasses import dataclass, field
import torch
from einops import rearrange
from torch import Tensor, nn
import math
import torch.utils.checkpoint as ckpt
@dataclass
class AutoEncoderParams:
resolution: int = 256
in_channels: int = 3
ch: int = 128
out_ch: int = 3
ch_mult: list[int] = field(default_factory=lambda: [1, 2, 4, 4])
num_res_blocks: int = 2
z_channels: int = 32
@dataclass
class AutoEncoderSmallDecoderParams:
resolution: int = 256
in_channels: int = 3
ch: int = 128
ch_encoder: int = 96
out_ch: int = 3
ch_mult: list[int] = field(default_factory=lambda: [1, 2, 4, 4])
num_res_blocks: int = 2
z_channels: int = 32
def swish(x: Tensor) -> Tensor:
return x * torch.sigmoid(x)
class AttnBlock(nn.Module):
def __init__(self, in_channels: int):
super().__init__()
self.in_channels = in_channels
self.norm = nn.GroupNorm(
num_groups=32, num_channels=in_channels, eps=1e-6, affine=True
)
self.q = nn.Conv2d(in_channels, in_channels, kernel_size=1)
self.k = nn.Conv2d(in_channels, in_channels, kernel_size=1)
self.v = nn.Conv2d(in_channels, in_channels, kernel_size=1)
self.proj_out = nn.Conv2d(in_channels, in_channels, kernel_size=1)
def attention(self, h_: Tensor) -> Tensor:
h_ = self.norm(h_)
q = self.q(h_)
k = self.k(h_)
v = self.v(h_)
b, c, h, w = q.shape
q = rearrange(q, "b c h w -> b 1 (h w) c").contiguous()
k = rearrange(k, "b c h w -> b 1 (h w) c").contiguous()
v = rearrange(v, "b c h w -> b 1 (h w) c").contiguous()
h_ = nn.functional.scaled_dot_product_attention(q, k, v)
return rearrange(h_, "b 1 (h w) c -> b c h w", h=h, w=w, c=c, b=b)
def forward(self, x: Tensor) -> Tensor:
return x + self.proj_out(self.attention(x))
class ResnetBlock(nn.Module):
def __init__(self, in_channels: int, out_channels: int):
super().__init__()
self.in_channels = in_channels
out_channels = in_channels if out_channels is None else out_channels
self.out_channels = out_channels
self.norm1 = nn.GroupNorm(
num_groups=32, num_channels=in_channels, eps=1e-6, affine=True
)
self.conv1 = nn.Conv2d(
in_channels, out_channels, kernel_size=3, stride=1, padding=1
)
self.norm2 = nn.GroupNorm(
num_groups=32, num_channels=out_channels, eps=1e-6, affine=True
)
self.conv2 = nn.Conv2d(
out_channels, out_channels, kernel_size=3, stride=1, padding=1
)
if self.in_channels != self.out_channels:
self.nin_shortcut = nn.Conv2d(
in_channels, out_channels, kernel_size=1, stride=1, padding=0
)
def forward(self, x):
h = x
h = self.norm1(h)
h = swish(h)
h = self.conv1(h)
h = self.norm2(h)
h = swish(h)
h = self.conv2(h)
if self.in_channels != self.out_channels:
x = self.nin_shortcut(x)
return x + h
class Downsample(nn.Module):
def __init__(self, in_channels: int):
super().__init__()
# no asymmetric padding in torch conv, must do it ourselves
self.conv = nn.Conv2d(
in_channels, in_channels, kernel_size=3, stride=2, padding=0
)
def forward(self, x: Tensor):
pad = (0, 1, 0, 1)
x = nn.functional.pad(x, pad, mode="constant", value=0)
x = self.conv(x)
return x
class Upsample(nn.Module):
def __init__(self, in_channels: int):
super().__init__()
self.conv = nn.Conv2d(
in_channels, in_channels, kernel_size=3, stride=1, padding=1
)
def forward(self, x: Tensor):
x = nn.functional.interpolate(x, scale_factor=2.0, mode="nearest")
x = self.conv(x)
return x
class Encoder(nn.Module):
def __init__(
self,
resolution: int,
in_channels: int,
ch: int,
ch_mult: list[int],
num_res_blocks: int,
z_channels: int,
):
super().__init__()
self.quant_conv = torch.nn.Conv2d(2 * z_channels, 2 * z_channels, 1)
self.ch = ch
self.num_resolutions = len(ch_mult)
self.num_res_blocks = num_res_blocks
self.resolution = resolution
self.in_channels = in_channels
# downsampling
self.conv_in = nn.Conv2d(
in_channels, self.ch, kernel_size=3, stride=1, padding=1
)
curr_res = resolution
in_ch_mult = (1,) + tuple(ch_mult)
self.in_ch_mult = in_ch_mult
self.down = nn.ModuleList()
block_in = self.ch
for i_level in range(self.num_resolutions):
block = nn.ModuleList()
attn = nn.ModuleList()
block_in = ch * in_ch_mult[i_level]
block_out = ch * ch_mult[i_level]
for _ in range(self.num_res_blocks):
block.append(ResnetBlock(in_channels=block_in, out_channels=block_out))
block_in = block_out
down = nn.Module()
down.block = block
down.attn = attn
if i_level != self.num_resolutions - 1:
down.downsample = Downsample(block_in)
curr_res = curr_res // 2
self.down.append(down)
# middle
self.mid = nn.Module()
self.mid.block_1 = ResnetBlock(in_channels=block_in, out_channels=block_in)
self.mid.attn_1 = AttnBlock(block_in)
self.mid.block_2 = ResnetBlock(in_channels=block_in, out_channels=block_in)
# end
self.norm_out = nn.GroupNorm(
num_groups=32, num_channels=block_in, eps=1e-6, affine=True
)
self.conv_out = nn.Conv2d(
block_in, 2 * z_channels, kernel_size=3, stride=1, padding=1
)
self.gradient_checkpointing = False
def enable_gradient_checkpointing(self):
self.gradient_checkpointing = True
def forward(self, x: Tensor) -> Tensor:
# downsampling
hs = [self.conv_in(x)]
for i_level in range(self.num_resolutions):
for i_block in range(self.num_res_blocks):
if torch.is_grad_enabled() and self.gradient_checkpointing:
h = ckpt.checkpoint(self.down[i_level].block[i_block], hs[-1])
if len(self.down[i_level].attn) > 0:
h = ckpt.checkpoint(self.down[i_level].attn[i_block], h)
else:
h = self.down[i_level].block[i_block](hs[-1])
if len(self.down[i_level].attn) > 0:
h = self.down[i_level].attn[i_block](h)
hs.append(h)
if i_level != self.num_resolutions - 1:
if torch.is_grad_enabled() and self.gradient_checkpointing:
hs.append(ckpt.checkpoint(self.down[i_level].downsample, hs[-1]))
else:
hs.append(self.down[i_level].downsample(hs[-1]))
# middle
h = hs[-1]
if torch.is_grad_enabled() and self.gradient_checkpointing:
h = ckpt.checkpoint(self.mid.block_1, h)
h = ckpt.checkpoint(self.mid.attn_1, h)
h = ckpt.checkpoint(self.mid.block_2, h)
else:
h = self.mid.block_1(h)
h = self.mid.attn_1(h)
h = self.mid.block_2(h)
# end
h = self.norm_out(h)
h = swish(h)
h = self.conv_out(h)
h = self.quant_conv(h)
return h
class Decoder(nn.Module):
def __init__(
self,
ch: int,
out_ch: int,
ch_mult: list[int],
num_res_blocks: int,
in_channels: int,
resolution: int,
z_channels: int,
):
super().__init__()
self.post_quant_conv = torch.nn.Conv2d(z_channels, z_channels, 1)
self.ch = ch
self.num_resolutions = len(ch_mult)
self.num_res_blocks = num_res_blocks
self.resolution = resolution
self.in_channels = in_channels
self.ffactor = 2 ** (self.num_resolutions - 1)
# compute in_ch_mult, block_in and curr_res at lowest res
block_in = ch * ch_mult[self.num_resolutions - 1]
curr_res = resolution // 2 ** (self.num_resolutions - 1)
self.z_shape = (1, z_channels, curr_res, curr_res)
# z to block_in
self.conv_in = nn.Conv2d(
z_channels, block_in, kernel_size=3, stride=1, padding=1
)
# middle
self.mid = nn.Module()
self.mid.block_1 = ResnetBlock(in_channels=block_in, out_channels=block_in)
self.mid.attn_1 = AttnBlock(block_in)
self.mid.block_2 = ResnetBlock(in_channels=block_in, out_channels=block_in)
# upsampling
self.up = nn.ModuleList()
for i_level in reversed(range(self.num_resolutions)):
block = nn.ModuleList()
attn = nn.ModuleList()
block_out = ch * ch_mult[i_level]
for _ in range(self.num_res_blocks + 1):
block.append(ResnetBlock(in_channels=block_in, out_channels=block_out))
block_in = block_out
up = nn.Module()
up.block = block
up.attn = attn
if i_level != 0:
up.upsample = Upsample(block_in)
curr_res = curr_res * 2
self.up.insert(0, up) # prepend to get consistent order
# end
self.norm_out = nn.GroupNorm(
num_groups=32, num_channels=block_in, eps=1e-6, affine=True
)
self.conv_out = nn.Conv2d(block_in, out_ch, kernel_size=3, stride=1, padding=1)
self.gradient_checkpointing = False
def enable_gradient_checkpointing(self):
self.gradient_checkpointing = True
def forward(self, z: Tensor) -> Tensor:
z = self.post_quant_conv(z)
# get dtype for proper tracing
upscale_dtype = next(self.up.parameters()).dtype
# z to block_in
h = self.conv_in(z)
# middle
if torch.is_grad_enabled() and self.gradient_checkpointing:
h = ckpt.checkpoint(self.mid.block_1, h)
h = ckpt.checkpoint(self.mid.attn_1, h)
h = ckpt.checkpoint(self.mid.block_2, h)
else:
h = self.mid.block_1(h)
h = self.mid.attn_1(h)
h = self.mid.block_2(h)
# cast to proper dtype
h = h.to(upscale_dtype)
# upsampling
for i_level in reversed(range(self.num_resolutions)):
for i_block in range(self.num_res_blocks + 1):
if torch.is_grad_enabled() and self.gradient_checkpointing:
h = ckpt.checkpoint(self.up[i_level].block[i_block], h)
if len(self.up[i_level].attn) > 0:
h = ckpt.checkpoint(self.up[i_level].attn[i_block], h)
else:
h = self.up[i_level].block[i_block](h)
if len(self.up[i_level].attn) > 0:
h = self.up[i_level].attn[i_block](h)
if i_level != 0:
if torch.is_grad_enabled() and self.gradient_checkpointing:
h = ckpt.checkpoint(self.up[i_level].upsample, h)
else:
h = self.up[i_level].upsample(h)
# end
h = self.norm_out(h)
h = swish(h)
h = self.conv_out(h)
return h
class AutoEncoder(nn.Module):
def __init__(self, params: AutoEncoderParams):
super().__init__()
self.params = params
self.encoder = Encoder(
resolution=params.resolution,
in_channels=params.in_channels,
ch=params.ch,
ch_mult=params.ch_mult,
num_res_blocks=params.num_res_blocks,
z_channels=params.z_channels,
)
decoder_ch = params.ch
if hasattr(params, "ch_encoder"):
decoder_ch = params.ch_encoder
self.decoder = Decoder(
resolution=params.resolution,
in_channels=params.in_channels,
ch=decoder_ch,
out_ch=params.out_ch,
ch_mult=params.ch_mult,
num_res_blocks=params.num_res_blocks,
z_channels=params.z_channels,
)
self.bn_eps = 1e-4
self.bn_momentum = 0.1
self.ps = [2, 2]
self.bn = torch.nn.BatchNorm2d(
math.prod(self.ps) * params.z_channels,
eps=self.bn_eps,
momentum=self.bn_momentum,
affine=False,
track_running_stats=True,
)
self._gradient_checkpointing = False
@property
def gradient_checkpointing(self):
return self._gradient_checkpointing
@gradient_checkpointing.setter
def gradient_checkpointing(self, value: bool):
self._gradient_checkpointing = value
self.encoder.gradient_checkpointing = value
self.decoder.gradient_checkpointing = value
@property
def device(self):
return next(self.parameters()).device
@property
def dtype(self):
return next(self.parameters()).dtype
def enable_gradient_checkpointing(self):
self.gradient_checkpointing = True
self.encoder.enable_gradient_checkpointing()
self.decoder.enable_gradient_checkpointing()
def normalize(self, z):
self.bn.eval()
return self.bn(z)
def inv_normalize(self, z):
self.bn.eval()
s = torch.sqrt(self.bn.running_var.view(1, -1, 1, 1) + self.bn_eps)
m = self.bn.running_mean.view(1, -1, 1, 1)
return z * s + m
def encode(self, x: Tensor) -> Tensor:
moments = self.encoder(x)
mean = torch.chunk(moments, 2, dim=1)[0]
z = rearrange(
mean,
"... c (i pi) (j pj) -> ... (c pi pj) i j",
pi=self.ps[0],
pj=self.ps[1],
)
z = self.normalize(z)
return z
def decode(self, z: Tensor) -> Tensor:
z = self.inv_normalize(z)
z = rearrange(
z,
"... (c pi pj) i j -> ... c (i pi) (j pj)",
pi=self.ps[0],
pj=self.ps[1],
)
dec = self.decoder(z)
return dec
@@ -12,7 +12,7 @@ from diffusers.utils import (
from diffusers.utils.torch_utils import randn_tensor from diffusers.utils.torch_utils import randn_tensor
from diffusers.pipelines.pipeline_utils import DiffusionPipeline from diffusers.pipelines.pipeline_utils import DiffusionPipeline
from diffusers.utils import BaseOutput from diffusers.utils import BaseOutput
from .autoencoder import AutoEncoder from toolkit.models.v2.vae.flux2_kl import AutoEncoder
from .model import Flux2 from .model import Flux2
from einops import rearrange from einops import rearrange
from transformers import AutoProcessor, Mistral3ForConditionalGeneration from transformers import AutoProcessor, Mistral3ForConditionalGeneration
@@ -8,7 +8,11 @@ from toolkit import train_tools
from toolkit.config_modules import GenerateImageConfig, ModelConfig from toolkit.config_modules import GenerateImageConfig, ModelConfig
from PIL import Image from PIL import Image
from toolkit.models.base_model import BaseModel from toolkit.models.base_model import BaseModel
from diffusers import FluxTransformer2DModel, AutoencoderKL, FluxKontextPipeline from toolkit.models.v2.text_encoders.t5 import T5TextEncoder
from toolkit.models.v2.text_encoders.clip import CLIPTextEncoder
from toolkit.models.v2.vae.autoencoder_kl import KLVAE
from diffusers import FluxKontextPipeline
from toolkit.models.v2.diffusion_models.flux import FluxTransformer2DModel
from toolkit.basic import flush from toolkit.basic import flush
from toolkit.prompt_utils import PromptEmbeds from toolkit.prompt_utils import PromptEmbeds
from toolkit.samplers.custom_flowmatch_sampler import CustomFlowMatchEulerDiscreteScheduler from toolkit.samplers.custom_flowmatch_sampler import CustomFlowMatchEulerDiscreteScheduler
@@ -18,7 +22,6 @@ from toolkit.accelerator import get_accelerator, unwrap_model
from optimum.quanto import freeze, QTensor from optimum.quanto import freeze, QTensor
from toolkit.util.mask import generate_random_mask, random_dialate_mask from toolkit.util.mask import generate_random_mask, random_dialate_mask
from toolkit.util.quantize import quantize, get_qtype from toolkit.util.quantize import quantize, get_qtype
from transformers import T5TokenizerFast, T5EncoderModel, CLIPTextModel, CLIPTokenizer
from einops import rearrange, repeat from einops import rearrange, repeat
import random import random
import torch.nn.functional as F import torch.nn.functional as F
@@ -80,11 +83,7 @@ class FluxKontextModel(BaseModel):
# so we need this for the VAE, te, etc # so we need this for the VAE, te, etc
base_model_path = self.model_config.extras_name_or_path base_model_path = self.model_config.extras_name_or_path
transformer_path = model_path if os.path.exists(model_path):
transformer_subfolder = 'transformer'
if os.path.exists(transformer_path):
transformer_subfolder = None
transformer_path = os.path.join(transformer_path, 'transformer')
# check if the path is a full checkpoint. # check if the path is a full checkpoint.
te_folder_path = os.path.join(model_path, 'text_encoder') te_folder_path = os.path.join(model_path, 'text_encoder')
# if we have the te, this folder is a full checkpoint, use it as the base # if we have the te, this folder is a full checkpoint, use it as the base
@@ -92,11 +91,7 @@ class FluxKontextModel(BaseModel):
base_model_path = model_path base_model_path = model_path
self.print_and_status_update("Loading transformer") self.print_and_status_update("Loading transformer")
transformer = FluxTransformer2DModel.from_pretrained( transformer = FluxTransformer2DModel.load_model(model_path, dtype=dtype)
transformer_path,
subfolder=transformer_subfolder,
torch_dtype=dtype
)
transformer.to(self.quantize_device, dtype=dtype) transformer.to(self.quantize_device, dtype=dtype)
if self.model_config.quantize: if self.model_config.quantize:
@@ -114,32 +109,18 @@ class FluxKontextModel(BaseModel):
flush() flush()
self.print_and_status_update("Loading T5") self.print_and_status_update("Loading T5")
tokenizer_2 = T5TokenizerFast.from_pretrained( tokenizer_2 = T5TextEncoder.load_tokenizer(base_model_path)
base_model_path, subfolder="tokenizer_2", torch_dtype=dtype text_encoder_2 = T5TextEncoder.load_model(base_model_path, dtype=dtype)
) self.prepare_text_encoder(text_encoder_2, dtype=dtype)
text_encoder_2 = T5EncoderModel.from_pretrained(
base_model_path, subfolder="text_encoder_2", torch_dtype=dtype
)
text_encoder_2.to(self.device_torch, dtype=dtype)
flush()
if self.model_config.quantize_te:
self.print_and_status_update("Quantizing T5")
quantize(text_encoder_2, weights=get_qtype(
self.model_config.qtype))
freeze(text_encoder_2)
flush()
self.print_and_status_update("Loading CLIP") self.print_and_status_update("Loading CLIP")
text_encoder = CLIPTextModel.from_pretrained( text_encoder = CLIPTextEncoder.load_model(
base_model_path, subfolder="text_encoder", torch_dtype=dtype) base_model_path, dtype=dtype, device=self.device_torch
tokenizer = CLIPTokenizer.from_pretrained( )
base_model_path, subfolder="tokenizer", torch_dtype=dtype) tokenizer = CLIPTextEncoder.load_tokenizer(base_model_path, use_fast=False)
text_encoder.to(self.device_torch, dtype=dtype)
self.print_and_status_update("Loading VAE") self.print_and_status_update("Loading VAE")
vae = AutoencoderKL.from_pretrained( vae = KLVAE.load_model(base_model_path, dtype=dtype)
base_model_path, subfolder="vae", torch_dtype=dtype)
self.noise_scheduler = FluxKontextModel.get_train_scheduler() self.noise_scheduler = FluxKontextModel.get_train_scheduler()
@@ -7,7 +7,7 @@ from toolkit.accelerator import unwrap_model
import torch import torch
from toolkit.prompt_utils import PromptEmbeds from toolkit.prompt_utils import PromptEmbeds
from toolkit.config_modules import GenerateImageConfig from toolkit.config_modules import GenerateImageConfig
from diffusers.models import HiDreamImageTransformer2DModel from toolkit.models.v2.diffusion_models.hidream import HiDreamImageTransformer2DModel
import torch.nn.functional as F import torch.nn.functional as F
from PIL import Image from PIL import Image
@@ -9,7 +9,10 @@ from toolkit import train_tools
from toolkit.config_modules import GenerateImageConfig, ModelConfig from toolkit.config_modules import GenerateImageConfig, ModelConfig
from PIL import Image from PIL import Image
from toolkit.models.base_model import BaseModel from toolkit.models.base_model import BaseModel
from diffusers import AutoencoderKL, TorchAoConfig from toolkit.models.v2.text_encoders.t5 import T5TextEncoder
from toolkit.models.v2.text_encoders.clip import CLIPTextEncoderWithProjection
from toolkit.models.v2.vae.autoencoder_kl import KLVAE
from diffusers import TorchAoConfig
from toolkit.basic import flush from toolkit.basic import flush
from toolkit.prompt_utils import PromptEmbeds from toolkit.prompt_utils import PromptEmbeds
from toolkit.samplers.custom_flowmatch_sampler import CustomFlowMatchEulerDiscreteScheduler from toolkit.samplers.custom_flowmatch_sampler import CustomFlowMatchEulerDiscreteScheduler
@@ -19,7 +22,6 @@ from toolkit.accelerator import get_accelerator, unwrap_model
from optimum.quanto import freeze, QTensor from optimum.quanto import freeze, QTensor
from toolkit.util.mask import generate_random_mask, random_dialate_mask from toolkit.util.mask import generate_random_mask, random_dialate_mask
from toolkit.util.quantize import quantize, get_qtype from toolkit.util.quantize import quantize, get_qtype
from transformers import T5TokenizerFast, T5EncoderModel, CLIPTextModel, CLIPTokenizer, TorchAoConfig as TorchAoConfigTransformers
from .src.pipelines.hidream_image.pipeline_hidream_image import HiDreamImagePipeline from .src.pipelines.hidream_image.pipeline_hidream_image import HiDreamImagePipeline
from .src.models.transformers.transformer_hidream_image import HiDreamImageTransformer2DModel from .src.models.transformers.transformer_hidream_image import HiDreamImageTransformer2DModel
from .src.schedulers.fm_solvers_unipc import FlowUniPCMultistepScheduler from .src.schedulers.fm_solvers_unipc import FlowUniPCMultistepScheduler
@@ -28,15 +30,6 @@ from einops import rearrange, repeat
import random import random
import torch.nn.functional as F import torch.nn.functional as F
from tqdm import tqdm from tqdm import tqdm
from transformers import (
CLIPTextModelWithProjection,
CLIPTokenizer,
T5EncoderModel,
T5Tokenizer,
LlamaForCausalLM,
PreTrainedTokenizerFast
)
if TYPE_CHECKING: if TYPE_CHECKING:
from toolkit.data_transfer_object.data_loader import DataLoaderBatchDTO from toolkit.data_transfer_object.data_loader import DataLoaderBatchDTO
@@ -125,10 +118,8 @@ class HidreamModel(BaseModel):
self.print_and_status_update("Loading transformer") self.print_and_status_update("Loading transformer")
transformer = self.hidream_transformer_class.from_pretrained( transformer = self.hidream_transformer_class.load_model(
model_path, model_path, dtype=torch.bfloat16
subfolder="transformer",
torch_dtype=torch.bfloat16
) )
if not self.low_vram: if not self.low_vram:
@@ -164,44 +155,34 @@ class HidreamModel(BaseModel):
self.print_and_status_update("Loading vae") self.print_and_status_update("Loading vae")
vae = AutoencoderKL.from_pretrained( vae = KLVAE.load_model(extras_path, dtype=torch.bfloat16).to(
extras_path, self.device_torch, dtype=dtype
subfolder="vae", )
torch_dtype=torch.bfloat16
).to(self.device_torch, dtype=dtype)
self.print_and_status_update("Loading clip encoders") self.print_and_status_update("Loading clip encoders")
text_encoder = CLIPTextModelWithProjection.from_pretrained( text_encoder = CLIPTextEncoderWithProjection.load_model(
extras_path, extras_path, dtype=torch.bfloat16
subfolder="text_encoder",
torch_dtype=torch.bfloat16
).to(self.device_torch, dtype=dtype) ).to(self.device_torch, dtype=dtype)
tokenizer = CLIPTokenizer.from_pretrained( tokenizer = CLIPTextEncoderWithProjection.load_tokenizer(
extras_path, extras_path, use_fast=False
subfolder="tokenizer"
) )
text_encoder_2 = CLIPTextModelWithProjection.from_pretrained( text_encoder_2 = CLIPTextEncoderWithProjection.load_model(
extras_path, extras_path, dtype=torch.bfloat16, subfolder="text_encoder_2"
subfolder="text_encoder_2",
torch_dtype=torch.bfloat16
).to(self.device_torch, dtype=dtype) ).to(self.device_torch, dtype=dtype)
tokenizer_2 = CLIPTokenizer.from_pretrained( tokenizer_2 = CLIPTextEncoderWithProjection.load_tokenizer(
extras_path, extras_path, subfolder="tokenizer_2", use_fast=False
subfolder="tokenizer_2"
) )
flush() flush()
self.print_and_status_update("Loading T5 encoders") self.print_and_status_update("Loading T5 encoders")
text_encoder_3 = T5EncoderModel.from_pretrained( text_encoder_3 = T5TextEncoder.load_model(
extras_path, extras_path, dtype=torch.bfloat16, subfolder="text_encoder_3"
subfolder="text_encoder_3",
torch_dtype=torch.bfloat16
).to(self.device_torch, dtype=dtype) ).to(self.device_torch, dtype=dtype)
if self.model_config.quantize_te: if self.model_config.quantize_te:
@@ -211,9 +192,8 @@ class HidreamModel(BaseModel):
freeze(text_encoder_3) freeze(text_encoder_3)
flush() flush()
tokenizer_3 = T5Tokenizer.from_pretrained( tokenizer_3 = T5TextEncoder.load_tokenizer(
extras_path, extras_path, subfolder="tokenizer_3", use_fast=False
subfolder="tokenizer_3"
) )
flush() flush()
@@ -8,6 +8,8 @@ from einops import repeat
from diffusers.configuration_utils import ConfigMixin, register_to_config from diffusers.configuration_utils import ConfigMixin, register_to_config
from diffusers.loaders import FromOriginalModelMixin, PeftAdapterMixin from diffusers.loaders import FromOriginalModelMixin, PeftAdapterMixin
from diffusers.models.modeling_utils import ModelMixin from diffusers.models.modeling_utils import ModelMixin
from toolkit.models.v2._mixin import OstrisModelMixin
from diffusers.utils import USE_PEFT_BACKEND, is_torch_version, logging, scale_lora_layers, unscale_lora_layers from diffusers.utils import USE_PEFT_BACKEND, is_torch_version, logging, scale_lora_layers, unscale_lora_layers
from diffusers.utils.torch_utils import maybe_allow_in_graph from diffusers.utils.torch_utils import maybe_allow_in_graph
from diffusers.models.modeling_outputs import Transformer2DModelOutput from diffusers.models.modeling_outputs import Transformer2DModelOutput
@@ -228,9 +230,15 @@ class HiDreamImageBlock(nn.Module):
) )
class HiDreamImageTransformer2DModel( class HiDreamImageTransformer2DModel(
ModelMixin, ConfigMixin, PeftAdapterMixin, FromOriginalModelMixin ModelMixin, ConfigMixin, PeftAdapterMixin, FromOriginalModelMixin, OstrisModelMixin
): ):
_supports_gradient_checkpointing = True _supports_gradient_checkpointing = True
aitk_subfolder = "transformer"
@classmethod
def get_transformer_block_names(cls):
return ["double_stream_blocks", "single_stream_blocks"]
_no_split_modules = ["HiDreamImageBlock"] _no_split_modules = ["HiDreamImageBlock"]
@register_to_config @register_to_config
@@ -26,7 +26,11 @@ from huggingface_hub.errors import EntryNotFoundError
from transformers import AutoModel, AutoTokenizer from transformers import AutoModel, AutoTokenizer
from .src.transformer import Ideogram4Config, Ideogram4Transformer2DModel from .src.transformer import Ideogram4Config, Ideogram4Transformer2DModel
from .src.vae import AutoEncoder, AutoEncoderParams, convert_diffusers_state_dict from toolkit.models.v2.vae.flux2_kl import (
AutoEncoder,
AutoEncoderParams,
convert_diffusers_state_dict,
)
from .src.latent_norm import get_latent_norm from .src.latent_norm import get_latent_norm
from .src.pipeline import ( from .src.pipeline import (
Ideogram4Pipeline, Ideogram4Pipeline,
@@ -25,18 +25,21 @@ from safetensors.torch import load_file, save_file
import huggingface_hub import huggingface_hub
from huggingface_hub.errors import EntryNotFoundError from huggingface_hub.errors import EntryNotFoundError
from diffusers import AutoencoderKLQwenImage
from transformers import ( from transformers import (
AutoProcessor, AutoProcessor,
AutoTokenizer, AutoTokenizer,
Qwen2TokenizerFast, Qwen2TokenizerFast,
Qwen3VLForConditionalGeneration,
) )
from optimum.quanto import freeze from optimum.quanto import freeze
from toolkit.config_modules import GenerateImageConfig, ModelConfig, NetworkConfig from toolkit.config_modules import GenerateImageConfig, ModelConfig, NetworkConfig
from toolkit.lora_special import LoRASpecialNetwork from toolkit.lora_special import LoRASpecialNetwork
from toolkit.models.base_model import BaseModel from toolkit.models.base_model import BaseModel
from toolkit.models.v2.vae.qwen_image import QwenImageVAE, QwenImageVAEHolderMixin
from toolkit.models.v2.text_encoders.qwen3_vl import (
Qwen3VLTextEncoder,
patch_qwen_vl_patch_embed,
)
from toolkit.basic import flush from toolkit.basic import flush
from toolkit.advanced_prompt_embeds import AdvancedPromptEmbeds from toolkit.advanced_prompt_embeds import AdvancedPromptEmbeds
from toolkit.samplers.custom_flowmatch_sampler import ( from toolkit.samplers.custom_flowmatch_sampler import (
@@ -100,30 +103,6 @@ QWEN_IMAGE_VAE_PATH = "Qwen/Qwen-Image"
HF_TOKEN = os.getenv("HF_TOKEN", None) HF_TOKEN = os.getenv("HF_TOKEN", None)
def patch_qwen_vl_patch_embed(model):
"""Qwen-VL's vision patch_embed is a Conv3d whose kernel == stride, i.e. a plain
linear projection of each flattened patch. bf16 Conv3d has no fast cuDNN kernel and
falls back to a slow, GPU-underutilizing path. Swap it for the equivalent F.linear
(a GEMM). The weight is read lazily so this survives later .to(device)/dtype moves.
Returns the number of patch_embed modules patched. (Same patch as the
Qwen3VLCaptioner extension.)"""
patched = 0
for module in model.modules():
proj = getattr(module, "proj", None)
if isinstance(proj, torch.nn.Conv3d) and tuple(proj.kernel_size) == tuple(
proj.stride
):
def fast_forward(hidden_states, _proj=proj):
w = _proj.weight.reshape(_proj.weight.shape[0], -1)
x = hidden_states.view(-1, w.shape[1]).to(w.dtype)
return F.linear(x, w, _proj.bias)
module.forward = fast_forward
patched += 1
return patched
def _load_mmdit_state_dict(name_or_path: str, filename: Optional[str]) -> dict: def _load_mmdit_state_dict(name_or_path: str, filename: Optional[str]) -> dict:
"""Load the MMDiT weights from a local safetensors file/dir or the HF hub. """Load the MMDiT weights from a local safetensors file/dir or the HF hub.
@@ -163,7 +142,7 @@ def _load_mmdit_state_dict(name_or_path: str, filename: Optional[str]) -> dict:
return load_file(path) return load_file(path)
class Krea2Model(BaseModel): class Krea2Model(QwenImageVAEHolderMixin, BaseModel):
arch = "krea2" arch = "krea2"
def __init__( def __init__(
@@ -268,8 +247,8 @@ class Krea2Model(BaseModel):
processor = Qwen2TokenizerFast.from_pretrained( processor = Qwen2TokenizerFast.from_pretrained(
te_path, max_length=self.max_text_length, token=HF_TOKEN te_path, max_length=self.max_text_length, token=HF_TOKEN
) )
text_encoder = Qwen3VLForConditionalGeneration.from_pretrained( text_encoder = Qwen3VLTextEncoder.load_model(
te_path, torch_dtype=dtype, token=HF_TOKEN te_path, dtype=dtype, subfolder="", token=HF_TOKEN
) )
vl_processor = None vl_processor = None
if self.is_edit: if self.is_edit:
@@ -281,8 +260,7 @@ class Krea2Model(BaseModel):
else: else:
# We only ever encode text, so the vision tower is dead weight -- drop it to # We only ever encode text, so the vision tower is dead weight -- drop it to
# free VRAM and skip loading its (bf16-slow) Conv3d patch_embed onto the GPU. # free VRAM and skip loading its (bf16-slow) Conv3d patch_embed onto the GPU.
if getattr(text_encoder.model, "visual", None) is not None: text_encoder.drop_vision_tower()
text_encoder.model.visual = None
text_encoder.eval() text_encoder.eval()
text_encoder.requires_grad_(False) text_encoder.requires_grad_(False)
flush() flush()
@@ -291,8 +269,8 @@ class Krea2Model(BaseModel):
def _load_vae(self): def _load_vae(self):
vae_path = self.model_config.model_kwargs.get("vae_path", QWEN_IMAGE_VAE_PATH) vae_path = self.model_config.model_kwargs.get("vae_path", QWEN_IMAGE_VAE_PATH)
self.print_and_status_update(f"Loading Qwen-Image VAE from {vae_path}") self.print_and_status_update(f"Loading Qwen-Image VAE from {vae_path}")
vae = AutoencoderKLQwenImage.from_pretrained( vae = QwenImageVAE.load_model(
vae_path, subfolder="vae", torch_dtype=self.vae_torch_dtype, token=HF_TOKEN vae_path, dtype=self.vae_torch_dtype, token=HF_TOKEN
) )
vae.eval() vae.eval()
vae.requires_grad_(False) vae.requires_grad_(False)
@@ -772,76 +750,9 @@ class Krea2Model(BaseModel):
return False return False
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# VAE (Qwen-Image AutoencoderKLQwenImage -- same handling as qwen_image arch) # VAE (Qwen-Image AutoencoderKLQwenImage -- shared QwenImageVAEHolderMixin)
# ------------------------------------------------------------------ # ------------------------------------------------------------------
def encode_images(self, image_list: List[torch.Tensor], device=None, dtype=None): vae_decode_tiled_on_low_vram = True
if device is None:
device = self.vae_device_torch
if dtype is None:
dtype = self.vae_torch_dtype
if self.vae.device == torch.device("cpu"):
self.vae.to(device)
self.vae.eval()
self.vae.requires_grad_(False)
image_list = [image.to(device, dtype=dtype) for image in image_list]
images = torch.stack(image_list).to(device, dtype=dtype)
# AutoencoderKLQwenImage is a video VAE: add a frame dim.
images = images.unsqueeze(2)
latents = self.vae.encode(images).latent_dist.sample()
latents_mean = (
torch.tensor(self.vae.config.latents_mean)
.view(1, self.vae.config.z_dim, 1, 1, 1)
.to(latents.device, latents.dtype)
)
latents_std = 1.0 / torch.tensor(self.vae.config.latents_std).view(
1, self.vae.config.z_dim, 1, 1, 1
).to(latents.device, latents.dtype)
latents = (latents - latents_mean) * latents_std
latents = latents.squeeze(2) # drop frame dim
return latents.to(device, dtype=dtype)
def decode_latents(self, latents: torch.Tensor, device=None, dtype=None):
if device is None:
device = self.vae_device_torch
if dtype is None:
dtype = self.vae_torch_dtype
if self.vae.device == torch.device("cpu"):
self.vae.to(device)
latents = latents.to(device, dtype=dtype)
latents = latents.unsqueeze(2) # add frame dim
latents_mean = (
torch.tensor(self.vae.config.latents_mean)
.view(1, self.vae.config.z_dim, 1, 1, 1)
.to(latents.device, latents.dtype)
)
latents_std = (
torch.tensor(self.vae.config.latents_std)
.view(1, self.vae.config.z_dim, 1, 1, 1)
.to(latents.device, latents.dtype)
)
latents = latents * latents_std + latents_mean
# Full-resolution decode spikes VRAM; tile it when low on VRAM (decode
# only -- encode stays untiled).
tiled = self.model_config.low_vram
if tiled:
self.vae.enable_tiling()
try:
images = self.vae.decode(latents).sample
finally:
if tiled:
self.vae.disable_tiling()
images = images.squeeze(2) # drop frame dim
return images.to(device, dtype=dtype)
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Saving / bookkeeping # Saving / bookkeeping
# ------------------------------------------------------------------ # ------------------------------------------------------------------
@@ -34,7 +34,8 @@ from torchvision.transforms.functional import to_tensor
from safetensors.torch import load_file, save_file from safetensors.torch import load_file, save_file
import huggingface_hub import huggingface_hub
from transformers import AutoProcessor, AutoTokenizer, Qwen3VLForConditionalGeneration from transformers import AutoProcessor, AutoTokenizer
from toolkit.models.v2.text_encoders.qwen3_vl import Qwen3VLTextEncoder
from optimum.quanto import freeze from optimum.quanto import freeze
from toolkit.config_modules import GenerateImageConfig, ModelConfig from toolkit.config_modules import GenerateImageConfig, ModelConfig
@@ -209,8 +210,11 @@ class MageFlowModel(BaseModel):
self.print_and_status_update(f"Loading Qwen3-VL text encoder from {te_path}") self.print_and_status_update(f"Loading Qwen3-VL text encoder from {te_path}")
tokenizer = AutoTokenizer.from_pretrained(te_path, token=HF_TOKEN, **te_kwargs) tokenizer = AutoTokenizer.from_pretrained(te_path, token=HF_TOKEN, **te_kwargs)
text_encoder = Qwen3VLForConditionalGeneration.from_pretrained( text_encoder = Qwen3VLTextEncoder.load_model(
te_path, torch_dtype=dtype, token=HF_TOKEN, **te_kwargs te_path,
dtype=dtype,
subfolder=te_kwargs.get("subfolder", ""),
token=HF_TOKEN,
) )
vl_processor = None vl_processor = None
if self.is_edit: if self.is_edit:
@@ -224,8 +228,7 @@ class MageFlowModel(BaseModel):
else: else:
# We only ever encode text, so the vision tower is dead weight -- # We only ever encode text, so the vision tower is dead weight --
# drop it to free VRAM. # drop it to free VRAM.
if getattr(text_encoder.model, "visual", None) is not None: text_encoder.drop_vision_tower()
text_encoder.model.visual = None
text_encoder.eval() text_encoder.eval()
text_encoder.requires_grad_(False) text_encoder.requires_grad_(False)
flush() flush()
@@ -18,7 +18,8 @@ import math
from typing import List, Optional from typing import List, Optional
import torch import torch
import torch.nn.functional as F
from toolkit.models.v2.text_encoders.qwen3_vl import patch_qwen_vl_patch_embed
# Prompt templates from the reference (mage_flow/models/utils.py). ``start_idx`` # Prompt templates from the reference (mage_flow/models/utils.py). ``start_idx``
@@ -57,30 +58,6 @@ def edit_prompt_body(instruction: str, num_refs: int) -> str:
return prefix + instruction return prefix + instruction
def patch_qwen_vl_patch_embed(model):
"""Qwen-VL's vision patch_embed is a Conv3d whose kernel == stride, i.e. a plain
linear projection of each flattened patch. bf16 Conv3d has no fast cuDNN kernel and
falls back to a slow, GPU-underutilizing path. Swap it for the equivalent F.linear
(a GEMM). The weight is read lazily so this survives later .to(device)/dtype moves.
Returns the number of patch_embed modules patched. (Same patch as the krea2
extension / Qwen3VLCaptioner.)"""
patched = 0
for module in model.modules():
proj = getattr(module, "proj", None)
if isinstance(proj, torch.nn.Conv3d) and tuple(proj.kernel_size) == tuple(
proj.stride
):
def fast_forward(hidden_states, _proj=proj):
w = _proj.weight.reshape(_proj.weight.shape[0], -1)
x = hidden_states.view(-1, w.shape[1]).to(w.dtype)
return F.linear(x, w, _proj.bias)
module.forward = fast_forward
patched += 1
return patched
def resize_vl_images( def resize_vl_images(
images: List[torch.Tensor], max_long_edge: int = 384 images: List[torch.Tensor], max_long_edge: int = 384
) -> List["PIL.Image.Image"]: ) -> List["PIL.Image.Image"]:
@@ -6,21 +6,25 @@ import torch
import yaml import yaml
from toolkit.config_modules import GenerateImageConfig, ModelConfig from toolkit.config_modules import GenerateImageConfig, ModelConfig
from toolkit.models.base_model import BaseModel from toolkit.models.base_model import BaseModel
from toolkit.models.v2.vae.qwen_image import QwenImageVAE, QwenImageVAEHolderMixin
from toolkit.models.v2.text_encoders.qwen3_vl import Qwen3VLTextEncoder
from toolkit.basic import flush from toolkit.basic import flush
from toolkit.advanced_prompt_embeds import AdvancedPromptEmbeds from toolkit.advanced_prompt_embeds import AdvancedPromptEmbeds
from toolkit.samplers.custom_flowmatch_sampler import ( from toolkit.samplers.custom_flowmatch_sampler import (
CustomFlowMatchEulerDiscreteScheduler, CustomFlowMatchEulerDiscreteScheduler,
) )
from toolkit.accelerator import unwrap_model from toolkit.accelerator import unwrap_model
from optimum.quanto import freeze from toolkit.util.quantize import quantize_model
from toolkit.util.quantize import quantize, get_qtype, quantize_model
from toolkit.memory_management import MemoryManager from toolkit.memory_management import MemoryManager
from transformers import Qwen3VLForConditionalGeneration, Qwen3VLProcessor from transformers import Qwen3VLProcessor
import torch.nn.functional as F import torch.nn.functional as F
try: try:
from diffusers import NucleusMoEImagePipeline, NucleusMoEImageTransformer2DModel, AutoencoderKLQwenImage from diffusers import NucleusMoEImagePipeline, AutoencoderKLQwenImage
from toolkit.models.v2.diffusion_models.nucleus_image import (
NucleusMoEImageTransformer2DModel,
)
from diffusers.models.transformers.transformer_nucleusmoe_image import SwiGLUExperts from diffusers.models.transformers.transformer_nucleusmoe_image import SwiGLUExperts
except ImportError: except ImportError:
raise ImportError( raise ImportError(
@@ -46,7 +50,7 @@ scheduler_config = {
} }
class NucleusImageModel(BaseModel): class NucleusImageModel(QwenImageVAEHolderMixin, BaseModel):
arch = "nucleus_image" arch = "nucleus_image"
def __init__( def __init__(
@@ -81,20 +85,14 @@ class NucleusImageModel(BaseModel):
self.print_and_status_update("Loading transformer") self.print_and_status_update("Loading transformer")
transformer_path = model_path if os.path.exists(model_path):
transformer_subfolder = "transformer"
if os.path.exists(transformer_path):
transformer_subfolder = None
transformer_path = os.path.join(transformer_path, "transformer")
# check if the path is a full checkpoint. # check if the path is a full checkpoint.
te_folder_path = os.path.join(model_path, "text_encoder") te_folder_path = os.path.join(model_path, "text_encoder")
# if we have the te, this folder is a full checkpoint, use it as the base # if we have the te, this folder is a full checkpoint, use it as the base
if os.path.exists(te_folder_path): if os.path.exists(te_folder_path):
base_model_path = model_path base_model_path = model_path
transformer = NucleusMoEImageTransformer2DModel.from_pretrained( transformer = NucleusMoEImageTransformer2DModel.load_model(model_path, dtype=dtype)
transformer_path, subfolder=transformer_subfolder, torch_dtype=dtype
)
# handle versions of pytorch that don't have grouped mm, by disabling it in the SwiGLUExperts # handle versions of pytorch that don't have grouped mm, by disabling it in the SwiGLUExperts
if not hasattr(torch.nn.functional, "grouped_mm"): if not hasattr(torch.nn.functional, "grouped_mm"):
@@ -129,33 +127,13 @@ class NucleusImageModel(BaseModel):
tokenizer = Qwen3VLProcessor.from_pretrained( tokenizer = Qwen3VLProcessor.from_pretrained(
base_model_path, subfolder="processor", torch_dtype=dtype base_model_path, subfolder="processor", torch_dtype=dtype
) )
text_encoder = Qwen3VLForConditionalGeneration.from_pretrained( text_encoder = Qwen3VLTextEncoder.load_model(base_model_path, dtype=dtype)
base_model_path, subfolder="text_encoder", torch_dtype=dtype self.prepare_text_encoder(text_encoder, dtype=dtype)
)
if (
self.model_config.layer_offloading
and self.model_config.layer_offloading_text_encoder_percent > 0
):
MemoryManager.attach(
text_encoder,
self.device_torch,
offload_percent=self.model_config.layer_offloading_text_encoder_percent,
)
text_encoder.to(self.device_torch, dtype=dtype)
flush()
if self.model_config.quantize_te:
self.print_and_status_update("Quantizing Text Encoder")
quantize(text_encoder, weights=get_qtype(self.model_config.qtype_te))
freeze(text_encoder)
flush()
self.print_and_status_update("Loading VAE") self.print_and_status_update("Loading VAE")
vae = AutoencoderKLQwenImage.from_pretrained( vae = QwenImageVAE.load_model(
base_model_path, subfolder="vae", torch_dtype=dtype base_model_path, dtype=dtype, device=self.device_torch
).to(self.device_torch, dtype=dtype) )
self.noise_scheduler = NucleusImageModel.get_train_scheduler() self.noise_scheduler = NucleusImageModel.get_train_scheduler()
@@ -214,41 +192,6 @@ class NucleusImageModel(BaseModel):
return pipeline return pipeline
def encode_images(self, image_list: List[torch.Tensor], device=None, dtype=None):
if device is None:
device = self.vae_device_torch
if dtype is None:
dtype = self.vae_torch_dtype
# Move to vae to device if on cpu
if self.vae.device == torch.device("cpu"):
self.vae.to(device)
self.vae.eval()
self.vae.requires_grad_(False)
# move to device and dtype
image_list = [image.to(device, dtype=dtype) for image in image_list]
images = torch.stack(image_list).to(device, dtype=dtype)
# it uses wan vae, so add dim for frame count
images = images.unsqueeze(2)
latents = self.vae.encode(images).latent_dist.sample()
latents_mean = (
torch.tensor(self.vae.config.latents_mean)
.view(1, self.vae.config.z_dim, 1, 1, 1)
.to(latents.device, latents.dtype)
)
latents_std = 1.0 / torch.tensor(self.vae.config.latents_std).view(
1, self.vae.config.z_dim, 1, 1, 1
).to(latents.device, latents.dtype)
latents = (latents - latents_mean) * latents_std
latents = latents.to(device, dtype=dtype)
latents = latents.squeeze(2) # remove the frame count dimension
return latents
def generate_single_image( def generate_single_image(
self, self,
pipeline: NucleusMoEImagePipeline, pipeline: NucleusMoEImagePipeline,
@@ -96,8 +96,8 @@ class OmniGen2Model(BaseModel):
self.print_and_status_update("Loading transformer") self.print_and_status_update("Loading transformer")
transformer = OmniGen2Transformer2DModel.from_pretrained( transformer = OmniGen2Transformer2DModel.load_model(
model_path, subfolder="transformer", torch_dtype=torch.bfloat16 model_path, dtype=torch.bfloat16
) )
if not self.low_vram: if not self.low_vram:
@@ -15,6 +15,8 @@ from diffusers.models.attention_processor import Attention
from diffusers.models.modeling_outputs import Transformer2DModelOutput from diffusers.models.modeling_outputs import Transformer2DModelOutput
from diffusers.models.modeling_utils import ModelMixin from diffusers.models.modeling_utils import ModelMixin
from toolkit.models.v2._mixin import OstrisModelMixin
from ..attention_processor import OmniGen2AttnProcessorFlash2Varlen, OmniGen2AttnProcessor from ..attention_processor import OmniGen2AttnProcessorFlash2Varlen, OmniGen2AttnProcessor
from .repo import OmniGen2RotaryPosEmbed from .repo import OmniGen2RotaryPosEmbed
from .block_lumina2 import LuminaLayerNormContinuous, LuminaRMSNormZero, LuminaFeedForward, Lumina2CombinedTimestepCaptionEmbedding from .block_lumina2 import LuminaLayerNormContinuous, LuminaRMSNormZero, LuminaFeedForward, Lumina2CombinedTimestepCaptionEmbedding
@@ -177,7 +179,15 @@ class OmniGen2TransformerBlock(nn.Module):
return hidden_states return hidden_states
class OmniGen2Transformer2DModel(ModelMixin, ConfigMixin, PeftAdapterMixin, FromOriginalModelMixin): class OmniGen2Transformer2DModel(
ModelMixin, ConfigMixin, PeftAdapterMixin, FromOriginalModelMixin, OstrisModelMixin
):
aitk_subfolder = "transformer"
@classmethod
def get_transformer_block_names(cls):
return ["noise_refiner", "ref_image_refiner", "context_refiner", "layers"]
""" """
OmniGen2 Transformer 2D Model. OmniGen2 Transformer 2D Model.
@@ -126,9 +126,7 @@ class PRXPixelT2IModel(BaseModel):
self.print_and_status_update("Loading transformer") self.print_and_status_update("Loading transformer")
# from_pretrained reads config.json (bottleneck_size, resolution_embeds, # from_pretrained reads config.json (bottleneck_size, resolution_embeds,
# in_channels=3, ...) and the safetensors in one shot. # in_channels=3, ...) and the safetensors in one shot.
transformer = PRXTransformer2DModel.from_pretrained( transformer = PRXTransformer2DModel.load_model(model_path, dtype=dtype)
model_path, subfolder="transformer", torch_dtype=dtype
)
transformer.to(dtype=dtype) transformer.to(dtype=dtype)
flush() flush()
@@ -40,6 +40,8 @@ from diffusers.models.attention_dispatch import dispatch_attention_fn
from diffusers.models.embeddings import get_timestep_embedding from diffusers.models.embeddings import get_timestep_embedding
from diffusers.models.modeling_outputs import Transformer2DModelOutput from diffusers.models.modeling_outputs import Transformer2DModelOutput
from diffusers.models.modeling_utils import ModelMixin from diffusers.models.modeling_utils import ModelMixin
from toolkit.models.v2._mixin import OstrisModelMixin
from diffusers.models.normalization import RMSNorm from diffusers.models.normalization import RMSNorm
@@ -666,7 +668,7 @@ def seq2img(seq: torch.Tensor, patch_size: int, shape: torch.Tensor) -> torch.Te
return seq return seq
class PRXTransformer2DModel(ModelMixin, ConfigMixin, AttentionMixin): class PRXTransformer2DModel(ModelMixin, ConfigMixin, AttentionMixin, OstrisModelMixin):
r""" r"""
Transformer-based 2D model for text to image generation. Transformer-based 2D model for text to image generation.
@@ -702,6 +704,12 @@ class PRXTransformer2DModel(ModelMixin, ConfigMixin, AttentionMixin):
`PRXResolutionEmbedder`. Used by the PRX-7B variant. `PRXResolutionEmbedder`. Used by the PRX-7B variant.
""" """
aitk_subfolder = "transformer"
@classmethod
def get_transformer_block_names(cls):
return ["blocks"]
config_name = "config.json" config_name = "config.json"
_supports_gradient_checkpointing = True _supports_gradient_checkpointing = True
@@ -7,28 +7,25 @@ from toolkit import train_tools
from toolkit.config_modules import GenerateImageConfig, ModelConfig from toolkit.config_modules import GenerateImageConfig, ModelConfig
from PIL import Image from PIL import Image
from toolkit.models.base_model import BaseModel from toolkit.models.base_model import BaseModel
from toolkit.models.v2.vae.qwen_image import QwenImageVAE, QwenImageVAEHolderMixin
from toolkit.basic import flush from toolkit.basic import flush
from toolkit.prompt_utils import PromptEmbeds from toolkit.prompt_utils import PromptEmbeds
from toolkit.samplers.custom_flowmatch_sampler import ( from toolkit.samplers.custom_flowmatch_sampler import (
CustomFlowMatchEulerDiscreteScheduler, CustomFlowMatchEulerDiscreteScheduler,
) )
from toolkit.accelerator import get_accelerator, unwrap_model from toolkit.accelerator import get_accelerator, unwrap_model
from optimum.quanto import freeze, QTensor from toolkit.util.quantize import quantize_model
from toolkit.util.quantize import quantize, get_qtype, quantize_model
import torch.nn.functional as F import torch.nn.functional as F
from toolkit.memory_management import MemoryManager from toolkit.memory_management import MemoryManager
from safetensors.torch import load_file from safetensors.torch import load_file
from diffusers import ( from diffusers import (
QwenImagePipeline, QwenImagePipeline,
QwenImageTransformer2DModel,
AutoencoderKLQwenImage, AutoencoderKLQwenImage,
) )
from transformers import ( from transformers import Qwen2VLProcessor
Qwen2_5_VLForConditionalGeneration, from toolkit.models.v2.diffusion_models.qwen_image import QwenImageTransformer2DModel
Qwen2Tokenizer, from toolkit.models.v2.text_encoders.qwen25_vl import Qwen25VLTextEncoder
Qwen2VLProcessor,
)
from tqdm import tqdm from tqdm import tqdm
from toolkit.util.qwen_vae_gradient_checkpointing import patch_qwen_vae_gradient_checkpointing from toolkit.util.qwen_vae_gradient_checkpointing import patch_qwen_vae_gradient_checkpointing
@@ -55,7 +52,7 @@ scheduler_config = {
} }
class QwenImageModel(BaseModel): class QwenImageModel(QwenImageVAEHolderMixin, BaseModel):
arch = "qwen_image" arch = "qwen_image"
_qwen_image_keep_visual = False _qwen_image_keep_visual = False
_qwen_pipeline = QwenImagePipeline _qwen_pipeline = QwenImagePipeline
@@ -97,31 +94,14 @@ class QwenImageModel(BaseModel):
self.print_and_status_update("Loading transformer") self.print_and_status_update("Loading transformer")
if model_path.endswith(".safetensors"): if not model_path.endswith(".safetensors") and os.path.exists(model_path):
# load the safetensors file # check if the path is a full checkpoint.
transformer = QwenImageTransformer2DModel.from_single_file( te_folder_path = os.path.join(model_path, "text_encoder")
model_path, # if we have the te, this folder is a full checkpoint, use it as the base
config="Qwen/Qwen-Image", if os.path.exists(te_folder_path):
subfolder="transformer", base_model_path = model_path
torch_dtype=model_dtype,
)
transformer.to(model_dtype)
else: transformer = QwenImageTransformer2DModel.load_model(model_path, dtype=model_dtype)
transformer_path = model_path
transformer_subfolder = "transformer"
if os.path.exists(transformer_path):
transformer_subfolder = None
transformer_path = os.path.join(transformer_path, "transformer")
# check if the path is a full checkpoint.
te_folder_path = os.path.join(model_path, "text_encoder")
# if we have the te, this folder is a full checkpoint, use it as the base
if os.path.exists(te_folder_path):
base_model_path = model_path
transformer = QwenImageTransformer2DModel.from_pretrained(
transformer_path, subfolder=transformer_subfolder, torch_dtype=dtype
)
if self.model_config.quantize: if self.model_config.quantize:
self.print_and_status_update("Quantizing Transformer") self.print_and_status_update("Quantizing Transformer")
@@ -145,41 +125,18 @@ class QwenImageModel(BaseModel):
flush() flush()
self.print_and_status_update("Text Encoder") self.print_and_status_update("Text Encoder")
tokenizer = Qwen2Tokenizer.from_pretrained( tokenizer = Qwen25VLTextEncoder.load_tokenizer(base_model_path, use_fast=False)
base_model_path, subfolder="tokenizer", torch_dtype=dtype text_encoder = Qwen25VLTextEncoder.load_model(base_model_path, dtype=dtype)
)
text_encoder = Qwen2_5_VLForConditionalGeneration.from_pretrained(
base_model_path, subfolder="text_encoder", torch_dtype=dtype
)
# remove the visual model as it is not needed for image generation # remove the visual model as it is not needed for image generation
self.processor = None self.processor = None
if not self._qwen_image_keep_visual: if not self._qwen_image_keep_visual:
text_encoder.model.visual = None text_encoder.drop_vision_tower()
if ( self.prepare_text_encoder(text_encoder, dtype=dtype)
self.model_config.layer_offloading
and self.model_config.layer_offloading_text_encoder_percent > 0
):
MemoryManager.attach(
text_encoder,
self.device_torch,
offload_percent=self.model_config.layer_offloading_text_encoder_percent,
)
text_encoder.to(self.device_torch, dtype=dtype)
flush()
if self.model_config.quantize_te:
self.print_and_status_update("Quantizing Text Encoder")
quantize(text_encoder, weights=get_qtype(self.model_config.qtype_te))
freeze(text_encoder)
flush()
self.print_and_status_update("Loading VAE") self.print_and_status_update("Loading VAE")
vae = AutoencoderKLQwenImage.from_pretrained( vae = QwenImageVAE.load_model(base_model_path, dtype=dtype)
base_model_path, subfolder="vae", torch_dtype=dtype
)
self.noise_scheduler = QwenImageModel.get_train_scheduler() self.noise_scheduler = QwenImageModel.get_train_scheduler()
@@ -417,69 +374,3 @@ class QwenImageModel(BaseModel):
lora_keys_use_comfy_prefix = True lora_keys_use_comfy_prefix = True
def encode_images(self, image_list: List[torch.Tensor], device=None, dtype=None):
if device is None:
device = self.vae_device_torch
if dtype is None:
dtype = self.vae_torch_dtype
# Move to vae to device if on cpu
if self.vae.device == torch.device("cpu"):
self.vae.to(device)
self.vae.eval()
self.vae.requires_grad_(False)
# move to device and dtype
image_list = [image.to(device, dtype=dtype) for image in image_list]
images = torch.stack(image_list).to(device, dtype=dtype)
# it uses wan vae, so add dim for frame count
images = images.unsqueeze(2)
latents = self.vae.encode(images).latent_dist.sample()
latents_mean = (
torch.tensor(self.vae.config.latents_mean)
.view(1, self.vae.config.z_dim, 1, 1, 1)
.to(latents.device, latents.dtype)
)
latents_std = 1.0 / torch.tensor(self.vae.config.latents_std).view(
1, self.vae.config.z_dim, 1, 1, 1
).to(latents.device, latents.dtype)
latents = (latents - latents_mean) * latents_std
latents = latents.to(device, dtype=dtype)
latents = latents.squeeze(2) # remove the frame count dimension
return latents
def decode_latents(self, latents: torch.Tensor, device=None, dtype=None):
if device is None:
device = self.vae_device_torch
if dtype is None:
dtype = self.vae_torch_dtype
if self.vae.device == torch.device("cpu"):
self.vae.to(device)
latents = latents.to(device, dtype=dtype)
# add frame count dim for wan vae
latents = latents.unsqueeze(2)
latents_mean = (
torch.tensor(self.vae.config.latents_mean)
.view(1, self.vae.config.z_dim, 1, 1, 1)
.to(latents.device, latents.dtype)
)
latents_std = (
torch.tensor(self.vae.config.latents_std)
.view(1, self.vae.config.z_dim, 1, 1, 1)
.to(latents.device, latents.dtype)
)
latents = latents * latents_std + latents_mean
images = self.vae.decode(latents).sample
images = images.squeeze(2) # remove the frame count dimension
return images.to(device, dtype=dtype)
@@ -17,7 +17,7 @@ from toolkit.samplers.custom_flowmatch_sampler import (
) )
from toolkit.util.quantize import quantize_model from toolkit.util.quantize import quantize_model
from .wan22_pipeline import Wan22Pipeline from .wan22_pipeline import Wan22Pipeline
from diffusers import WanTransformer3DModel from toolkit.models.v2.diffusion_models.wan import WanTransformer3DModel
from toolkit.data_transfer_object.data_loader import DataLoaderBatchDTO from toolkit.data_transfer_object.data_loader import DataLoaderBatchDTO
from torchvision.transforms import functional as TF from torchvision.transforms import functional as TF
@@ -287,11 +287,9 @@ class Wan2214bModel(Wan21):
self.print_and_status_update("Loading transformer 1") self.print_and_status_update("Loading transformer 1")
dtype = self.torch_dtype dtype = self.torch_dtype
transformer_1 = WanTransformer3DModel.from_pretrained( transformer_1 = WanTransformer3DModel.load_model(
transformer_path_1, transformer_path_1, dtype=dtype, subfolder=subfolder_1
subfolder=subfolder_1, )
torch_dtype=dtype,
).to(dtype=dtype)
flush() flush()
@@ -317,11 +315,9 @@ class Wan2214bModel(Wan21):
self.print_and_status_update("Loading transformer 2") self.print_and_status_update("Loading transformer 2")
dtype = self.torch_dtype dtype = self.torch_dtype
transformer_2 = WanTransformer3DModel.from_pretrained( transformer_2 = WanTransformer3DModel.load_model(
transformer_path_2, transformer_path_2, dtype=dtype, subfolder=subfolder_2
subfolder=subfolder_2, )
torch_dtype=dtype,
).to(dtype=dtype)
flush() flush()
@@ -13,19 +13,16 @@ from toolkit.samplers.custom_flowmatch_sampler import (
CustomFlowMatchEulerDiscreteScheduler, CustomFlowMatchEulerDiscreteScheduler,
) )
from toolkit.accelerator import unwrap_model from toolkit.accelerator import unwrap_model
from optimum.quanto import freeze
from toolkit.util.quantize import ( from toolkit.util.quantize import (
quantize,
get_qtype,
quantize_model, quantize_model,
dequantize_if_quantized, dequantize_if_quantized,
) )
from toolkit.memory_management import MemoryManager from toolkit.memory_management import MemoryManager
from toolkit.metadata import get_meta_for_safetensors from toolkit.metadata import get_meta_for_safetensors
from toolkit.models.v2.text_encoders.qwen3 import Qwen3TextEncoder
from toolkit.models.v2.vae.autoencoder_kl import KLVAE
from safetensors.torch import load_file, save_file from safetensors.torch import load_file, save_file
from transformers import AutoTokenizer, Qwen3ForCausalLM
from diffusers import AutoencoderKL
try: try:
from diffusers import ZImagePipeline from diffusers import ZImagePipeline
@@ -255,36 +252,12 @@ class ZImageModel(BaseModel):
flush() flush()
self.print_and_status_update("Text Encoder") self.print_and_status_update("Text Encoder")
tokenizer = AutoTokenizer.from_pretrained( tokenizer = Qwen3TextEncoder.load_tokenizer(base_model_path)
base_model_path, subfolder="tokenizer", torch_dtype=dtype text_encoder = Qwen3TextEncoder.load_model(base_model_path, dtype=dtype)
) self.prepare_text_encoder(text_encoder, dtype=dtype)
text_encoder = Qwen3ForCausalLM.from_pretrained(
base_model_path, subfolder="text_encoder", torch_dtype=dtype
)
if (
self.model_config.layer_offloading
and self.model_config.layer_offloading_text_encoder_percent > 0
):
MemoryManager.attach(
text_encoder,
self.device_torch,
offload_percent=self.model_config.layer_offloading_text_encoder_percent,
)
text_encoder.to(self.device_torch, dtype=dtype)
flush()
if self.model_config.quantize_te:
self.print_and_status_update("Quantizing Text Encoder")
quantize(text_encoder, weights=get_qtype(self.model_config.qtype_te))
freeze(text_encoder)
flush()
self.print_and_status_update("Loading VAE") self.print_and_status_update("Loading VAE")
vae = AutoencoderKL.from_pretrained( vae = KLVAE.load_model(base_model_path, dtype=dtype)
base_model_path, subfolder="vae", torch_dtype=dtype
)
self.noise_scheduler = ZImageModel.get_train_scheduler() self.noise_scheduler = ZImageModel.get_train_scheduler()
@@ -9,11 +9,10 @@ import torch.nn.functional as F
import yaml import yaml
from toolkit.basic import flush from toolkit.basic import flush
from toolkit.accelerator import unwrap_model from toolkit.accelerator import unwrap_model
from optimum.quanto import freeze from toolkit.util.quantize import quantize_model
from toolkit.util.quantize import quantize, get_qtype, quantize_model
from toolkit.memory_management import MemoryManager from toolkit.memory_management import MemoryManager
from transformers import AutoTokenizer, Qwen3ForCausalLM from toolkit.models.v2.text_encoders.qwen3 import Qwen3TextEncoder
from toolkit.models.FakeVAE import FakeVAE from toolkit.models.FakeVAE import FakeVAE
from toolkit.paths import MODELS_PATH from toolkit.paths import MODELS_PATH
from safetensors.torch import load_file, save_file from safetensors.torch import load_file, save_file
@@ -468,31 +467,9 @@ class ZImageL2PModel(ZImageModel):
flush() flush()
self.print_and_status_update("Text Encoder") self.print_and_status_update("Text Encoder")
tokenizer = AutoTokenizer.from_pretrained( tokenizer = Qwen3TextEncoder.load_tokenizer(base_model_path)
base_model_path, subfolder="tokenizer", torch_dtype=dtype text_encoder = Qwen3TextEncoder.load_model(base_model_path, dtype=dtype)
) self.prepare_text_encoder(text_encoder, dtype=dtype)
text_encoder = Qwen3ForCausalLM.from_pretrained(
base_model_path, subfolder="text_encoder", torch_dtype=dtype
)
if (
self.model_config.layer_offloading
and self.model_config.layer_offloading_text_encoder_percent > 0
):
MemoryManager.attach(
text_encoder,
self.device_torch,
offload_percent=self.model_config.layer_offloading_text_encoder_percent,
)
text_encoder.to(self.device_torch, dtype=dtype)
flush()
if self.model_config.quantize_te:
self.print_and_status_update("Quantizing Text Encoder")
quantize(text_encoder, weights=get_qtype(self.model_config.qtype_te))
freeze(text_encoder)
flush()
self.print_and_status_update("Loading VAE") self.print_and_status_update("Loading VAE")
# vae = AutoencoderKL.from_pretrained( # vae = AutoencoderKL.from_pretrained(
@@ -13,14 +13,13 @@ from toolkit.samplers.custom_flowmatch_sampler import (
CustomFlowMatchEulerDiscreteScheduler, CustomFlowMatchEulerDiscreteScheduler,
) )
from toolkit.accelerator import unwrap_model from toolkit.accelerator import unwrap_model
from optimum.quanto import freeze from toolkit.util.quantize import quantize_model
from toolkit.util.quantize import quantize, get_qtype, quantize_model
from toolkit.memory_management import MemoryManager from toolkit.memory_management import MemoryManager
from safetensors.torch import load_file from safetensors.torch import load_file
from optimum.quanto import QTensor from optimum.quanto import QTensor
from toolkit.metadata import get_meta_for_safetensors from toolkit.metadata import get_meta_for_safetensors
from safetensors.torch import load_file, save_file from safetensors.torch import load_file, save_file
from transformers import AutoTokenizer, Qwen3ForCausalLM from toolkit.models.v2.text_encoders.qwen3 import Qwen3TextEncoder
from diffusers import AutoencoderKL from diffusers import AutoencoderKL
from toolkit.models.FakeVAE import FakeVAE from toolkit.models.FakeVAE import FakeVAE
from .zeta_chroma_transformer import ZImageDCT, ZImageDCTParams, vae_flatten, vae_unflatten, prepare_latent_image_ids, make_text_position_ids from .zeta_chroma_transformer import ZImageDCT, ZImageDCTParams, vae_flatten, vae_unflatten, prepare_latent_image_ids, make_text_position_ids
@@ -142,31 +141,9 @@ class ZetaChromaModel(BaseModel):
flush() flush()
self.print_and_status_update("Text Encoder") self.print_and_status_update("Text Encoder")
tokenizer = AutoTokenizer.from_pretrained( tokenizer = Qwen3TextEncoder.load_tokenizer(base_model_path)
base_model_path, subfolder="tokenizer", torch_dtype=dtype text_encoder = Qwen3TextEncoder.load_model(base_model_path, dtype=dtype)
) self.prepare_text_encoder(text_encoder, dtype=dtype)
text_encoder = Qwen3ForCausalLM.from_pretrained(
base_model_path, subfolder="text_encoder", torch_dtype=dtype
)
if (
self.model_config.layer_offloading
and self.model_config.layer_offloading_text_encoder_percent > 0
):
MemoryManager.attach(
text_encoder,
self.device_torch,
offload_percent=self.model_config.layer_offloading_text_encoder_percent,
)
text_encoder.to(self.device_torch, dtype=dtype)
flush()
if self.model_config.quantize_te:
self.print_and_status_update("Quantizing Text Encoder")
quantize(text_encoder, weights=get_qtype(self.model_config.qtype_te))
freeze(text_encoder)
flush()
self.print_and_status_update("Loading VAE") self.print_and_status_update("Loading VAE")
vae = FakeVAE(scaling_factor=1.0) vae = FakeVAE(scaling_factor=1.0)
+33 -1
View File
@@ -1627,7 +1627,39 @@ class BaseModel:
encoder.to(*args, **kwargs) encoder.to(*args, **kwargs)
else: else:
self.text_encoder.to(*args, **kwargs) self.text_encoder.to(*args, **kwargs)
def prepare_text_encoder(self, text_encoder, dtype=None):
"""Standard post-load text-encoder policy: layer offloading, device
placement, then quantize_te. Skips quantization when the checkpoint
loaded pre-quantized."""
from optimum.quanto import freeze
from toolkit.memory_management import MemoryManager
from toolkit.util.quantize import get_qtype, quantize
dtype = dtype if dtype is not None else self.torch_dtype
if (
self.model_config.layer_offloading
and self.model_config.layer_offloading_text_encoder_percent > 0
):
MemoryManager.attach(
text_encoder,
self.device_torch,
offload_percent=self.model_config.layer_offloading_text_encoder_percent,
)
text_encoder.to(self.device_torch, dtype=dtype)
flush()
if self.model_config.quantize_te and not getattr(
text_encoder, "aitk_is_quantized", False
):
self.print_and_status_update("Quantizing Text Encoder")
quantize(text_encoder, weights=get_qtype(self.model_config.qtype_te))
freeze(text_encoder)
flush()
return text_encoder
def convert_lora_weights_before_save(self, state_dict): def convert_lora_weights_before_save(self, state_dict):
# can be overridden in child classes to convert weights before saving # can be overridden in child classes to convert weights before saving
if self.lora_keys_use_comfy_prefix: if self.lora_keys_use_comfy_prefix:
+18 -35
View File
@@ -1,46 +1,29 @@
from typing import List from typing import List
import torch
from transformers import T5Tokenizer, UMT5EncoderModel
class PatchedT5Tokenizer(T5Tokenizer): import torch
def __init__(
self, from toolkit.models.v2.text_encoders.umt5 import (
vocab: str | list[tuple[str, float]] | None = None, PatchedT5Tokenizer,
eos_token="</s>", UMT5TextEncoder,
unk_token="<unk>", )
pad_token="<pad>",
_spm_precompiled_charsmap=None,
extra_ids=100,
additional_special_tokens=None,
**kwargs,
):
super().__init__(
vocab=vocab,
eos_token=eos_token,
unk_token=unk_token,
pad_token=pad_token,
_spm_precompiled_charsmap=None, # this is passing a empty byte string for some reason now.
extra_ids=extra_ids,
additional_special_tokens=additional_special_tokens,
**kwargs,
)
def get_umt5_encoder( def get_umt5_encoder(
model_path: str, model_path: str,
tokenizer_subfolder: str = None, tokenizer_subfolder: str = None,
encoder_subfolder: str = None, encoder_subfolder: str = None,
torch_dtype: str = torch.bfloat16, torch_dtype: str = torch.bfloat16,
comfy_files: List[str] = [ # reserved for the comfy-weights flip (Phase 2); accepted for
"text_encoders/umt5_xxl_fp16.safetensors", # signature compatibility, not consulted yet
"text_encoders/umt5_xxl_fp8_e4m3fn_scaled.safetensors", comfy_files: List[str] = None,
], ):
) -> UMT5EncoderModel: """Load the UMT5 tokenizer + encoder. Thin compatibility wrapper around
""" toolkit/models/v2/text_encoders/umt5.py."""
Load the UMT5 encoder model from the specified path. tokenizer = UMT5TextEncoder.load_tokenizer(
""" model_path, subfolder=tokenizer_subfolder or ""
tokenizer = PatchedT5Tokenizer.from_pretrained(model_path, subfolder=tokenizer_subfolder) )
print(f"Using {model_path} for UMT5 encoder.") print(f"Using {model_path} for UMT5 encoder.")
text_encoder = UMT5EncoderModel.from_pretrained( text_encoder = UMT5TextEncoder.load_model(
model_path, subfolder=encoder_subfolder, torch_dtype=torch_dtype model_path, dtype=torch_dtype, subfolder=encoder_subfolder or ""
) )
return tokenizer, text_encoder return tokenizer, text_encoder
+82 -15
View File
@@ -166,21 +166,88 @@ Every arch's components become v2 classes; if `name_or_path` is diffusers, it st
loads via diffusers. Nothing about sources or outputs changes yet. Suggested order loads via diffusers. Nothing about sources or outputs changes yet. Suggested order
(worst duplication first), each including its loading test (see Testing): (worst duplication first), each including its loading test (see Testing):
- [ ] Shared TEs: `text_encoders/qwen3_vl.py`, `text_encoders/qwen3.py`, - [x] `text_encoders/qwen3.py` — Qwen3TextEncoder + `OstrisTransformersMixin`
`text_encoders/t5.py`, `text_encoders/clip.py` backend + `BaseModel.prepare_text_encoder` policy helper; the 3 verbatim
- [ ] Shared VAEs: `vae/flux_kl.py` (kills flux2 + ideogram4 copies and the 4 TE stanzas (z_image, z_image_l2p, zeta_chroma) replaced. Verified with
scattered `AutoencoderKL` loads), `vae/qwen_image.py` (mean/std handling real Z-Image weights (load + encode on GPU).
built in) - [x] `text_encoders/qwen3_vl.py` — Qwen3VLTextEncoder with
- [ ] z_image / z_image_l2p (already half-migrated) `drop_vision_tower` / `patch_vision_patch_embed`; the 4 identical
- [ ] qwen_image family (qwen_image, qwen_image_edit, qwen_image_edit_plus) `patch_qwen_vl_patch_embed` copies (krea2, mageflow, boogu_image,
- [ ] nucleus_image, krea2, ideogram4, mageflow Qwen3VLCaptioner) consolidated; TE loads migrated in krea2, mageflow,
- [ ] chroma, chroma_radiance, zeta_chroma nucleus_image. Still on their own paths: ideogram4 (loads via AutoModel),
- [ ] flux2, flux_kontext minimax_h3 (custom truncated/prequantized comfy load — port later),
- [ ] minimax_h3 (+ ref2va), ltx2 family qwen_image (Qwen2.5-VL, needs its own class)
- [ ] wan21 / wan22 family - [x] `text_encoders/t5.py`, `text_encoders/clip.py` — T5TextEncoder,
- [ ] hidream family, omnigen2 CLIPTextEncoder, CLIPTextEncoderWithProjection; migrated chroma ×2,
- [ ] anima, boogu_image, ernie_image, f_light, prx_pixel_t2i flux_kontext, f_light (T5 stanzas → `prepare_text_encoder`, fixing their
- [ ] audio_models (ace_step) `qtype` → `qtype_te` bug) and hidream (CLIP ×2 + T5 with subfolder
overrides; slow-tokenizer classes preserved via `use_fast=False`)
- [x] `vae/qwen_image.py` — QwenImageVAE + QwenImageVAEHolderMixin (frame-dim +
latents mean/std handling built in, tiling opt-in via
`vae_decode_tiled_on_low_vram`); the triplicated encode/decode deleted
from qwen_image, nucleus_image, krea2 and all three VAE loads routed
through the v2 loader
- [x] `vae/autoencoder_kl.py` — KLVAE (diffusers AutoencoderKL through the
universal loader); migrated the scattered loads in chroma, flux_kontext,
f_light, hidream, z_image
- [x] `vae/flux2_kl.py` — the BFL-style Flux2 KL autoencoder unified from the
flux2 + ideogram4 copies (both files deleted; flux2's
encode/decode/small-decoder superset + ideogram4's diffusers key
converter). Verified bit-identical to both originals (weights, encode/
decode outputs, converter mapping) and round-tripped real ae.safetensors
weights on GPU. Packing/normalization stays per-model — flux2 packs
`(c pi pj)` with BatchNorm running stats, ideogram4 packs `(ph pw c)`
with its latent_norm tables; the conventions are incompatible.
- [x] z_image — transformer, TE (qwen3), and VAE (KLVAE) all on v2 modules.
z_image_l2p still has its local progressive-transformer subclass
(rebasing it onto the v2 class deferred; its TE is migrated)
- [x] qwen_image family — `v2/diffusion_models/qwen_image.py` (single-file
loads stay on diffusers' from_single_file until the comfy flip) +
`v2/text_encoders/qwen25_vl.py` (slow tokenizer preserved); edit
variants inherit
- [x] nucleus_image — `v2/diffusion_models/nucleus_image.py`, TE stanza
collapsed to prepare_text_encoder
- [ ] krea2, ideogram4, mageflow — TE/VAE migrated; their custom local DiT
classes still to be rebased onto the mixin
- [x] chroma, chroma_radiance — both vendored Chroma classes now carry
`OstrisModelMixin` with the block-count sniff moved into a new
`aitk_config_from_state_dict` hook (mixin now supports checkpoint-derived
configs + `load_from_state_dict` for non-safetensors sources, used by
radiance's .pth path). zeta_chroma transformer left as-is: its config
depends on holder state (patch_size), not the checkpoint
- [x] flux_kontext — `v2/diffusion_models/flux.py` (FluxTransformer2DModel);
whole model now loads through v2 (transformer, T5, CLIP, KLVAE)
- [ ] flux2 — TE/VAE partially migrated (flux2_kl); custom DiT still local.
krea2/mageflow/ideogram4/zeta_chroma DiTs stay model-specific: their
configs come from model_kwargs / holder state, so the mixin adds nothing
until the comfy-weights flip (Phase 2)
- [ ] minimax_h3 (+ ref2va), ltx2 family — already on the shared resolver +
comfy_quant_import; the full mixin port waits for Phase 2, when the
mixin's single-file precision policy (stored-precision loading, fp32-key
protection) is settled to match their deliberate behavior
- [x] wan21 / wan22 family — `v2/diffusion_models/wan.py`
(WanTransformer3DModel, both wan22 dual loads included) +
`v2/text_encoders/umt5.py` (UMT5TextEncoder + PatchedT5Tokenizer;
`loaders/umt5.py` is now a thin compat shim, `comfy_files` still
reserved for Phase 2 — no local comfy umt5 file to verify the key
conversion against). wan21's TE `qtype` → `qtype_te` bug fixed via
prepare_text_encoder
- [x] hidream family — vendored transformer carries the mixin;
`v2/diffusion_models/hidream.py` wraps the diffusers class for
hidream_e1; both load via the switchable `hidream_transformer_class`
through `load_model`
- [x] omnigen2 — vendored transformer carries the mixin, load migrated
- [x] boogu_image, ernie_image, prx_pixel_t2i — their vendored diffusers-style
DiT classes now carry OstrisModelMixin (subfolder + block names on the
class) and the holders load via `load_model`
- [x] f_light — DiT class carries the mixin (`aitk_subfolder="dit_model"`),
load migrated
- [ ] anima — loads through diffusers modular pipelines (AnimaModularPipeline);
not a mixin fit, revisit at Phase 2
- [ ] flux2 DiT — holder-config params classes (Flux2/Klein variants), defer
like krea2/mageflow
- [ ] ace_step — one bundled safetensors holds model+TE+VAE+tokenizer via its
own load_models; decomposing into v2 components is its own task
- [ ] Per-model fixes folded in as each migrates: `qtype_te` bug, dequant-on-save - [ ] Per-model fixes folded in as each migrates: `qtype_te` bug, dequant-on-save
(`dequantize_if_quantized` everywhere), raw-`quantize()` → `quantize_model()` (`dequantize_if_quantized` everywhere), raw-`quantize()` → `quantize_model()`
+86 -8
View File
@@ -141,9 +141,13 @@ class OstrisModelMixin:
they were. they were.
config_path: config source for single-file loads, overriding aitk_config_repo. config_path: config source for single-file loads, overriding aitk_config_repo.
device: move the finished model there before returning. device: move the finished model there before returning.
subfolder: overrides the class's aitk_subfolder; pass "" to explicitly
load from the checkpoint root (e.g. a raw hub repo).
""" """
if subfolder is None: if subfolder is None:
subfolder = cls.aitk_subfolder subfolder = cls.aitk_subfolder
elif subfolder == "":
subfolder = None
if name_or_path.endswith(".safetensors"): if name_or_path.endswith(".safetensors"):
file_path = cls._resolve_single_file(name_or_path) file_path = cls._resolve_single_file(name_or_path)
@@ -213,11 +217,34 @@ class OstrisModelMixin:
config_path: Optional[str] = None, config_path: Optional[str] = None,
subfolder: Optional[str] = None, subfolder: Optional[str] = None,
): ):
config = cls._load_single_file_config(config_path, subfolder)
state_dict = load_file(file_path) state_dict = load_file(file_path)
return cls.load_from_state_dict(
state_dict, dtype, config_path=config_path, subfolder=subfolder
)
@classmethod
def aitk_config_from_state_dict(cls, state_dict: Dict[str, torch.Tensor]):
"""Derive the model config from the checkpoint itself (e.g. sniffing
block counts from key indices). Return None (the default) to load the
config from config_path / aitk_config_repo instead."""
return None
@classmethod
def load_from_state_dict(
cls,
state_dict: Dict[str, torch.Tensor],
dtype: torch.dtype,
config_path: Optional[str] = None,
subfolder: Optional[str] = None,
):
"""Build the model and load an already-read single-file state dict
(the tail of the single-file path; also callable directly for
checkpoints read from non-safetensors sources)."""
state_dict = cls.convert_state_dict_on_load(state_dict) state_dict = cls.convert_state_dict_on_load(state_dict)
has_quant_markers = any(k.endswith(".comfy_quant") for k in state_dict) has_quant_markers = any(k.endswith(".comfy_quant") for k in state_dict)
config = cls.aitk_config_from_state_dict(state_dict)
if config is None:
config = cls._load_single_file_config(config_path, subfolder)
model = cls.aitk_from_config(config) model = cls.aitk_from_config(config)
if has_quant_markers: if has_quant_markers:
@@ -290,23 +317,51 @@ class OstrisModelMixin:
# ------------------------------------------------------------------ # ------------------------------------------------------------------
@classmethod @classmethod
def load_tokenizer(cls, **kwargs): def load_tokenizer(
cls,
name_or_path: Optional[str] = None,
subfolder: Optional[str] = None,
**kwargs,
):
"""Load the tokenizer from ``name_or_path`` (a checkpoint dir or repo
holding it at aitk_tokenizer_subfolder), falling back to the class's
aitk_tokenizer_repo. subfolder overrides the class default; "" loads
from the checkpoint root."""
from transformers import AutoTokenizer from transformers import AutoTokenizer
if cls.aitk_tokenizer_repo is None: source = name_or_path if name_or_path is not None else cls.aitk_tokenizer_repo
if source is None:
raise ValueError(f"{cls.__name__} does not declare aitk_tokenizer_repo") raise ValueError(f"{cls.__name__} does not declare aitk_tokenizer_repo")
if subfolder is None:
subfolder = cls.aitk_tokenizer_subfolder
if subfolder and os.path.isdir(source) and not os.path.isdir(
os.path.join(source, subfolder)
):
subfolder = None
return AutoTokenizer.from_pretrained( return AutoTokenizer.from_pretrained(
cls.aitk_tokenizer_repo, subfolder=cls.aitk_tokenizer_subfolder, **kwargs source, subfolder=subfolder or "", **kwargs
) )
@classmethod @classmethod
def load_processor(cls, **kwargs): def load_processor(
cls,
name_or_path: Optional[str] = None,
subfolder: Optional[str] = None,
**kwargs,
):
from transformers import AutoProcessor from transformers import AutoProcessor
if cls.aitk_processor_repo is None: source = name_or_path if name_or_path is not None else cls.aitk_processor_repo
if source is None:
raise ValueError(f"{cls.__name__} does not declare aitk_processor_repo") raise ValueError(f"{cls.__name__} does not declare aitk_processor_repo")
if subfolder is None:
subfolder = cls.aitk_processor_subfolder
if subfolder and os.path.isdir(source) and not os.path.isdir(
os.path.join(source, subfolder)
):
subfolder = None
return AutoProcessor.from_pretrained( return AutoProcessor.from_pretrained(
cls.aitk_processor_repo, subfolder=cls.aitk_processor_subfolder, **kwargs source, subfolder=subfolder or "", **kwargs
) )
# ------------------------------------------------------------------ # ------------------------------------------------------------------
@@ -403,3 +458,26 @@ class OstrisModelMixin:
self.aitk_qtype = qtype self.aitk_qtype = qtype
flush() flush()
return self return self
class OstrisTransformersMixin(OstrisModelMixin):
"""OstrisModelMixin with the backend hooks speaking the transformers-lib
API (PreTrainedModel / AutoConfig) instead of diffusers ModelMixin. Base
for text-encoder and vision-encoder modules."""
@classmethod
def aitk_from_pretrained(cls, path, subfolder=None, dtype=None, **kwargs):
return cls.from_pretrained(
path, subfolder=subfolder or "", torch_dtype=dtype, **kwargs
)
@classmethod
def aitk_load_config(cls, path, subfolder=None):
from transformers import AutoConfig
return AutoConfig.from_pretrained(path, subfolder=subfolder or "")
@classmethod
def aitk_from_config(cls, config):
with torch.device("meta"):
return cls(config)
@@ -0,0 +1,14 @@
from diffusers import FluxTransformer2DModel as DiffusersFluxTransformer2DModel
from .._mixin import OstrisModelMixin
class FluxTransformer2DModel(DiffusersFluxTransformer2DModel, OstrisModelMixin):
"""Flux1-family DiT (flux, flux_kontext, chroma-adjacent finetunes in
diffusers layout)."""
aitk_subfolder = "transformer"
@classmethod
def get_transformer_block_names(cls):
return ["transformer_blocks", "single_transformer_blocks"]
@@ -0,0 +1,18 @@
from diffusers.models import (
HiDreamImageTransformer2DModel as DiffusersHiDreamImageTransformer2DModel,
)
from .._mixin import OstrisModelMixin
class HiDreamImageTransformer2DModel(
DiffusersHiDreamImageTransformer2DModel, OstrisModelMixin
):
"""The diffusers HiDream DiT (hidream_e1; the base hidream arch uses the
vendored copy in the hidream extension)."""
aitk_subfolder = "transformer"
@classmethod
def get_transformer_block_names(cls):
return ["double_stream_blocks", "single_stream_blocks"]
@@ -0,0 +1,15 @@
from diffusers import (
NucleusMoEImageTransformer2DModel as DiffusersNucleusMoEImageTransformer2DModel,
)
from .._mixin import OstrisModelMixin
class NucleusMoEImageTransformer2DModel(
DiffusersNucleusMoEImageTransformer2DModel, OstrisModelMixin
):
aitk_subfolder = "transformer"
@classmethod
def get_transformer_block_names(cls):
return ["transformer_blocks"]
@@ -0,0 +1,29 @@
from diffusers import (
QwenImageTransformer2DModel as DiffusersQwenImageTransformer2DModel,
)
from .._mixin import OstrisModelMixin
class QwenImageTransformer2DModel(
DiffusersQwenImageTransformer2DModel, OstrisModelMixin
):
aitk_subfolder = "transformer"
aitk_config_repo = "Qwen/Qwen-Image"
@classmethod
def get_transformer_block_names(cls):
return ["transformer_blocks"]
@classmethod
def _load_single_file(cls, file_path, dtype, config_path=None, subfolder=None):
# single-file checkpoints in the wild carry diffusers or original key
# layouts; diffusers' single-file machinery owns that conversion
model = cls.from_single_file(
file_path,
config=config_path if config_path is not None else cls.aitk_config_repo,
subfolder="transformer",
torch_dtype=dtype,
)
model.to(dtype)
return model
+13
View File
@@ -0,0 +1,13 @@
from diffusers import WanTransformer3DModel as DiffusersWanTransformer3DModel
from .._mixin import OstrisModelMixin
class WanTransformer3DModel(DiffusersWanTransformer3DModel, OstrisModelMixin):
"""Wan 2.1/2.2 video DiT (wan22 loads two of these into its dual wrapper)."""
aitk_subfolder = "transformer"
@classmethod
def get_transformer_block_names(cls):
return ["blocks"]
+27
View File
@@ -0,0 +1,27 @@
from transformers import CLIPTextModel, CLIPTextModelWithProjection
from .._mixin import OstrisTransformersMixin
class CLIPTextEncoder(CLIPTextModel, OstrisTransformersMixin):
"""CLIP-L text encoder (flux-style checkpoints: text_encoder/ +
tokenizer/)."""
aitk_subfolder = "text_encoder"
aitk_tokenizer_subfolder = "tokenizer"
@classmethod
def get_transformer_block_names(cls):
return ["text_model.encoder.layers"]
class CLIPTextEncoderWithProjection(CLIPTextModelWithProjection, OstrisTransformersMixin):
"""CLIP text encoder with the projection head (SDXL / SD3 / HiDream style
checkpoints; the second encoder lives at text_encoder_2/ + tokenizer_2/)."""
aitk_subfolder = "text_encoder"
aitk_tokenizer_subfolder = "tokenizer"
@classmethod
def get_transformer_block_names(cls):
return ["text_model.encoder.layers"]
@@ -0,0 +1,22 @@
from transformers import Qwen2_5_VLForConditionalGeneration
from .._mixin import OstrisTransformersMixin
class Qwen25VLTextEncoder(Qwen2_5_VLForConditionalGeneration, OstrisTransformersMixin):
"""Qwen2.5-VL conditioning stack (qwen_image family). Loads from a
checkpoint's text_encoder/ subfolder; the edit variants keep the vision
tower, plain t2i drops it."""
aitk_subfolder = "text_encoder"
aitk_tokenizer_subfolder = "tokenizer"
@classmethod
def get_transformer_block_names(cls):
return ["model.language_model.layers"]
def drop_vision_tower(self):
"""Text-only conditioning: the vision tower is dead weight."""
if getattr(self.model, "visual", None) is not None:
self.model.visual = None
return self
+17
View File
@@ -0,0 +1,17 @@
from transformers import Qwen3ForCausalLM
from .._mixin import OstrisTransformersMixin
class Qwen3TextEncoder(Qwen3ForCausalLM, OstrisTransformersMixin):
"""Qwen3 causal-LM text encoder (Z-Image family, Zeta-Chroma, ...). Loads
from a checkpoint's text_encoder/ subfolder, a hub repo, or a single
.safetensors file; the tokenizer rides in the checkpoint's tokenizer/
subfolder."""
aitk_subfolder = "text_encoder"
aitk_tokenizer_subfolder = "tokenizer"
@classmethod
def get_transformer_block_names(cls):
return ["model.layers"]
@@ -0,0 +1,53 @@
import torch
import torch.nn.functional as F
from transformers import Qwen3VLForConditionalGeneration
from .._mixin import OstrisTransformersMixin
def patch_qwen_vl_patch_embed(model) -> int:
"""Qwen-VL's vision patch_embed is a Conv3d whose kernel == stride, i.e. a plain
linear projection of each flattened patch. bf16 Conv3d has no fast cuDNN kernel and
falls back to a slow, GPU-underutilizing path. Swap it for the equivalent F.linear
(a GEMM). The weight is read lazily so this survives later .to(device)/dtype moves.
Returns the number of patch_embed modules patched."""
patched = 0
for module in model.modules():
proj = getattr(module, "proj", None)
if isinstance(proj, torch.nn.Conv3d) and tuple(proj.kernel_size) == tuple(
proj.stride
):
def fast_forward(hidden_states, _proj=proj):
w = _proj.weight.reshape(_proj.weight.shape[0], -1)
x = hidden_states.view(-1, w.shape[1]).to(w.dtype)
return F.linear(x, w, _proj.bias)
module.forward = fast_forward
patched += 1
return patched
class Qwen3VLTextEncoder(Qwen3VLForConditionalGeneration, OstrisTransformersMixin):
"""Qwen3-VL conditioning stack (krea2, mageflow, nucleus_image, ideogram4,
minimax_h3, ...). Loads from a checkpoint's text_encoder/ subfolder or a
raw Qwen repo (pass subfolder=\"\" for the latter)."""
aitk_subfolder = "text_encoder"
aitk_processor_subfolder = "processor"
@classmethod
def get_transformer_block_names(cls):
return ["model.language_model.layers"]
def drop_vision_tower(self):
"""Text-only conditioning: the vision tower is dead weight — drop it to
free VRAM and skip loading its (bf16-slow) Conv3d patch_embed."""
if getattr(self.model, "visual", None) is not None:
self.model.visual = None
return self
def patch_vision_patch_embed(self) -> int:
"""Keep the vision tower (reference images ride into the embeddings)
but swap its Conv3d patch_embed for an equivalent GEMM."""
return patch_qwen_vl_patch_embed(self)
+16
View File
@@ -0,0 +1,16 @@
from transformers import T5EncoderModel
from .._mixin import OstrisTransformersMixin
class T5TextEncoder(T5EncoderModel, OstrisTransformersMixin):
"""T5-XXL text encoder. Defaults to the flux-style checkpoint layout
(text_encoder_2/ + tokenizer_2/); pass subfolder overrides for checkpoints
that keep it at text_encoder/ + tokenizer/ (e.g. f-lite)."""
aitk_subfolder = "text_encoder_2"
aitk_tokenizer_subfolder = "tokenizer_2"
@classmethod
def get_transformer_block_names(cls):
return ["encoder.block"]
+49
View File
@@ -0,0 +1,49 @@
import torch
from transformers import T5Tokenizer, UMT5EncoderModel
from .._mixin import OstrisTransformersMixin
class PatchedT5Tokenizer(T5Tokenizer):
def __init__(
self,
vocab=None,
eos_token="</s>",
unk_token="<unk>",
pad_token="<pad>",
_spm_precompiled_charsmap=None,
extra_ids=100,
additional_special_tokens=None,
**kwargs,
):
super().__init__(
vocab=vocab,
eos_token=eos_token,
unk_token=unk_token,
pad_token=pad_token,
_spm_precompiled_charsmap=None, # this is passing a empty byte string for some reason now.
extra_ids=extra_ids,
additional_special_tokens=additional_special_tokens,
**kwargs,
)
class UMT5TextEncoder(UMT5EncoderModel, OstrisTransformersMixin):
"""UMT5-XXL text encoder (wan family)."""
aitk_subfolder = "text_encoder"
aitk_tokenizer_subfolder = "tokenizer"
@classmethod
def get_transformer_block_names(cls):
return ["encoder.block"]
@classmethod
def load_tokenizer(cls, name_or_path=None, subfolder=None, **kwargs):
# T5's tokenizer needs the _spm_precompiled_charsmap patch
source = name_or_path if name_or_path is not None else cls.aitk_tokenizer_repo
if subfolder is None:
subfolder = cls.aitk_tokenizer_subfolder
return PatchedT5Tokenizer.from_pretrained(
source, subfolder=subfolder or "", **kwargs
)
+10
View File
@@ -0,0 +1,10 @@
from diffusers import AutoencoderKL
from .._mixin import OstrisModelMixin
class KLVAE(AutoencoderKL, OstrisModelMixin):
"""The diffusers AutoencoderKL (SD/SDXL/Flux1/Z-Image image VAEs), loaded
from a checkpoint's vae/ subfolder through the universal loader."""
aitk_subfolder = "vae"
@@ -1,4 +1,13 @@
"""Flux2 KL autoencoder.""" """The BFL-style Flux2 KL autoencoder (32ch latents, 2x2 pixel-shuffle
packing to 128ch), shared by the flux2 family and ideogram4.
flux2 loads the raw BFL ae.safetensors layout and uses encode/decode (with the
BatchNorm running-stats latent normalization); ideogram4 loads diffusers-format
checkpoints via convert_diffusers_state_dict and drives encoder/decoder
directly with its own patchify + latent-norm tables. The two models pack the
128 latent channels in different orders — the packing/normalization stays
per-model.
"""
from __future__ import annotations from __future__ import annotations
@@ -22,6 +31,17 @@ class AutoEncoderParams:
num_res_blocks: int = 2 num_res_blocks: int = 2
z_channels: int = 32 z_channels: int = 32
@dataclass
class AutoEncoderSmallDecoderParams:
resolution: int = 256
in_channels: int = 3
ch: int = 128
ch_encoder: int = 96
out_ch: int = 3
ch_mult: list[int] = field(default_factory=lambda: [1, 2, 4, 4])
num_res_blocks: int = 2
z_channels: int = 32
def swish(x: Tensor) -> Tensor: def swish(x: Tensor) -> Tensor:
return x * torch.sigmoid(x) return x * torch.sigmoid(x)
@@ -194,30 +214,30 @@ class Encoder(nn.Module):
for i_level in range(self.num_resolutions): for i_level in range(self.num_resolutions):
for i_block in range(self.num_res_blocks): for i_block in range(self.num_res_blocks):
if torch.is_grad_enabled() and self.gradient_checkpointing: if torch.is_grad_enabled() and self.gradient_checkpointing:
h = ckpt.checkpoint(self.down[i_level].block[i_block], hs[-1]) # type: ignore[index, operator] h = ckpt.checkpoint(self.down[i_level].block[i_block], hs[-1])
if len(self.down[i_level].attn) > 0: # type: ignore[arg-type] if len(self.down[i_level].attn) > 0:
h = ckpt.checkpoint(self.down[i_level].attn[i_block], h) # type: ignore[index, operator] h = ckpt.checkpoint(self.down[i_level].attn[i_block], h)
else: else:
h = self.down[i_level].block[i_block](hs[-1]) # type: ignore[index, operator] h = self.down[i_level].block[i_block](hs[-1])
if len(self.down[i_level].attn) > 0: # type: ignore[arg-type] if len(self.down[i_level].attn) > 0:
h = self.down[i_level].attn[i_block](h) # type: ignore[index, operator] h = self.down[i_level].attn[i_block](h)
hs.append(h) hs.append(h)
if i_level != self.num_resolutions - 1: if i_level != self.num_resolutions - 1:
if torch.is_grad_enabled() and self.gradient_checkpointing: if torch.is_grad_enabled() and self.gradient_checkpointing:
hs.append(ckpt.checkpoint(self.down[i_level].downsample, hs[-1])) # type: ignore[operator] hs.append(ckpt.checkpoint(self.down[i_level].downsample, hs[-1]))
else: else:
hs.append(self.down[i_level].downsample(hs[-1])) # type: ignore[operator] hs.append(self.down[i_level].downsample(hs[-1]))
# middle # middle
h = hs[-1] h = hs[-1]
if torch.is_grad_enabled() and self.gradient_checkpointing: if torch.is_grad_enabled() and self.gradient_checkpointing:
h = ckpt.checkpoint(self.mid.block_1, h) # type: ignore[operator] h = ckpt.checkpoint(self.mid.block_1, h)
h = ckpt.checkpoint(self.mid.attn_1, h) # type: ignore[operator] h = ckpt.checkpoint(self.mid.attn_1, h)
h = ckpt.checkpoint(self.mid.block_2, h) # type: ignore[operator] h = ckpt.checkpoint(self.mid.block_2, h)
else: else:
h = self.mid.block_1(h) # type: ignore[operator] h = self.mid.block_1(h)
h = self.mid.attn_1(h) # type: ignore[operator] h = self.mid.attn_1(h)
h = self.mid.block_2(h) # type: ignore[operator] h = self.mid.block_2(h)
# end # end
h = self.norm_out(h) h = self.norm_out(h)
h = swish(h) h = swish(h)
@@ -300,13 +320,13 @@ class Decoder(nn.Module):
# middle # middle
if torch.is_grad_enabled() and self.gradient_checkpointing: if torch.is_grad_enabled() and self.gradient_checkpointing:
h = ckpt.checkpoint(self.mid.block_1, h) # type: ignore[operator] h = ckpt.checkpoint(self.mid.block_1, h)
h = ckpt.checkpoint(self.mid.attn_1, h) # type: ignore[operator] h = ckpt.checkpoint(self.mid.attn_1, h)
h = ckpt.checkpoint(self.mid.block_2, h) # type: ignore[operator] h = ckpt.checkpoint(self.mid.block_2, h)
else: else:
h = self.mid.block_1(h) # type: ignore[operator] h = self.mid.block_1(h)
h = self.mid.attn_1(h) # type: ignore[operator] h = self.mid.attn_1(h)
h = self.mid.block_2(h) # type: ignore[operator] h = self.mid.block_2(h)
# cast to proper dtype # cast to proper dtype
h = h.to(upscale_dtype) h = h.to(upscale_dtype)
@@ -314,18 +334,18 @@ class Decoder(nn.Module):
for i_level in reversed(range(self.num_resolutions)): for i_level in reversed(range(self.num_resolutions)):
for i_block in range(self.num_res_blocks + 1): for i_block in range(self.num_res_blocks + 1):
if torch.is_grad_enabled() and self.gradient_checkpointing: if torch.is_grad_enabled() and self.gradient_checkpointing:
h = ckpt.checkpoint(self.up[i_level].block[i_block], h) # type: ignore[index, operator] h = ckpt.checkpoint(self.up[i_level].block[i_block], h)
if len(self.up[i_level].attn) > 0: # type: ignore[arg-type] if len(self.up[i_level].attn) > 0:
h = ckpt.checkpoint(self.up[i_level].attn[i_block], h) # type: ignore[index, operator] h = ckpt.checkpoint(self.up[i_level].attn[i_block], h)
else: else:
h = self.up[i_level].block[i_block](h) # type: ignore[index, operator] h = self.up[i_level].block[i_block](h)
if len(self.up[i_level].attn) > 0: # type: ignore[arg-type] if len(self.up[i_level].attn) > 0:
h = self.up[i_level].attn[i_block](h) # type: ignore[index, operator] h = self.up[i_level].attn[i_block](h)
if i_level != 0: if i_level != 0:
if torch.is_grad_enabled() and self.gradient_checkpointing: if torch.is_grad_enabled() and self.gradient_checkpointing:
h = ckpt.checkpoint(self.up[i_level].upsample, h) # type: ignore[operator] h = ckpt.checkpoint(self.up[i_level].upsample, h)
else: else:
h = self.up[i_level].upsample(h) # type: ignore[operator] h = self.up[i_level].upsample(h)
# end # end
h = self.norm_out(h) h = self.norm_out(h)
@@ -346,10 +366,13 @@ class AutoEncoder(nn.Module):
num_res_blocks=params.num_res_blocks, num_res_blocks=params.num_res_blocks,
z_channels=params.z_channels, z_channels=params.z_channels,
) )
decoder_ch = params.ch
if hasattr(params, "ch_encoder"):
decoder_ch = params.ch_encoder
self.decoder = Decoder( self.decoder = Decoder(
resolution=params.resolution, resolution=params.resolution,
in_channels=params.in_channels, in_channels=params.in_channels,
ch=params.ch, ch=decoder_ch,
out_ch=params.out_ch, out_ch=params.out_ch,
ch_mult=params.ch_mult, ch_mult=params.ch_mult,
num_res_blocks=params.num_res_blocks, num_res_blocks=params.num_res_blocks,
@@ -369,7 +392,7 @@ class AutoEncoder(nn.Module):
self._gradient_checkpointing = False self._gradient_checkpointing = False
@property @property
def gradient_checkpointing(self) -> bool: def gradient_checkpointing(self):
return self._gradient_checkpointing return self._gradient_checkpointing
@gradient_checkpointing.setter @gradient_checkpointing.setter
@@ -378,18 +401,52 @@ class AutoEncoder(nn.Module):
self.encoder.gradient_checkpointing = value self.encoder.gradient_checkpointing = value
self.decoder.gradient_checkpointing = value self.decoder.gradient_checkpointing = value
@property
def device(self):
return next(self.parameters()).device
@property
def dtype(self):
return next(self.parameters()).dtype
def enable_gradient_checkpointing(self): def enable_gradient_checkpointing(self):
self.gradient_checkpointing = True self.gradient_checkpointing = True
self.encoder.enable_gradient_checkpointing() self.encoder.enable_gradient_checkpointing()
self.decoder.enable_gradient_checkpointing() self.decoder.enable_gradient_checkpointing()
@property def normalize(self, z):
def device(self) -> torch.device: self.bn.eval()
return next(self.parameters()).device return self.bn(z)
@property def inv_normalize(self, z):
def dtype(self) -> torch.dtype: self.bn.eval()
return next(self.parameters()).dtype s = torch.sqrt(self.bn.running_var.view(1, -1, 1, 1) + self.bn_eps)
m = self.bn.running_mean.view(1, -1, 1, 1)
return z * s + m
def encode(self, x: Tensor) -> Tensor:
moments = self.encoder(x)
mean = torch.chunk(moments, 2, dim=1)[0]
z = rearrange(
mean,
"... c (i pi) (j pj) -> ... (c pi pj) i j",
pi=self.ps[0],
pj=self.ps[1],
)
z = self.normalize(z)
return z
def decode(self, z: Tensor) -> Tensor:
z = self.inv_normalize(z)
z = rearrange(
z,
"... (c pi pj) i j -> ... c (i pi) (j pj)",
pi=self.ps[0],
pj=self.ps[1],
)
dec = self.decoder(z)
return dec
_NUM_RESOLUTIONS = 4 _NUM_RESOLUTIONS = 4
+86
View File
@@ -0,0 +1,86 @@
import torch
from diffusers import AutoencoderKLQwenImage
from .._mixin import OstrisModelMixin
class QwenImageVAE(AutoencoderKLQwenImage, OstrisModelMixin):
"""The Qwen-Image (wan-style video) VAE, shared by the qwen_image family,
nucleus_image and krea2."""
aitk_subfolder = "vae"
class QwenImageVAEHolderMixin:
"""BaseModel-side encode_images/decode_latents for models whose self.vae
is the Qwen-Image VAE: it is a video VAE, so images ride in a single-frame
dim and latents are normalized with the config's latents_mean/std."""
# tile the decode when low_vram (decode only; encode stays untiled)
vae_decode_tiled_on_low_vram = False
def encode_images(self, image_list, device=None, dtype=None):
if device is None:
device = self.vae_device_torch
if dtype is None:
dtype = self.vae_torch_dtype
if self.vae.device == torch.device("cpu"):
self.vae.to(device)
self.vae.eval()
self.vae.requires_grad_(False)
image_list = [image.to(device, dtype=dtype) for image in image_list]
images = torch.stack(image_list).to(device, dtype=dtype)
images = images.unsqueeze(2) # add the frame dim
latents = self.vae.encode(images).latent_dist.sample()
latents_mean = (
torch.tensor(self.vae.config.latents_mean)
.view(1, self.vae.config.z_dim, 1, 1, 1)
.to(latents.device, latents.dtype)
)
latents_std = 1.0 / torch.tensor(self.vae.config.latents_std).view(
1, self.vae.config.z_dim, 1, 1, 1
).to(latents.device, latents.dtype)
latents = (latents - latents_mean) * latents_std
latents = latents.squeeze(2) # drop the frame dim
return latents.to(device, dtype=dtype)
def decode_latents(self, latents: torch.Tensor, device=None, dtype=None):
if device is None:
device = self.vae_device_torch
if dtype is None:
dtype = self.vae_torch_dtype
if self.vae.device == torch.device("cpu"):
self.vae.to(device)
latents = latents.to(device, dtype=dtype)
latents = latents.unsqueeze(2) # add the frame dim
latents_mean = (
torch.tensor(self.vae.config.latents_mean)
.view(1, self.vae.config.z_dim, 1, 1, 1)
.to(latents.device, latents.dtype)
)
latents_std = (
torch.tensor(self.vae.config.latents_std)
.view(1, self.vae.config.z_dim, 1, 1, 1)
.to(latents.device, latents.dtype)
)
latents = latents * latents_std + latents_mean
# full-resolution decode spikes VRAM; models opt in to tiling it
tiled = self.vae_decode_tiled_on_low_vram and self.model_config.low_vram
if tiled:
self.vae.enable_tiling()
try:
images = self.vae.decode(latents).sample
finally:
if tiled:
self.vae.disable_tiling()
images = images.squeeze(2) # drop the frame dim
return images.to(device, dtype=dtype)
+9 -32
View File
@@ -10,7 +10,8 @@ from toolkit.memory_management.manager import MemoryManager
from toolkit.models.base_model import BaseModel from toolkit.models.base_model import BaseModel
from toolkit.prompt_utils import PromptEmbeds from toolkit.prompt_utils import PromptEmbeds
from transformers import AutoTokenizer, UMT5EncoderModel from transformers import AutoTokenizer, UMT5EncoderModel
from diffusers import WanPipeline, WanTransformer3DModel, AutoencoderKL from diffusers import WanPipeline, AutoencoderKL
from toolkit.models.v2.diffusion_models.wan import WanTransformer3DModel
from .autoencoder_kl_wan import AutoencoderKLWan from .autoencoder_kl_wan import AutoencoderKLWan
import os import os
import sys import sys
@@ -29,8 +30,6 @@ import os
import copy import copy
from toolkit.config_modules import ModelConfig, GenerateImageConfig, ModelArch from toolkit.config_modules import ModelConfig, GenerateImageConfig, ModelArch
import torch import torch
from optimum.quanto import freeze, qfloat8, QTensor, qint4
from toolkit.util.quantize import quantize, get_qtype
from diffusers import FlowMatchEulerDiscreteScheduler, UniPCMultistepScheduler from diffusers import FlowMatchEulerDiscreteScheduler, UniPCMultistepScheduler
from typing import TYPE_CHECKING, List from typing import TYPE_CHECKING, List
from toolkit.accelerator import unwrap_model from toolkit.accelerator import unwrap_model
@@ -44,7 +43,7 @@ from diffusers.callbacks import MultiPipelineCallbacks, PipelineCallback
from typing import Any, Callable, Dict, List, Optional, Union from typing import Any, Callable, Dict, List, Optional, Union
from toolkit.models.wan21.wan_lora_convert import convert_to_diffusers, convert_to_original from toolkit.models.wan21.wan_lora_convert import convert_to_diffusers, convert_to_original
from toolkit.util.quantize import quantize_model from toolkit.util.quantize import quantize_model
from toolkit.models.loaders.umt5 import get_umt5_encoder from toolkit.models.v2.text_encoders.umt5 import UMT5TextEncoder
# for generation only? # for generation only?
scheduler_configUniPC = { scheduler_configUniPC = {
@@ -344,11 +343,9 @@ class Wan21(BaseModel):
def load_wan_transformer(self, transformer_path, subfolder=None): def load_wan_transformer(self, transformer_path, subfolder=None):
self.print_and_status_update("Loading transformer") self.print_and_status_update("Loading transformer")
dtype = self.torch_dtype dtype = self.torch_dtype
transformer = WanTransformer3DModel.from_pretrained( transformer = WanTransformer3DModel.load_model(
transformer_path, transformer_path, dtype=dtype, subfolder=subfolder
subfolder=subfolder, )
torch_dtype=dtype,
).to(dtype=dtype)
if self.model_config.split_model_over_gpus: if self.model_config.split_model_over_gpus:
raise ValueError( raise ValueError(
@@ -418,29 +415,9 @@ class Wan21(BaseModel):
self.print_and_status_update("Loading UMT5EncoderModel") self.print_and_status_update("Loading UMT5EncoderModel")
tokenizer, text_encoder = get_umt5_encoder( tokenizer = UMT5TextEncoder.load_tokenizer(te_path)
model_path=te_path, text_encoder = UMT5TextEncoder.load_model(te_path, dtype=dtype)
tokenizer_subfolder="tokenizer", self.prepare_text_encoder(text_encoder, dtype=dtype)
encoder_subfolder="text_encoder",
torch_dtype=dtype,
comfy_files=self._comfy_te_file
)
text_encoder.to(self.device_torch, dtype=dtype)
flush()
if self.model_config.quantize_te:
self.print_and_status_update("Quantizing UMT5EncoderModel")
quantize(text_encoder, weights=get_qtype(self.model_config.qtype))
freeze(text_encoder)
flush()
if self.model_config.layer_offloading and self.model_config.layer_offloading_text_encoder_percent > 0:
MemoryManager.attach(
text_encoder,
self.device_torch,
offload_percent=self.model_config.layer_offloading_text_encoder_percent
)
if self.model_config.low_vram: if self.model_config.low_vram:
print("Moving transformer back to GPU") print("Moving transformer back to GPU")