Merge pull request #1025 from ostris/models/v2
Reworked the entire model loading to make everything testable, hot swappable, and uniform.
This commit is contained in:
114 files changed
+5561
-3238
No files matched your search
+2
-1
@@ -191,4 +191,5 @@ aitk_db.db-shm
|
||||
/data
|
||||
.claude
|
||||
original_repo
|
||||
.next
|
||||
.next
|
||||
testing/.model_test_outputs
|
||||
@@ -11,9 +11,7 @@ from toolkit.prompt_utils import PromptEmbeds, concat_prompt_embeds
|
||||
from toolkit.samplers.custom_flowmatch_sampler import (
|
||||
CustomFlowMatchEulerDiscreteScheduler,
|
||||
)
|
||||
from toolkit.util.quantize import get_qtype, quantize, quantize_model
|
||||
|
||||
from optimum.quanto import freeze
|
||||
from .src.model import (
|
||||
AceStep15,
|
||||
OobleckVAE,
|
||||
@@ -116,32 +114,22 @@ class AceStep15Model(BaseAudioModel):
|
||||
models = load_models(model_path, device=load_device, dtype=dtype)
|
||||
|
||||
self.model = models["model"]
|
||||
|
||||
if self.model_config.quantize:
|
||||
self.print_and_status_update("Quantizing Transformer")
|
||||
# quantize_model(self, self.model.decoder)
|
||||
quantize(self.model, weights=get_qtype(self.model_config.qtype))
|
||||
freeze(self.model)
|
||||
flush()
|
||||
|
||||
if self.model_config.low_vram:
|
||||
self.print_and_status_update("Moving transformer to CPU")
|
||||
self.model.to("cpu")
|
||||
|
||||
|
||||
|
||||
if (
|
||||
self.model_config.layer_offloading
|
||||
and self.model_config.layer_offloading_transformer_percent > 0
|
||||
):
|
||||
raise NotImplementedError("Layer offloading not yet implemented for AceStep15Model")
|
||||
|
||||
|
||||
# quantize + offload + placement, all driven by model_config
|
||||
self.model.aitk_post_load(**self.component_load_kwargs("transformer"))
|
||||
flush()
|
||||
|
||||
self.text_encoder = models["text_encoder"]
|
||||
|
||||
if self.model_config.quantize_te:
|
||||
self.print_and_status_update("Quantizing Text Encoder")
|
||||
quantize(self.text_encoder, weights=get_qtype(self.model_config.qtype_te))
|
||||
freeze(self.text_encoder)
|
||||
flush()
|
||||
|
||||
# quantize + offload + placement, all driven by model_config
|
||||
self.text_encoder.aitk_post_load(**self.component_load_kwargs("te"))
|
||||
flush()
|
||||
|
||||
self.vae = models["vae"]
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@ import os
|
||||
import time
|
||||
|
||||
import torch
|
||||
|
||||
from toolkit.models.v2._mixin import OstrisModelMixin
|
||||
import torch.nn.functional as F
|
||||
import torchaudio
|
||||
from safetensors.torch import load_file
|
||||
@@ -857,7 +859,35 @@ class DiTModel(nn.Module):
|
||||
# ── Top-level model ──
|
||||
|
||||
|
||||
class AceStep15(nn.Module):
|
||||
class AceStep15(nn.Module, OstrisModelMixin):
|
||||
@classmethod
|
||||
def load_from_state_dict(cls, state_dict, dtype=None, **kwargs):
|
||||
cfg = infer_dit_config(state_dict)
|
||||
model = cls(
|
||||
hidden=cfg["hidden"],
|
||||
inter=cfg["inter"],
|
||||
heads=cfg["heads"],
|
||||
kv=cfg["kv"],
|
||||
head_dim=cfg["head_dim"],
|
||||
n_dit=cfg["n_dit"],
|
||||
n_lyric=cfg["n_lyric"],
|
||||
n_timbre=cfg["n_timbre"],
|
||||
enc_hidden=cfg["enc_hidden"],
|
||||
enc_heads=cfg["enc_heads"],
|
||||
enc_kv=cfg["enc_kv"],
|
||||
enc_inter=cfg["enc_inter"],
|
||||
)
|
||||
missing, unexpected = model.load_state_dict(state_dict, strict=False)
|
||||
# tokenizer/detokenizer keys are expected to be unused (cover mode only)
|
||||
unexpected = [
|
||||
k for k in unexpected if not k.startswith(("tokenizer.", "detokenizer."))
|
||||
]
|
||||
if missing:
|
||||
print(f" DiT missing: {len(missing)} (first 3: {missing[:3]})")
|
||||
if unexpected:
|
||||
print(f" DiT unexpected: {len(unexpected)} (first 3: {unexpected[:3]})")
|
||||
return model
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hidden=2048,
|
||||
@@ -1051,7 +1081,17 @@ class _SeqWrap(nn.Module):
|
||||
return self.layers(x)
|
||||
|
||||
|
||||
class OobleckVAE(nn.Module):
|
||||
class OobleckVAE(nn.Module, OstrisModelMixin):
|
||||
@classmethod
|
||||
def load_from_state_dict(cls, state_dict, dtype=None, **kwargs):
|
||||
vae = cls()
|
||||
m, u = vae.load_state_dict(state_dict, strict=False)
|
||||
if m:
|
||||
print(f" VAE missing: {len(m)} (first 3: {m[:3]})")
|
||||
if u:
|
||||
print(f" VAE unexpected: {len(u)}")
|
||||
return vae
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
in_ch=2,
|
||||
@@ -1152,7 +1192,28 @@ class OobleckVAE(nn.Module):
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
class TextEncoder(nn.Module):
|
||||
class TextEncoder(nn.Module, OstrisModelMixin):
|
||||
@classmethod
|
||||
def load_from_state_dict(cls, state_dict, dtype=None, **kwargs):
|
||||
from transformers import Qwen3Config, Qwen3Model
|
||||
|
||||
qwen_cfg = Qwen3Config(
|
||||
vocab_size=151669,
|
||||
hidden_size=1024,
|
||||
intermediate_size=3072,
|
||||
num_hidden_layers=28,
|
||||
num_attention_heads=16,
|
||||
num_key_value_heads=8,
|
||||
head_dim=128,
|
||||
max_position_embeddings=32768,
|
||||
rms_norm_eps=1e-6,
|
||||
)
|
||||
qwen = Qwen3Model(qwen_cfg)
|
||||
m2, u2 = qwen.load_state_dict(state_dict, strict=False)
|
||||
if m2:
|
||||
print(f" TE missing: {len(m2)} (first 3: {m2[:3]})")
|
||||
return cls(qwen)
|
||||
|
||||
"""Wraps Qwen3 weights loaded from AIO. Forward returns last_hidden_state."""
|
||||
|
||||
def __init__(self, qwen_model):
|
||||
@@ -1261,41 +1322,13 @@ def load_models(checkpoint_path, device="cuda", dtype=torch.bfloat16):
|
||||
for k, v in sd.items()
|
||||
if k.startswith("model.diffusion_model.")
|
||||
}
|
||||
cfg = infer_dit_config(dit_sd)
|
||||
model = AceStep15(
|
||||
hidden=cfg["hidden"],
|
||||
inter=cfg["inter"],
|
||||
heads=cfg["heads"],
|
||||
kv=cfg["kv"],
|
||||
head_dim=cfg["head_dim"],
|
||||
n_dit=cfg["n_dit"],
|
||||
n_lyric=cfg["n_lyric"],
|
||||
n_timbre=cfg["n_timbre"],
|
||||
enc_hidden=cfg["enc_hidden"],
|
||||
enc_heads=cfg["enc_heads"],
|
||||
enc_kv=cfg["enc_kv"],
|
||||
enc_inter=cfg["enc_inter"],
|
||||
)
|
||||
missing, unexpected = model.load_state_dict(dit_sd, strict=False)
|
||||
# tokenizer/detokenizer keys are expected to be unused (cover mode only)
|
||||
unexpected = [
|
||||
k for k in unexpected if not k.startswith(("tokenizer.", "detokenizer."))
|
||||
]
|
||||
if missing:
|
||||
print(f" DiT missing: {len(missing)} (first 3: {missing[:3]})")
|
||||
if unexpected:
|
||||
print(f" DiT unexpected: {len(unexpected)} (first 3: {unexpected[:3]})")
|
||||
model = AceStep15.load_from_state_dict(dit_sd, dtype)
|
||||
model = model.to(device).to(dtype).eval()
|
||||
|
||||
# --- VAE ---
|
||||
print(" Loading VAE...")
|
||||
vae_sd = {k.removeprefix("vae."): v for k, v in sd.items() if k.startswith("vae.")}
|
||||
vae = OobleckVAE()
|
||||
m, u = vae.load_state_dict(vae_sd, strict=False)
|
||||
if m:
|
||||
print(f" VAE missing: {len(m)} (first 3: {m[:3]})")
|
||||
if u:
|
||||
print(f" VAE unexpected: {len(u)}")
|
||||
vae = OobleckVAE.load_from_state_dict(vae_sd, dtype)
|
||||
vae = vae.to(device).to(dtype).eval()
|
||||
|
||||
# --- Text encoder (Qwen3-Embedding from AIO) ---
|
||||
@@ -1305,25 +1338,7 @@ def load_models(checkpoint_path, device="cuda", dtype=torch.bfloat16):
|
||||
for k, v in sd.items()
|
||||
if k.startswith("text_encoders.qwen3_06b.transformer.model.")
|
||||
}
|
||||
# Load Qwen3 model structure from transformers, then override weights
|
||||
from transformers import Qwen3Model, Qwen3Config
|
||||
|
||||
qwen_cfg = Qwen3Config(
|
||||
vocab_size=151669,
|
||||
hidden_size=1024,
|
||||
intermediate_size=3072,
|
||||
num_hidden_layers=28,
|
||||
num_attention_heads=16,
|
||||
num_key_value_heads=8,
|
||||
head_dim=128,
|
||||
max_position_embeddings=32768,
|
||||
rms_norm_eps=1e-6,
|
||||
)
|
||||
qwen = Qwen3Model(qwen_cfg)
|
||||
m2, u2 = qwen.load_state_dict(te_sd, strict=False)
|
||||
if m2:
|
||||
print(f" TE missing: {len(m2)} (first 3: {m2[:3]})")
|
||||
te = TextEncoder(qwen).to(device).to(dtype).eval()
|
||||
te = TextEncoder.load_from_state_dict(te_sd, dtype).to(device).to(dtype).eval()
|
||||
|
||||
# Tokenizer — download from HF
|
||||
print(" Loading tokenizer...")
|
||||
|
||||
@@ -69,21 +69,7 @@ class BaseAudioModel(BaseModel):
|
||||
"save_model is not implemented for this model. Use the pipeline directly instead."
|
||||
)
|
||||
|
||||
def convert_lora_weights_before_save(self, state_dict):
|
||||
# currently starte with transformer. but needs to start with diffusion_model. for comfyui
|
||||
new_sd = {}
|
||||
for key, value in state_dict.items():
|
||||
new_key = key.replace("transformer.", "diffusion_model.")
|
||||
new_sd[new_key] = value
|
||||
return new_sd
|
||||
|
||||
def convert_lora_weights_before_load(self, state_dict):
|
||||
# saved as diffusion_model. but needs to be transformer. for ai-toolkit
|
||||
new_sd = {}
|
||||
for key, value in state_dict.items():
|
||||
new_key = key.replace("diffusion_model.", "transformer.")
|
||||
new_sd[new_key] = value
|
||||
return new_sd
|
||||
lora_keys_use_comfy_prefix = True
|
||||
|
||||
def encode_images(self, image_list: torch.Tensor, device=None, dtype=None):
|
||||
# make it more obvious for audio models
|
||||
|
||||
@@ -12,6 +12,8 @@ from optimum.quanto import freeze
|
||||
from toolkit.basic import flush
|
||||
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
|
||||
import transformers
|
||||
import logging
|
||||
@@ -19,29 +21,6 @@ import traceback
|
||||
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()
|
||||
warnings.filterwarnings("ignore")
|
||||
logging.disable(logging.WARNING)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
ideogram4_prompt = """
|
||||
ideogram4_prompt = r"""
|
||||
[META]
|
||||
frozen: false
|
||||
description: Slim single-shot magic prompt — splatter planning + v15 output discipline, deduped for faster inference. Thinking off.
|
||||
@@ -15,7 +15,7 @@ You convert a natural-language user idea into a structured JSON caption an image
|
||||
|
||||
- Emit a SINGLE-LINE MINIFIED JSON object — no markdown fences, no commentary, no other top-level keys.
|
||||
- Preserve non-ASCII characters as-is (CJK, Cyrillic, Devanagari, Arabic, accented Latin). Never escape with `\uNNNN`, transliterate, or replace `café` with `cafe`.
|
||||
- Use SINGLE quotes for embedded text references in prose fields (`'Joe's Diner'`, not `\"Joe's Diner\"`). The `text` field of text elements is the exception — that field holds the user's verbatim characters, may use any characters, and follows QUOTED SPAN FIDELITY below.
|
||||
- Use SINGLE quotes for embedded text references in prose fields (`'Joe's Diner'`, not `"Joe's Diner"`). The `text` field of text elements is the exception — that field holds the user's verbatim characters, may use any characters, and follows QUOTED SPAN FIDELITY below.
|
||||
|
||||
### Target aspect ratio (input only — never emit it)
|
||||
|
||||
|
||||
@@ -3,21 +3,21 @@ from typing import List, Optional
|
||||
|
||||
import torch
|
||||
import yaml
|
||||
from optimum.quanto import freeze
|
||||
from safetensors.torch import load_file, save_file
|
||||
|
||||
from toolkit.accelerator import unwrap_model
|
||||
from toolkit.basic import flush
|
||||
from toolkit.config_modules import GenerateImageConfig, ModelConfig
|
||||
from toolkit.memory_management import MemoryManager
|
||||
from toolkit.models.base_model import BaseModel
|
||||
from toolkit.models.v2.diffusion_models.cosmos import CosmosTransformer3DModel
|
||||
from toolkit.models.v2.text_encoders.anima import AnimaTextConditioner
|
||||
from toolkit.models.v2.text_encoders.qwen3 import Qwen3ModelEncoder
|
||||
from toolkit.models.v2.vae.qwen_image import QwenImageVAE
|
||||
from toolkit.prompt_utils import PromptEmbeds
|
||||
from toolkit.samplers.custom_flowmatch_sampler import CustomFlowMatchEulerDiscreteScheduler
|
||||
from toolkit.util.quantize import get_qtype, quantize, quantize_model
|
||||
|
||||
try:
|
||||
from diffusers import AnimaAutoBlocks, AnimaModularPipeline, AnimaTextConditioner
|
||||
from diffusers.models import CosmosTransformer3DModel
|
||||
from diffusers import AnimaAutoBlocks, AnimaModularPipeline
|
||||
from diffusers.modular_pipelines import SequentialPipelineBlocks
|
||||
from diffusers.modular_pipelines.anima.modular_blocks_anima import AnimaCoreDenoiseStep, AnimaDecodeStep
|
||||
except ImportError as e:
|
||||
@@ -255,72 +255,52 @@ class AnimaModel(BaseModel):
|
||||
self.print_and_status_update("Loading Anima model")
|
||||
|
||||
pipe: AnimaModularPipeline = AnimaAutoBlocks().init_pipeline(self.model_config.name_or_path)
|
||||
load_kwargs = {"torch_dtype": dtype}
|
||||
model_path = os.path.abspath(os.path.expanduser(str(self.model_config.name_or_path)))
|
||||
if os.path.isdir(model_path):
|
||||
load_kwargs["pretrained_model_name_or_path"] = model_path
|
||||
pipe.load_components(**load_kwargs)
|
||||
pipe.update_components(scheduler=self.get_train_scheduler())
|
||||
name = self.model_config.name_or_path
|
||||
local_path = os.path.abspath(os.path.expanduser(str(name)))
|
||||
if os.path.isdir(local_path):
|
||||
name = local_path
|
||||
|
||||
# components load individually through the v2 module classes and are
|
||||
# handed to the modular pipeline
|
||||
from transformers import AutoTokenizer
|
||||
|
||||
self.print_and_status_update("Loading components")
|
||||
transformer = CosmosTransformer3DModel.load_model(name, dtype=dtype)
|
||||
vae = QwenImageVAE.load_model(name, dtype=dtype)
|
||||
text_encoder = Qwen3ModelEncoder.load_model(name, dtype=dtype)
|
||||
text_conditioner = AnimaTextConditioner.load_model(name, dtype=dtype)
|
||||
tokenizer = AutoTokenizer.from_pretrained(name, subfolder="tokenizer")
|
||||
t5_tokenizer = AutoTokenizer.from_pretrained(name, subfolder="t5_tokenizer")
|
||||
pipe.update_components(
|
||||
transformer=transformer,
|
||||
vae=vae,
|
||||
text_encoder=text_encoder,
|
||||
text_conditioner=text_conditioner,
|
||||
tokenizer=tokenizer,
|
||||
t5_tokenizer=t5_tokenizer,
|
||||
scheduler=self.get_train_scheduler(),
|
||||
)
|
||||
|
||||
transformer = pipe.transformer
|
||||
text_conditioner = pipe.text_conditioner
|
||||
|
||||
if self.model_config.quantize:
|
||||
self.print_and_status_update("Quantizing Transformer")
|
||||
quantize_model(self, transformer)
|
||||
flush()
|
||||
# quantize + offload + placement, all driven by model_config
|
||||
transformer.aitk_post_load(**self.component_load_kwargs("transformer"))
|
||||
|
||||
self.print_and_status_update("Quantizing Text Conditioner")
|
||||
quantize(text_conditioner, weights=get_qtype(self.model_config.qtype))
|
||||
freeze(text_conditioner)
|
||||
flush()
|
||||
# the text conditioner rides the transformer quantize flag (at qtype_te)
|
||||
# but takes the text-encoder offload/placement policy
|
||||
tc_kwargs = self.component_load_kwargs("te")
|
||||
tc_kwargs["qtype"] = (
|
||||
self.model_config.qtype_te if self.model_config.quantize else None
|
||||
)
|
||||
text_conditioner.aitk_post_load(**tc_kwargs)
|
||||
flush()
|
||||
|
||||
if (
|
||||
self.model_config.layer_offloading
|
||||
and self.model_config.layer_offloading_transformer_percent > 0
|
||||
):
|
||||
MemoryManager.attach(
|
||||
transformer,
|
||||
self.device_torch,
|
||||
offload_percent=self.model_config.layer_offloading_transformer_percent,
|
||||
)
|
||||
|
||||
if (
|
||||
self.model_config.layer_offloading
|
||||
and self.model_config.layer_offloading_text_encoder_percent > 0
|
||||
):
|
||||
MemoryManager.attach(
|
||||
pipe.text_encoder,
|
||||
self.device_torch,
|
||||
offload_percent=self.model_config.layer_offloading_text_encoder_percent,
|
||||
)
|
||||
MemoryManager.attach(
|
||||
text_conditioner,
|
||||
self.device_torch,
|
||||
offload_percent=self.model_config.layer_offloading_text_encoder_percent,
|
||||
)
|
||||
|
||||
if self.model_config.low_vram:
|
||||
self.print_and_status_update("Moving transformer to CPU")
|
||||
transformer.to("cpu")
|
||||
text_conditioner.to("cpu")
|
||||
else:
|
||||
transformer.to(self.device_torch, dtype=dtype)
|
||||
text_conditioner.to(self.device_torch, dtype=dtype)
|
||||
|
||||
pipe.text_encoder.to(self.device_torch, dtype=dtype)
|
||||
# quantize + offload + placement, all driven by model_config
|
||||
pipe.text_encoder.aitk_post_load(**self.component_load_kwargs("te"))
|
||||
pipe.text_encoder.requires_grad_(False)
|
||||
pipe.text_encoder.eval()
|
||||
|
||||
if self.model_config.quantize_te:
|
||||
self.print_and_status_update("Quantizing Text Encoder")
|
||||
quantize(pipe.text_encoder, weights=get_qtype(self.model_config.qtype_te))
|
||||
freeze(pipe.text_encoder)
|
||||
flush()
|
||||
|
||||
if self.model_config.low_vram:
|
||||
pipe.text_encoder.to("cpu")
|
||||
flush()
|
||||
flush()
|
||||
|
||||
self.noise_scheduler = pipe.scheduler
|
||||
self.vae = pipe.vae
|
||||
|
||||
@@ -30,12 +30,15 @@ from toolkit.advanced_prompt_embeds import AdvancedPromptEmbeds
|
||||
from toolkit.basic import flush
|
||||
from toolkit.config_modules import GenerateImageConfig, ModelConfig
|
||||
from toolkit.models.base_model import BaseModel
|
||||
from toolkit.models.v2.text_encoders.qwen3_vl import patch_qwen_vl_patch_embed
|
||||
from toolkit.models.v2.text_encoders.qwen3_vl import Qwen3VLModelEncoder
|
||||
from toolkit.models.v2.vae.autoencoder_kl import KLVAE
|
||||
from toolkit.samplers.custom_flowmatch_sampler import (
|
||||
CustomFlowMatchEulerDiscreteScheduler,
|
||||
)
|
||||
from toolkit.util.quantize import quantize, get_qtype, quantize_model
|
||||
|
||||
from optimum.quanto import freeze, QTensor
|
||||
|
||||
from optimum.quanto import QTensor
|
||||
from diffusers import AutoencoderKL
|
||||
|
||||
from .src.transformer import BooguImageTransformer2DModel
|
||||
@@ -71,33 +74,6 @@ SYSTEM_PROMPT_T2I = (
|
||||
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):
|
||||
arch = "boogu_image"
|
||||
# Default HF repo when model.name_or_path is unset (overridden by the edit model).
|
||||
@@ -167,8 +143,8 @@ class BooguImageModel(BaseModel):
|
||||
# deserialize -- use the bf16 repo and let ai-toolkit quantize if wanted.
|
||||
self.print_and_status_update("Loading transformer")
|
||||
try:
|
||||
transformer = BooguImageTransformer2DModel.from_pretrained(
|
||||
base, subfolder="transformer", torch_dtype=dtype, token=HF_TOKEN
|
||||
transformer = BooguImageTransformer2DModel.load_model(
|
||||
base, dtype=dtype, token=HF_TOKEN
|
||||
)
|
||||
except OSError as e:
|
||||
raise OSError(
|
||||
@@ -188,16 +164,8 @@ class BooguImageModel(BaseModel):
|
||||
if attention_backend != "native":
|
||||
transformer.set_attention_backend(attention_backend)
|
||||
|
||||
if self.model_config.quantize:
|
||||
self.print_and_status_update("Quantizing transformer")
|
||||
quantize_model(self, transformer)
|
||||
flush()
|
||||
|
||||
if self.model_config.low_vram:
|
||||
self.print_and_status_update("Moving transformer to CPU")
|
||||
transformer.to("cpu")
|
||||
else:
|
||||
transformer.to(self.device_torch, dtype=dtype)
|
||||
# quantize + offload + placement, all driven by model_config
|
||||
transformer.aitk_post_load(**self.component_load_kwargs("transformer"))
|
||||
flush()
|
||||
|
||||
# --- instruction encoder (Qwen3-VL) + processor ---
|
||||
@@ -212,8 +180,8 @@ class BooguImageModel(BaseModel):
|
||||
# AutoModel yields the inner Qwen3VLModel (the ``.model`` of the
|
||||
# *ForConditionalGeneration), whose last_hidden_state is exactly the
|
||||
# instruction feature the Boogu pipeline consumes.
|
||||
text_encoder = AutoModel.from_pretrained(
|
||||
te_path, subfolder=te_subfolder, torch_dtype=dtype, token=HF_TOKEN
|
||||
text_encoder = Qwen3VLModelEncoder.load_model(
|
||||
te_path, dtype=dtype, subfolder=te_subfolder, token=HF_TOKEN
|
||||
)
|
||||
text_encoder.eval()
|
||||
text_encoder.requires_grad_(False)
|
||||
@@ -227,25 +195,13 @@ class BooguImageModel(BaseModel):
|
||||
)
|
||||
flush()
|
||||
|
||||
if self.model_config.quantize_te:
|
||||
self.print_and_status_update("Quantizing instruction encoder")
|
||||
text_encoder.to(self.device_torch)
|
||||
quantize(text_encoder, weights=get_qtype(self.model_config.qtype_te))
|
||||
freeze(text_encoder)
|
||||
flush()
|
||||
|
||||
if self.model_config.low_vram:
|
||||
self.print_and_status_update("Moving instruction encoder to CPU")
|
||||
text_encoder.to("cpu")
|
||||
else:
|
||||
text_encoder.to(self.device_torch)
|
||||
# quantize + offload + placement, all driven by model_config
|
||||
text_encoder.aitk_post_load(**self.component_load_kwargs("te"))
|
||||
flush()
|
||||
|
||||
# --- VAE (FLUX AutoencoderKL) ---
|
||||
self.print_and_status_update("Loading VAE")
|
||||
vae = AutoencoderKL.from_pretrained(
|
||||
base, subfolder="vae", torch_dtype=self.vae_torch_dtype, token=HF_TOKEN
|
||||
)
|
||||
vae = KLVAE.load_model(base, dtype=self.vae_torch_dtype, token=HF_TOKEN)
|
||||
vae.to(self.vae_device_torch, dtype=self.vae_torch_dtype)
|
||||
vae.eval()
|
||||
vae.requires_grad_(False)
|
||||
@@ -446,14 +402,5 @@ class BooguImageModel(BaseModel):
|
||||
def get_transformer_block_names(self) -> Optional[List[str]]:
|
||||
return ["double_stream_layers", "single_stream_layers"]
|
||||
|
||||
def convert_lora_weights_before_save(self, state_dict):
|
||||
return {
|
||||
k.replace("transformer.", "diffusion_model."): v
|
||||
for k, v in state_dict.items()
|
||||
}
|
||||
lora_keys_use_comfy_prefix = True
|
||||
|
||||
def convert_lora_weights_before_load(self, state_dict):
|
||||
return {
|
||||
k.replace("diffusion_model.", "transformer."): v
|
||||
for k, v in state_dict.items()
|
||||
}
|
||||
@@ -21,6 +21,8 @@ from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from diffusers.configuration_utils import ConfigMixin, register_to_config
|
||||
|
||||
from toolkit.models.v2._mixin import OstrisModelMixin
|
||||
from diffusers.loaders import PeftAdapterMixin
|
||||
from diffusers.loaders.single_file_model import FromOriginalModelMixin
|
||||
from diffusers.models.attention_processor import Attention
|
||||
@@ -490,10 +492,17 @@ class BooguImageDoubleStreamTransformerBlock(nn.Module):
|
||||
|
||||
|
||||
class BooguImageTransformer2DModel(
|
||||
ModelMixin, ConfigMixin, PeftAdapterMixin, FromOriginalModelMixin
|
||||
ModelMixin, ConfigMixin, PeftAdapterMixin, FromOriginalModelMixin, OstrisModelMixin
|
||||
):
|
||||
"""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
|
||||
_no_split_modules = [
|
||||
"BooguImageTransformerBlock",
|
||||
|
||||
@@ -5,16 +5,14 @@ import torch
|
||||
from toolkit.config_modules import GenerateImageConfig, ModelConfig
|
||||
from PIL import Image
|
||||
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 diffusers import AutoencoderKL
|
||||
# from toolkit.pixel_shuffle_encoder import AutoencoderPixelMixer
|
||||
from toolkit.prompt_utils import PromptEmbeds
|
||||
from toolkit.samplers.custom_flowmatch_sampler import CustomFlowMatchEulerDiscreteScheduler
|
||||
from toolkit.dequantize import patch_dequantization_on_save
|
||||
from toolkit.accelerator import unwrap_model
|
||||
from optimum.quanto import freeze, QTensor
|
||||
from toolkit.util.quantize import quantize, get_qtype
|
||||
from transformers import T5TokenizerFast, T5EncoderModel, CLIPTextModel, CLIPTokenizer
|
||||
from optimum.quanto import QTensor
|
||||
from .pipeline import ChromaPipeline, prepare_latent_image_ids
|
||||
from einops import rearrange, repeat
|
||||
import random
|
||||
@@ -50,10 +48,12 @@ class FakeConfig:
|
||||
self.patch_size = 1
|
||||
|
||||
class FakeCLIP(torch.nn.Module):
|
||||
def __init__(self):
|
||||
def __init__(self, device='cuda'):
|
||||
super().__init__()
|
||||
self.dtype = torch.bfloat16
|
||||
self.device = 'cuda'
|
||||
# the pipeline derives its execution device from this attribute;
|
||||
# nn.Module.to() does not update it
|
||||
self.device = device
|
||||
self.text_model = None
|
||||
self.tokenizer = None
|
||||
self.model_max_length = 77
|
||||
@@ -65,6 +65,9 @@ class FakeCLIP(torch.nn.Module):
|
||||
class ChromaModel(BaseModel):
|
||||
arch = "chroma"
|
||||
|
||||
def get_transformer_block_names(self):
|
||||
return ["double_blocks", "single_blocks"]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
device,
|
||||
@@ -148,83 +151,37 @@ class ChromaModel(BaseModel):
|
||||
|
||||
self.print_and_status_update("Loading transformer")
|
||||
|
||||
chroma_state_dict = load_file(model_path, 'cpu')
|
||||
|
||||
# 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)
|
||||
|
||||
if model_path.endswith(".safetensors"):
|
||||
transformer = Chroma.load_model(model_path, dtype=dtype)
|
||||
else:
|
||||
transformer = Chroma.load_from_state_dict(load_file(model_path, "cpu"), dtype)
|
||||
# add dtype, not sure why it doesnt have it
|
||||
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.config = FakeConfig()
|
||||
transformer.config.num_layers = double_blocks
|
||||
transformer.config.num_single_layers = single_blocks
|
||||
|
||||
if self.model_config.quantize:
|
||||
# patch the state dict method
|
||||
patch_dequantization_on_save(transformer)
|
||||
quantization_type = get_qtype(self.model_config.qtype)
|
||||
self.print_and_status_update("Quantizing transformer")
|
||||
quantize(transformer, weights=quantization_type,
|
||||
**self.model_config.quantize_kwargs)
|
||||
freeze(transformer)
|
||||
transformer.to(self.device_torch)
|
||||
else:
|
||||
transformer.to(self.device_torch, dtype=dtype)
|
||||
transformer.config = FakeConfig()
|
||||
transformer.config.num_layers = transformer.params.depth
|
||||
transformer.config.num_single_layers = transformer.params.depth_single_blocks
|
||||
|
||||
# quantize + offload + placement, all driven by model_config
|
||||
transformer.aitk_post_load(**self.component_load_kwargs("transformer"))
|
||||
|
||||
flush()
|
||||
|
||||
self.print_and_status_update("Loading T5")
|
||||
tokenizer_2 = T5TokenizerFast.from_pretrained(
|
||||
extras_path, subfolder="tokenizer_2", torch_dtype=dtype
|
||||
tokenizer_2 = T5TextEncoder.load_tokenizer(extras_path)
|
||||
text_encoder_2 = T5TextEncoder.load(
|
||||
extras_path, **self.component_load_kwargs("te")
|
||||
)
|
||||
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")
|
||||
text_encoder = FakeCLIP()
|
||||
tokenizer = FakeCLIP()
|
||||
text_encoder = FakeCLIP(device=self.device_torch)
|
||||
tokenizer = FakeCLIP(device=self.device_torch)
|
||||
text_encoder.to(self.device_torch, dtype=dtype)
|
||||
|
||||
self.noise_scheduler = ChromaModel.get_train_scheduler()
|
||||
|
||||
self.print_and_status_update("Loading VAE")
|
||||
vae = AutoencoderKL.from_pretrained(
|
||||
extras_path,
|
||||
subfolder="vae",
|
||||
torch_dtype=dtype
|
||||
)
|
||||
vae = vae.to(self.device_torch, dtype=dtype)
|
||||
vae = KLVAE.load_model(extras_path, dtype=dtype, device=self.device_torch)
|
||||
|
||||
self.print_and_status_update("Making pipe")
|
||||
|
||||
@@ -249,11 +206,13 @@ class ChromaModel(BaseModel):
|
||||
pipe.transformer = pipe.transformer.to(self.device_torch)
|
||||
|
||||
flush()
|
||||
# just to make sure everything is on the right device and dtype
|
||||
text_encoder[0].to(self.device_torch)
|
||||
# low_vram: text encoders stay on cpu; get_prompt_embeds moves them
|
||||
# to the gpu on demand
|
||||
if not self.low_vram:
|
||||
text_encoder[0].to(self.device_torch)
|
||||
text_encoder[1].to(self.device_torch)
|
||||
text_encoder[0].requires_grad_(False)
|
||||
text_encoder[0].eval()
|
||||
text_encoder[1].to(self.device_torch)
|
||||
text_encoder[1].requires_grad_(False)
|
||||
text_encoder[1].eval()
|
||||
pipe.transformer = pipe.transformer.to(self.device_torch)
|
||||
@@ -424,40 +383,24 @@ class ChromaModel(BaseModel):
|
||||
return self.text_encoder[1].encoder.block[0].layer[0].SelfAttention.q.weight.requires_grad
|
||||
|
||||
def save_model(self, output_path, meta, save_dtype):
|
||||
# comfy-format single-file save via the mixin (chroma's class keys ARE
|
||||
# the original layout); handles torchao/Ostris dequant, not just quanto
|
||||
if not output_path.endswith(".safetensors"):
|
||||
output_path = output_path + ".safetensors"
|
||||
# only save the unet
|
||||
output_path = output_path + ".safetensors"
|
||||
transformer: Chroma = unwrap_model(self.model)
|
||||
state_dict = transformer.state_dict()
|
||||
save_dict = {}
|
||||
for k, v in state_dict.items():
|
||||
if isinstance(v, QTensor):
|
||||
v = v.dequantize()
|
||||
save_dict[k] = v.clone().to('cpu', dtype=save_dtype)
|
||||
|
||||
meta = get_meta_for_safetensors(meta, name='chroma')
|
||||
save_file(save_dict, output_path, metadata=meta)
|
||||
transformer.save_model(
|
||||
output_path,
|
||||
dtype=save_dtype,
|
||||
metadata=get_meta_for_safetensors(meta, name="chroma"),
|
||||
)
|
||||
|
||||
def get_loss_target(self, *args, **kwargs):
|
||||
noise = kwargs.get('noise')
|
||||
batch = kwargs.get('batch')
|
||||
return (noise - batch.latents).detach()
|
||||
|
||||
def convert_lora_weights_before_save(self, state_dict):
|
||||
# currently starte with transformer. but needs to start with diffusion_model. for comfyui
|
||||
new_sd = {}
|
||||
for key, value in state_dict.items():
|
||||
new_key = key.replace("transformer.", "diffusion_model.")
|
||||
new_sd[new_key] = value
|
||||
return new_sd
|
||||
lora_keys_use_comfy_prefix = True
|
||||
|
||||
def convert_lora_weights_before_load(self, state_dict):
|
||||
# saved as diffusion_model. but needs to be transformer. for ai-toolkit
|
||||
new_sd = {}
|
||||
for key, value in state_dict.items():
|
||||
new_key = key.replace("diffusion_model.", "transformer.")
|
||||
new_sd[new_key] = value
|
||||
return new_sd
|
||||
|
||||
def get_base_model_version(self):
|
||||
return "chroma"
|
||||
@@ -5,16 +5,13 @@ import torch
|
||||
from toolkit.config_modules import GenerateImageConfig, ModelConfig
|
||||
from PIL import Image
|
||||
from toolkit.models.base_model import BaseModel
|
||||
from toolkit.models.v2.text_encoders.t5 import T5TextEncoder
|
||||
from toolkit.basic import flush
|
||||
from diffusers import AutoencoderKL
|
||||
# from toolkit.pixel_shuffle_encoder import AutoencoderPixelMixer
|
||||
from toolkit.prompt_utils import PromptEmbeds
|
||||
from toolkit.samplers.custom_flowmatch_sampler import CustomFlowMatchEulerDiscreteScheduler
|
||||
from toolkit.dequantize import patch_dequantization_on_save
|
||||
from toolkit.accelerator import unwrap_model
|
||||
from optimum.quanto import freeze, QTensor
|
||||
from toolkit.util.quantize import quantize, get_qtype
|
||||
from transformers import T5TokenizerFast, T5EncoderModel, CLIPTextModel, CLIPTokenizer
|
||||
from optimum.quanto import QTensor
|
||||
from .pipeline import ChromaPipeline, prepare_latent_image_ids
|
||||
from einops import rearrange, repeat
|
||||
import random
|
||||
@@ -38,34 +35,16 @@ scheduler_config = {
|
||||
"use_dynamic_shifting": True
|
||||
}
|
||||
|
||||
class FakeConfig:
|
||||
# for diffusers compatability
|
||||
def __init__(self):
|
||||
self.attention_head_dim = 128
|
||||
self.guidance_embeds = True
|
||||
self.in_channels = 64
|
||||
self.joint_attention_dim = 4096
|
||||
self.num_attention_heads = 24
|
||||
self.num_layers = 19
|
||||
self.num_single_layers = 38
|
||||
self.patch_size = 1
|
||||
|
||||
class FakeCLIP(torch.nn.Module):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.dtype = torch.bfloat16
|
||||
self.device = 'cuda'
|
||||
self.text_model = None
|
||||
self.tokenizer = None
|
||||
self.model_max_length = 77
|
||||
|
||||
def forward(self, *args, **kwargs):
|
||||
return torch.zeros(1, 1, 1).to(self.device)
|
||||
# shared with the base chroma model (identical stubs)
|
||||
from .chroma_model import FakeCLIP, FakeConfig
|
||||
|
||||
|
||||
class ChromaRadianceModel(BaseModel):
|
||||
arch = "chroma_radiance"
|
||||
|
||||
def get_transformer_block_names(self):
|
||||
return ["double_blocks", "single_blocks"]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
device,
|
||||
@@ -152,73 +131,30 @@ class ChromaRadianceModel(BaseModel):
|
||||
|
||||
if model_path.endswith('.pth') or model_path.endswith('.pt'):
|
||||
chroma_state_dict = torch.load(model_path, map_location='cpu', weights_only=True)
|
||||
transformer = Chroma.load_from_state_dict(chroma_state_dict, dtype)
|
||||
else:
|
||||
chroma_state_dict = load_file(model_path, 'cpu')
|
||||
|
||||
# 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)
|
||||
|
||||
transformer = Chroma.load_model(model_path, dtype=dtype)
|
||||
# add dtype, not sure why it doesnt have it
|
||||
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.config = FakeConfig()
|
||||
transformer.config.num_layers = double_blocks
|
||||
transformer.config.num_single_layers = single_blocks
|
||||
|
||||
if self.model_config.quantize:
|
||||
# patch the state dict method
|
||||
patch_dequantization_on_save(transformer)
|
||||
quantization_type = get_qtype(self.model_config.qtype)
|
||||
self.print_and_status_update("Quantizing transformer")
|
||||
quantize(transformer, weights=quantization_type,
|
||||
**self.model_config.quantize_kwargs)
|
||||
freeze(transformer)
|
||||
transformer.to(self.device_torch)
|
||||
else:
|
||||
transformer.to(self.device_torch, dtype=dtype)
|
||||
transformer.config = FakeConfig()
|
||||
transformer.config.num_layers = transformer.params.depth
|
||||
transformer.config.num_single_layers = transformer.params.depth_single_blocks
|
||||
|
||||
# quantize + offload + placement, all driven by model_config
|
||||
transformer.aitk_post_load(**self.component_load_kwargs("transformer"))
|
||||
|
||||
flush()
|
||||
|
||||
self.print_and_status_update("Loading T5")
|
||||
tokenizer_2 = T5TokenizerFast.from_pretrained(
|
||||
extras_path, subfolder="tokenizer_2", torch_dtype=dtype
|
||||
tokenizer_2 = T5TextEncoder.load_tokenizer(extras_path)
|
||||
text_encoder_2 = T5TextEncoder.load(
|
||||
extras_path, **self.component_load_kwargs("te")
|
||||
)
|
||||
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")
|
||||
text_encoder = FakeCLIP()
|
||||
tokenizer = FakeCLIP()
|
||||
text_encoder = FakeCLIP(device=self.device_torch)
|
||||
tokenizer = FakeCLIP(device=self.device_torch)
|
||||
text_encoder.to(self.device_torch, dtype=dtype)
|
||||
|
||||
self.noise_scheduler = ChromaRadianceModel.get_train_scheduler()
|
||||
@@ -256,11 +192,13 @@ class ChromaRadianceModel(BaseModel):
|
||||
pipe.transformer = pipe.transformer.to(self.device_torch)
|
||||
|
||||
flush()
|
||||
# just to make sure everything is on the right device and dtype
|
||||
text_encoder[0].to(self.device_torch)
|
||||
# low_vram: text encoders stay on cpu; get_prompt_embeds moves them
|
||||
# to the gpu on demand
|
||||
if not self.low_vram:
|
||||
text_encoder[0].to(self.device_torch)
|
||||
text_encoder[1].to(self.device_torch)
|
||||
text_encoder[0].requires_grad_(False)
|
||||
text_encoder[0].eval()
|
||||
text_encoder[1].to(self.device_torch)
|
||||
text_encoder[1].requires_grad_(False)
|
||||
text_encoder[1].eval()
|
||||
pipe.transformer = pipe.transformer.to(self.device_torch)
|
||||
@@ -406,40 +344,24 @@ class ChromaRadianceModel(BaseModel):
|
||||
return False
|
||||
|
||||
def save_model(self, output_path, meta, save_dtype):
|
||||
# comfy-format single-file save via the mixin (chroma's class keys ARE
|
||||
# the original layout); handles torchao/Ostris dequant, not just quanto
|
||||
if not output_path.endswith(".safetensors"):
|
||||
output_path = output_path + ".safetensors"
|
||||
# only save the unet
|
||||
output_path = output_path + ".safetensors"
|
||||
transformer: Chroma = unwrap_model(self.model)
|
||||
state_dict = transformer.state_dict()
|
||||
save_dict = {}
|
||||
for k, v in state_dict.items():
|
||||
if isinstance(v, QTensor):
|
||||
v = v.dequantize()
|
||||
save_dict[k] = v.clone().to('cpu', dtype=save_dtype)
|
||||
|
||||
meta = get_meta_for_safetensors(meta, name='chroma')
|
||||
save_file(save_dict, output_path, metadata=meta)
|
||||
transformer.save_model(
|
||||
output_path,
|
||||
dtype=save_dtype,
|
||||
metadata=get_meta_for_safetensors(meta, name="chroma"),
|
||||
)
|
||||
|
||||
def get_loss_target(self, *args, **kwargs):
|
||||
noise = kwargs.get('noise')
|
||||
batch = kwargs.get('batch')
|
||||
return (noise - batch.latents).detach()
|
||||
|
||||
def convert_lora_weights_before_save(self, state_dict):
|
||||
# currently starte with transformer. but needs to start with diffusion_model. for comfyui
|
||||
new_sd = {}
|
||||
for key, value in state_dict.items():
|
||||
new_key = key.replace("transformer.", "diffusion_model.")
|
||||
new_sd[new_key] = value
|
||||
return new_sd
|
||||
lora_keys_use_comfy_prefix = True
|
||||
|
||||
def convert_lora_weights_before_load(self, state_dict):
|
||||
# saved as diffusion_model. but needs to be transformer. for ai-toolkit
|
||||
new_sd = {}
|
||||
for key, value in state_dict.items():
|
||||
new_key = key.replace("diffusion_model.", "transformer.")
|
||||
new_sd[new_key] = value
|
||||
return new_sd
|
||||
|
||||
def get_base_model_version(self):
|
||||
return "chroma_radiance"
|
||||
@@ -1,4 +1,6 @@
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, replace
|
||||
|
||||
from toolkit.models.v2._mixin import OstrisModelMixin
|
||||
|
||||
import torch
|
||||
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
|
||||
|
||||
|
||||
class Chroma(nn.Module):
|
||||
class Chroma(nn.Module, OstrisModelMixin):
|
||||
"""
|
||||
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):
|
||||
super().__init__()
|
||||
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
|
||||
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
|
||||
|
||||
|
||||
class Chroma(nn.Module):
|
||||
class Chroma(nn.Module, OstrisModelMixin):
|
||||
"""
|
||||
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):
|
||||
super().__init__()
|
||||
self.params = params
|
||||
|
||||
@@ -5,15 +5,14 @@ import torch
|
||||
import yaml
|
||||
from toolkit.config_modules import GenerateImageConfig, ModelConfig
|
||||
from toolkit.models.base_model import BaseModel
|
||||
from toolkit.models.v2.text_encoders.mistral3 import Mistral3ModelEncoder
|
||||
from toolkit.models.v2.vae.autoencoder_kl_flux2 import Flux2KLVAE
|
||||
from toolkit.basic import flush
|
||||
from toolkit.advanced_prompt_embeds import AdvancedPromptEmbeds
|
||||
from toolkit.samplers.custom_flowmatch_sampler import (
|
||||
CustomFlowMatchEulerDiscreteScheduler,
|
||||
)
|
||||
from toolkit.accelerator import unwrap_model
|
||||
from optimum.quanto import freeze
|
||||
from toolkit.util.quantize import quantize, get_qtype, quantize_model
|
||||
from toolkit.memory_management import MemoryManager
|
||||
|
||||
from transformers import AutoTokenizer, AutoModel
|
||||
|
||||
@@ -79,75 +78,31 @@ class ErnieImageModel(BaseModel):
|
||||
|
||||
self.print_and_status_update("Loading transformer")
|
||||
|
||||
transformer_path = model_path
|
||||
transformer_subfolder = "transformer"
|
||||
if os.path.exists(transformer_path):
|
||||
transformer_subfolder = None
|
||||
transformer_path = os.path.join(transformer_path, "transformer")
|
||||
if os.path.exists(model_path):
|
||||
# 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 = ErnieImageTransformer2DModel.from_pretrained(
|
||||
transformer_path, subfolder=transformer_subfolder, torch_dtype=dtype
|
||||
# load + quantize + offload + placement, all driven by model_config
|
||||
transformer = ErnieImageTransformer2DModel.load(
|
||||
model_path, **self.component_load_kwargs("transformer")
|
||||
)
|
||||
|
||||
if self.model_config.quantize:
|
||||
self.print_and_status_update("Quantizing Transformer")
|
||||
quantize_model(self, transformer)
|
||||
flush()
|
||||
|
||||
if (
|
||||
self.model_config.layer_offloading
|
||||
and self.model_config.layer_offloading_transformer_percent > 0
|
||||
):
|
||||
MemoryManager.attach(
|
||||
transformer,
|
||||
self.device_torch,
|
||||
offload_percent=self.model_config.layer_offloading_transformer_percent,
|
||||
ignore_modules=[
|
||||
transformer.x_embedder,
|
||||
],
|
||||
)
|
||||
|
||||
if self.model_config.low_vram:
|
||||
self.print_and_status_update("Moving transformer to CPU")
|
||||
transformer.to("cpu")
|
||||
|
||||
flush()
|
||||
|
||||
self.print_and_status_update("Text Encoder")
|
||||
tokenizer = AutoTokenizer.from_pretrained(
|
||||
base_model_path, subfolder="tokenizer", torch_dtype=dtype
|
||||
)
|
||||
text_encoder = AutoModel.from_pretrained(
|
||||
base_model_path, subfolder="text_encoder", torch_dtype=dtype
|
||||
text_encoder = Mistral3ModelEncoder.load(
|
||||
base_model_path, subfolder="text_encoder", **self.component_load_kwargs("te")
|
||||
)
|
||||
|
||||
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")
|
||||
vae = AutoencoderKLFlux2.from_pretrained(
|
||||
base_model_path, subfolder="vae", torch_dtype=dtype
|
||||
vae = Flux2KLVAE.load_model( base_model_path, dtype=dtype
|
||||
).to(self.device_torch, dtype=dtype)
|
||||
|
||||
self.noise_scheduler = ErnieImageModel.get_train_scheduler()
|
||||
@@ -178,8 +133,10 @@ class ErnieImageModel(BaseModel):
|
||||
pipe.transformer = pipe.transformer.to(self.device_torch)
|
||||
|
||||
flush()
|
||||
# just to make sure everything is on the right device and dtype
|
||||
text_encoder[0].to(self.device_torch)
|
||||
# low_vram: the text encoder stays on cpu; get_prompt_embeds moves it
|
||||
# to the gpu on demand
|
||||
if not self.low_vram:
|
||||
text_encoder[0].to(self.device_torch)
|
||||
text_encoder[0].requires_grad_(False)
|
||||
text_encoder[0].eval()
|
||||
flush()
|
||||
@@ -377,16 +334,5 @@ class ErnieImageModel(BaseModel):
|
||||
def get_transformer_block_names(self) -> Optional[List[str]]:
|
||||
return ["layers"]
|
||||
|
||||
def convert_lora_weights_before_save(self, state_dict):
|
||||
new_sd = {}
|
||||
for key, value in state_dict.items():
|
||||
new_key = key.replace("transformer.", "diffusion_model.")
|
||||
new_sd[new_key] = value
|
||||
return new_sd
|
||||
lora_keys_use_comfy_prefix = True
|
||||
|
||||
def convert_lora_weights_before_load(self, state_dict):
|
||||
new_sd = {}
|
||||
for key, value in state_dict.items():
|
||||
new_key = key.replace("diffusion_model.", "transformer.")
|
||||
new_sd[new_key] = value
|
||||
return new_sd
|
||||
@@ -33,6 +33,8 @@ from diffusers.models.attention_dispatch import dispatch_attention_fn
|
||||
from diffusers.models.attention_processor import Attention
|
||||
from diffusers.models.embeddings import TimestepEmbedding, Timesteps
|
||||
from diffusers.models.modeling_utils import ModelMixin
|
||||
|
||||
from toolkit.models.v2._mixin import OstrisModelMixin
|
||||
from diffusers.models.normalization import RMSNorm
|
||||
|
||||
|
||||
@@ -288,8 +290,17 @@ class ErnieImageAdaLNContinuous(nn.Module):
|
||||
return x
|
||||
|
||||
|
||||
class ErnieImageTransformer2DModel(ModelMixin, ConfigMixin):
|
||||
class ErnieImageTransformer2DModel(ModelMixin, ConfigMixin, OstrisModelMixin):
|
||||
_supports_gradient_checkpointing = True
|
||||
aitk_subfolder = "transformer"
|
||||
|
||||
@classmethod
|
||||
def get_transformer_block_names(cls):
|
||||
return ["layers"]
|
||||
|
||||
def get_offload_ignore_modules(self):
|
||||
return [self.x_embedder]
|
||||
|
||||
|
||||
@register_to_config
|
||||
def __init__(
|
||||
|
||||
@@ -32,7 +32,7 @@ from toolkit.models.base_model import BaseModel
|
||||
from toolkit.samplers.custom_flowmatch_sampler import (
|
||||
CustomFlowMatchEulerDiscreteScheduler,
|
||||
)
|
||||
from toolkit.util.quantize import quantize, get_qtype, quantize_model
|
||||
from toolkit.util.quantize import quantize, get_qtype
|
||||
|
||||
from .src.model import ExampleTransformer2DModel
|
||||
from .src.pipeline import ExamplePipeline, pad_prompt_embeds
|
||||
@@ -165,21 +165,14 @@ class ExampleModel(BaseModel):
|
||||
del state_dict
|
||||
flush() # gc + empty cuda cache; call it after dropping anything big
|
||||
|
||||
if self.model_config.quantize:
|
||||
# quantize_model handles qtype selection, exclusions and device
|
||||
# juggling, and leaves the model on CPU
|
||||
self.print_and_status_update("Quantizing transformer")
|
||||
quantize_model(self, transformer)
|
||||
flush()
|
||||
|
||||
if self.model_config.low_vram:
|
||||
# leave it on CPU; get_noise_prediction moves it over when needed
|
||||
transformer.to("cpu")
|
||||
else:
|
||||
transformer.to(self.device_torch, dtype=dtype)
|
||||
# quantize + offload + placement, all driven by model_config:
|
||||
# component_load_kwargs derives qtype (incl. an accuracy recovery
|
||||
# adapter), the layer-offload fraction and the target device
|
||||
# (low_vram parks on CPU); aitk_post_load applies them. Models whose
|
||||
# checkpoint sourcing is standard can collapse the build + this into
|
||||
# one call: ExampleTransformer2DModel.load(path, **kwargs).
|
||||
transformer.aitk_post_load(**self.component_load_kwargs("transformer"))
|
||||
flush()
|
||||
# For partial layer offloading support see MemoryManager.attach usage
|
||||
# in ../ideogram4/ideogram4.py or ../z_image/z_image.py.
|
||||
|
||||
# --- text encoder + tokenizer (stock transformers model) ---
|
||||
self.print_and_status_update("Loading text encoder")
|
||||
@@ -504,19 +497,8 @@ class ExampleModel(BaseModel):
|
||||
attribute in src/model.py."""
|
||||
return ["blocks"]
|
||||
|
||||
def convert_lora_weights_before_save(self, state_dict):
|
||||
"""Map internal LoRA keys to the ecosystem-standard naming right before
|
||||
the .safetensors is written. Most modern models ship LoRAs with a
|
||||
``diffusion_model.`` prefix (ComfyUI convention); internally ai-toolkit
|
||||
uses ``transformer.``."""
|
||||
return {
|
||||
k.replace("transformer.", "diffusion_model."): v
|
||||
for k, v in state_dict.items()
|
||||
}
|
||||
# LoRA keys save with the ecosystem-standard ``diffusion_model.`` prefix
|
||||
# (ComfyUI convention) and load back to the internal ``transformer.``
|
||||
# prefix; see BaseModel.convert_lora_weights_before_save/load
|
||||
lora_keys_use_comfy_prefix = True
|
||||
|
||||
def convert_lora_weights_before_load(self, state_dict):
|
||||
"""Inverse of the above, applied when resuming from a saved LoRA."""
|
||||
return {
|
||||
k.replace("diffusion_model.", "transformer."): v
|
||||
for k, v in state_dict.items()
|
||||
}
|
||||
@@ -51,6 +51,8 @@ import torch.nn.functional as F
|
||||
from torch import nn
|
||||
from torch.utils.checkpoint import checkpoint
|
||||
|
||||
from toolkit.models.v2._mixin import OstrisModelMixin
|
||||
|
||||
|
||||
def timestep_embedding(t: torch.Tensor, dim: int, max_period: int = 10000) -> torch.Tensor:
|
||||
"""Standard sinusoidal embedding.
|
||||
@@ -135,8 +137,16 @@ class ExampleTransformerBlock(nn.Module):
|
||||
return x
|
||||
|
||||
|
||||
class ExampleTransformer2DModel(nn.Module):
|
||||
"""The denoiser. Plain ``nn.Module`` on purpose.
|
||||
class ExampleTransformer2DModel(nn.Module, OstrisModelMixin):
|
||||
"""The denoiser. Plain ``nn.Module`` plus ``OstrisModelMixin``.
|
||||
|
||||
The mixin supplies the universal component API every v2 module shares:
|
||||
``load()`` / ``load_model()`` / ``aitk_post_load()`` (quantize, layer
|
||||
offloading and device placement driven by the holder's model_config via
|
||||
``BaseModel.component_load_kwargs(role)``), plus comfy-format save/load.
|
||||
Override its class hooks (``get_transformer_block_names``,
|
||||
``get_quantization_exclude_modules``, ``get_offload_ignore_modules``,
|
||||
``convert_state_dict_on_load/save``) as needed.
|
||||
|
||||
You could also subclass ``diffusers.ModelMixin``/``ConfigMixin`` (see
|
||||
../../ernie_image/transformer.py) to get ``save_pretrained``,
|
||||
@@ -153,6 +163,12 @@ class ExampleTransformer2DModel(nn.Module):
|
||||
names when deciding where to attach LoRA layers.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def get_transformer_block_names(cls):
|
||||
# attribute name(s) of the repeated-block ModuleList(s); the quantizer
|
||||
# streams these blocks through the GPU one at a time
|
||||
return ["blocks"]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
in_channels: int = 16, # VAE latent channels
|
||||
@@ -183,8 +199,8 @@ class ExampleTransformer2DModel(nn.Module):
|
||||
)
|
||||
|
||||
# ``blocks`` is the repeated-layer ModuleList. The attribute name is
|
||||
# what ExampleModel.get_transformer_block_names() returns, which the
|
||||
# LoRA code uses for block targeting / "transformer only" training.
|
||||
# what get_transformer_block_names() returns, which the LoRA code uses
|
||||
# for block targeting and the quantizer uses for block streaming.
|
||||
self.blocks = nn.ModuleList(
|
||||
[
|
||||
ExampleTransformerBlock(hidden_size, num_heads)
|
||||
|
||||
@@ -6,15 +6,14 @@ import yaml
|
||||
from toolkit.config_modules import GenerateImageConfig, ModelConfig
|
||||
from PIL import Image
|
||||
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 diffusers import AutoencoderKL
|
||||
from toolkit.prompt_utils import PromptEmbeds
|
||||
from toolkit.samplers.custom_flowmatch_sampler import CustomFlowMatchEulerDiscreteScheduler
|
||||
from toolkit.dequantize import patch_dequantization_on_save
|
||||
from toolkit.accelerator import unwrap_model
|
||||
from optimum.quanto import freeze, QTensor
|
||||
from toolkit.util.quantize import quantize, get_qtype
|
||||
from transformers import T5TokenizerFast, T5EncoderModel
|
||||
from optimum.quanto import QTensor
|
||||
|
||||
from .src import FLitePipeline, DiT
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -34,6 +33,9 @@ scheduler_config = {
|
||||
class FLiteModel(BaseModel):
|
||||
arch = "f-lite"
|
||||
|
||||
def get_transformer_block_names(self):
|
||||
return ["blocks"]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
device,
|
||||
@@ -74,54 +76,22 @@ class FLiteModel(BaseModel):
|
||||
|
||||
self.print_and_status_update("Loading transformer")
|
||||
|
||||
transformer = DiT.from_pretrained(
|
||||
model_path,
|
||||
subfolder="dit_model",
|
||||
torch_dtype=dtype,
|
||||
transformer = DiT.load(
|
||||
model_path, **self.component_load_kwargs("transformer")
|
||||
)
|
||||
|
||||
transformer.to(self.quantize_device, dtype=dtype)
|
||||
|
||||
if self.model_config.quantize:
|
||||
# patch the state dict method
|
||||
patch_dequantization_on_save(transformer)
|
||||
quantization_type = get_qtype(self.model_config.qtype)
|
||||
self.print_and_status_update("Quantizing transformer")
|
||||
quantize(transformer, weights=quantization_type,
|
||||
**self.model_config.quantize_kwargs)
|
||||
freeze(transformer)
|
||||
transformer.to(self.device_torch)
|
||||
else:
|
||||
transformer.to(self.device_torch, dtype=dtype)
|
||||
|
||||
flush()
|
||||
|
||||
self.print_and_status_update("Loading T5")
|
||||
tokenizer = T5TokenizerFast.from_pretrained(
|
||||
extras_path, subfolder="tokenizer", torch_dtype=dtype
|
||||
tokenizer = T5TextEncoder.load_tokenizer(extras_path, subfolder="tokenizer")
|
||||
text_encoder = T5TextEncoder.load(
|
||||
extras_path, subfolder="text_encoder", **self.component_load_kwargs("te")
|
||||
)
|
||||
text_encoder = T5EncoderModel.from_pretrained(
|
||||
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.print_and_status_update("Loading VAE")
|
||||
vae = AutoencoderKL.from_pretrained(
|
||||
extras_path,
|
||||
subfolder="vae",
|
||||
torch_dtype=dtype
|
||||
)
|
||||
vae = vae.to(self.device_torch, dtype=dtype)
|
||||
vae = KLVAE.load_model(extras_path, dtype=dtype, device=self.device_torch)
|
||||
|
||||
self.print_and_status_update("Making pipe")
|
||||
|
||||
@@ -145,8 +115,10 @@ class FLiteModel(BaseModel):
|
||||
pipe.transformer = pipe.transformer.to(self.device_torch)
|
||||
|
||||
flush()
|
||||
# just to make sure everything is on the right device and dtype
|
||||
text_encoder[0].to(self.device_torch)
|
||||
# low_vram: the text encoder stays on cpu; get_prompt_embeds moves it
|
||||
# to the gpu on demand
|
||||
if not self.low_vram:
|
||||
text_encoder[0].to(self.device_torch)
|
||||
text_encoder[0].requires_grad_(False)
|
||||
text_encoder[0].eval()
|
||||
pipe.transformer = pipe.transformer.to(self.device_torch)
|
||||
@@ -270,21 +242,8 @@ class FLiteModel(BaseModel):
|
||||
# return (noise - batch.latents).detach()
|
||||
return (batch.latents - noise).detach()
|
||||
|
||||
def convert_lora_weights_before_save(self, state_dict):
|
||||
# currently starte with transformer. but needs to start with diffusion_model. for comfyui
|
||||
new_sd = {}
|
||||
for key, value in state_dict.items():
|
||||
new_key = key.replace("transformer.", "diffusion_model.")
|
||||
new_sd[new_key] = value
|
||||
return new_sd
|
||||
lora_keys_use_comfy_prefix = True
|
||||
|
||||
def convert_lora_weights_before_load(self, state_dict):
|
||||
# saved as diffusion_model. but needs to be transformer. for ai-toolkit
|
||||
new_sd = {}
|
||||
for key, value in state_dict.items():
|
||||
new_key = key.replace("diffusion_model.", "transformer.")
|
||||
new_sd[new_key] = value
|
||||
return new_sd
|
||||
|
||||
def get_base_model_version(self):
|
||||
return "f-lite"
|
||||
|
||||
@@ -7,6 +7,8 @@ import torch.nn.functional as F
|
||||
from diffusers.configuration_utils import ConfigMixin, register_to_config
|
||||
from diffusers.loaders import FromOriginalModelMixin, PeftAdapterMixin
|
||||
from diffusers.models.modeling_utils import ModelMixin
|
||||
|
||||
from toolkit.models.v2._mixin import OstrisModelMixin
|
||||
from diffusers.utils.accelerate_utils import apply_forward_hook
|
||||
from einops import rearrange
|
||||
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)
|
||||
|
||||
|
||||
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
|
||||
|
||||
@register_to_config
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
from .flux2_model import Flux2Model
|
||||
from transformers import Qwen3ForCausalLM, Qwen2Tokenizer
|
||||
from optimum.quanto import freeze
|
||||
from toolkit.util.quantize import quantize, get_qtype
|
||||
from toolkit.models.v2.text_encoders.qwen3 import Qwen3TextEncoder
|
||||
from toolkit.config_modules import ModelConfig
|
||||
from toolkit.memory_management.manager import MemoryManager
|
||||
from toolkit.basic import flush
|
||||
from .src.model import Klein9BParams, Klein4BParams
|
||||
|
||||
@@ -40,28 +38,11 @@ class Flux2KleinModel(Flux2Model):
|
||||
dtype = self.torch_dtype
|
||||
self.print_and_status_update("Loading Qwen3")
|
||||
|
||||
text_encoder: Qwen3ForCausalLM = Qwen3ForCausalLM.from_pretrained(
|
||||
self.flux2_klein_te_path,
|
||||
torch_dtype=dtype,
|
||||
# load + quantize + offload + placement, all driven by model_config
|
||||
text_encoder = Qwen3TextEncoder.load(
|
||||
self.flux2_klein_te_path, subfolder="", **self.component_load_kwargs("te")
|
||||
)
|
||||
if self.model_config.quantize_te:
|
||||
self.print_and_status_update("Quantizing Qwen3")
|
||||
quantize(text_encoder, weights=get_qtype(self.model_config.qtype_te))
|
||||
freeze(text_encoder)
|
||||
flush()
|
||||
elif not self.model_config.low_vram:
|
||||
text_encoder.to(self.device_torch, dtype=dtype)
|
||||
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,
|
||||
)
|
||||
flush()
|
||||
|
||||
tokenizer = Qwen2Tokenizer.from_pretrained(self.flux2_klein_te_path)
|
||||
return text_encoder, tokenizer
|
||||
|
||||
@@ -5,7 +5,6 @@ from typing import TYPE_CHECKING, List, Optional
|
||||
import huggingface_hub
|
||||
import torch
|
||||
from toolkit.config_modules import GenerateImageConfig, ModelConfig
|
||||
from toolkit.memory_management.manager import MemoryManager
|
||||
from toolkit.metadata import get_meta_for_safetensors
|
||||
from toolkit.models.base_model import BaseModel
|
||||
from toolkit.basic import flush
|
||||
@@ -13,15 +12,18 @@ from toolkit.prompt_utils import PromptEmbeds
|
||||
from toolkit.samplers.custom_flowmatch_sampler import (
|
||||
CustomFlowMatchEulerDiscreteScheduler,
|
||||
)
|
||||
from toolkit.dequantize import patch_dequantization_on_save
|
||||
from toolkit.accelerator import unwrap_model
|
||||
from optimum.quanto import freeze, QTensor
|
||||
from toolkit.util.quantize import quantize, get_qtype, quantize_model
|
||||
from optimum.quanto import QTensor
|
||||
|
||||
from transformers import AutoProcessor, Mistral3ForConditionalGeneration
|
||||
from toolkit.models.v2.text_encoders.mistral3 import Mistral3TextEncoder
|
||||
from .src.model import Flux2, Flux2Params
|
||||
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 PIL import Image
|
||||
import torch.nn.functional as F
|
||||
@@ -95,33 +97,22 @@ class Flux2Model(BaseModel):
|
||||
dtype = self.torch_dtype
|
||||
self.print_and_status_update("Loading Mistral")
|
||||
|
||||
text_encoder: Mistral3ForConditionalGeneration = (
|
||||
Mistral3ForConditionalGeneration.from_pretrained(
|
||||
MISTRAL_PATH,
|
||||
torch_dtype=dtype,
|
||||
)
|
||||
# load + quantize + offload + placement, all driven by model_config
|
||||
# tie_word_embeddings=False: the checkpoint carries both embed_tokens
|
||||
# and lm_head with different values; the config's tie claim is wrong
|
||||
text_encoder = Mistral3TextEncoder.load(
|
||||
MISTRAL_PATH,
|
||||
subfolder="",
|
||||
tie_word_embeddings=False,
|
||||
**self.component_load_kwargs("te"),
|
||||
)
|
||||
text_encoder.to(self.device_torch, dtype=dtype)
|
||||
|
||||
flush()
|
||||
|
||||
if self.model_config.quantize_te:
|
||||
self.print_and_status_update("Quantizing Mistral")
|
||||
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,
|
||||
)
|
||||
|
||||
tokenizer = AutoProcessor.from_pretrained(MISTRAL_PATH)
|
||||
# fix_mistral_regex=False: keep the exact tokenization flux2 has always
|
||||
# used (True would change the pre-tokenizer and shift conditioning)
|
||||
tokenizer = AutoProcessor.from_pretrained(
|
||||
MISTRAL_PATH, fix_mistral_regex=False
|
||||
)
|
||||
return text_encoder, tokenizer
|
||||
|
||||
def load_model(self):
|
||||
@@ -132,9 +123,6 @@ class Flux2Model(BaseModel):
|
||||
transformer_path = model_path
|
||||
|
||||
self.print_and_status_update("Loading transformer")
|
||||
with torch.device("meta"):
|
||||
transformer = Flux2(self.get_flux2_params())
|
||||
|
||||
# use local path if provided
|
||||
if os.path.exists(os.path.join(transformer_path, self.flux2_te_filename)):
|
||||
transformer_path = os.path.join(transformer_path, self.flux2_te_filename)
|
||||
@@ -148,39 +136,14 @@ class Flux2Model(BaseModel):
|
||||
)
|
||||
|
||||
transformer_state_dict = load_file(transformer_path, device="cpu")
|
||||
transformer = Flux2.load_from_state_dict(
|
||||
transformer_state_dict, dtype, config=self.get_flux2_params()
|
||||
)
|
||||
|
||||
# cast to dtype
|
||||
for key in transformer_state_dict:
|
||||
transformer_state_dict[key] = transformer_state_dict[key].to(dtype)
|
||||
|
||||
transformer.load_state_dict(transformer_state_dict, assign=True)
|
||||
|
||||
if self.model_config.quantize:
|
||||
# patch the state dict method
|
||||
patch_dequantization_on_save(transformer)
|
||||
# Avoid full-model peak VRAM allocation before quantization.
|
||||
self.print_and_status_update("Keeping transformer on CPU for quantization")
|
||||
self.print_and_status_update("Quantizing Transformer")
|
||||
quantize_model(self, transformer)
|
||||
flush()
|
||||
else:
|
||||
transformer.to(self.device_torch, dtype=dtype)
|
||||
# quantize + offload + placement, all driven by model_config
|
||||
transformer.aitk_post_load(**self.component_load_kwargs("transformer"))
|
||||
flush()
|
||||
|
||||
if (
|
||||
self.model_config.layer_offloading
|
||||
and self.model_config.layer_offloading_transformer_percent > 0
|
||||
):
|
||||
MemoryManager.attach(
|
||||
transformer,
|
||||
self.device_torch,
|
||||
offload_percent=self.model_config.layer_offloading_transformer_percent,
|
||||
)
|
||||
|
||||
if self.model_config.low_vram:
|
||||
self.print_and_status_update("Moving transformer to CPU")
|
||||
transformer.to("cpu")
|
||||
|
||||
text_encoder, tokenizer = self.load_te()
|
||||
|
||||
self.print_and_status_update("Loading VAE")
|
||||
@@ -207,21 +170,8 @@ class Flux2Model(BaseModel):
|
||||
token=HF_TOKEN,
|
||||
)
|
||||
|
||||
vae_state_dict = load_file(vae_path, device="cpu")
|
||||
|
||||
autoencoder_params = AutoEncoderParams()
|
||||
if vae_state_dict['decoder.up.0.block.0.conv1.bias'].shape[0] == 96:
|
||||
# this is the small decoder version
|
||||
autoencoder_params = AutoEncoderSmallDecoderParams()
|
||||
|
||||
with torch.device("meta"):
|
||||
vae = AutoEncoder(autoencoder_params)
|
||||
|
||||
# cast to dtype
|
||||
for key in vae_state_dict:
|
||||
vae_state_dict[key] = vae_state_dict[key].to(dtype)
|
||||
|
||||
vae.load_state_dict(vae_state_dict, assign=True)
|
||||
# config sniffed from the checkpoint (small-decoder detection)
|
||||
vae = AutoEncoder.load_model(vae_path, dtype=dtype)
|
||||
|
||||
self.noise_scheduler = Flux2Model.get_train_scheduler()
|
||||
|
||||
@@ -505,19 +455,7 @@ class Flux2Model(BaseModel):
|
||||
def get_transformer_block_names(self) -> Optional[List[str]]:
|
||||
return ["double_blocks", "single_blocks"]
|
||||
|
||||
def convert_lora_weights_before_save(self, state_dict):
|
||||
new_sd = {}
|
||||
for key, value in state_dict.items():
|
||||
new_key = key.replace("transformer.", "diffusion_model.")
|
||||
new_sd[new_key] = value
|
||||
return new_sd
|
||||
|
||||
def convert_lora_weights_before_load(self, state_dict):
|
||||
new_sd = {}
|
||||
for key, value in state_dict.items():
|
||||
new_key = key.replace("diffusion_model.", "transformer.")
|
||||
new_sd[new_key] = value
|
||||
return new_sd
|
||||
lora_keys_use_comfy_prefix = True
|
||||
|
||||
def encode_images(self, image_list: List[torch.Tensor], device=None, dtype=None):
|
||||
if device is None:
|
||||
|
||||
@@ -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
|
||||
@@ -1,4 +1,6 @@
|
||||
import torch
|
||||
|
||||
from toolkit.models.v2._mixin import OstrisModelMixin
|
||||
from einops import rearrange
|
||||
from torch import Tensor, nn
|
||||
import torch.utils.checkpoint as ckpt
|
||||
@@ -54,7 +56,11 @@ class FakeConfig:
|
||||
self.patch_size = 1
|
||||
|
||||
|
||||
class Flux2(nn.Module):
|
||||
class Flux2(nn.Module, OstrisModelMixin):
|
||||
@classmethod
|
||||
def get_transformer_block_names(cls):
|
||||
return ["double_blocks", "single_blocks"]
|
||||
|
||||
def __init__(self, params: Flux2Params):
|
||||
super().__init__()
|
||||
self.config = FakeConfig()
|
||||
|
||||
@@ -12,7 +12,7 @@ from diffusers.utils import (
|
||||
from diffusers.utils.torch_utils import randn_tensor
|
||||
from diffusers.pipelines.pipeline_utils import DiffusionPipeline
|
||||
from diffusers.utils import BaseOutput
|
||||
from .autoencoder import AutoEncoder
|
||||
from toolkit.models.v2.vae.flux2_kl import AutoEncoder
|
||||
from .model import Flux2
|
||||
from einops import rearrange
|
||||
from transformers import AutoProcessor, Mistral3ForConditionalGeneration
|
||||
@@ -109,15 +109,19 @@ class Flux2Pipeline(DiffusionPipeline):
|
||||
# Process all messages at once
|
||||
# with image processing a too short max length can throw an error in here.
|
||||
try:
|
||||
# tokenization kwargs ride in processor_kwargs (same values end up
|
||||
# in the same place; loose **kwargs just warn on new transformers)
|
||||
inputs = self.tokenizer.apply_chat_template(
|
||||
messages_batch,
|
||||
add_generation_prompt=False,
|
||||
tokenize=True,
|
||||
return_dict=True,
|
||||
return_tensors="pt",
|
||||
padding="max_length",
|
||||
truncation=True,
|
||||
max_length=max_sequence_length,
|
||||
processor_kwargs={
|
||||
"padding": "max_length",
|
||||
"truncation": True,
|
||||
"max_length": max_sequence_length,
|
||||
},
|
||||
)
|
||||
except ValueError as e:
|
||||
print(
|
||||
|
||||
@@ -8,17 +8,19 @@ from toolkit import train_tools
|
||||
from toolkit.config_modules import GenerateImageConfig, ModelConfig
|
||||
from PIL import Image
|
||||
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.prompt_utils import PromptEmbeds
|
||||
from toolkit.samplers.custom_flowmatch_sampler import CustomFlowMatchEulerDiscreteScheduler
|
||||
from toolkit.models.flux import add_model_gpu_splitter_to_flux, bypass_flux_guidance, restore_flux_guidance
|
||||
from toolkit.dequantize import patch_dequantization_on_save
|
||||
from toolkit.accelerator import get_accelerator, unwrap_model
|
||||
from optimum.quanto import freeze, QTensor
|
||||
from optimum.quanto import QTensor
|
||||
from toolkit.util.mask import generate_random_mask, random_dialate_mask
|
||||
from toolkit.util.quantize import quantize, get_qtype
|
||||
from transformers import T5TokenizerFast, T5EncoderModel, CLIPTextModel, CLIPTokenizer
|
||||
|
||||
from einops import rearrange, repeat
|
||||
import random
|
||||
import torch.nn.functional as F
|
||||
@@ -36,11 +38,12 @@ scheduler_config = {
|
||||
"use_dynamic_shifting": True
|
||||
}
|
||||
|
||||
|
||||
|
||||
class FluxKontextModel(BaseModel):
|
||||
arch = "flux_kontext"
|
||||
|
||||
def get_transformer_block_names(self):
|
||||
return ["transformer_blocks", "single_transformer_blocks"]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
device,
|
||||
@@ -80,11 +83,7 @@ class FluxKontextModel(BaseModel):
|
||||
# so we need this for the VAE, te, etc
|
||||
base_model_path = self.model_config.extras_name_or_path
|
||||
|
||||
transformer_path = model_path
|
||||
transformer_subfolder = 'transformer'
|
||||
if os.path.exists(transformer_path):
|
||||
transformer_subfolder = None
|
||||
transformer_path = os.path.join(transformer_path, 'transformer')
|
||||
if os.path.exists(model_path):
|
||||
# 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
|
||||
@@ -92,54 +91,26 @@ class FluxKontextModel(BaseModel):
|
||||
base_model_path = model_path
|
||||
|
||||
self.print_and_status_update("Loading transformer")
|
||||
transformer = FluxTransformer2DModel.from_pretrained(
|
||||
transformer_path,
|
||||
subfolder=transformer_subfolder,
|
||||
torch_dtype=dtype
|
||||
transformer = FluxTransformer2DModel.load(
|
||||
model_path, **self.component_load_kwargs("transformer")
|
||||
)
|
||||
transformer.to(self.quantize_device, dtype=dtype)
|
||||
|
||||
if self.model_config.quantize:
|
||||
# patch the state dict method
|
||||
patch_dequantization_on_save(transformer)
|
||||
quantization_type = get_qtype(self.model_config.qtype)
|
||||
self.print_and_status_update("Quantizing transformer")
|
||||
quantize(transformer, weights=quantization_type,
|
||||
**self.model_config.quantize_kwargs)
|
||||
freeze(transformer)
|
||||
transformer.to(self.device_torch)
|
||||
else:
|
||||
transformer.to(self.device_torch, dtype=dtype)
|
||||
|
||||
flush()
|
||||
|
||||
self.print_and_status_update("Loading T5")
|
||||
tokenizer_2 = T5TokenizerFast.from_pretrained(
|
||||
base_model_path, subfolder="tokenizer_2", torch_dtype=dtype
|
||||
tokenizer_2 = T5TextEncoder.load_tokenizer(base_model_path)
|
||||
text_encoder_2 = T5TextEncoder.load(
|
||||
base_model_path, **self.component_load_kwargs("te")
|
||||
)
|
||||
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")
|
||||
text_encoder = CLIPTextModel.from_pretrained(
|
||||
base_model_path, subfolder="text_encoder", torch_dtype=dtype)
|
||||
tokenizer = CLIPTokenizer.from_pretrained(
|
||||
base_model_path, subfolder="tokenizer", torch_dtype=dtype)
|
||||
text_encoder.to(self.device_torch, dtype=dtype)
|
||||
text_encoder = CLIPTextEncoder.load_model(
|
||||
base_model_path, dtype=dtype, device=self.device_torch
|
||||
)
|
||||
tokenizer = CLIPTextEncoder.load_tokenizer(base_model_path, use_fast=False)
|
||||
|
||||
self.print_and_status_update("Loading VAE")
|
||||
vae = AutoencoderKL.from_pretrained(
|
||||
base_model_path, subfolder="vae", torch_dtype=dtype)
|
||||
vae = KLVAE.load_model(base_model_path, dtype=dtype)
|
||||
|
||||
self.noise_scheduler = FluxKontextModel.get_train_scheduler()
|
||||
|
||||
@@ -166,11 +137,13 @@ class FluxKontextModel(BaseModel):
|
||||
pipe.transformer = pipe.transformer.to(self.device_torch)
|
||||
|
||||
flush()
|
||||
# just to make sure everything is on the right device and dtype
|
||||
text_encoder[0].to(self.device_torch)
|
||||
# low_vram: text encoders stay on cpu; get_prompt_embeds moves them
|
||||
# to the gpu on demand
|
||||
if not self.low_vram:
|
||||
text_encoder[0].to(self.device_torch)
|
||||
text_encoder[1].to(self.device_torch)
|
||||
text_encoder[0].requires_grad_(False)
|
||||
text_encoder[0].eval()
|
||||
text_encoder[1].to(self.device_torch)
|
||||
text_encoder[1].requires_grad_(False)
|
||||
text_encoder[1].eval()
|
||||
pipe.transformer = pipe.transformer.to(self.device_torch)
|
||||
|
||||
@@ -7,7 +7,7 @@ from toolkit.accelerator import unwrap_model
|
||||
import torch
|
||||
from toolkit.prompt_utils import PromptEmbeds
|
||||
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
|
||||
from PIL import Image
|
||||
|
||||
@@ -9,7 +9,11 @@ from toolkit import train_tools
|
||||
from toolkit.config_modules import GenerateImageConfig, ModelConfig
|
||||
from PIL import Image
|
||||
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.llama import LlamaTextEncoder
|
||||
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.prompt_utils import PromptEmbeds
|
||||
from toolkit.samplers.custom_flowmatch_sampler import CustomFlowMatchEulerDiscreteScheduler
|
||||
@@ -18,8 +22,6 @@ from toolkit.dequantize import patch_dequantization_on_save
|
||||
from toolkit.accelerator import get_accelerator, unwrap_model
|
||||
from optimum.quanto import freeze, QTensor
|
||||
from toolkit.util.mask import generate_random_mask, random_dialate_mask
|
||||
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.models.transformers.transformer_hidream_image import HiDreamImageTransformer2DModel
|
||||
from .src.schedulers.fm_solvers_unipc import FlowUniPCMultistepScheduler
|
||||
@@ -28,15 +30,6 @@ from einops import rearrange, repeat
|
||||
import random
|
||||
import torch.nn.functional as F
|
||||
from tqdm import tqdm
|
||||
from transformers import (
|
||||
CLIPTextModelWithProjection,
|
||||
CLIPTokenizer,
|
||||
T5EncoderModel,
|
||||
T5Tokenizer,
|
||||
LlamaForCausalLM,
|
||||
PreTrainedTokenizerFast
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from toolkit.data_transfer_object.data_loader import DataLoaderBatchDTO
|
||||
|
||||
@@ -103,117 +96,61 @@ class HidreamModel(BaseModel):
|
||||
use_fast=False
|
||||
)
|
||||
|
||||
text_encoder_4 = LlamaForCausalLM.from_pretrained(
|
||||
# load + quantize + offload + placement, all driven by model_config
|
||||
text_encoder_4 = LlamaTextEncoder.load(
|
||||
llama_model_path,
|
||||
subfolder="",
|
||||
output_hidden_states=True,
|
||||
output_attentions=True,
|
||||
torch_dtype=torch.bfloat16,
|
||||
**self.component_load_kwargs("te"),
|
||||
)
|
||||
text_encoder_4.to(self.device_torch, dtype=dtype)
|
||||
|
||||
if self.model_config.quantize_te:
|
||||
self.print_and_status_update("Quantizing llama 8b model")
|
||||
quantization_type = get_qtype(self.model_config.qtype_te)
|
||||
quantize(text_encoder_4, weights=quantization_type)
|
||||
freeze(text_encoder_4)
|
||||
|
||||
if self.low_vram:
|
||||
# unload it for now
|
||||
text_encoder_4.to('cpu')
|
||||
|
||||
|
||||
flush()
|
||||
|
||||
|
||||
self.print_and_status_update("Loading transformer")
|
||||
|
||||
transformer = self.hidream_transformer_class.from_pretrained(
|
||||
model_path,
|
||||
subfolder="transformer",
|
||||
torch_dtype=torch.bfloat16
|
||||
|
||||
transformer = self.hidream_transformer_class.load(
|
||||
model_path, **self.component_load_kwargs("transformer")
|
||||
)
|
||||
|
||||
if not self.low_vram:
|
||||
transformer.to(self.device_torch, dtype=dtype)
|
||||
|
||||
if self.model_config.quantize:
|
||||
self.print_and_status_update("Quantizing transformer")
|
||||
quantization_type = get_qtype(self.model_config.qtype)
|
||||
if self.low_vram:
|
||||
# move and quantize only certain pieces at a time.
|
||||
all_blocks = list(transformer.double_stream_blocks) + list(transformer.single_stream_blocks)
|
||||
self.print_and_status_update(" - quantizing transformer blocks")
|
||||
for block in tqdm(all_blocks):
|
||||
block.to(self.device_torch, dtype=dtype)
|
||||
quantize(block, weights=quantization_type)
|
||||
freeze(block)
|
||||
block.to('cpu')
|
||||
# flush()
|
||||
|
||||
self.print_and_status_update(" - quantizing extras")
|
||||
transformer.to(self.device_torch, dtype=dtype)
|
||||
quantize(transformer, weights=quantization_type)
|
||||
freeze(transformer)
|
||||
else:
|
||||
quantize(transformer, weights=quantization_type)
|
||||
freeze(transformer)
|
||||
|
||||
if self.low_vram:
|
||||
# unload it for now
|
||||
transformer.to('cpu')
|
||||
|
||||
|
||||
flush()
|
||||
|
||||
self.print_and_status_update("Loading vae")
|
||||
|
||||
vae = AutoencoderKL.from_pretrained(
|
||||
extras_path,
|
||||
subfolder="vae",
|
||||
torch_dtype=torch.bfloat16
|
||||
).to(self.device_torch, dtype=dtype)
|
||||
vae = KLVAE.load_model(extras_path, dtype=torch.bfloat16).to(
|
||||
self.device_torch, dtype=dtype
|
||||
)
|
||||
|
||||
|
||||
self.print_and_status_update("Loading clip encoders")
|
||||
|
||||
text_encoder = CLIPTextModelWithProjection.from_pretrained(
|
||||
extras_path,
|
||||
subfolder="text_encoder",
|
||||
torch_dtype=torch.bfloat16
|
||||
text_encoder = CLIPTextEncoderWithProjection.load_model(
|
||||
extras_path, dtype=torch.bfloat16
|
||||
).to(self.device_torch, dtype=dtype)
|
||||
|
||||
tokenizer = CLIPTokenizer.from_pretrained(
|
||||
extras_path,
|
||||
subfolder="tokenizer"
|
||||
|
||||
tokenizer = CLIPTextEncoderWithProjection.load_tokenizer(
|
||||
extras_path, use_fast=False
|
||||
)
|
||||
|
||||
text_encoder_2 = CLIPTextModelWithProjection.from_pretrained(
|
||||
extras_path,
|
||||
subfolder="text_encoder_2",
|
||||
torch_dtype=torch.bfloat16
|
||||
|
||||
text_encoder_2 = CLIPTextEncoderWithProjection.load_model(
|
||||
extras_path, dtype=torch.bfloat16, subfolder="text_encoder_2"
|
||||
).to(self.device_torch, dtype=dtype)
|
||||
|
||||
tokenizer_2 = CLIPTokenizer.from_pretrained(
|
||||
extras_path,
|
||||
subfolder="tokenizer_2"
|
||||
|
||||
tokenizer_2 = CLIPTextEncoderWithProjection.load_tokenizer(
|
||||
extras_path, subfolder="tokenizer_2", use_fast=False
|
||||
)
|
||||
|
||||
flush()
|
||||
self.print_and_status_update("Loading T5 encoders")
|
||||
|
||||
text_encoder_3 = T5EncoderModel.from_pretrained(
|
||||
extras_path,
|
||||
subfolder="text_encoder_3",
|
||||
torch_dtype=torch.bfloat16
|
||||
).to(self.device_torch, dtype=dtype)
|
||||
# load + quantize + offload + placement, all driven by model_config
|
||||
text_encoder_3 = T5TextEncoder.load(
|
||||
extras_path, subfolder="text_encoder_3", **self.component_load_kwargs("te")
|
||||
)
|
||||
flush()
|
||||
|
||||
if self.model_config.quantize_te:
|
||||
self.print_and_status_update("Quantizing T5")
|
||||
quantization_type = get_qtype(self.model_config.qtype_te)
|
||||
quantize(text_encoder_3, weights=quantization_type)
|
||||
freeze(text_encoder_3)
|
||||
flush()
|
||||
|
||||
tokenizer_3 = T5Tokenizer.from_pretrained(
|
||||
extras_path,
|
||||
subfolder="tokenizer_3"
|
||||
tokenizer_3 = T5TextEncoder.load_tokenizer(
|
||||
extras_path, subfolder="tokenizer_3", use_fast=False
|
||||
)
|
||||
flush()
|
||||
|
||||
@@ -432,21 +369,8 @@ class HidreamModel(BaseModel):
|
||||
def get_transformer_block_names(self) -> Optional[List[str]]:
|
||||
return ['double_stream_blocks', 'single_stream_blocks']
|
||||
|
||||
def convert_lora_weights_before_save(self, state_dict):
|
||||
# currently starte with transformer. but needs to start with diffusion_model. for comfyui
|
||||
new_sd = {}
|
||||
for key, value in state_dict.items():
|
||||
new_key = key.replace("transformer.", "diffusion_model.")
|
||||
new_sd[new_key] = value
|
||||
return new_sd
|
||||
lora_keys_use_comfy_prefix = True
|
||||
|
||||
def convert_lora_weights_before_load(self, state_dict):
|
||||
# saved as diffusion_model. but needs to be transformer. for ai-toolkit
|
||||
new_sd = {}
|
||||
for key, value in state_dict.items():
|
||||
new_key = key.replace("diffusion_model.", "transformer.")
|
||||
new_sd[new_key] = value
|
||||
return new_sd
|
||||
|
||||
def get_base_model_version(self):
|
||||
return "hidream_i1"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import os
|
||||
from toolkit.models.v2._mixin import OstrisTransformersMixin
|
||||
from typing import List, Optional
|
||||
|
||||
import torch
|
||||
@@ -15,8 +16,6 @@ from toolkit.samplers.custom_flowmatch_sampler import (
|
||||
from safetensors.torch import load_file, save_file
|
||||
from toolkit.accelerator import unwrap_model
|
||||
from optimum.quanto import freeze
|
||||
from toolkit.util.quantize import quantize, get_qtype, quantize_model
|
||||
from toolkit.memory_management import MemoryManager
|
||||
|
||||
from transformers import AutoProcessor
|
||||
from transformers.models.qwen3_vl.configuration_qwen3_vl import Qwen3VLConfig
|
||||
@@ -29,6 +28,17 @@ from .src.hidream_o1.model_config import model_config
|
||||
if TYPE_CHECKING:
|
||||
from toolkit.data_transfer_object.data_loader import DataLoaderBatchDTO
|
||||
|
||||
|
||||
class HidreamO1Transformer(Qwen3VLForConditionalGeneration, OstrisTransformersMixin):
|
||||
"""The o1 DiT-in-LLM: the vendored Qwen3VL with the image-diffusion heads
|
||||
(x_embedder / t_embedder1 / final_layer2). The generic Qwen3VLTextEncoder
|
||||
must NOT be used here — it drops those keys as unexpected."""
|
||||
|
||||
@classmethod
|
||||
def get_transformer_block_names(cls):
|
||||
return ["model.language_model.layers"]
|
||||
|
||||
|
||||
scheduler_config = {
|
||||
"num_train_timesteps": 1000,
|
||||
"shift": 3.0,
|
||||
@@ -130,7 +140,10 @@ class HidreamO1Model(BaseModel):
|
||||
self.use_old_lokr_format = False
|
||||
self.is_flow_matching = True
|
||||
self.is_transformer = True
|
||||
self.target_lora_modules = ["Qwen3VLForConditionalGeneration"]
|
||||
self.target_lora_modules = [
|
||||
"Qwen3VLForConditionalGeneration",
|
||||
"HidreamO1Transformer",
|
||||
]
|
||||
self.noise_scale = self.model_config.model_kwargs.get(
|
||||
"noise_scale", DEFAULT_NOISE_SCALE
|
||||
)
|
||||
@@ -189,7 +202,7 @@ class HidreamO1Model(BaseModel):
|
||||
)
|
||||
|
||||
# transformer.load_state_dict(state_dict, assign=True)
|
||||
transformer = Qwen3VLForConditionalGeneration.from_pretrained(
|
||||
transformer = HidreamO1Transformer.from_pretrained(
|
||||
None,
|
||||
config=Qwen3VLConfig(**model_config),
|
||||
state_dict=state_dict,
|
||||
@@ -197,30 +210,13 @@ class HidreamO1Model(BaseModel):
|
||||
)
|
||||
del state_dict # free memory
|
||||
else:
|
||||
transformer = Qwen3VLForConditionalGeneration.from_pretrained(
|
||||
transformer = HidreamO1Transformer.from_pretrained(
|
||||
model_path,
|
||||
torch_dtype=self.torch_dtype,
|
||||
)
|
||||
flush()
|
||||
if not self.model_config.low_vram:
|
||||
transformer.to(self.device_torch)
|
||||
|
||||
if self.model_config.quantize:
|
||||
self.print_and_status_update("Quantizing Transformer")
|
||||
quantize_model(self, transformer)
|
||||
flush()
|
||||
|
||||
if (
|
||||
self.model_config.layer_offloading
|
||||
and self.model_config.layer_offloading_transformer_percent > 0
|
||||
):
|
||||
MemoryManager.attach(
|
||||
transformer,
|
||||
self.device_torch,
|
||||
offload_percent=self.model_config.layer_offloading_transformer_percent,
|
||||
ignore_modules=[],
|
||||
)
|
||||
|
||||
# quantize + offload + placement, all driven by model_config
|
||||
transformer.aitk_post_load(**self.component_load_kwargs("transformer"))
|
||||
flush()
|
||||
|
||||
# move over to device now if low vram
|
||||
|
||||
+9
-1
@@ -8,6 +8,8 @@ from einops import repeat
|
||||
from diffusers.configuration_utils import ConfigMixin, register_to_config
|
||||
from diffusers.loaders import FromOriginalModelMixin, PeftAdapterMixin
|
||||
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.torch_utils import maybe_allow_in_graph
|
||||
from diffusers.models.modeling_outputs import Transformer2DModelOutput
|
||||
@@ -228,9 +230,15 @@ class HiDreamImageBlock(nn.Module):
|
||||
)
|
||||
|
||||
class HiDreamImageTransformer2DModel(
|
||||
ModelMixin, ConfigMixin, PeftAdapterMixin, FromOriginalModelMixin
|
||||
ModelMixin, ConfigMixin, PeftAdapterMixin, FromOriginalModelMixin, OstrisModelMixin
|
||||
):
|
||||
_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"]
|
||||
|
||||
@register_to_config
|
||||
|
||||
@@ -17,16 +17,19 @@ from toolkit.samplers.custom_flowmatch_sampler import (
|
||||
)
|
||||
from toolkit.accelerator import unwrap_model
|
||||
from toolkit.metadata import get_meta_for_safetensors
|
||||
from toolkit.memory_management import MemoryManager
|
||||
from toolkit.util.quantize import quantize, get_qtype, quantize_model
|
||||
from optimum.quanto import freeze, QTensor
|
||||
from optimum.quanto import QTensor
|
||||
|
||||
import huggingface_hub
|
||||
from huggingface_hub.errors import EntryNotFoundError
|
||||
from transformers import AutoModel, AutoTokenizer
|
||||
|
||||
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 toolkit.models.v2.text_encoders.qwen3_vl import Qwen3VLModelEncoder
|
||||
from .src.latent_norm import get_latent_norm
|
||||
from .src.pipeline import (
|
||||
Ideogram4Pipeline,
|
||||
@@ -220,8 +223,8 @@ class Ideogram4Model(BaseModel):
|
||||
self.print_and_status_update(f"Loading Qwen3-VL text encoder from {te_path}")
|
||||
|
||||
tokenizer = AutoTokenizer.from_pretrained(te_path, token=HF_TOKEN)
|
||||
text_encoder = AutoModel.from_pretrained(
|
||||
te_path, torch_dtype=dtype, token=HF_TOKEN
|
||||
text_encoder = Qwen3VLModelEncoder.load_model(
|
||||
te_path, dtype=dtype, subfolder="", token=HF_TOKEN
|
||||
)
|
||||
flush()
|
||||
|
||||
@@ -234,9 +237,6 @@ class Ideogram4Model(BaseModel):
|
||||
self.print_and_status_update("Loading transformer")
|
||||
|
||||
transformer_config = Ideogram4Config()
|
||||
with torch.device("meta"):
|
||||
transformer = Ideogram4Transformer2DModel(transformer_config)
|
||||
|
||||
self.print_and_status_update(" - fetching transformer weights")
|
||||
state_dict = _load_component_state_dict(
|
||||
base, "transformer", "diffusion_pytorch_model"
|
||||
@@ -246,7 +246,9 @@ class Ideogram4Model(BaseModel):
|
||||
state_dict, dtype, self.device_torch, self.model_config.low_vram
|
||||
)
|
||||
self.print_and_status_update(" - loading transformer state dict")
|
||||
transformer.load_state_dict(state_dict, assign=True)
|
||||
transformer = Ideogram4Transformer2DModel.load_from_state_dict(
|
||||
state_dict, dtype, config=transformer_config
|
||||
)
|
||||
del state_dict
|
||||
flush()
|
||||
|
||||
@@ -265,8 +267,7 @@ class Ideogram4Model(BaseModel):
|
||||
self.print_and_status_update("Loading VAE")
|
||||
vae_sd = _load_component_state_dict(base, "vae", "diffusion_pytorch_model")
|
||||
vae_sd = convert_diffusers_state_dict(vae_sd)
|
||||
vae = AutoEncoder(AutoEncoderParams())
|
||||
vae.load_state_dict(vae_sd)
|
||||
vae = AutoEncoder.load_from_state_dict(vae_sd, self.vae_torch_dtype)
|
||||
del vae_sd
|
||||
vae.to(self.vae_device_torch, dtype=dtype)
|
||||
vae.eval()
|
||||
@@ -362,58 +363,13 @@ class Ideogram4Model(BaseModel):
|
||||
|
||||
transformer = self._load_transformer(base)
|
||||
|
||||
if self.model_config.quantize:
|
||||
self.print_and_status_update("Quantizing Transformer")
|
||||
quantize_model(self, transformer)
|
||||
flush()
|
||||
else:
|
||||
transformer.to(self.device_torch, dtype=dtype)
|
||||
flush()
|
||||
|
||||
if (
|
||||
self.model_config.layer_offloading
|
||||
and self.model_config.layer_offloading_transformer_percent > 0
|
||||
):
|
||||
MemoryManager.attach(
|
||||
transformer,
|
||||
self.device_torch,
|
||||
offload_percent=self.model_config.layer_offloading_transformer_percent,
|
||||
ignore_modules=[
|
||||
transformer.rotary_emb.inv_freq,
|
||||
transformer.input_proj,
|
||||
transformer.llm_cond_proj,
|
||||
],
|
||||
)
|
||||
elif self.model_config.low_vram:
|
||||
self.print_and_status_update("Moving transformer to CPU")
|
||||
transformer.to("cpu")
|
||||
else:
|
||||
# quantize_model leaves the model on CPU; make sure it lands on device.
|
||||
transformer.to(self.device_torch)
|
||||
# quantize + offload + placement, all driven by model_config
|
||||
transformer.aitk_post_load(**self.component_load_kwargs("transformer"))
|
||||
flush()
|
||||
|
||||
tokenizer, text_encoder = self._load_text_encoder(base)
|
||||
if self.model_config.quantize_te:
|
||||
self.print_and_status_update("Quantizing Text Encoder")
|
||||
text_encoder.to(self.device_torch)
|
||||
quantize(text_encoder, weights=get_qtype(self.model_config.qtype_te))
|
||||
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,
|
||||
)
|
||||
elif self.model_config.low_vram:
|
||||
self.print_and_status_update("Moving text encoder to CPU")
|
||||
text_encoder.to("cpu")
|
||||
else:
|
||||
self.print_and_status_update("Moving text encoder to device")
|
||||
text_encoder.to(self.device_torch)
|
||||
# quantize + offload + placement, all driven by model_config
|
||||
text_encoder.aitk_post_load(**self.component_load_kwargs("te"))
|
||||
flush()
|
||||
|
||||
vae = self._load_vae(base)
|
||||
@@ -620,16 +576,5 @@ class Ideogram4Model(BaseModel):
|
||||
def get_transformer_block_names(self) -> Optional[List[str]]:
|
||||
return ["layers"]
|
||||
|
||||
def convert_lora_weights_before_save(self, state_dict):
|
||||
new_sd = {}
|
||||
for key, value in state_dict.items():
|
||||
new_key = key.replace("transformer.", "diffusion_model.")
|
||||
new_sd[new_key] = value
|
||||
return new_sd
|
||||
lora_keys_use_comfy_prefix = True
|
||||
|
||||
def convert_lora_weights_before_load(self, state_dict):
|
||||
new_sd = {}
|
||||
for key, value in state_dict.items():
|
||||
new_key = key.replace("diffusion_model.", "transformer.")
|
||||
new_sd[new_key] = value
|
||||
return new_sd
|
||||
@@ -12,6 +12,8 @@ import math
|
||||
from dataclasses import dataclass
|
||||
|
||||
import torch
|
||||
|
||||
from toolkit.models.v2._mixin import OstrisModelMixin
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
from torch.utils.checkpoint import checkpoint
|
||||
@@ -355,9 +357,20 @@ class Ideogram4FinalLayer(nn.Module):
|
||||
return self.linear(self.norm_final(x) * scale)
|
||||
|
||||
|
||||
class Ideogram4Transformer2DModel(nn.Module):
|
||||
class Ideogram4Transformer2DModel(nn.Module, OstrisModelMixin):
|
||||
"""Ideogram 4 flow-matching transformer."""
|
||||
|
||||
@classmethod
|
||||
def get_transformer_block_names(cls):
|
||||
return ["layers"]
|
||||
|
||||
def get_offload_ignore_modules(self):
|
||||
return [
|
||||
self.rotary_emb.inv_freq,
|
||||
self.input_proj,
|
||||
self.llm_cond_proj,
|
||||
]
|
||||
|
||||
def __init__(self, config: Ideogram4Config) -> None:
|
||||
super().__init__()
|
||||
self.config = config
|
||||
|
||||
@@ -25,18 +25,21 @@ from safetensors.torch import load_file, save_file
|
||||
|
||||
import huggingface_hub
|
||||
from huggingface_hub.errors import EntryNotFoundError
|
||||
from diffusers import AutoencoderKLQwenImage
|
||||
from transformers import (
|
||||
AutoProcessor,
|
||||
AutoTokenizer,
|
||||
Qwen2TokenizerFast,
|
||||
Qwen3VLForConditionalGeneration,
|
||||
)
|
||||
from optimum.quanto import freeze
|
||||
|
||||
from toolkit.config_modules import GenerateImageConfig, ModelConfig, NetworkConfig
|
||||
from toolkit.lora_special import LoRASpecialNetwork
|
||||
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.advanced_prompt_embeds import AdvancedPromptEmbeds
|
||||
from toolkit.samplers.custom_flowmatch_sampler import (
|
||||
@@ -44,7 +47,6 @@ from toolkit.samplers.custom_flowmatch_sampler import (
|
||||
)
|
||||
from toolkit.accelerator import unwrap_model
|
||||
from toolkit.metadata import get_meta_for_safetensors
|
||||
from toolkit.util.quantize import quantize, get_qtype, quantize_model
|
||||
from toolkit.memory_management import MemoryManager
|
||||
|
||||
from .src.mmdit import (
|
||||
@@ -100,30 +102,6 @@ QWEN_IMAGE_VAE_PATH = "Qwen/Qwen-Image"
|
||||
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:
|
||||
"""Load the MMDiT weights from a local safetensors file/dir or the HF hub.
|
||||
|
||||
@@ -163,7 +141,7 @@ def _load_mmdit_state_dict(name_or_path: str, filename: Optional[str]) -> dict:
|
||||
return load_file(path)
|
||||
|
||||
|
||||
class Krea2Model(BaseModel):
|
||||
class Krea2Model(QwenImageVAEHolderMixin, BaseModel):
|
||||
arch = "krea2"
|
||||
|
||||
def __init__(
|
||||
@@ -238,21 +216,15 @@ class Krea2Model(BaseModel):
|
||||
mmdit_kwargs.update(self.model_config.model_kwargs.get("mmdit_config", {}))
|
||||
config = SingleMMDiTConfig(**mmdit_kwargs)
|
||||
|
||||
# Build on meta, then materialize straight from the checkpoint.
|
||||
with torch.device("meta"):
|
||||
transformer = SingleStreamDiT(config)
|
||||
|
||||
self.print_and_status_update(" - fetching transformer weights")
|
||||
state_dict = _load_mmdit_state_dict(
|
||||
self.model_config.name_or_path,
|
||||
self.model_config.model_kwargs.get("checkpoint_filename", None),
|
||||
)
|
||||
state_dict = {
|
||||
k: (v.to(dtype) if v.is_floating_point() else v)
|
||||
for k, v in state_dict.items()
|
||||
}
|
||||
self.print_and_status_update(" - loading transformer state dict")
|
||||
transformer.load_state_dict(state_dict, strict=True, assign=True)
|
||||
transformer = SingleStreamDiT.load_from_state_dict(
|
||||
state_dict, dtype, config=config
|
||||
)
|
||||
del state_dict
|
||||
flush()
|
||||
return transformer
|
||||
@@ -268,8 +240,8 @@ class Krea2Model(BaseModel):
|
||||
processor = Qwen2TokenizerFast.from_pretrained(
|
||||
te_path, max_length=self.max_text_length, token=HF_TOKEN
|
||||
)
|
||||
text_encoder = Qwen3VLForConditionalGeneration.from_pretrained(
|
||||
te_path, torch_dtype=dtype, token=HF_TOKEN
|
||||
text_encoder = Qwen3VLTextEncoder.load_model(
|
||||
te_path, dtype=dtype, subfolder="", token=HF_TOKEN
|
||||
)
|
||||
vl_processor = None
|
||||
if self.is_edit:
|
||||
@@ -281,8 +253,7 @@ class Krea2Model(BaseModel):
|
||||
else:
|
||||
# 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.
|
||||
if getattr(text_encoder.model, "visual", None) is not None:
|
||||
text_encoder.model.visual = None
|
||||
text_encoder.drop_vision_tower()
|
||||
text_encoder.eval()
|
||||
text_encoder.requires_grad_(False)
|
||||
flush()
|
||||
@@ -291,8 +262,8 @@ class Krea2Model(BaseModel):
|
||||
def _load_vae(self):
|
||||
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}")
|
||||
vae = AutoencoderKLQwenImage.from_pretrained(
|
||||
vae_path, subfolder="vae", torch_dtype=self.vae_torch_dtype, token=HF_TOKEN
|
||||
vae = QwenImageVAE.load_model(
|
||||
vae_path, dtype=self.vae_torch_dtype, token=HF_TOKEN
|
||||
)
|
||||
vae.eval()
|
||||
vae.requires_grad_(False)
|
||||
@@ -411,55 +382,12 @@ class Krea2Model(BaseModel):
|
||||
if self.model_config.qtype == "qfloat8":
|
||||
self.model_config.qtype = "float8"
|
||||
|
||||
if self.model_config.quantize:
|
||||
self.print_and_status_update("Quantizing transformer")
|
||||
quantize_model(self, transformer)
|
||||
flush()
|
||||
|
||||
if (
|
||||
self.model_config.layer_offloading
|
||||
and self.model_config.layer_offloading_transformer_percent > 0
|
||||
):
|
||||
MemoryManager.attach(
|
||||
transformer,
|
||||
self.device_torch,
|
||||
offload_percent=self.model_config.layer_offloading_transformer_percent,
|
||||
ignore_modules=[
|
||||
module
|
||||
for module in transformer.modules()
|
||||
if isinstance(module, (SimpleModulation, DoubleSharedModulation))
|
||||
],
|
||||
)
|
||||
|
||||
if self.model_config.low_vram:
|
||||
self.print_and_status_update("Moving transformer to CPU")
|
||||
transformer.to("cpu")
|
||||
else:
|
||||
transformer.to(self.device_torch, dtype=dtype)
|
||||
# quantize + offload + placement, all driven by model_config
|
||||
transformer.aitk_post_load(**self.component_load_kwargs("transformer"))
|
||||
flush()
|
||||
|
||||
tokenizer, processor, vl_processor, text_encoder = self._load_text_encoder()
|
||||
if self.model_config.quantize_te:
|
||||
self.print_and_status_update("Quantizing text encoder")
|
||||
text_encoder.to(self.device_torch)
|
||||
quantize(text_encoder, weights=get_qtype(self.model_config.qtype_te))
|
||||
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:
|
||||
self.print_and_status_update("Moving text encoder to CPU")
|
||||
text_encoder.to("cpu")
|
||||
else:
|
||||
text_encoder.to(self.device_torch)
|
||||
text_encoder.aitk_post_load(**self.component_load_kwargs("te"))
|
||||
flush()
|
||||
|
||||
vae = self._load_vae()
|
||||
@@ -772,76 +700,9 @@ class Krea2Model(BaseModel):
|
||||
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):
|
||||
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)
|
||||
|
||||
vae_decode_tiled_on_low_vram = True
|
||||
# ------------------------------------------------------------------
|
||||
# Saving / bookkeeping
|
||||
# ------------------------------------------------------------------
|
||||
@@ -867,14 +728,5 @@ class Krea2Model(BaseModel):
|
||||
def get_transformer_block_names(self) -> Optional[List[str]]:
|
||||
return ["blocks"]
|
||||
|
||||
def convert_lora_weights_before_save(self, state_dict):
|
||||
return {
|
||||
k.replace("transformer.", "diffusion_model."): v
|
||||
for k, v in state_dict.items()
|
||||
}
|
||||
lora_keys_use_comfy_prefix = True
|
||||
|
||||
def convert_lora_weights_before_load(self, state_dict):
|
||||
return {
|
||||
k.replace("diffusion_model.", "transformer."): v
|
||||
for k, v in state_dict.items()
|
||||
}
|
||||
@@ -20,6 +20,8 @@ import math
|
||||
from dataclasses import dataclass
|
||||
|
||||
import torch
|
||||
|
||||
from toolkit.models.v2._mixin import OstrisModelMixin
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
from einops import rearrange
|
||||
@@ -408,7 +410,15 @@ class SingleStreamBlock(nn.Module):
|
||||
return x
|
||||
|
||||
|
||||
class SingleStreamDiT(nn.Module):
|
||||
class SingleStreamDiT(nn.Module, OstrisModelMixin):
|
||||
def get_offload_ignore_modules(self):
|
||||
# modulation modules hold tiny live state the offloader must not page
|
||||
return [
|
||||
module
|
||||
for module in self.modules()
|
||||
if isinstance(module, (SimpleModulation, DoubleSharedModulation))
|
||||
]
|
||||
|
||||
def __init__(self, config: SingleMMDiTConfig):
|
||||
super().__init__()
|
||||
self.config = config
|
||||
|
||||
@@ -7,12 +7,16 @@ from typing import Any, Dict, Tuple
|
||||
import torch
|
||||
from accelerate import init_empty_weights
|
||||
|
||||
from diffusers import (
|
||||
AutoencoderKLLTX2Audio,
|
||||
AutoencoderKLLTX2Video,
|
||||
from toolkit.models.v2.diffusion_models.ltx2 import (
|
||||
LTX2TextConnectors,
|
||||
LTX2VideoTransformer3DModel,
|
||||
LTX2Vocoder,
|
||||
LTX2VocoderWithBWE,
|
||||
)
|
||||
from toolkit.models.v2.vae.ltx2 import (
|
||||
LTX2AudioVAE as AutoencoderKLLTX2Audio,
|
||||
LTX2VideoVAE as AutoencoderKLLTX2Video,
|
||||
)
|
||||
from diffusers.pipelines.ltx2 import LTX2TextConnectors, LTX2Vocoder, LTX2VocoderWithBWE
|
||||
from diffusers.utils.import_utils import is_accelerate_available
|
||||
|
||||
|
||||
|
||||
@@ -17,28 +17,30 @@ from toolkit.samplers.custom_flowmatch_sampler import (
|
||||
)
|
||||
from accelerate import init_empty_weights
|
||||
from toolkit.accelerator import unwrap_model
|
||||
from optimum.quanto import freeze
|
||||
from toolkit.util.quantize import quantize, get_qtype, quantize_model
|
||||
from toolkit.memory_management import MemoryManager
|
||||
from toolkit.paths import MODELS_PATH
|
||||
from toolkit.util.mixed_precision import attach_per_op_casting, pin_stored_fp32
|
||||
from safetensors.torch import load_file
|
||||
from PIL import Image
|
||||
import huggingface_hub
|
||||
|
||||
try:
|
||||
from diffusers import LTX2Pipeline, LTX2ImageToVideoPipeline
|
||||
from diffusers.models.autoencoders import (
|
||||
AutoencoderKLLTX2Audio,
|
||||
AutoencoderKLLTX2Video,
|
||||
)
|
||||
from diffusers.models.transformers import LTX2VideoTransformer3DModel
|
||||
from diffusers.pipelines.ltx2.export_utils import encode_video
|
||||
from transformers import (
|
||||
Gemma3ForConditionalGeneration,
|
||||
GemmaTokenizerFast,
|
||||
)
|
||||
from diffusers.pipelines.ltx2.vocoder import LTX2Vocoder, LTX2VocoderWithBWE
|
||||
from diffusers.pipelines.ltx2.connectors import LTX2TextConnectors
|
||||
from toolkit.models.v2.diffusion_models.ltx2 import (
|
||||
LTX2TextConnectors,
|
||||
LTX2VideoTransformer3DModel,
|
||||
LTX2Vocoder,
|
||||
LTX2VocoderWithBWE,
|
||||
)
|
||||
from toolkit.models.v2.vae.ltx2 import (
|
||||
LTX2AudioVAE as AutoencoderKLLTX2Audio,
|
||||
LTX2VideoVAE as AutoencoderKLLTX2Video,
|
||||
)
|
||||
from toolkit.models.v2.text_encoders.gemma3 import Gemma3TextEncoder
|
||||
from .convert_ltx2_to_diffusers import (
|
||||
get_model_state_dict_from_combined_ckpt,
|
||||
convert_ltx2_transformer,
|
||||
@@ -293,48 +295,17 @@ class LTX2Model(BaseModel):
|
||||
)
|
||||
transformer = transformer.to(dtype)
|
||||
else:
|
||||
transformer_path = model_path
|
||||
transformer_subfolder = "transformer"
|
||||
if os.path.exists(transformer_path):
|
||||
transformer_subfolder = None
|
||||
transformer_path = os.path.join(transformer_path, "transformer")
|
||||
if os.path.exists(model_path):
|
||||
# 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 = LTX2VideoTransformer3DModel.from_pretrained(
|
||||
transformer_path, subfolder=transformer_subfolder, torch_dtype=dtype
|
||||
)
|
||||
transformer = LTX2VideoTransformer3DModel.load_model(model_path, dtype=dtype)
|
||||
|
||||
if self.model_config.quantize:
|
||||
self.print_and_status_update("Quantizing Transformer")
|
||||
quantize_model(self, transformer)
|
||||
flush()
|
||||
|
||||
if (
|
||||
self.model_config.layer_offloading
|
||||
and self.model_config.layer_offloading_transformer_percent > 0
|
||||
):
|
||||
ignore_modules = []
|
||||
for block in transformer.transformer_blocks:
|
||||
ignore_modules.append(block.scale_shift_table)
|
||||
ignore_modules.append(block.audio_scale_shift_table)
|
||||
ignore_modules.append(block.video_a2v_cross_attn_scale_shift_table)
|
||||
ignore_modules.append(block.audio_a2v_cross_attn_scale_shift_table)
|
||||
ignore_modules.append(transformer.scale_shift_table)
|
||||
ignore_modules.append(transformer.audio_scale_shift_table)
|
||||
MemoryManager.attach(
|
||||
transformer,
|
||||
self.device_torch,
|
||||
offload_percent=self.model_config.layer_offloading_transformer_percent,
|
||||
ignore_modules=ignore_modules,
|
||||
)
|
||||
|
||||
if self.model_config.low_vram:
|
||||
self.print_and_status_update("Moving transformer to CPU")
|
||||
transformer.to("cpu")
|
||||
# quantize + offload + placement, all driven by model_config
|
||||
transformer.aitk_post_load(**self.component_load_kwargs("transformer"))
|
||||
|
||||
flush()
|
||||
|
||||
@@ -347,7 +318,7 @@ class LTX2Model(BaseModel):
|
||||
tokenizer = GemmaTokenizerFast.from_pretrained(base_te_path)
|
||||
|
||||
with init_empty_weights():
|
||||
text_encoder = Gemma3ForConditionalGeneration(
|
||||
text_encoder = Gemma3TextEncoder(
|
||||
Gemma3Config(
|
||||
**{
|
||||
"boi_token_index": 255999,
|
||||
@@ -420,47 +391,30 @@ class LTX2Model(BaseModel):
|
||||
tokenizer = GemmaTokenizerFast.from_pretrained(
|
||||
self.model_config.te_name_or_path
|
||||
)
|
||||
text_encoder = Gemma3ForConditionalGeneration.from_pretrained(
|
||||
self.model_config.te_name_or_path, dtype=dtype
|
||||
text_encoder = Gemma3TextEncoder.load_model(
|
||||
self.model_config.te_name_or_path, dtype=dtype, subfolder=""
|
||||
)
|
||||
elif self.ltx_te_path is not None:
|
||||
# pull from model specific te
|
||||
tokenizer = GemmaTokenizerFast.from_pretrained(self.ltx_te_path)
|
||||
text_encoder = Gemma3ForConditionalGeneration.from_pretrained(
|
||||
self.ltx_te_path, dtype=dtype
|
||||
text_encoder = Gemma3TextEncoder.load_model(
|
||||
self.ltx_te_path, dtype=dtype, subfolder=""
|
||||
)
|
||||
else:
|
||||
# using combo hf repo
|
||||
tokenizer = GemmaTokenizerFast.from_pretrained(
|
||||
self.model_config.name_or_path, subfolder="tokenizer"
|
||||
)
|
||||
text_encoder = Gemma3ForConditionalGeneration.from_pretrained(
|
||||
self.model_config.name_or_path, subfolder="text_encoder", dtype=dtype
|
||||
text_encoder = Gemma3TextEncoder.load_model(
|
||||
self.model_config.name_or_path, dtype=dtype
|
||||
)
|
||||
|
||||
# remove the vision tower
|
||||
text_encoder.model.vision_tower = None
|
||||
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()
|
||||
|
||||
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,
|
||||
ignore_modules=[
|
||||
text_encoder.model.language_model.base_model.embed_tokens
|
||||
],
|
||||
)
|
||||
|
||||
# quantize + offload + placement, all driven by model_config
|
||||
text_encoder.aitk_post_load(**self.component_load_kwargs("te"))
|
||||
text_encoder.to(self.device_torch, dtype=dtype)
|
||||
flush()
|
||||
|
||||
@@ -497,24 +451,16 @@ class LTX2Model(BaseModel):
|
||||
del combined_state_dict
|
||||
flush()
|
||||
else:
|
||||
vae = AutoencoderKLLTX2Video.from_pretrained(
|
||||
base_model_path, subfolder="vae", torch_dtype=dtype
|
||||
)
|
||||
audio_vae = AutoencoderKLLTX2Audio.from_pretrained(
|
||||
base_model_path, subfolder="audio_vae", torch_dtype=dtype
|
||||
)
|
||||
vae = AutoencoderKLLTX2Video.load_model(base_model_path, dtype=dtype)
|
||||
audio_vae = AutoencoderKLLTX2Audio.load_model(base_model_path, dtype=dtype)
|
||||
|
||||
connectors = LTX2TextConnectors.from_pretrained(
|
||||
base_model_path, subfolder="connectors", torch_dtype=dtype
|
||||
)
|
||||
connectors = LTX2TextConnectors.load_model(base_model_path, dtype=dtype)
|
||||
|
||||
vocoder_cls = LTX2Vocoder
|
||||
if self.ltx_version in ("2.3", "2.5"):
|
||||
vocoder_cls = LTX2VocoderWithBWE
|
||||
|
||||
vocoder = vocoder_cls.from_pretrained(
|
||||
base_model_path, subfolder="vocoder", torch_dtype=dtype
|
||||
)
|
||||
vocoder = vocoder_cls.load_model(base_model_path, dtype=dtype)
|
||||
|
||||
self.noise_scheduler = LTX2Model.get_train_scheduler()
|
||||
|
||||
@@ -544,8 +490,10 @@ class LTX2Model(BaseModel):
|
||||
pipe.transformer = pipe.transformer.to(self.device_torch)
|
||||
|
||||
flush()
|
||||
# just to make sure everything is on the right device and dtype
|
||||
text_encoder[0].to(self.device_torch)
|
||||
# low_vram: the text encoder stays on cpu; get_prompt_embeds moves it
|
||||
# to the gpu on demand
|
||||
if not self.low_vram:
|
||||
text_encoder[0].to(self.device_torch)
|
||||
text_encoder[0].requires_grad_(False)
|
||||
text_encoder[0].eval()
|
||||
flush()
|
||||
@@ -1191,23 +1139,17 @@ class LTX2Model(BaseModel):
|
||||
def get_transformer_block_names(self) -> Optional[List[str]]:
|
||||
return ["transformer_blocks"]
|
||||
|
||||
lora_keys_use_comfy_prefix = True
|
||||
|
||||
def convert_lora_weights_before_save(self, state_dict):
|
||||
new_sd = {}
|
||||
for key, value in state_dict.items():
|
||||
new_key = key.replace("transformer.", "diffusion_model.")
|
||||
new_sd[new_key] = value
|
||||
new_sd = convert_lora_diffusers_to_original(new_sd, version=self.ltx_version)
|
||||
return new_sd
|
||||
state_dict = super().convert_lora_weights_before_save(state_dict)
|
||||
return convert_lora_diffusers_to_original(state_dict, version=self.ltx_version)
|
||||
|
||||
def convert_lora_weights_before_load(self, state_dict):
|
||||
state_dict = convert_lora_original_to_diffusers(
|
||||
state_dict, version=self.ltx_version
|
||||
)
|
||||
new_sd = {}
|
||||
for key, value in state_dict.items():
|
||||
new_key = key.replace("diffusion_model.", "transformer.")
|
||||
new_sd[new_key] = value
|
||||
return new_sd
|
||||
return super().convert_lora_weights_before_load(state_dict)
|
||||
|
||||
|
||||
class LTX23Model(LTX2Model):
|
||||
@@ -1239,88 +1181,30 @@ class LTX25Model(LTX2Model):
|
||||
ltx_te_path = None
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# ComfyUI-style file resolution (mirrors MinimaxH3Model)
|
||||
# ComfyUI-style file resolution (toolkit/models/v2/resolver.py)
|
||||
# ------------------------------------------------------------------
|
||||
@staticmethod
|
||||
def _find_file_recursive(root_dir: str, filename: str) -> Optional[str]:
|
||||
if not os.path.isdir(root_dir):
|
||||
return None
|
||||
for dirpath, dirnames, filenames in os.walk(root_dir):
|
||||
dirnames.sort()
|
||||
if filename in filenames:
|
||||
return os.path.join(dirpath, filename)
|
||||
return None
|
||||
|
||||
def _resolve_comfy_file(self, component: str) -> str:
|
||||
"""Find a weight file at its local location, or download it there
|
||||
when (and only when) it is missing.
|
||||
|
||||
Search order: model_kwargs override, the repo-relative path under
|
||||
MODELS_PATH (diffusion_models/, text_encoders/, vae/), the bare
|
||||
filename at the root, any subfolder of the component's category
|
||||
folder (recursive), then the hub — downloaded to the repo-relative
|
||||
path under MODELS_PATH.
|
||||
"""
|
||||
override = self.model_config.model_kwargs.get(f"{component}_path", None)
|
||||
if override is not None:
|
||||
if not os.path.exists(override):
|
||||
raise FileNotFoundError(
|
||||
f"model_kwargs.{component}_path does not exist: {override}"
|
||||
)
|
||||
return override
|
||||
|
||||
rel_path = COMFY_LTX25_FILES[component]
|
||||
filename = os.path.basename(rel_path)
|
||||
category = os.path.dirname(rel_path)
|
||||
for rel in (rel_path, filename):
|
||||
candidate = os.path.join(MODELS_PATH, rel)
|
||||
if os.path.exists(candidate):
|
||||
return candidate
|
||||
found = self._find_file_recursive(os.path.join(MODELS_PATH, category), filename)
|
||||
if found is not None:
|
||||
return found
|
||||
|
||||
repo_id = COMFY_LTX25_REPO
|
||||
name_or_path = self.model_config.name_or_path
|
||||
if (
|
||||
name_or_path
|
||||
and not os.path.exists(name_or_path)
|
||||
and not name_or_path.endswith(".safetensors")
|
||||
and "/" in name_or_path
|
||||
):
|
||||
repo_id = name_or_path
|
||||
self.print_and_status_update(
|
||||
f"Downloading {rel_path} from {repo_id} into {MODELS_PATH}"
|
||||
from toolkit.models.v2.resolver import (
|
||||
repo_id_from_name_or_path,
|
||||
resolve_comfy_file,
|
||||
)
|
||||
return huggingface_hub.hf_hub_download(
|
||||
repo_id=repo_id, filename=rel_path, token=HF_TOKEN, local_dir=MODELS_PATH
|
||||
|
||||
return resolve_comfy_file(
|
||||
COMFY_LTX25_FILES[component],
|
||||
repo_id=repo_id_from_name_or_path(
|
||||
self.model_config.name_or_path, COMFY_LTX25_REPO
|
||||
),
|
||||
override_path=self.model_config.model_kwargs.get(
|
||||
f"{component}_path", None
|
||||
),
|
||||
hf_token=HF_TOKEN,
|
||||
status_fn=self.print_and_status_update,
|
||||
)
|
||||
|
||||
def _resolve_named_file(self, path: str, component: str) -> str:
|
||||
"""Resolve an explicit .safetensors path: local file, models-folder
|
||||
file, or an 'org/repo/path/file.safetensors' hub path (downloaded
|
||||
into the models folder)."""
|
||||
if os.path.exists(path):
|
||||
return path
|
||||
splits = path.split("/")
|
||||
if len(splits) < 3:
|
||||
raise ValueError(
|
||||
f"Invalid {component} path: {path}. Must be a local file or "
|
||||
"'repo_id/repo/filename.safetensors' to download from the Hugging Face Hub."
|
||||
)
|
||||
rel_path = "/".join(splits[2:])
|
||||
for candidate in (
|
||||
os.path.join(MODELS_PATH, rel_path),
|
||||
os.path.join(MODELS_PATH, splits[-1]),
|
||||
):
|
||||
if os.path.exists(candidate):
|
||||
return candidate
|
||||
return huggingface_hub.hf_hub_download(
|
||||
repo_id="/".join(splits[:2]),
|
||||
filename=rel_path,
|
||||
token=HF_TOKEN,
|
||||
local_dir=MODELS_PATH,
|
||||
)
|
||||
from toolkit.models.v2.resolver import resolve_named_file
|
||||
|
||||
return resolve_named_file(path, component=component, hf_token=HF_TOKEN)
|
||||
|
||||
def _resolve_dit_path(self) -> str:
|
||||
name_or_path = self.model_config.name_or_path
|
||||
@@ -1343,7 +1227,7 @@ class LTX25Model(LTX2Model):
|
||||
from the state dict. Works unchanged for bf16 checkpoints, where no
|
||||
quant markers exist and everything strict-loads."""
|
||||
from toolkit.util.comfy_quant_import import import_comfy_quantized_layers
|
||||
from toolkit.util.ostris_quant import OstrisLinear
|
||||
from toolkit.models.v2._mixin import OstrisModelMixin
|
||||
|
||||
state_dict, num_quantized = import_comfy_quantized_layers(
|
||||
module, state_dict, orig_dtype=self.torch_dtype
|
||||
@@ -1352,32 +1236,8 @@ class LTX25Model(LTX2Model):
|
||||
self.print_and_status_update(
|
||||
f" - attached {num_quantized} pre-quantized ConvRot layers to {name}"
|
||||
)
|
||||
result = module.load_state_dict(state_dict, assign=True, strict=False)
|
||||
# quantized linears hold their weight as backend buffers and had their
|
||||
# bias assigned by the importer, so both report as "missing" here
|
||||
quantized_param_keys = set()
|
||||
for mod_name, m in module.named_modules():
|
||||
if isinstance(m, OstrisLinear):
|
||||
quantized_param_keys.add(f"{mod_name}.weight")
|
||||
if m.bias is not None:
|
||||
quantized_param_keys.add(f"{mod_name}.bias")
|
||||
bad_missing = [k for k in result.missing_keys if k not in quantized_param_keys]
|
||||
if bad_missing or result.unexpected_keys:
|
||||
raise ValueError(
|
||||
f"LTX-2.5 {name} load mismatch: missing {bad_missing[:8]}, "
|
||||
f"unexpected {result.unexpected_keys[:8]}"
|
||||
)
|
||||
# nothing may be left on the meta device (e.g. a bias the importer
|
||||
# should have filled)
|
||||
leftover_meta = [
|
||||
param_name
|
||||
for param_name, p in module.named_parameters()
|
||||
if p.is_meta
|
||||
]
|
||||
if leftover_meta:
|
||||
raise ValueError(
|
||||
f"LTX-2.5 {name} load left meta parameters: {leftover_meta[:8]}"
|
||||
)
|
||||
# whitelist for quantized weights + leftover-meta check
|
||||
OstrisModelMixin._load_state_dict_with_quantized(module, state_dict)
|
||||
return num_quantized
|
||||
|
||||
def _load_gemma4_text_encoder(self, te_path: str, te_state_dict: dict):
|
||||
@@ -1387,7 +1247,7 @@ class LTX25Model(LTX2Model):
|
||||
dropped, matching how the Gemma-3 vision tower was discarded."""
|
||||
from safetensors import safe_open
|
||||
from transformers import Gemma4TextConfig
|
||||
from transformers.models.gemma4.modeling_gemma4 import Gemma4TextModel
|
||||
from toolkit.models.v2.text_encoders.gemma3 import Gemma4TextEncoder
|
||||
|
||||
with safe_open(te_path, framework="pt") as f:
|
||||
metadata = f.metadata() or {}
|
||||
@@ -1397,7 +1257,7 @@ class LTX25Model(LTX2Model):
|
||||
}
|
||||
|
||||
with init_empty_weights():
|
||||
text_encoder = Gemma4TextModel(Gemma4TextConfig(**text_config))
|
||||
text_encoder = Gemma4TextEncoder(Gemma4TextConfig(**text_config))
|
||||
|
||||
def strip_model_prefix(key: str) -> str:
|
||||
return key[len("model.") :] if key.startswith("model.") else key
|
||||
@@ -1464,40 +1324,19 @@ class LTX25Model(LTX2Model):
|
||||
del transformer_sd
|
||||
if num_quantized_dit == 0:
|
||||
transformer = transformer.to(dtype)
|
||||
else:
|
||||
# mixed-precision comfy file (fp32 scale_shift tables next to bf16
|
||||
# weights): comfy-style per-op input casting, with the stored-fp32
|
||||
# pieces pinned so parent .to(dtype) casts cannot downcast them
|
||||
attach_per_op_casting(transformer)
|
||||
pin_stored_fp32(transformer)
|
||||
flush()
|
||||
|
||||
if self.model_config.quantize:
|
||||
if num_quantized_dit:
|
||||
self.print_and_status_update(
|
||||
"Transformer is pre-quantized (ConvRot); skipping quantize"
|
||||
)
|
||||
else:
|
||||
self.print_and_status_update("Quantizing Transformer")
|
||||
quantize_model(self, transformer)
|
||||
flush()
|
||||
|
||||
if (
|
||||
self.model_config.layer_offloading
|
||||
and self.model_config.layer_offloading_transformer_percent > 0
|
||||
):
|
||||
ignore_modules = []
|
||||
for block in transformer.transformer_blocks:
|
||||
ignore_modules.append(block.scale_shift_table)
|
||||
ignore_modules.append(block.audio_scale_shift_table)
|
||||
ignore_modules.append(block.video_a2v_cross_attn_scale_shift_table)
|
||||
ignore_modules.append(block.audio_a2v_cross_attn_scale_shift_table)
|
||||
ignore_modules.append(transformer.scale_shift_table)
|
||||
ignore_modules.append(transformer.audio_scale_shift_table)
|
||||
MemoryManager.attach(
|
||||
transformer,
|
||||
self.device_torch,
|
||||
offload_percent=self.model_config.layer_offloading_transformer_percent,
|
||||
ignore_modules=ignore_modules,
|
||||
)
|
||||
|
||||
if self.model_config.low_vram:
|
||||
self.print_and_status_update("Moving transformer to CPU")
|
||||
transformer.to("cpu")
|
||||
if num_quantized_dit:
|
||||
# pre-quantized (ConvRot) comfy file; aitk_post_load skips quantize
|
||||
transformer.aitk_is_quantized = True
|
||||
# quantize + offload + placement, all driven by model_config
|
||||
transformer.aitk_post_load(**self.component_load_kwargs("transformer"))
|
||||
flush()
|
||||
|
||||
self.print_and_status_update("Loading connectors")
|
||||
@@ -1510,6 +1349,9 @@ class LTX25Model(LTX2Model):
|
||||
del connectors_sd, dit_sd
|
||||
if num_quantized_connectors == 0:
|
||||
connectors = connectors.to(dtype)
|
||||
else:
|
||||
attach_per_op_casting(connectors)
|
||||
pin_stored_fp32(connectors)
|
||||
flush()
|
||||
|
||||
# ---- text encoder (Gemma-4 12B, single comfy file) ----
|
||||
@@ -1521,32 +1363,11 @@ class LTX25Model(LTX2Model):
|
||||
del te_state_dict
|
||||
flush()
|
||||
|
||||
if self.model_config.quantize_te:
|
||||
if num_quantized_te:
|
||||
self.print_and_status_update(
|
||||
"Text encoder is pre-quantized (ConvRot); skipping quantize"
|
||||
)
|
||||
else:
|
||||
self.print_and_status_update("Quantizing Text Encoder")
|
||||
quantize(text_encoder, weights=get_qtype(self.model_config.qtype_te))
|
||||
freeze(text_encoder)
|
||||
flush()
|
||||
|
||||
if (
|
||||
self.model_config.layer_offloading
|
||||
and self.model_config.layer_offloading_text_encoder_percent > 0
|
||||
):
|
||||
# layer_scalar is a bare tensor buffer on each decoder layer; the
|
||||
# manager never enumerates it, so it must ride along explicitly
|
||||
ignore_modules = [text_encoder.embed_tokens]
|
||||
for layer in text_encoder.layers:
|
||||
ignore_modules.append(layer.layer_scalar)
|
||||
MemoryManager.attach(
|
||||
text_encoder,
|
||||
self.device_torch,
|
||||
offload_percent=self.model_config.layer_offloading_text_encoder_percent,
|
||||
ignore_modules=ignore_modules,
|
||||
)
|
||||
if num_quantized_te:
|
||||
# pre-quantized (ConvRot) comfy file; aitk_post_load skips quantize
|
||||
text_encoder.aitk_is_quantized = True
|
||||
# quantize + offload + placement, all driven by model_config
|
||||
text_encoder.aitk_post_load(**self.component_load_kwargs("te"))
|
||||
|
||||
text_encoder.to(self.device_torch)
|
||||
text_encoder.requires_grad_(False)
|
||||
|
||||
@@ -34,8 +34,8 @@ from torchvision.transforms.functional import to_tensor
|
||||
from safetensors.torch import load_file, save_file
|
||||
|
||||
import huggingface_hub
|
||||
from transformers import AutoProcessor, AutoTokenizer, Qwen3VLForConditionalGeneration
|
||||
from optimum.quanto import freeze
|
||||
from transformers import AutoProcessor, AutoTokenizer
|
||||
from toolkit.models.v2.text_encoders.qwen3_vl import Qwen3VLTextEncoder
|
||||
|
||||
from toolkit.config_modules import GenerateImageConfig, ModelConfig
|
||||
from toolkit.models.base_model import BaseModel
|
||||
@@ -46,8 +46,6 @@ from toolkit.samplers.custom_flowmatch_sampler import (
|
||||
)
|
||||
from toolkit.accelerator import unwrap_model
|
||||
from toolkit.metadata import get_meta_for_safetensors
|
||||
from toolkit.util.quantize import quantize, get_qtype, quantize_model
|
||||
from toolkit.memory_management import MemoryManager
|
||||
|
||||
from .src.transformer import MageFlow, MageFlowParams
|
||||
from .src.vae import MageVAE
|
||||
@@ -177,22 +175,13 @@ class MageFlowModel(BaseModel):
|
||||
structure.update(self.model_config.model_kwargs.get("transformer_config", {}))
|
||||
params = MageFlowParams(**structure)
|
||||
|
||||
# Build on meta, then materialize straight from the checkpoint.
|
||||
with torch.device("meta"):
|
||||
transformer = MageFlow(params)
|
||||
|
||||
self.print_and_status_update(" - fetching transformer weights")
|
||||
state_dict = load_file(
|
||||
self._get_model_file("transformer/diffusion_pytorch_model.safetensors")
|
||||
)
|
||||
state_dict = {
|
||||
k: (v.to(dtype) if v.is_floating_point() else v)
|
||||
for k, v in state_dict.items()
|
||||
}
|
||||
self.print_and_status_update(" - loading transformer state dict")
|
||||
transformer.load_state_dict(state_dict, strict=True, assign=True)
|
||||
# The RoPE tables are plain tensors (not buffers/params), so the meta
|
||||
# init left them unmaterialized — rebuild them for real.
|
||||
transformer = MageFlow.load_from_state_dict(state_dict, dtype, config=params)
|
||||
# rebuild the plain-tensor RoPE tables at full precision
|
||||
transformer.reset_rope()
|
||||
del state_dict
|
||||
flush()
|
||||
@@ -209,8 +198,11 @@ class MageFlowModel(BaseModel):
|
||||
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)
|
||||
text_encoder = Qwen3VLForConditionalGeneration.from_pretrained(
|
||||
te_path, torch_dtype=dtype, token=HF_TOKEN, **te_kwargs
|
||||
text_encoder = Qwen3VLTextEncoder.load_model(
|
||||
te_path,
|
||||
dtype=dtype,
|
||||
subfolder=te_kwargs.get("subfolder", ""),
|
||||
token=HF_TOKEN,
|
||||
)
|
||||
vl_processor = None
|
||||
if self.is_edit:
|
||||
@@ -224,8 +216,7 @@ class MageFlowModel(BaseModel):
|
||||
else:
|
||||
# We only ever encode text, so the vision tower is dead weight --
|
||||
# drop it to free VRAM.
|
||||
if getattr(text_encoder.model, "visual", None) is not None:
|
||||
text_encoder.model.visual = None
|
||||
text_encoder.drop_vision_tower()
|
||||
text_encoder.eval()
|
||||
text_encoder.requires_grad_(False)
|
||||
flush()
|
||||
@@ -236,8 +227,8 @@ class MageFlowModel(BaseModel):
|
||||
vae_path = self.model_config.model_kwargs.get("vae_path", None)
|
||||
if vae_path is None:
|
||||
vae_path = self._get_model_file("vae/diffusion_pytorch_model.safetensors")
|
||||
vae = MageVAE(
|
||||
ckpt_path=vae_path,
|
||||
vae = MageVAE.load_model(
|
||||
vae_path,
|
||||
sample_posterior=bool(
|
||||
self.model_config.model_kwargs.get("vae_sample_posterior", True)
|
||||
),
|
||||
@@ -268,50 +259,13 @@ class MageFlowModel(BaseModel):
|
||||
|
||||
transformer = self._load_transformer()
|
||||
|
||||
if self.model_config.quantize:
|
||||
self.print_and_status_update("Quantizing transformer")
|
||||
quantize_model(self, transformer)
|
||||
flush()
|
||||
|
||||
if (
|
||||
self.model_config.layer_offloading
|
||||
and self.model_config.layer_offloading_transformer_percent > 0
|
||||
):
|
||||
MemoryManager.attach(
|
||||
transformer,
|
||||
self.device_torch,
|
||||
offload_percent=self.model_config.layer_offloading_transformer_percent,
|
||||
)
|
||||
|
||||
if self.model_config.low_vram:
|
||||
self.print_and_status_update("Moving transformer to CPU")
|
||||
transformer.to("cpu")
|
||||
else:
|
||||
transformer.to(self.device_torch, dtype=dtype)
|
||||
# quantize + offload + placement, all driven by model_config
|
||||
transformer.aitk_post_load(**self.component_load_kwargs("transformer"))
|
||||
flush()
|
||||
|
||||
tokenizer, vl_processor, text_encoder = self._load_text_encoder()
|
||||
if self.model_config.quantize_te:
|
||||
self.print_and_status_update("Quantizing text encoder")
|
||||
text_encoder.to(self.device_torch)
|
||||
quantize(text_encoder, weights=get_qtype(self.model_config.qtype_te))
|
||||
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:
|
||||
self.print_and_status_update("Moving text encoder to CPU")
|
||||
text_encoder.to("cpu")
|
||||
else:
|
||||
text_encoder.to(self.device_torch)
|
||||
# quantize + offload + placement, all driven by model_config
|
||||
text_encoder.aitk_post_load(**self.component_load_kwargs("te"))
|
||||
flush()
|
||||
|
||||
vae = self._load_vae()
|
||||
@@ -629,18 +583,7 @@ class MageFlowModel(BaseModel):
|
||||
def get_transformer_block_names(self) -> Optional[List[str]]:
|
||||
return ["transformer_blocks"]
|
||||
|
||||
def convert_lora_weights_before_save(self, state_dict):
|
||||
return {
|
||||
k.replace("transformer.", "diffusion_model."): v
|
||||
for k, v in state_dict.items()
|
||||
}
|
||||
|
||||
def convert_lora_weights_before_load(self, state_dict):
|
||||
return {
|
||||
k.replace("diffusion_model.", "transformer."): v
|
||||
for k, v in state_dict.items()
|
||||
}
|
||||
|
||||
lora_keys_use_comfy_prefix = True
|
||||
|
||||
class MageFlowEditModel(MageFlowModel):
|
||||
arch = "mageflow_edit"
|
||||
|
||||
@@ -18,7 +18,8 @@ import math
|
||||
from typing import List, Optional
|
||||
|
||||
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``
|
||||
@@ -57,30 +58,6 @@ def edit_prompt_body(instruction: str, num_refs: int) -> str:
|
||||
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(
|
||||
images: List[torch.Tensor], max_long_edge: int = 384
|
||||
) -> List["PIL.Image.Image"]:
|
||||
|
||||
@@ -28,6 +28,8 @@ from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
|
||||
from toolkit.models.v2._mixin import OstrisModelMixin
|
||||
import torch.nn as nn
|
||||
from torch import Tensor
|
||||
from torch.utils.checkpoint import checkpoint
|
||||
@@ -681,7 +683,27 @@ class MageFlowParams:
|
||||
patch_size: int = 1
|
||||
|
||||
|
||||
class MageFlow(nn.Module):
|
||||
class MageFlow(nn.Module, OstrisModelMixin):
|
||||
@classmethod
|
||||
def get_transformer_block_names(cls):
|
||||
return ["transformer_blocks"]
|
||||
|
||||
@classmethod
|
||||
def get_quantization_exclude_modules(cls):
|
||||
# sensitive modules kept in full precision (fnmatch patterns):
|
||||
# img_in / txt_in / txt_norm - input projections
|
||||
# time_text_embed* - timestep embedder feeding every
|
||||
# block's modulation
|
||||
# norm_out* / proj_out - final adaptive norm / output projection
|
||||
return [
|
||||
"img_in",
|
||||
"txt_in",
|
||||
"txt_norm",
|
||||
"time_text_embed*",
|
||||
"norm_out*",
|
||||
"proj_out",
|
||||
]
|
||||
|
||||
def __init__(self, params: MageFlowParams):
|
||||
super().__init__()
|
||||
self.params = params
|
||||
|
||||
@@ -17,6 +17,8 @@ import os
|
||||
from functools import lru_cache
|
||||
|
||||
import torch
|
||||
|
||||
from toolkit.models.v2._mixin import OstrisModelMixin
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
|
||||
@@ -591,7 +593,7 @@ def _load_state_dict(ckpt_path: str):
|
||||
return state
|
||||
|
||||
|
||||
class MageVAE(nn.Module):
|
||||
class MageVAE(nn.Module, OstrisModelMixin):
|
||||
"""
|
||||
Encode: DConvEncoder (one-step diffusion) → latent [B, 128, H/16, W/16]
|
||||
Decode: DConvDenoiser + CoD Decoder → image [B, 3, H, W] in [-1, 1]
|
||||
@@ -600,20 +602,32 @@ class MageVAE(nn.Module):
|
||||
latent_channels = 128
|
||||
downsample_factor = 16
|
||||
|
||||
def __init__(self, ckpt_path: str, sample_posterior: bool = True):
|
||||
def __init__(self, ckpt_path: str = None, sample_posterior: bool = True):
|
||||
super().__init__()
|
||||
self.sample_posterior = sample_posterior
|
||||
|
||||
self.dconv_encoder = _DConvEncoder()
|
||||
self.decoder_model = _DConvDenoiser()
|
||||
|
||||
sd = _load_state_dict(ckpt_path)
|
||||
self._load_encoder(sd, ckpt_path)
|
||||
self._load_decoder(sd, ckpt_path)
|
||||
if ckpt_path is not None:
|
||||
sd = _load_state_dict(ckpt_path)
|
||||
self._load_encoder(sd, ckpt_path)
|
||||
self._load_decoder(sd, ckpt_path)
|
||||
|
||||
# adaLN modulation depends only on t, and we always run at t=0.
|
||||
# Precompute and drop the MLPs once at construction (~37M params saved).
|
||||
self._freeze_adaln_cache()
|
||||
# adaLN modulation depends only on t, and we always run at t=0.
|
||||
# Precompute and drop the MLPs once at construction (~37M params saved).
|
||||
self._freeze_adaln_cache()
|
||||
|
||||
@classmethod
|
||||
def load_from_state_dict(cls, state_dict, dtype=None, **kwargs):
|
||||
model = cls(
|
||||
ckpt_path=None,
|
||||
sample_posterior=kwargs.pop("sample_posterior", True),
|
||||
)
|
||||
model._load_encoder(state_dict, "<state dict>")
|
||||
model._load_decoder(state_dict, "<state dict>")
|
||||
model._freeze_adaln_cache()
|
||||
return model
|
||||
|
||||
def _load_encoder(self, sd, ckpt_path):
|
||||
prefix = "student.dconv_encoder."
|
||||
|
||||
@@ -49,22 +49,25 @@ from toolkit.accelerator import unwrap_model
|
||||
from toolkit.advanced_prompt_embeds import AdvancedPromptEmbeds
|
||||
from toolkit.basic import flush
|
||||
from toolkit.config_modules import GenerateImageConfig, ModelConfig
|
||||
from toolkit.memory_management import MemoryManager
|
||||
from toolkit.metadata import get_meta_for_safetensors
|
||||
from toolkit.models.base_model import BaseModel
|
||||
from toolkit.models.v2.text_encoders.qwen3_vl import Qwen3VLTextEncoder
|
||||
from toolkit.models.v2.resolver import (
|
||||
find_file_recursive,
|
||||
repo_id_from_name_or_path,
|
||||
resolve_comfy_file,
|
||||
)
|
||||
from toolkit.paths import MODELS_PATH
|
||||
from toolkit.util.comfy_quant_import import import_comfy_quantized_layers
|
||||
from toolkit.util.ostris_quant import OstrisLinear
|
||||
from toolkit.samplers.custom_flowmatch_sampler import (
|
||||
CustomFlowMatchEulerDiscreteScheduler,
|
||||
)
|
||||
from toolkit.util.quantize import get_qtype, quantize, quantize_model
|
||||
from optimum.quanto import freeze
|
||||
|
||||
from .src import packing
|
||||
|
||||
packing_video_exts = [".mp4", ".avi", ".mov", ".webm", ".mkv", ".wmv", ".m4v", ".flv"]
|
||||
from .src.audio_vae import MiniMaxH3AudioVAE, fold_audio_vae_weight_norm
|
||||
from .src.audio_vae import MiniMaxH3AudioVAE
|
||||
from .src.packing import (
|
||||
KEYFRAME_ENCODE_SEED,
|
||||
KEYFRAME_NOISE_AUG_T,
|
||||
@@ -233,64 +236,23 @@ class MinimaxH3Model(BaseModel):
|
||||
# ------------------------------------------------------------------
|
||||
# Loading
|
||||
# ------------------------------------------------------------------
|
||||
@staticmethod
|
||||
def _find_file_recursive(root_dir: str, filename: str) -> Optional[str]:
|
||||
"""First (breadth-stable, sorted) match of ``filename`` anywhere under
|
||||
``root_dir``."""
|
||||
if not os.path.isdir(root_dir):
|
||||
return None
|
||||
for dirpath, dirnames, filenames in os.walk(root_dir):
|
||||
dirnames.sort()
|
||||
if filename in filenames:
|
||||
return os.path.join(dirpath, filename)
|
||||
return None
|
||||
|
||||
def _resolve_comfy_file(self, component: str) -> str:
|
||||
"""Find a weight file at its local location, or download it there
|
||||
when (and only when) it is missing.
|
||||
|
||||
Search order: model_kwargs override, the repo-relative path under
|
||||
MODELS_PATH (diffusion_models/, text_encoders/, vae/), the bare
|
||||
filename at the root, any subfolder of the component's category
|
||||
folder (recursive — e.g. diffusion_models/my_custom_sub/), the same
|
||||
spots under name_or_path when it is a local folder, then the hub —
|
||||
downloaded to the repo-relative path under MODELS_PATH.
|
||||
"""
|
||||
override = self.model_config.model_kwargs.get(f"{component}_path", None)
|
||||
if override is not None:
|
||||
if not os.path.exists(override):
|
||||
raise FileNotFoundError(
|
||||
f"model_kwargs.{component}_path does not exist: {override}"
|
||||
)
|
||||
return override
|
||||
|
||||
rel_path = COMFY_FILES[component]
|
||||
filename = os.path.basename(rel_path)
|
||||
category = os.path.dirname(rel_path)
|
||||
roots = [MODELS_PATH]
|
||||
"""Find a weight file at its local location (model_kwargs override,
|
||||
comfy layout under MODELS_PATH or a local name_or_path dir), or
|
||||
download it there when (and only when) it is missing — see
|
||||
toolkit/models/v2/resolver.py for the search order."""
|
||||
name_or_path = self.model_config.name_or_path
|
||||
if name_or_path and os.path.isdir(name_or_path):
|
||||
roots.append(name_or_path)
|
||||
for root in roots:
|
||||
for rel in (rel_path, filename):
|
||||
candidate = os.path.join(root, rel)
|
||||
if os.path.exists(candidate):
|
||||
return candidate
|
||||
for root in roots:
|
||||
found = self._find_file_recursive(os.path.join(root, category), filename)
|
||||
if found is not None:
|
||||
return found
|
||||
|
||||
import huggingface_hub
|
||||
|
||||
repo_id = COMFY_REPO
|
||||
if name_or_path and not os.path.exists(name_or_path) and "/" in name_or_path:
|
||||
repo_id = name_or_path
|
||||
self.print_and_status_update(
|
||||
f"Downloading {rel_path} from {repo_id} into {MODELS_PATH}"
|
||||
extra_roots = (
|
||||
[name_or_path] if name_or_path and os.path.isdir(name_or_path) else []
|
||||
)
|
||||
return huggingface_hub.hf_hub_download(
|
||||
repo_id=repo_id, filename=rel_path, local_dir=MODELS_PATH
|
||||
return resolve_comfy_file(
|
||||
COMFY_FILES[component],
|
||||
repo_id=repo_id_from_name_or_path(name_or_path, COMFY_REPO),
|
||||
override_path=self.model_config.model_kwargs.get(
|
||||
f"{component}_path", None
|
||||
),
|
||||
extra_roots=extra_roots,
|
||||
status_fn=self.print_and_status_update,
|
||||
)
|
||||
|
||||
def _dit_component(self) -> str:
|
||||
@@ -322,7 +284,7 @@ class MinimaxH3Model(BaseModel):
|
||||
lora_path = self.model_config.assistant_lora_path
|
||||
if not os.path.exists(lora_path):
|
||||
filename = os.path.basename(lora_path)
|
||||
found = self._find_file_recursive(
|
||||
found = find_file_recursive(
|
||||
os.path.join(MODELS_PATH, "loras"), filename
|
||||
)
|
||||
if found is not None:
|
||||
@@ -409,46 +371,12 @@ class MinimaxH3Model(BaseModel):
|
||||
self.invert_assistant_lora = False
|
||||
|
||||
def _load_transformer(self) -> MiniMaxH3Transformer:
|
||||
dtype = self.torch_dtype
|
||||
dit_path = self._resolve_comfy_file(self._dit_component())
|
||||
self.print_and_status_update(f"Loading transformer from {dit_path}")
|
||||
state_dict = load_file(dit_path)
|
||||
|
||||
params = MiniMaxH3TransformerParams()
|
||||
table = state_dict.get("adaln_t_table", None)
|
||||
if table is not None:
|
||||
# pruned checkpoint: factored timestep table instead of the MLP
|
||||
params.adaln_t_table_size = table.shape[0]
|
||||
params.time_embed_dim = table.shape[1]
|
||||
|
||||
with torch.device("meta"):
|
||||
transformer = MiniMaxH3Transformer(params)
|
||||
|
||||
# attach the pre-quantized (int8 ConvRot) linears onto the toolkit's
|
||||
# quantization backends; the rest loads at its stored precision (the
|
||||
# checkpoint's bf16/fp16/fp32 mix is deliberate)
|
||||
state_dict, num_quantized = import_comfy_quantized_layers(
|
||||
transformer, state_dict, orig_dtype=dtype
|
||||
)
|
||||
if num_quantized:
|
||||
self.print_and_status_update(
|
||||
f" - attached {num_quantized} pre-quantized ConvRot layers"
|
||||
)
|
||||
result = transformer.load_state_dict(state_dict, assign=True, strict=False)
|
||||
quantized_weight_keys = {
|
||||
f"{name}.weight"
|
||||
for name, m in transformer.named_modules()
|
||||
if isinstance(m, OstrisLinear)
|
||||
}
|
||||
bad_missing = [k for k in result.missing_keys if k not in quantized_weight_keys]
|
||||
if bad_missing or result.unexpected_keys:
|
||||
raise ValueError(
|
||||
f"MiniMax-H3 transformer load mismatch: missing {bad_missing[:8]}, "
|
||||
f"unexpected {result.unexpected_keys[:8]}"
|
||||
)
|
||||
del state_dict
|
||||
flush()
|
||||
return transformer
|
||||
# the mixin single-file path: config sniffed from the checkpoint
|
||||
# (adaln_t_table), pre-quantized ConvRot linears attached, everything
|
||||
# else at its stored precision (the bf16/fp16/fp32 mix is deliberate)
|
||||
return MiniMaxH3Transformer.load_model(dit_path, dtype=self.torch_dtype)
|
||||
|
||||
def _load_text_encoder(self):
|
||||
from accelerate import init_empty_weights
|
||||
@@ -474,7 +402,7 @@ class MinimaxH3Model(BaseModel):
|
||||
)
|
||||
config = AutoConfig.from_pretrained(te_path)
|
||||
config.text_config.num_hidden_layers = TEXT_ENCODER_LAYER
|
||||
text_encoder = Qwen3VLForConditionalGeneration.from_pretrained(
|
||||
text_encoder = Qwen3VLTextEncoder.from_pretrained(
|
||||
te_path, config=config, torch_dtype=self.te_torch_dtype
|
||||
)
|
||||
else:
|
||||
@@ -496,7 +424,7 @@ class MinimaxH3Model(BaseModel):
|
||||
config.text_config.num_hidden_layers = TEXT_ENCODER_LAYER
|
||||
config.tie_word_embeddings = False
|
||||
with init_empty_weights():
|
||||
text_encoder = Qwen3VLForConditionalGeneration(config)
|
||||
text_encoder = Qwen3VLTextEncoder(config)
|
||||
text_encoder.lm_head = None
|
||||
|
||||
state_dict = load_file(te_file)
|
||||
@@ -551,38 +479,9 @@ class MinimaxH3Model(BaseModel):
|
||||
|
||||
def _load_vaes(self) -> MiniMaxH3VaeBundle:
|
||||
self.print_and_status_update("Loading video VAE")
|
||||
video_sd = load_file(self._resolve_comfy_file("video_vae"))
|
||||
# normalization stats ride along in the comfy file; the module holds
|
||||
# them as non-persistent buffers, keep them float32
|
||||
video_stats = {
|
||||
k: video_sd.pop(k).float()
|
||||
for k in ("latents_mean", "latents_std")
|
||||
if k in video_sd
|
||||
}
|
||||
video_vae = MiniMaxH3VideoVAE()
|
||||
video_vae.load_state_dict(video_sd, strict=True, assign=True)
|
||||
for k, v in video_stats.items():
|
||||
getattr(video_vae, k).copy_(v)
|
||||
video_vae.eval().requires_grad_(False)
|
||||
del video_sd
|
||||
|
||||
video_vae = MiniMaxH3VideoVAE.load_model(self._resolve_comfy_file("video_vae"))
|
||||
self.print_and_status_update("Loading audio VAE")
|
||||
audio_sd = load_file(self._resolve_comfy_file("audio_vae"))
|
||||
audio_stats = {
|
||||
k: audio_sd.pop(k).float()
|
||||
for k in ("latents_mean", "latents_std")
|
||||
if k in audio_sd
|
||||
}
|
||||
# comfy repack ships the weight norm already folded; fold only if the
|
||||
# raw parametrization is present (original-repo file)
|
||||
if any(k.endswith("weight_g") for k in audio_sd.keys()):
|
||||
audio_sd = fold_audio_vae_weight_norm(audio_sd)
|
||||
audio_vae = MiniMaxH3AudioVAE()
|
||||
audio_vae.load_state_dict(audio_sd, strict=True, assign=True)
|
||||
for k, v in audio_stats.items():
|
||||
getattr(audio_vae, k).copy_(v)
|
||||
audio_vae.to(torch.float32).eval().requires_grad_(False)
|
||||
del audio_sd
|
||||
audio_vae = MiniMaxH3AudioVAE.load_model(self._resolve_comfy_file("audio_vae"))
|
||||
flush()
|
||||
return MiniMaxH3VaeBundle(video_vae, audio_vae)
|
||||
|
||||
@@ -596,54 +495,16 @@ class MinimaxH3Model(BaseModel):
|
||||
if self.model_config.assistant_lora_path is not None:
|
||||
self.load_training_adapter(transformer)
|
||||
|
||||
if self.model_config.quantize:
|
||||
self.print_and_status_update("Quantizing transformer")
|
||||
quantize_model(self, transformer)
|
||||
flush()
|
||||
|
||||
if (
|
||||
self.model_config.layer_offloading
|
||||
and self.model_config.layer_offloading_transformer_percent > 0
|
||||
):
|
||||
MemoryManager.attach(
|
||||
transformer,
|
||||
self.device_torch,
|
||||
offload_percent=self.model_config.layer_offloading_transformer_percent,
|
||||
)
|
||||
|
||||
if self.model_config.low_vram:
|
||||
self.print_and_status_update("Keeping transformer on CPU")
|
||||
transformer.to("cpu")
|
||||
else:
|
||||
transformer.to(self.device_torch)
|
||||
# quantize + offload + placement, all driven by model_config
|
||||
transformer.aitk_post_load(**self.component_load_kwargs("transformer"))
|
||||
flush()
|
||||
|
||||
tokenizer, processor, text_encoder = self._load_text_encoder()
|
||||
te_prequantized = any(
|
||||
isinstance(m, OstrisLinear) for m in text_encoder.modules()
|
||||
)
|
||||
if self.model_config.quantize_te and not te_prequantized:
|
||||
self.print_and_status_update("Quantizing text encoder")
|
||||
quantize(text_encoder, weights=get_qtype(self.model_config.qtype_te))
|
||||
freeze(text_encoder)
|
||||
flush()
|
||||
elif self.model_config.quantize_te:
|
||||
self.print_and_status_update(
|
||||
"Text encoder is already nvfp4/int8 quantized; skipping quantize_te"
|
||||
)
|
||||
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:
|
||||
text_encoder.to("cpu")
|
||||
else:
|
||||
text_encoder.to(self.device_torch)
|
||||
if any(isinstance(m, OstrisLinear) for m in text_encoder.modules()):
|
||||
# already nvfp4/int8 quantized; aitk_post_load skips quantize_te
|
||||
text_encoder.aitk_is_quantized = True
|
||||
# quantize + offload + placement, all driven by model_config
|
||||
text_encoder.aitk_post_load(**self.component_load_kwargs("te"))
|
||||
flush()
|
||||
|
||||
vae_bundle = self._load_vaes()
|
||||
@@ -1226,20 +1087,9 @@ class MinimaxH3Model(BaseModel):
|
||||
"*adaln_proj*",
|
||||
]
|
||||
|
||||
def convert_lora_weights_before_save(self, state_dict):
|
||||
# ComfyUI's MiniMax-H3 keys are the original checkpoint keys, so the
|
||||
# standard diffusion_model prefix maps directly
|
||||
return {
|
||||
k.replace("transformer.", "diffusion_model."): v
|
||||
for k, v in state_dict.items()
|
||||
}
|
||||
|
||||
def convert_lora_weights_before_load(self, state_dict):
|
||||
return {
|
||||
k.replace("diffusion_model.", "transformer."): v
|
||||
for k, v in state_dict.items()
|
||||
}
|
||||
|
||||
# ComfyUI's MiniMax-H3 keys are the original checkpoint keys, so the
|
||||
# standard diffusion_model prefix maps directly
|
||||
lora_keys_use_comfy_prefix = True
|
||||
|
||||
class MinimaxH3Ref2VAModel(MinimaxH3Model):
|
||||
"""Reference-to-video (ref2va): the control images ride along as reference
|
||||
|
||||
@@ -17,6 +17,8 @@ weights::
|
||||
import math
|
||||
|
||||
import torch
|
||||
|
||||
from toolkit.models.v2._mixin import OstrisModelMixin
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
from torch import Tensor
|
||||
@@ -420,7 +422,26 @@ class BigVGANDecoder(nn.Module):
|
||||
return torch.clamp(x, min=-1.0, max=1.0)
|
||||
|
||||
|
||||
class MiniMaxH3AudioVAE(nn.Module):
|
||||
class MiniMaxH3AudioVAE(nn.Module, OstrisModelMixin):
|
||||
@classmethod
|
||||
def load_from_state_dict(cls, state_dict, dtype=None, **kwargs):
|
||||
"""Comfy single-file load: stats buffers ride along; original-repo
|
||||
files still carry the raw weight-norm parametrization and get folded."""
|
||||
state_dict = dict(state_dict)
|
||||
stats = {
|
||||
k: state_dict.pop(k).float()
|
||||
for k in ("latents_mean", "latents_std")
|
||||
if k in state_dict
|
||||
}
|
||||
if any(k.endswith("weight_g") for k in state_dict.keys()):
|
||||
state_dict = fold_audio_vae_weight_norm(state_dict)
|
||||
model = cls()
|
||||
model.load_state_dict(state_dict, strict=True, assign=True)
|
||||
for k, v in stats.items():
|
||||
getattr(model, k).copy_(v)
|
||||
model.to(torch.float32).eval().requires_grad_(False)
|
||||
return model
|
||||
|
||||
"""Frozen MiniMax-H3 audio autoencoder with diagonal latent normalization.
|
||||
|
||||
- ``encode``: waveform ``(B, 1, samples)`` at 32 kHz -> latents ``(B, 32, T)``
|
||||
|
||||
@@ -30,6 +30,8 @@ import math
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from toolkit.models.v2._mixin import OstrisModelMixin
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
from torch import nn
|
||||
@@ -356,7 +358,31 @@ class MiniMaxH3FinalLayer(nn.Module):
|
||||
return self.video_out(h), self.audio_out(h)
|
||||
|
||||
|
||||
class MiniMaxH3Transformer(nn.Module):
|
||||
class MiniMaxH3Transformer(nn.Module, OstrisModelMixin):
|
||||
# comfy checkpoints carry a deliberate bf16/fp16/fp32 mix
|
||||
aitk_cast_on_load = False
|
||||
|
||||
@classmethod
|
||||
def aitk_config_from_state_dict(cls, state_dict):
|
||||
params = MiniMaxH3TransformerParams()
|
||||
table = state_dict.get("adaln_t_table", None)
|
||||
if table is not None:
|
||||
# pruned checkpoint: factored timestep table instead of the MLP
|
||||
params.adaln_t_table_size = table.shape[0]
|
||||
params.time_embed_dim = table.shape[1]
|
||||
return params
|
||||
|
||||
@classmethod
|
||||
def aitk_from_config(cls, config):
|
||||
from accelerate import init_empty_weights
|
||||
|
||||
with init_empty_weights(include_buffers=False):
|
||||
return cls(config)
|
||||
|
||||
@classmethod
|
||||
def get_transformer_block_names(cls):
|
||||
return ["blocks"]
|
||||
|
||||
def __init__(self, params: Optional[MiniMaxH3TransformerParams] = None):
|
||||
super().__init__()
|
||||
if params is None:
|
||||
|
||||
@@ -22,6 +22,8 @@ import math
|
||||
from contextlib import nullcontext
|
||||
|
||||
import torch
|
||||
|
||||
from toolkit.models.v2._mixin import OstrisModelMixin
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
from torch.utils.checkpoint import checkpoint
|
||||
@@ -434,7 +436,24 @@ class ViTDecoder3d(nn.Module):
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class MiniMaxH3VideoVAE(nn.Module):
|
||||
class MiniMaxH3VideoVAE(nn.Module, OstrisModelMixin):
|
||||
@classmethod
|
||||
def load_from_state_dict(cls, state_dict, dtype=None, **kwargs):
|
||||
"""Comfy single-file load: the normalization stats ride along in the
|
||||
file and land in the non-persistent fp32 buffers."""
|
||||
state_dict = dict(state_dict)
|
||||
stats = {
|
||||
k: state_dict.pop(k).float()
|
||||
for k in ("latents_mean", "latents_std")
|
||||
if k in state_dict
|
||||
}
|
||||
model = cls()
|
||||
model.load_state_dict(state_dict, strict=True, assign=True)
|
||||
for k, v in stats.items():
|
||||
getattr(model, k).copy_(v)
|
||||
model.eval().requires_grad_(False)
|
||||
return model
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
in_channels=3,
|
||||
|
||||
@@ -6,21 +6,23 @@ import torch
|
||||
import yaml
|
||||
from toolkit.config_modules import GenerateImageConfig, ModelConfig
|
||||
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.advanced_prompt_embeds import AdvancedPromptEmbeds
|
||||
from toolkit.samplers.custom_flowmatch_sampler import (
|
||||
CustomFlowMatchEulerDiscreteScheduler,
|
||||
)
|
||||
from toolkit.accelerator import unwrap_model
|
||||
from optimum.quanto import freeze
|
||||
from toolkit.util.quantize import quantize, get_qtype, quantize_model
|
||||
from toolkit.memory_management import MemoryManager
|
||||
|
||||
from transformers import Qwen3VLForConditionalGeneration, Qwen3VLProcessor
|
||||
from transformers import Qwen3VLProcessor
|
||||
import torch.nn.functional as F
|
||||
|
||||
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
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
@@ -46,7 +48,7 @@ scheduler_config = {
|
||||
}
|
||||
|
||||
|
||||
class NucleusImageModel(BaseModel):
|
||||
class NucleusImageModel(QwenImageVAEHolderMixin, BaseModel):
|
||||
arch = "nucleus_image"
|
||||
|
||||
def __init__(
|
||||
@@ -81,20 +83,14 @@ class NucleusImageModel(BaseModel):
|
||||
|
||||
self.print_and_status_update("Loading transformer")
|
||||
|
||||
transformer_path = model_path
|
||||
transformer_subfolder = "transformer"
|
||||
if os.path.exists(transformer_path):
|
||||
transformer_subfolder = None
|
||||
transformer_path = os.path.join(transformer_path, "transformer")
|
||||
if os.path.exists(model_path):
|
||||
# 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 = NucleusMoEImageTransformer2DModel.from_pretrained(
|
||||
transformer_path, subfolder=transformer_subfolder, torch_dtype=dtype
|
||||
)
|
||||
transformer = NucleusMoEImageTransformer2DModel.load_model(model_path, dtype=dtype)
|
||||
|
||||
# handle versions of pytorch that don't have grouped mm, by disabling it in the SwiGLUExperts
|
||||
if not hasattr(torch.nn.functional, "grouped_mm"):
|
||||
@@ -102,26 +98,8 @@ class NucleusImageModel(BaseModel):
|
||||
if isinstance(m, SwiGLUExperts):
|
||||
m.use_grouped_mm = False
|
||||
|
||||
if self.model_config.quantize:
|
||||
self.print_and_status_update("Quantizing Transformer")
|
||||
quantize_model(self, transformer)
|
||||
flush()
|
||||
|
||||
if (
|
||||
self.model_config.layer_offloading
|
||||
and self.model_config.layer_offloading_transformer_percent > 0
|
||||
):
|
||||
MemoryManager.attach(
|
||||
transformer,
|
||||
self.device_torch,
|
||||
offload_percent=self.model_config.layer_offloading_transformer_percent,
|
||||
ignore_modules=[
|
||||
],
|
||||
)
|
||||
|
||||
if self.model_config.low_vram:
|
||||
self.print_and_status_update("Moving transformer to CPU")
|
||||
transformer.to("cpu")
|
||||
# quantize + offload + placement, all driven by model_config
|
||||
transformer.aitk_post_load(**self.component_load_kwargs("transformer"))
|
||||
|
||||
flush()
|
||||
|
||||
@@ -129,33 +107,14 @@ class NucleusImageModel(BaseModel):
|
||||
tokenizer = Qwen3VLProcessor.from_pretrained(
|
||||
base_model_path, subfolder="processor", torch_dtype=dtype
|
||||
)
|
||||
text_encoder = Qwen3VLForConditionalGeneration.from_pretrained(
|
||||
base_model_path, subfolder="text_encoder", torch_dtype=dtype
|
||||
text_encoder = Qwen3VLTextEncoder.load(
|
||||
base_model_path, **self.component_load_kwargs("te")
|
||||
)
|
||||
|
||||
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")
|
||||
vae = AutoencoderKLQwenImage.from_pretrained(
|
||||
base_model_path, subfolder="vae", torch_dtype=dtype
|
||||
).to(self.device_torch, dtype=dtype)
|
||||
vae = QwenImageVAE.load_model(
|
||||
base_model_path, dtype=dtype, device=self.device_torch
|
||||
)
|
||||
|
||||
self.noise_scheduler = NucleusImageModel.get_train_scheduler()
|
||||
|
||||
@@ -185,8 +144,10 @@ class NucleusImageModel(BaseModel):
|
||||
pipe.transformer = pipe.transformer.to(self.device_torch)
|
||||
|
||||
flush()
|
||||
# just to make sure everything is on the right device and dtype
|
||||
text_encoder[0].to(self.device_torch)
|
||||
# low_vram: the text encoder stays on cpu; get_prompt_embeds moves it
|
||||
# to the gpu on demand
|
||||
if not self.low_vram:
|
||||
text_encoder[0].to(self.device_torch)
|
||||
text_encoder[0].requires_grad_(False)
|
||||
text_encoder[0].eval()
|
||||
flush()
|
||||
@@ -214,41 +175,6 @@ class NucleusImageModel(BaseModel):
|
||||
|
||||
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(
|
||||
self,
|
||||
pipeline: NucleusMoEImagePipeline,
|
||||
@@ -405,16 +331,5 @@ class NucleusImageModel(BaseModel):
|
||||
def get_transformer_block_names(self) -> Optional[List[str]]:
|
||||
return ["transformer_blocks"]
|
||||
|
||||
def convert_lora_weights_before_save(self, state_dict):
|
||||
new_sd = {}
|
||||
for key, value in state_dict.items():
|
||||
new_key = key.replace("transformer.", "diffusion_model.")
|
||||
new_sd[new_key] = value
|
||||
return new_sd
|
||||
lora_keys_use_comfy_prefix = True
|
||||
|
||||
def convert_lora_weights_before_load(self, state_dict):
|
||||
new_sd = {}
|
||||
for key, value in state_dict.items():
|
||||
new_key = key.replace("diffusion_model.", "transformer.")
|
||||
new_sd[new_key] = value
|
||||
return new_sd
|
||||
@@ -1,4 +1,6 @@
|
||||
import os
|
||||
from toolkit.models.v2.text_encoders.qwen25_vl import Qwen25VLTextEncoder
|
||||
from toolkit.models.v2.vae.autoencoder_kl import KLVAE
|
||||
from typing import TYPE_CHECKING, List, Optional
|
||||
|
||||
import torch
|
||||
@@ -12,8 +14,6 @@ from toolkit.samplers.custom_flowmatch_sampler import (
|
||||
CustomFlowMatchEulerDiscreteScheduler,
|
||||
)
|
||||
from toolkit.accelerator import unwrap_model
|
||||
from optimum.quanto import freeze
|
||||
from toolkit.util.quantize import quantize, get_qtype
|
||||
from .src.pipelines.omnigen2.pipeline_omnigen2 import OmniGen2Pipeline
|
||||
from .src.models.transformers import OmniGen2Transformer2DModel
|
||||
from .src.models.transformers.repo import OmniGen2RotaryPosEmbed
|
||||
@@ -78,48 +78,26 @@ class OmniGen2Model(BaseModel):
|
||||
extras_path, subfolder="processor", use_fast=True
|
||||
)
|
||||
|
||||
mllm = Qwen2_5_VLForConditionalGeneration.from_pretrained(
|
||||
extras_path, subfolder="mllm", torch_dtype=torch.bfloat16
|
||||
# load + quantize + offload + placement, all driven by model_config
|
||||
mllm = Qwen25VLTextEncoder.load(
|
||||
extras_path, subfolder="mllm", **self.component_load_kwargs("te")
|
||||
)
|
||||
mllm.to(self.device_torch, dtype=dtype)
|
||||
if self.model_config.quantize_te:
|
||||
self.print_and_status_update("Quantizing Qwen2.5 VL model")
|
||||
quantization_type = get_qtype(self.model_config.qtype_te)
|
||||
quantize(mllm, weights=quantization_type)
|
||||
freeze(mllm)
|
||||
|
||||
if self.low_vram:
|
||||
# unload it for now
|
||||
mllm.to("cpu")
|
||||
|
||||
flush()
|
||||
|
||||
self.print_and_status_update("Loading transformer")
|
||||
|
||||
transformer = OmniGen2Transformer2DModel.from_pretrained(
|
||||
model_path, subfolder="transformer", torch_dtype=torch.bfloat16
|
||||
transformer = OmniGen2Transformer2DModel.load(
|
||||
model_path, **self.component_load_kwargs("transformer")
|
||||
)
|
||||
|
||||
if not self.low_vram:
|
||||
transformer.to(self.device_torch, dtype=dtype)
|
||||
|
||||
if self.model_config.quantize:
|
||||
self.print_and_status_update("Quantizing transformer")
|
||||
quantization_type = get_qtype(self.model_config.qtype)
|
||||
quantize(transformer, weights=quantization_type)
|
||||
freeze(transformer)
|
||||
|
||||
if self.low_vram:
|
||||
# unload it for now
|
||||
transformer.to("cpu")
|
||||
|
||||
flush()
|
||||
|
||||
self.print_and_status_update("Loading vae")
|
||||
|
||||
vae = AutoencoderKL.from_pretrained(
|
||||
extras_path, subfolder="vae", torch_dtype=torch.bfloat16
|
||||
).to(self.device_torch, dtype=dtype)
|
||||
vae = KLVAE.load_model(extras_path, dtype=torch.bfloat16).to(
|
||||
self.device_torch, dtype=dtype
|
||||
)
|
||||
|
||||
flush()
|
||||
self.print_and_status_update("Loading Qwen2.5 VLProcessor")
|
||||
@@ -343,21 +321,7 @@ class OmniGen2Model(BaseModel):
|
||||
return ["noise_refiner", "context_refiner", "ref_image_refiner", "layers"]
|
||||
return ["noise_refiner", "context_refiner", "layers"]
|
||||
|
||||
def convert_lora_weights_before_save(self, state_dict):
|
||||
# currently starte with transformer. but needs to start with diffusion_model. for comfyui
|
||||
new_sd = {}
|
||||
for key, value in state_dict.items():
|
||||
new_key = key.replace("transformer.", "diffusion_model.")
|
||||
new_sd[new_key] = value
|
||||
return new_sd
|
||||
|
||||
def convert_lora_weights_before_load(self, state_dict):
|
||||
# saved as diffusion_model. but needs to be transformer. for ai-toolkit
|
||||
new_sd = {}
|
||||
for key, value in state_dict.items():
|
||||
new_key = key.replace("diffusion_model.", "transformer.")
|
||||
new_sd[new_key] = value
|
||||
return new_sd
|
||||
lora_keys_use_comfy_prefix = True
|
||||
|
||||
def get_base_model_version(self):
|
||||
return "omnigen2"
|
||||
+11
-1
@@ -15,6 +15,8 @@ from diffusers.models.attention_processor import Attention
|
||||
from diffusers.models.modeling_outputs import Transformer2DModelOutput
|
||||
from diffusers.models.modeling_utils import ModelMixin
|
||||
|
||||
from toolkit.models.v2._mixin import OstrisModelMixin
|
||||
|
||||
from ..attention_processor import OmniGen2AttnProcessorFlash2Varlen, OmniGen2AttnProcessor
|
||||
from .repo import OmniGen2RotaryPosEmbed
|
||||
from .block_lumina2 import LuminaLayerNormContinuous, LuminaRMSNormZero, LuminaFeedForward, Lumina2CombinedTimestepCaptionEmbedding
|
||||
@@ -177,7 +179,15 @@ class OmniGen2TransformerBlock(nn.Module):
|
||||
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.
|
||||
|
||||
|
||||
@@ -43,19 +43,20 @@ import torch
|
||||
import yaml
|
||||
|
||||
from transformers import AutoTokenizer, Qwen3VLTextModel
|
||||
from optimum.quanto import freeze
|
||||
|
||||
|
||||
from toolkit.accelerator import unwrap_model
|
||||
from toolkit.basic import flush
|
||||
from toolkit.config_modules import GenerateImageConfig, ModelConfig
|
||||
from toolkit.models.base_model import BaseModel
|
||||
from toolkit.models.v2.text_encoders.qwen3_vl import Qwen3VLTextOnlyEncoder
|
||||
from toolkit.models.FakeVAE import FakeVAE
|
||||
from toolkit.prompt_utils import PromptEmbeds
|
||||
from toolkit.samplers.custom_flowmatch_sampler import (
|
||||
CustomFlowMatchEulerDiscreteScheduler,
|
||||
)
|
||||
from toolkit.train_tools import apply_noise_offset
|
||||
from toolkit.util.quantize import quantize, get_qtype, quantize_model
|
||||
|
||||
|
||||
from .src.transformer_prx import PRXTransformer2DModel
|
||||
from .src.pipeline import PRXPixelPipeline
|
||||
@@ -126,40 +127,22 @@ class PRXPixelT2IModel(BaseModel):
|
||||
self.print_and_status_update("Loading transformer")
|
||||
# from_pretrained reads config.json (bottleneck_size, resolution_embeds,
|
||||
# in_channels=3, ...) and the safetensors in one shot.
|
||||
transformer = PRXTransformer2DModel.from_pretrained(
|
||||
model_path, subfolder="transformer", torch_dtype=dtype
|
||||
# load + quantize + offload + placement, all driven by model_config
|
||||
transformer = PRXTransformer2DModel.load(
|
||||
model_path, **self.component_load_kwargs("transformer")
|
||||
)
|
||||
transformer.to(dtype=dtype)
|
||||
flush()
|
||||
|
||||
if self.model_config.quantize:
|
||||
self.print_and_status_update("Quantizing transformer")
|
||||
quantize_model(self, transformer)
|
||||
flush()
|
||||
|
||||
if self.model_config.low_vram:
|
||||
transformer.to("cpu")
|
||||
else:
|
||||
transformer.to(self.device_torch, dtype=dtype)
|
||||
flush()
|
||||
|
||||
# --- text encoder + tokenizer (Qwen3-VL text tower) ---
|
||||
self.print_and_status_update("Loading text encoder")
|
||||
tokenizer = AutoTokenizer.from_pretrained(model_path, subfolder="tokenizer")
|
||||
text_encoder = Qwen3VLTextModel.from_pretrained(
|
||||
model_path, subfolder="text_encoder", torch_dtype=dtype
|
||||
text_encoder = Qwen3VLTextOnlyEncoder.load(
|
||||
model_path, **self.component_load_kwargs("te")
|
||||
)
|
||||
text_encoder.to(self.te_device_torch)
|
||||
text_encoder.eval()
|
||||
text_encoder.requires_grad_(False)
|
||||
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()
|
||||
|
||||
# --- "VAE": identity, since PRXPixel is pixel space ---
|
||||
self.print_and_status_update("Preparing pixel-space VAE (identity)")
|
||||
vae = FakeVAE(scaling_factor=1.0)
|
||||
@@ -332,14 +315,5 @@ class PRXPixelT2IModel(BaseModel):
|
||||
def get_transformer_block_names(self) -> Optional[List[str]]:
|
||||
return ["blocks"]
|
||||
|
||||
def convert_lora_weights_before_save(self, state_dict):
|
||||
return {
|
||||
k.replace("transformer.", "diffusion_model."): v
|
||||
for k, v in state_dict.items()
|
||||
}
|
||||
lora_keys_use_comfy_prefix = True
|
||||
|
||||
def convert_lora_weights_before_load(self, state_dict):
|
||||
return {
|
||||
k.replace("diffusion_model.", "transformer."): v
|
||||
for k, v in state_dict.items()
|
||||
}
|
||||
@@ -40,6 +40,8 @@ from diffusers.models.attention_dispatch import dispatch_attention_fn
|
||||
from diffusers.models.embeddings import get_timestep_embedding
|
||||
from diffusers.models.modeling_outputs import Transformer2DModelOutput
|
||||
from diffusers.models.modeling_utils import ModelMixin
|
||||
|
||||
from toolkit.models.v2._mixin import OstrisModelMixin
|
||||
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
|
||||
|
||||
|
||||
class PRXTransformer2DModel(ModelMixin, ConfigMixin, AttentionMixin):
|
||||
class PRXTransformer2DModel(ModelMixin, ConfigMixin, AttentionMixin, OstrisModelMixin):
|
||||
r"""
|
||||
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.
|
||||
"""
|
||||
|
||||
aitk_subfolder = "transformer"
|
||||
|
||||
@classmethod
|
||||
def get_transformer_block_names(cls):
|
||||
return ["blocks"]
|
||||
|
||||
config_name = "config.json"
|
||||
_supports_gradient_checkpointing = True
|
||||
|
||||
|
||||
@@ -7,28 +7,25 @@ from toolkit import train_tools
|
||||
from toolkit.config_modules import GenerateImageConfig, ModelConfig
|
||||
from PIL import Image
|
||||
from toolkit.models.base_model import BaseModel
|
||||
from toolkit.models.v2.vae.qwen_image import QwenImageVAE, QwenImageVAEHolderMixin
|
||||
from toolkit.basic import flush
|
||||
from toolkit.prompt_utils import PromptEmbeds
|
||||
from toolkit.samplers.custom_flowmatch_sampler import (
|
||||
CustomFlowMatchEulerDiscreteScheduler,
|
||||
)
|
||||
from toolkit.accelerator import get_accelerator, unwrap_model
|
||||
from optimum.quanto import freeze, QTensor
|
||||
from toolkit.util.quantize import quantize, get_qtype, quantize_model
|
||||
import torch.nn.functional as F
|
||||
from toolkit.memory_management import MemoryManager
|
||||
from toolkit.metadata import get_meta_for_safetensors
|
||||
from safetensors.torch import load_file
|
||||
|
||||
from diffusers import (
|
||||
QwenImagePipeline,
|
||||
QwenImageTransformer2DModel,
|
||||
AutoencoderKLQwenImage,
|
||||
)
|
||||
from transformers import (
|
||||
Qwen2_5_VLForConditionalGeneration,
|
||||
Qwen2Tokenizer,
|
||||
Qwen2VLProcessor,
|
||||
)
|
||||
from transformers import Qwen2VLProcessor
|
||||
from toolkit.models.v2.diffusion_models.qwen_image import QwenImageTransformer2DModel
|
||||
from toolkit.models.v2.text_encoders.qwen25_vl import Qwen25VLTextEncoder
|
||||
from tqdm import tqdm
|
||||
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"
|
||||
_qwen_image_keep_visual = False
|
||||
_qwen_pipeline = QwenImagePipeline
|
||||
@@ -97,89 +94,33 @@ class QwenImageModel(BaseModel):
|
||||
|
||||
self.print_and_status_update("Loading transformer")
|
||||
|
||||
if model_path.endswith(".safetensors"):
|
||||
# load the safetensors file
|
||||
transformer = QwenImageTransformer2DModel.from_single_file(
|
||||
model_path,
|
||||
config="Qwen/Qwen-Image",
|
||||
subfolder="transformer",
|
||||
torch_dtype=model_dtype,
|
||||
)
|
||||
transformer.to(model_dtype)
|
||||
|
||||
else:
|
||||
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:
|
||||
self.print_and_status_update("Quantizing Transformer")
|
||||
quantize_model(self, transformer)
|
||||
flush()
|
||||
|
||||
if (
|
||||
self.model_config.layer_offloading
|
||||
and self.model_config.layer_offloading_transformer_percent > 0
|
||||
):
|
||||
MemoryManager.attach(
|
||||
transformer,
|
||||
self.device_torch,
|
||||
offload_percent=self.model_config.layer_offloading_transformer_percent,
|
||||
)
|
||||
|
||||
if self.model_config.low_vram:
|
||||
self.print_and_status_update("Moving transformer to CPU")
|
||||
transformer.to("cpu")
|
||||
if not model_path.endswith(".safetensors") and os.path.exists(model_path):
|
||||
# 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
|
||||
|
||||
# load + quantize + offload + placement, all driven by model_config
|
||||
transformer = QwenImageTransformer2DModel.load(
|
||||
model_path, **self.component_load_kwargs("transformer")
|
||||
)
|
||||
flush()
|
||||
|
||||
self.print_and_status_update("Text Encoder")
|
||||
tokenizer = Qwen2Tokenizer.from_pretrained(
|
||||
base_model_path, subfolder="tokenizer", torch_dtype=dtype
|
||||
)
|
||||
text_encoder = Qwen2_5_VLForConditionalGeneration.from_pretrained(
|
||||
base_model_path, subfolder="text_encoder", torch_dtype=dtype
|
||||
)
|
||||
tokenizer = Qwen25VLTextEncoder.load_tokenizer(base_model_path, use_fast=False)
|
||||
text_encoder = Qwen25VLTextEncoder.load_model(base_model_path, dtype=dtype)
|
||||
|
||||
# remove the visual model as it is not needed for image generation
|
||||
# (before quantization, so the dead tower is never quantized)
|
||||
self.processor = None
|
||||
if not self._qwen_image_keep_visual:
|
||||
text_encoder.model.visual = None
|
||||
text_encoder.drop_vision_tower()
|
||||
|
||||
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()
|
||||
text_encoder.aitk_post_load(**self.component_load_kwargs("te"))
|
||||
|
||||
self.print_and_status_update("Loading VAE")
|
||||
vae = AutoencoderKLQwenImage.from_pretrained(
|
||||
base_model_path, subfolder="vae", torch_dtype=dtype
|
||||
)
|
||||
vae = QwenImageVAE.load_model(base_model_path, dtype=dtype)
|
||||
|
||||
self.noise_scheduler = QwenImageModel.get_train_scheduler()
|
||||
|
||||
@@ -220,8 +161,10 @@ class QwenImageModel(BaseModel):
|
||||
pipe.transformer = pipe.transformer.to(self.device_torch)
|
||||
|
||||
flush()
|
||||
# just to make sure everything is on the right device and dtype
|
||||
text_encoder[0].to(self.device_torch)
|
||||
# low_vram: the text encoder stays on cpu; get_prompt_embeds moves it
|
||||
# to the gpu on demand
|
||||
if not self.low_vram:
|
||||
text_encoder[0].to(self.device_torch)
|
||||
text_encoder[0].requires_grad_(False)
|
||||
text_encoder[0].eval()
|
||||
flush()
|
||||
@@ -393,17 +336,17 @@ class QwenImageModel(BaseModel):
|
||||
return False
|
||||
|
||||
def save_model(self, output_path, meta, save_dtype):
|
||||
# only save the unet
|
||||
# comfy-format single-file save (diffusers keys ARE the comfy layout
|
||||
# for qwen image); prequantized layers keep their quantized storage
|
||||
transformer: QwenImageTransformer2DModel = unwrap_model(self.model)
|
||||
transformer.save_pretrained(
|
||||
save_directory=os.path.join(output_path, "transformer"),
|
||||
safe_serialization=True,
|
||||
if not output_path.endswith(".safetensors"):
|
||||
output_path += ".safetensors"
|
||||
transformer.save_model(
|
||||
output_path,
|
||||
dtype=save_dtype,
|
||||
metadata=get_meta_for_safetensors(meta, name=self.arch),
|
||||
)
|
||||
|
||||
meta_path = os.path.join(output_path, "aitk_meta.yaml")
|
||||
with open(meta_path, "w") as f:
|
||||
yaml.dump(meta, f)
|
||||
|
||||
def get_loss_target(self, *args, **kwargs):
|
||||
noise = kwargs.get("noise")
|
||||
batch = kwargs.get("batch")
|
||||
@@ -415,83 +358,5 @@ class QwenImageModel(BaseModel):
|
||||
def get_transformer_block_names(self) -> Optional[List[str]]:
|
||||
return ["transformer_blocks"]
|
||||
|
||||
def convert_lora_weights_before_save(self, state_dict):
|
||||
new_sd = {}
|
||||
for key, value in state_dict.items():
|
||||
new_key = key.replace("transformer.", "diffusion_model.")
|
||||
new_sd[new_key] = value
|
||||
return new_sd
|
||||
lora_keys_use_comfy_prefix = True
|
||||
|
||||
def convert_lora_weights_before_load(self, state_dict):
|
||||
new_sd = {}
|
||||
for key, value in state_dict.items():
|
||||
new_key = key.replace("diffusion_model.", "transformer.")
|
||||
new_sd[new_key] = value
|
||||
return new_sd
|
||||
|
||||
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)
|
||||
@@ -15,7 +15,6 @@ from toolkit.samplers.custom_flowmatch_sampler import (
|
||||
)
|
||||
from toolkit.accelerator import get_accelerator, unwrap_model
|
||||
from optimum.quanto import freeze, QTensor
|
||||
from toolkit.util.quantize import quantize, get_qtype, quantize_model
|
||||
import torch.nn.functional as F
|
||||
|
||||
from diffusers import (
|
||||
|
||||
@@ -15,7 +15,6 @@ from toolkit.samplers.custom_flowmatch_sampler import (
|
||||
)
|
||||
from toolkit.accelerator import get_accelerator, unwrap_model
|
||||
from optimum.quanto import freeze, QTensor
|
||||
from toolkit.util.quantize import quantize, get_qtype, quantize_model
|
||||
import torch.nn.functional as F
|
||||
|
||||
from diffusers import (
|
||||
|
||||
@@ -17,11 +17,12 @@ from toolkit.samplers.custom_flowmatch_sampler import (
|
||||
)
|
||||
from toolkit.util.quantize import quantize_model
|
||||
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 torchvision.transforms import functional as TF
|
||||
|
||||
from toolkit.metadata import get_meta_for_safetensors
|
||||
from toolkit.models.wan21.wan21 import Wan21
|
||||
from .wan22_5b_model import (
|
||||
scheduler_config,
|
||||
@@ -94,6 +95,13 @@ class DualWanTransformer3DModel(torch.nn.Module):
|
||||
def dtype(self) -> torch.dtype:
|
||||
return self.torch_dtype
|
||||
|
||||
def get_offload_ignore_modules(self):
|
||||
# both DiTs' fp32 modulation tables stay resident on the compute device
|
||||
return (
|
||||
self.transformer_1.get_offload_ignore_modules()
|
||||
+ self.transformer_2.get_offload_ignore_modules()
|
||||
)
|
||||
|
||||
@property
|
||||
def config(self):
|
||||
return self.transformer_1.config
|
||||
@@ -127,17 +135,26 @@ class DualWanTransformer3DModel(torch.nn.Module):
|
||||
else:
|
||||
t_name = "transformer_2"
|
||||
|
||||
# memory-managed transformers own their placement (layers bounce
|
||||
# from cpu per forward); whole-model swaps would haul the full
|
||||
# 14B up and defeat the offloading
|
||||
managed = (
|
||||
hasattr(self, "_memory_manager")
|
||||
or hasattr(self.transformer_1, "_memory_manager")
|
||||
or hasattr(self.transformer_2, "_memory_manager")
|
||||
)
|
||||
|
||||
# check if we are changing the active transformer, if so, we need to swap the one in
|
||||
# vram if low_vram is enabled
|
||||
# todo swap the loras as well
|
||||
if t_name != self._active_transformer_name:
|
||||
if self.low_vram:
|
||||
if self.low_vram and not managed:
|
||||
getattr(self, self._active_transformer_name).to("cpu")
|
||||
getattr(self, t_name).to(self.device_torch)
|
||||
torch.cuda.empty_cache()
|
||||
self._active_transformer_name = t_name
|
||||
|
||||
if self.transformer.device != hidden_states.device:
|
||||
if not managed and self.transformer.device != hidden_states.device:
|
||||
if self.low_vram:
|
||||
# move other transformer to cpu
|
||||
other_tname = (
|
||||
@@ -285,66 +302,25 @@ class Wan2214bModel(Wan21):
|
||||
# we have a hf path, replace it with transformer_2 subfolder
|
||||
subfolder_2 = "transformer_2"
|
||||
|
||||
# per-transformer load kwargs; the ARA (if any) applies to the combined
|
||||
# dual model below, offload attaches per transformer after that
|
||||
per_kwargs = self.component_load_kwargs("transformer")
|
||||
per_kwargs["offload"] = 0.0
|
||||
if self.model_config.accuracy_recovery_adapter is not None:
|
||||
per_kwargs["qtype"] = None
|
||||
|
||||
self.print_and_status_update("Loading transformer 1")
|
||||
dtype = self.torch_dtype
|
||||
transformer_1 = WanTransformer3DModel.from_pretrained(
|
||||
transformer_path_1,
|
||||
subfolder=subfolder_1,
|
||||
torch_dtype=dtype,
|
||||
).to(dtype=dtype)
|
||||
|
||||
transformer_1 = WanTransformer3DModel.load(
|
||||
transformer_path_1, subfolder=subfolder_1, **per_kwargs
|
||||
)
|
||||
flush()
|
||||
|
||||
if self.model_config.low_vram:
|
||||
# quantize on the device
|
||||
transformer_1.to('cpu', dtype=dtype)
|
||||
flush()
|
||||
else:
|
||||
transformer_1.to(self.device_torch, dtype=dtype)
|
||||
flush()
|
||||
|
||||
if self.model_config.quantize and self.model_config.accuracy_recovery_adapter is None:
|
||||
# todo handle two ARAs
|
||||
self.print_and_status_update("Quantizing Transformer 1")
|
||||
quantize_model(self, transformer_1)
|
||||
flush()
|
||||
|
||||
if self.model_config.low_vram:
|
||||
self.print_and_status_update("Moving transformer 1 to CPU")
|
||||
transformer_1.to("cpu")
|
||||
else:
|
||||
transformer_1.to(self.device_torch)
|
||||
|
||||
self.print_and_status_update("Loading transformer 2")
|
||||
dtype = self.torch_dtype
|
||||
transformer_2 = WanTransformer3DModel.from_pretrained(
|
||||
transformer_path_2,
|
||||
subfolder=subfolder_2,
|
||||
torch_dtype=dtype,
|
||||
).to(dtype=dtype)
|
||||
|
||||
transformer_2 = WanTransformer3DModel.load(
|
||||
transformer_path_2, subfolder=subfolder_2, **per_kwargs
|
||||
)
|
||||
flush()
|
||||
|
||||
if self.model_config.low_vram:
|
||||
# quantize on the device
|
||||
transformer_2.to('cpu', dtype=dtype)
|
||||
flush()
|
||||
else:
|
||||
transformer_2.to(self.device_torch, dtype=dtype)
|
||||
flush()
|
||||
|
||||
if self.model_config.quantize and self.model_config.accuracy_recovery_adapter is None:
|
||||
# todo handle two ARAs
|
||||
self.print_and_status_update("Quantizing Transformer 2")
|
||||
quantize_model(self, transformer_2)
|
||||
flush()
|
||||
|
||||
if self.model_config.low_vram:
|
||||
self.print_and_status_update("Moving transformer 2 to CPU")
|
||||
transformer_2.to("cpu")
|
||||
else:
|
||||
transformer_2.to(self.device_torch)
|
||||
|
||||
layer_offloading_transformer = self.model_config.layer_offloading and self.model_config.layer_offloading_transformer_percent > 0
|
||||
# make the combined model
|
||||
self.print_and_status_update("Creating DualWanTransformer3DModel")
|
||||
@@ -365,18 +341,13 @@ class Wan2214bModel(Wan21):
|
||||
|
||||
|
||||
if layer_offloading_transformer:
|
||||
MemoryManager.attach(
|
||||
transformer_1,
|
||||
self.device_torch,
|
||||
offload_percent=self.model_config.layer_offloading_transformer_percent,
|
||||
ignore_modules=[transformer_1.scale_shift_table] + [block.scale_shift_table for block in transformer_1.blocks]
|
||||
)
|
||||
MemoryManager.attach(
|
||||
transformer_2,
|
||||
self.device_torch,
|
||||
offload_percent=self.model_config.layer_offloading_transformer_percent,
|
||||
ignore_modules=[transformer_2.scale_shift_table] + [block.scale_shift_table for block in transformer_2.blocks]
|
||||
)
|
||||
for t in (transformer_1, transformer_2):
|
||||
MemoryManager.attach(
|
||||
t,
|
||||
self.device_torch,
|
||||
offload_percent=self.model_config.layer_offloading_transformer_percent,
|
||||
ignore_modules=t.get_offload_ignore_modules(),
|
||||
)
|
||||
|
||||
return transformer
|
||||
|
||||
@@ -435,20 +406,20 @@ class Wan2214bModel(Wan21):
|
||||
return False
|
||||
|
||||
def save_model(self, output_path, meta, save_dtype):
|
||||
# comfy-format single-file saves, one per DiT (comfy convention:
|
||||
# separate high/low noise files)
|
||||
transformer_combo: DualWanTransformer3DModel = unwrap_model(self.model)
|
||||
transformer_combo.transformer_1.save_pretrained(
|
||||
save_directory=os.path.join(output_path, "transformer"),
|
||||
safe_serialization=True,
|
||||
base = output_path
|
||||
if base.endswith(".safetensors"):
|
||||
base = base[: -len(".safetensors")]
|
||||
metadata = get_meta_for_safetensors(meta, name=self.arch)
|
||||
transformer_combo.transformer_1.save_model(
|
||||
f"{base}_high_noise.safetensors", dtype=save_dtype, metadata=metadata
|
||||
)
|
||||
transformer_combo.transformer_2.save_pretrained(
|
||||
save_directory=os.path.join(output_path, "transformer_2"),
|
||||
safe_serialization=True,
|
||||
transformer_combo.transformer_2.save_model(
|
||||
f"{base}_low_noise.safetensors", dtype=save_dtype, metadata=metadata
|
||||
)
|
||||
|
||||
meta_path = os.path.join(output_path, "aitk_meta.yaml")
|
||||
with open(meta_path, "w") as f:
|
||||
yaml.dump(meta, f)
|
||||
|
||||
def save_lora(
|
||||
self,
|
||||
state_dict: Dict[str, torch.Tensor],
|
||||
|
||||
@@ -3,7 +3,6 @@ from typing import List, Optional
|
||||
|
||||
import huggingface_hub
|
||||
import torch
|
||||
import yaml
|
||||
from toolkit.config_modules import GenerateImageConfig, ModelConfig, NetworkConfig
|
||||
from toolkit.lora_special import LoRASpecialNetwork
|
||||
from toolkit.models.base_model import BaseModel
|
||||
@@ -13,26 +12,19 @@ from toolkit.samplers.custom_flowmatch_sampler import (
|
||||
CustomFlowMatchEulerDiscreteScheduler,
|
||||
)
|
||||
from toolkit.accelerator import unwrap_model
|
||||
from optimum.quanto import freeze
|
||||
from toolkit.util.quantize import (
|
||||
quantize,
|
||||
get_qtype,
|
||||
quantize_model,
|
||||
dequantize_if_quantized,
|
||||
)
|
||||
from toolkit.memory_management import MemoryManager
|
||||
from toolkit.metadata import get_meta_for_safetensors
|
||||
from safetensors.torch import load_file, save_file
|
||||
from toolkit.models.v2.text_encoders.qwen3 import Qwen3TextEncoder
|
||||
from toolkit.models.v2.vae.autoencoder_kl import KLVAE
|
||||
from safetensors.torch import load_file
|
||||
|
||||
from transformers import AutoTokenizer, Qwen3ForCausalLM
|
||||
from diffusers import AutoencoderKL
|
||||
|
||||
try:
|
||||
from diffusers import ZImagePipeline
|
||||
|
||||
# our subclass of the diffusers transformer with the universal loading /
|
||||
# quantization mixin (see toolkit/models/classes/_mixin.py)
|
||||
from toolkit.models.v2.z_image import ZImageTransformer2DModel
|
||||
# quantization mixin (see toolkit/models/v2/_mixin.py)
|
||||
from toolkit.models.v2.diffusion_models.z_image import ZImageTransformer2DModel
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"Diffusers is out of date. Update diffusers to the latest version by doing pip uninstall diffusers and then pip install -r requirements.txt"
|
||||
@@ -189,20 +181,13 @@ class ZImageModel(BaseModel):
|
||||
# quantization happens inside load_model unless an adapter has to be merged
|
||||
# into the full precision weights first (assistant lora / accuracy recovery
|
||||
# adapter); those paths quantize after the merge via quantize_model
|
||||
qtype = None
|
||||
if (
|
||||
self.model_config.quantize
|
||||
and self.model_config.assistant_lora_path is None
|
||||
and self.model_config.accuracy_recovery_adapter is None
|
||||
):
|
||||
qtype = self.model_config.qtype
|
||||
|
||||
transformer = ZImageTransformer2DModel.load_model(
|
||||
model_path,
|
||||
dtype=dtype,
|
||||
qtype=qtype,
|
||||
quantize_device=self.device_torch,
|
||||
config_path=base_model_path if self.is_single_file else None,
|
||||
use_comfy_weights=self.model_config.model_kwargs.get(
|
||||
"use_comfy_weights", True
|
||||
),
|
||||
)
|
||||
flush()
|
||||
|
||||
@@ -227,64 +212,18 @@ class ZImageModel(BaseModel):
|
||||
if self.model_config.qtype == "qfloat8":
|
||||
self.model_config.qtype = "float8"
|
||||
|
||||
# already quantized inside load_transformer unless an adapter had to merge
|
||||
# into full precision weights first (or the checkpoint was pre-quantized)
|
||||
if self.model_config.quantize and not transformer.aitk_is_quantized:
|
||||
self.print_and_status_update("Quantizing Transformer")
|
||||
quantize_model(self, transformer)
|
||||
flush()
|
||||
|
||||
if (
|
||||
self.model_config.layer_offloading
|
||||
and self.model_config.layer_offloading_transformer_percent > 0
|
||||
):
|
||||
MemoryManager.attach(
|
||||
transformer,
|
||||
self.device_torch,
|
||||
offload_percent=self.model_config.layer_offloading_transformer_percent,
|
||||
ignore_modules=[
|
||||
transformer.x_pad_token,
|
||||
transformer.cap_pad_token,
|
||||
],
|
||||
)
|
||||
|
||||
if self.model_config.low_vram:
|
||||
self.print_and_status_update("Moving transformer to CPU")
|
||||
transformer.to("cpu")
|
||||
|
||||
# quantize + offload + placement, all driven by model_config
|
||||
transformer.aitk_post_load(**self.component_load_kwargs("transformer"))
|
||||
flush()
|
||||
|
||||
self.print_and_status_update("Text Encoder")
|
||||
tokenizer = AutoTokenizer.from_pretrained(
|
||||
base_model_path, subfolder="tokenizer", torch_dtype=dtype
|
||||
tokenizer = Qwen3TextEncoder.load_tokenizer(base_model_path)
|
||||
text_encoder = Qwen3TextEncoder.load(
|
||||
base_model_path, **self.component_load_kwargs("te")
|
||||
)
|
||||
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")
|
||||
vae = AutoencoderKL.from_pretrained(
|
||||
base_model_path, subfolder="vae", torch_dtype=dtype
|
||||
)
|
||||
vae = KLVAE.load_model(base_model_path, dtype=dtype)
|
||||
|
||||
self.noise_scheduler = ZImageModel.get_train_scheduler()
|
||||
|
||||
@@ -427,31 +366,17 @@ class ZImageModel(BaseModel):
|
||||
return ZImageTransformer2DModel.get_quantization_exclude_modules()
|
||||
|
||||
def save_model(self, output_path, meta, save_dtype):
|
||||
# comfy-format single-file save (the standard save format regardless
|
||||
# of how the model was loaded); prequantized layers keep their
|
||||
# quantized storage
|
||||
transformer: ZImageTransformer2DModel = unwrap_model(self.model)
|
||||
if self.is_single_file:
|
||||
# loaded from a single-file checkpoint, save back in that format
|
||||
sd = transformer.state_dict()
|
||||
save_dict = {}
|
||||
for key, value in sd.items():
|
||||
# dequantize any quantized (e.g. torchao) weights so we save plain tensors
|
||||
save_dict[key] = (
|
||||
dequantize_if_quantized(value).clone().to("cpu", dtype=save_dtype)
|
||||
)
|
||||
save_dict = transformer.convert_state_dict_on_save(save_dict)
|
||||
|
||||
if not output_path.endswith(".safetensors"):
|
||||
output_path += ".safetensors"
|
||||
meta = get_meta_for_safetensors(meta, name=self.arch)
|
||||
save_file(save_dict, output_path, metadata=meta)
|
||||
else:
|
||||
transformer.save_pretrained(
|
||||
save_directory=os.path.join(output_path, "transformer"),
|
||||
safe_serialization=True,
|
||||
)
|
||||
|
||||
meta_path = os.path.join(output_path, "aitk_meta.yaml")
|
||||
with open(meta_path, "w") as f:
|
||||
yaml.dump(meta, f)
|
||||
if not output_path.endswith(".safetensors"):
|
||||
output_path += ".safetensors"
|
||||
transformer.save_model(
|
||||
output_path,
|
||||
dtype=save_dtype,
|
||||
metadata=get_meta_for_safetensors(meta, name=self.arch),
|
||||
)
|
||||
|
||||
def get_loss_target(self, *args, **kwargs):
|
||||
noise = kwargs.get("noise")
|
||||
@@ -464,16 +389,5 @@ class ZImageModel(BaseModel):
|
||||
def get_transformer_block_names(self) -> Optional[List[str]]:
|
||||
return ["layers"]
|
||||
|
||||
def convert_lora_weights_before_save(self, state_dict):
|
||||
new_sd = {}
|
||||
for key, value in state_dict.items():
|
||||
new_key = key.replace("transformer.", "diffusion_model.")
|
||||
new_sd[new_key] = value
|
||||
return new_sd
|
||||
lora_keys_use_comfy_prefix = True
|
||||
|
||||
def convert_lora_weights_before_load(self, state_dict):
|
||||
new_sd = {}
|
||||
for key, value in state_dict.items():
|
||||
new_key = key.replace("diffusion_model.", "transformer.")
|
||||
new_sd[new_key] = value
|
||||
return new_sd
|
||||
@@ -9,11 +9,8 @@ import torch.nn.functional as F
|
||||
import yaml
|
||||
from toolkit.basic import flush
|
||||
from toolkit.accelerator import unwrap_model
|
||||
from optimum.quanto import freeze
|
||||
from toolkit.util.quantize import quantize, get_qtype, quantize_model
|
||||
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.paths import MODELS_PATH
|
||||
from safetensors.torch import load_file, save_file
|
||||
@@ -23,7 +20,7 @@ HF_TOKEN = os.getenv("HF_TOKEN", None)
|
||||
|
||||
try:
|
||||
from diffusers import ZImagePipeline
|
||||
from diffusers.models.transformers.transformer_z_image import (
|
||||
from toolkit.models.v2.diffusion_models.z_image import (
|
||||
ZImageTransformer2DModel as ZImageTransformer2DModelOriginal,
|
||||
)
|
||||
from diffusers.models.modeling_outputs import Transformer2DModelOutput
|
||||
@@ -442,57 +439,16 @@ class ZImageL2PModel(ZImageModel):
|
||||
if self.model_config.qtype == "qfloat8":
|
||||
self.model_config.qtype = "float8"
|
||||
|
||||
if self.model_config.quantize:
|
||||
self.print_and_status_update("Quantizing Transformer")
|
||||
quantize_model(self, transformer)
|
||||
flush()
|
||||
|
||||
if (
|
||||
self.model_config.layer_offloading
|
||||
and self.model_config.layer_offloading_transformer_percent > 0
|
||||
):
|
||||
MemoryManager.attach(
|
||||
transformer,
|
||||
self.device_torch,
|
||||
offload_percent=self.model_config.layer_offloading_transformer_percent,
|
||||
ignore_modules=[
|
||||
transformer.x_pad_token,
|
||||
transformer.cap_pad_token,
|
||||
],
|
||||
)
|
||||
|
||||
if self.model_config.low_vram:
|
||||
self.print_and_status_update("Moving transformer to CPU")
|
||||
transformer.to("cpu")
|
||||
# quantize + offload + placement, all driven by model_config
|
||||
transformer.aitk_post_load(**self.component_load_kwargs("transformer"))
|
||||
|
||||
flush()
|
||||
|
||||
self.print_and_status_update("Text Encoder")
|
||||
tokenizer = AutoTokenizer.from_pretrained(
|
||||
base_model_path, subfolder="tokenizer", torch_dtype=dtype
|
||||
tokenizer = Qwen3TextEncoder.load_tokenizer(base_model_path)
|
||||
text_encoder = Qwen3TextEncoder.load(
|
||||
base_model_path, **self.component_load_kwargs("te")
|
||||
)
|
||||
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")
|
||||
# vae = AutoencoderKL.from_pretrained(
|
||||
|
||||
@@ -13,21 +13,17 @@ from toolkit.samplers.custom_flowmatch_sampler import (
|
||||
CustomFlowMatchEulerDiscreteScheduler,
|
||||
)
|
||||
from toolkit.accelerator import unwrap_model
|
||||
from optimum.quanto import freeze
|
||||
from toolkit.util.quantize import quantize, get_qtype, quantize_model
|
||||
from toolkit.memory_management import MemoryManager
|
||||
from safetensors.torch import load_file
|
||||
from optimum.quanto import QTensor
|
||||
from toolkit.metadata import get_meta_for_safetensors
|
||||
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 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_pipeline import ZetaChromaPipeline
|
||||
|
||||
|
||||
|
||||
scheduler_config = {
|
||||
"num_train_timesteps": 1000,
|
||||
"use_dynamic_shifting": False,
|
||||
@@ -94,10 +90,6 @@ class ZetaChromaModel(BaseModel):
|
||||
|
||||
transformer_state_dict = load_file(transformer_path, device="cpu")
|
||||
|
||||
# cast to dtype
|
||||
for key in transformer_state_dict:
|
||||
transformer_state_dict[key] = transformer_state_dict[key].to(dtype)
|
||||
|
||||
# Auto-detect use_x0 from checkpoint
|
||||
use_x0 = "__x0__" in transformer_state_dict
|
||||
|
||||
@@ -109,65 +101,21 @@ class ZetaChromaModel(BaseModel):
|
||||
use_x0=use_x0,
|
||||
)
|
||||
|
||||
with torch.device("meta"):
|
||||
transformer = ZImageDCT(model_params)
|
||||
|
||||
transformer.load_state_dict(transformer_state_dict, assign=True)
|
||||
transformer = ZImageDCT.load_from_state_dict(
|
||||
transformer_state_dict, dtype, config=model_params
|
||||
)
|
||||
del transformer_state_dict
|
||||
|
||||
transformer.to(self.quantize_device, dtype=dtype)
|
||||
|
||||
if self.model_config.quantize:
|
||||
self.print_and_status_update("Quantizing Transformer")
|
||||
quantize_model(self, transformer)
|
||||
flush()
|
||||
|
||||
if (
|
||||
self.model_config.layer_offloading
|
||||
and self.model_config.layer_offloading_transformer_percent > 0
|
||||
):
|
||||
MemoryManager.attach(
|
||||
transformer,
|
||||
self.device_torch,
|
||||
offload_percent=self.model_config.layer_offloading_transformer_percent,
|
||||
ignore_modules=[
|
||||
transformer.x_pad_token,
|
||||
transformer.cap_pad_token,
|
||||
],
|
||||
)
|
||||
|
||||
if self.model_config.low_vram:
|
||||
self.print_and_status_update("Moving transformer to CPU")
|
||||
transformer.to("cpu")
|
||||
# quantize + offload + placement, all driven by model_config
|
||||
transformer.aitk_post_load(**self.component_load_kwargs("transformer"))
|
||||
|
||||
flush()
|
||||
|
||||
self.print_and_status_update("Text Encoder")
|
||||
tokenizer = AutoTokenizer.from_pretrained(
|
||||
base_model_path, subfolder="tokenizer", torch_dtype=dtype
|
||||
tokenizer = Qwen3TextEncoder.load_tokenizer(base_model_path)
|
||||
text_encoder = Qwen3TextEncoder.load(
|
||||
base_model_path, **self.component_load_kwargs("te")
|
||||
)
|
||||
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")
|
||||
vae = FakeVAE(scaling_factor=1.0)
|
||||
@@ -201,8 +149,10 @@ class ZetaChromaModel(BaseModel):
|
||||
pipe.transformer = pipe.transformer.to(self.device_torch)
|
||||
|
||||
flush()
|
||||
# just to make sure everything is on the right device and dtype
|
||||
text_encoder[0].to(self.device_torch)
|
||||
# low_vram: the text encoder stays on cpu; get_prompt_embeds moves it
|
||||
# to the gpu on demand
|
||||
if not self.low_vram:
|
||||
text_encoder[0].to(self.device_torch)
|
||||
text_encoder[0].requires_grad_(False)
|
||||
text_encoder[0].eval()
|
||||
flush()
|
||||
@@ -368,16 +318,5 @@ class ZetaChromaModel(BaseModel):
|
||||
def get_transformer_block_names(self) -> Optional[List[str]]:
|
||||
return ["layers"]
|
||||
|
||||
def convert_lora_weights_before_save(self, state_dict):
|
||||
new_sd = {}
|
||||
for key, value in state_dict.items():
|
||||
new_key = key.replace("transformer.", "diffusion_model.")
|
||||
new_sd[new_key] = value
|
||||
return new_sd
|
||||
lora_keys_use_comfy_prefix = True
|
||||
|
||||
def convert_lora_weights_before_load(self, state_dict):
|
||||
new_sd = {}
|
||||
for key, value in state_dict.items():
|
||||
new_key = key.replace("diffusion_model.", "transformer.")
|
||||
new_sd[new_key] = value
|
||||
return new_sd
|
||||
@@ -6,6 +6,8 @@ from typing import List, Optional
|
||||
import math
|
||||
|
||||
import torch
|
||||
|
||||
from toolkit.models.v2._mixin import OstrisModelMixin
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
from torch import Tensor
|
||||
@@ -449,7 +451,14 @@ class SimpleMLPAdaLN(nn.Module):
|
||||
return self.final_layer(x)
|
||||
|
||||
|
||||
class ZImageDCT(nn.Module):
|
||||
class ZImageDCT(nn.Module, OstrisModelMixin):
|
||||
@classmethod
|
||||
def get_transformer_block_names(cls):
|
||||
return ["layers"]
|
||||
|
||||
def get_offload_ignore_modules(self):
|
||||
return [self.x_pad_token, self.cap_pad_token]
|
||||
|
||||
def __init__(self, params: ZImageDCTParams):
|
||||
super().__init__()
|
||||
self.config = FakeConfig()
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
"""Per-stage speed / memory / CPU profiler for the model loading test harness.
|
||||
|
||||
A StageProfiler splits a run into named stages (the harness hooks
|
||||
BaseModel.print_and_status_update so every holder status line — "Loading
|
||||
transformer", "Quantizing (convrot8)", "Loading VAE", ... — opens a new
|
||||
stage). For every stage it records:
|
||||
|
||||
- seconds wall time
|
||||
- vram_peak_gb torch.cuda.max_memory_allocated during the stage
|
||||
- vram_reserved_gb torch.cuda.max_memory_reserved during the stage
|
||||
- vram_end_gb memory_allocated at the stage boundary (resident model)
|
||||
- rss_peak_gb peak process RSS (sampled at 50ms)
|
||||
- cpu_avg_pct / process CPU utilization (100 = one full core), sampled
|
||||
cpu_max_pct at 50ms — this is what shows where quantization burns CPU
|
||||
- threads_max peak thread count
|
||||
|
||||
A background sampler thread does the RSS/CPU sampling so short spikes inside
|
||||
a stage are caught, not just the boundary values.
|
||||
"""
|
||||
|
||||
import threading
|
||||
import time
|
||||
|
||||
import psutil
|
||||
import torch
|
||||
|
||||
_GB = 1024**3
|
||||
|
||||
|
||||
class _Sampler:
|
||||
"""Background thread sampling RSS / CPU% / thread count at 50ms."""
|
||||
|
||||
def __init__(self):
|
||||
self.proc = psutil.Process()
|
||||
self._lock = threading.Lock()
|
||||
self._stop = False
|
||||
self.reset()
|
||||
self.proc.cpu_percent(None) # prime the cpu_percent window
|
||||
self._thread = threading.Thread(target=self._run, daemon=True)
|
||||
self._thread.start()
|
||||
|
||||
def reset(self):
|
||||
with self._lock:
|
||||
self.rss_peak = self.proc.memory_info().rss
|
||||
self.cpu_samples = []
|
||||
self.threads_max = self.proc.num_threads()
|
||||
|
||||
def _run(self):
|
||||
while not self._stop:
|
||||
try:
|
||||
rss = self.proc.memory_info().rss
|
||||
cpu = self.proc.cpu_percent(None)
|
||||
threads = self.proc.num_threads()
|
||||
with self._lock:
|
||||
self.rss_peak = max(self.rss_peak, rss)
|
||||
self.cpu_samples.append(cpu)
|
||||
self.threads_max = max(self.threads_max, threads)
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(0.05)
|
||||
|
||||
def stats(self):
|
||||
with self._lock:
|
||||
samples = self.cpu_samples or [0.0]
|
||||
return {
|
||||
"rss_peak_gb": round(self.rss_peak / _GB, 3),
|
||||
"cpu_avg_pct": round(sum(samples) / len(samples), 1),
|
||||
"cpu_max_pct": round(max(samples), 1),
|
||||
"threads_max": self.threads_max,
|
||||
}
|
||||
|
||||
def stop(self):
|
||||
self._stop = True
|
||||
|
||||
|
||||
class StageProfiler:
|
||||
def __init__(self, device):
|
||||
self.device = torch.device(device) if torch.cuda.is_available() else None
|
||||
self.stages = []
|
||||
self._current = None
|
||||
self._sampler = _Sampler()
|
||||
|
||||
def _cuda_sync_reset(self):
|
||||
if self.device is not None:
|
||||
torch.cuda.synchronize(self.device)
|
||||
torch.cuda.reset_peak_memory_stats(self.device)
|
||||
|
||||
def stage(self, name: str):
|
||||
"""Close the current stage (if any) and open a new one."""
|
||||
self._close()
|
||||
self._cuda_sync_reset()
|
||||
self._sampler.reset()
|
||||
self._current = {"name": str(name), "t0": time.perf_counter()}
|
||||
|
||||
def _close(self):
|
||||
if self._current is None:
|
||||
return
|
||||
cur = self._current
|
||||
self._current = None
|
||||
seconds = time.perf_counter() - cur["t0"]
|
||||
entry = {"name": cur["name"], "seconds": round(seconds, 3)}
|
||||
if self.device is not None:
|
||||
torch.cuda.synchronize(self.device)
|
||||
entry["vram_peak_gb"] = round(
|
||||
torch.cuda.max_memory_allocated(self.device) / _GB, 3
|
||||
)
|
||||
entry["vram_reserved_gb"] = round(
|
||||
torch.cuda.max_memory_reserved(self.device) / _GB, 3
|
||||
)
|
||||
entry["vram_end_gb"] = round(
|
||||
torch.cuda.memory_allocated(self.device) / _GB, 3
|
||||
)
|
||||
entry.update(self._sampler.stats())
|
||||
self.stages.append(entry)
|
||||
|
||||
def finish(self):
|
||||
self._close()
|
||||
self._sampler.stop()
|
||||
return self.stages
|
||||
|
||||
|
||||
def profile_top(profile, limit=40):
|
||||
"""Top functions from a cProfile.Profile by cumulative time, as dicts."""
|
||||
import pstats
|
||||
|
||||
stats = pstats.Stats(profile)
|
||||
stats.sort_stats("cumulative")
|
||||
rows = []
|
||||
for func in stats.fcn_list[: limit * 3]:
|
||||
cc, nc, tt, ct, _ = stats.stats[func]
|
||||
filename, line, name = func
|
||||
# drop the profiler/exec wrappers and trivial rows
|
||||
if name in ("<module>",) or ct < 0.05:
|
||||
continue
|
||||
# cProfile catches this module's own 50ms sampling thread; its idle
|
||||
# sleep loop and psutil polling otherwise show up as a giant fake
|
||||
# "time.sleep" hotspot spanning the whole stage
|
||||
if "stage_profiler" in filename or "psutil" in filename or "_pslinux" in filename:
|
||||
continue
|
||||
if name == "<built-in method time.sleep>":
|
||||
continue
|
||||
# shorten site-packages / repo paths to keep the report readable
|
||||
for marker in ("site-packages/", "ai-toolkit/"):
|
||||
if marker in filename:
|
||||
filename = filename.split(marker, 1)[1]
|
||||
break
|
||||
rows.append(
|
||||
{
|
||||
"func": f"{filename}:{line}({name})",
|
||||
"ncalls": nc,
|
||||
"tottime": round(tt, 3),
|
||||
"cumtime": round(ct, 3),
|
||||
}
|
||||
)
|
||||
if len(rows) >= limit:
|
||||
break
|
||||
return rows
|
||||
@@ -0,0 +1,702 @@
|
||||
"""Per-arch model loading + inference smoke test (toolkit/models/v2/PLANNING.md).
|
||||
|
||||
Loads one registered arch through its normal loading path (the same
|
||||
get_model_class -> ModelClass(...).load_model() flow training uses), runs one
|
||||
small sample generation, and asserts an output file was produced.
|
||||
|
||||
Usage:
|
||||
python testing/test_model_loading.py --arch zimage # one arch, in-process
|
||||
python testing/test_model_loading.py --all # every registered arch,
|
||||
# one subprocess each (full
|
||||
# unload between archs)
|
||||
--allow-download permit hub downloads (default: HF_HUB_OFFLINE=1, so archs
|
||||
whose weights are not local/cached report SKIP)
|
||||
--list list registered archs
|
||||
--device cuda:0
|
||||
|
||||
Add a new model type by adding an entry to MODEL_TESTS.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
TOOLKIT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.path.insert(0, TOOLKIT_ROOT)
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# repo .env carries HF_TOKEN / HF_HOME / MODELS_PATH etc., same as run.py
|
||||
load_dotenv(os.path.join(TOOLKIT_ROOT, ".env"))
|
||||
|
||||
OUTPUT_ROOT = os.path.join(TOOLKIT_ROOT, "testing", ".model_test_outputs")
|
||||
|
||||
# arch -> {"model": ModelConfig kwargs, "sample": GenerateImageConfig kwargs}
|
||||
# Keep samples tiny: this asserts the load/encode/denoise/decode/save path
|
||||
# works, not quality.
|
||||
IMG = {"width": 512, "height": 512, "num_inference_steps": 8, "seed": 42}
|
||||
VID = {"width": 256, "height": 256, "num_inference_steps": 6, "seed": 42, "num_frames": 9}
|
||||
|
||||
MODEL_TESTS = {
|
||||
"zimage": {
|
||||
"model": {"name_or_path": "Tongyi-MAI/Z-Image-Turbo"},
|
||||
"sample": {**IMG, "guidance_scale": 1.0},
|
||||
},
|
||||
"qwen_image": {
|
||||
# 20B: quantize to fit a 32GB card
|
||||
"model": {"name_or_path": "Qwen/Qwen-Image", "quantize": True, "quantize_te": True},
|
||||
"sample": {**IMG, "num_inference_steps": 20, "guidance_scale": 4.0},
|
||||
},
|
||||
"krea2": {
|
||||
"model": {"name_or_path": "krea/Krea-2-Turbo", "quantize": True, "quantize_te": True},
|
||||
"sample": {**IMG, "guidance_scale": 1.0},
|
||||
},
|
||||
"boogu_image": {
|
||||
# native ~1024; 512/low-step/high-CFG degenerates to a black frame
|
||||
"model": {"name_or_path": "Boogu/Boogu-Image-0.1-Base", "quantize": True, "quantize_te": True},
|
||||
"sample": {"width": 512, "height": 512, "num_inference_steps": 25, "guidance_scale": 4.0, "seed": 42},
|
||||
},
|
||||
"ernie_image": {
|
||||
"model": {"name_or_path": "baidu/ERNIE-Image", "quantize": True, "quantize_te": True},
|
||||
"sample": {"width": 512, "height": 512, "num_inference_steps": 25, "guidance_scale": 4.0, "seed": 42},
|
||||
},
|
||||
"mageflow": {
|
||||
"model": {"name_or_path": "microsoft/Mage-Flow-Base", "quantize": True, "quantize_te": True},
|
||||
"sample": IMG,
|
||||
},
|
||||
"ideogram4": {
|
||||
"model": {"name_or_path": "ideogram-ai/ideogram-4-fp8", "quantize": True, "quantize_te": True},
|
||||
"sample": {"width": 512, "height": 512, "num_inference_steps": 25, "guidance_scale": 4.0, "seed": 42},
|
||||
},
|
||||
"hidream_o1": {
|
||||
"model": {"name_or_path": "HiDream-ai/HiDream-O1-Image", "quantize": True, "quantize_te": True},
|
||||
"sample": {"width": 512, "height": 512, "num_inference_steps": 28, "guidance_scale": 5.0, "seed": 42},
|
||||
},
|
||||
"anima": {
|
||||
"model": {"name_or_path": "circlestone-labs/Anima-Base-v1.0-Diffusers"},
|
||||
"sample": {"width": 512, "height": 512, "num_inference_steps": 25, "guidance_scale": 4.5, "seed": 42},
|
||||
},
|
||||
"wan21": {
|
||||
"model": {"name_or_path": "Wan-AI/Wan2.1-T2V-1.3B-Diffusers"},
|
||||
"sample": {"width": 480, "height": 480, "num_inference_steps": 20, "guidance_scale": 5.0, "seed": 42, "num_frames": 17},
|
||||
},
|
||||
"wan22_5b": {
|
||||
"model": {"name_or_path": "Wan-AI/Wan2.2-TI2V-5B-Diffusers", "quantize": True, "quantize_te": True},
|
||||
"sample": {"width": 480, "height": 480, "num_inference_steps": 20, "guidance_scale": 5.0, "seed": 42, "num_frames": 17},
|
||||
},
|
||||
"ltx2.3": {
|
||||
# even quantized, the 22B stack does not fit a 32GB card — needs the big GPU
|
||||
"model": {"name_or_path": "Lightricks/LTX-2.3/ltx-2.3-22b-dev.safetensors", "quantize": True, "quantize_te": True},
|
||||
"sample": {"width": 512, "height": 512, "num_inference_steps": 25, "guidance_scale": 3.0, "seed": 42, "num_frames": 25},
|
||||
},
|
||||
# single-file / comfy-layout archs: weights resolve under MODELS_PATH (or
|
||||
# download there with --allow-download)
|
||||
"chroma": {
|
||||
"model": {"name_or_path": "lodestones/Chroma1-HD", "quantize": True, "quantize_te": True},
|
||||
"sample": {**IMG, "num_inference_steps": 26, "guidance_scale": 4.0},
|
||||
},
|
||||
"flux_kontext": {
|
||||
"model": {"name_or_path": "black-forest-labs/FLUX.1-Kontext-dev", "quantize": True, "quantize_te": True},
|
||||
"sample": {**IMG, "num_inference_steps": 20, "guidance_scale": 2.5},
|
||||
"needs_control_image": True,
|
||||
},
|
||||
"flux2_klein_4b": {
|
||||
"model": {"name_or_path": "black-forest-labs/FLUX.2-klein-base-4B", "quantize_te": True},
|
||||
"sample": {**IMG, "num_inference_steps": 25, "guidance_scale": 4.0},
|
||||
},
|
||||
# ---- coverage for every UI-default arch (big downloads on first run) ----
|
||||
"wan22_14b": {
|
||||
"model": {"name_or_path": "ai-toolkit/Wan2.2-T2V-A14B-Diffusers-bf16", "quantize": True, "quantize_te": True, "low_vram": True},
|
||||
"sample": {"width": 480, "height": 480, "num_inference_steps": 20, "guidance_scale": 3.5, "seed": 42, "num_frames": 17},
|
||||
},
|
||||
"wan22_14b_i2v": {
|
||||
"model": {"name_or_path": "ai-toolkit/Wan2.2-I2V-A14B-Diffusers-bf16", "quantize": True, "quantize_te": True, "low_vram": True},
|
||||
"sample": {"width": 480, "height": 480, "num_inference_steps": 20, "guidance_scale": 3.5, "seed": 42, "num_frames": 17},
|
||||
"needs_control_image": True,
|
||||
},
|
||||
"wan21_i2v": {
|
||||
"model": {"name_or_path": "Wan-AI/Wan2.1-I2V-14B-480P-Diffusers", "quantize": True, "quantize_te": True},
|
||||
"sample": {"width": 480, "height": 480, "num_inference_steps": 20, "guidance_scale": 5.0, "seed": 42, "num_frames": 17},
|
||||
"needs_control_image": True,
|
||||
},
|
||||
"hidream": {
|
||||
"model": {"name_or_path": "HiDream-ai/HiDream-I1-Full", "quantize": True, "quantize_te": True},
|
||||
"sample": {"width": 512, "height": 512, "num_inference_steps": 28, "guidance_scale": 5.0, "seed": 42},
|
||||
},
|
||||
"hidream_e1": {
|
||||
# editing seq budget fits 768x768 (source+target concat)
|
||||
"model": {"name_or_path": "HiDream-ai/HiDream-E1-1", "quantize": True, "quantize_te": True},
|
||||
"sample": {"width": 768, "height": 768, "num_inference_steps": 28, "guidance_scale": 5.0, "seed": 42},
|
||||
"needs_control_image": True,
|
||||
},
|
||||
"nucleus_image": {
|
||||
"model": {"name_or_path": "NucleusAI/Nucleus-Image", "quantize": True, "quantize_te": True},
|
||||
"sample": {"width": 512, "height": 512, "num_inference_steps": 25, "guidance_scale": 4.0, "seed": 42},
|
||||
},
|
||||
"omnigen2": {
|
||||
"model": {"name_or_path": "OmniGen2/OmniGen2", "quantize": True, "quantize_te": True},
|
||||
"sample": {"width": 512, "height": 512, "num_inference_steps": 25, "guidance_scale": 4.0, "seed": 42},
|
||||
},
|
||||
"ltx2.5": {
|
||||
"model": {"name_or_path": "Lightricks/LTX-2.5", "quantize": True, "quantize_te": True},
|
||||
"sample": {"width": 512, "height": 512, "num_inference_steps": 25, "guidance_scale": 3.0, "seed": 42, "num_frames": 25},
|
||||
},
|
||||
"flux2": {
|
||||
"model": {"name_or_path": "black-forest-labs/FLUX.2-dev", "quantize": True, "quantize_te": True},
|
||||
"sample": {**IMG, "num_inference_steps": 25, "guidance_scale": 4.0},
|
||||
},
|
||||
"flux2_klein_9b": {
|
||||
"model": {"name_or_path": "black-forest-labs/FLUX.2-klein-base-9B", "quantize": True, "quantize_te": True},
|
||||
"sample": {**IMG, "num_inference_steps": 25, "guidance_scale": 4.0},
|
||||
},
|
||||
"prx_pixel": {
|
||||
"model": {"name_or_path": "Photoroom/prxpixel-t2i", "quantize_te": True},
|
||||
"sample": {**IMG, "num_inference_steps": 25, "guidance_scale": 4.0},
|
||||
},
|
||||
"zeta_chroma": {
|
||||
"model": {"name_or_path": "lodestones/Zeta-Chroma/zeta-chroma-base-x0-pixel-dino-distance.safetensors", "extras_name_or_path": "Tongyi-MAI/Z-Image-Turbo", "quantize": True, "quantize_te": True},
|
||||
"sample": {**IMG, "num_inference_steps": 25, "guidance_scale": 4.0},
|
||||
},
|
||||
"zimage_l2p": {
|
||||
"model": {"name_or_path": "zhen-nan/L2P/model-1k-merge.safetensors", "extras_name_or_path": "Tongyi-MAI/Z-Image-Turbo", "quantize_te": True},
|
||||
"sample": {**IMG, "guidance_scale": 1.0},
|
||||
},
|
||||
"qwen_image_edit": {
|
||||
"model": {"name_or_path": "Qwen/Qwen-Image-Edit", "quantize": True, "quantize_te": True},
|
||||
"sample": {**IMG, "num_inference_steps": 20, "guidance_scale": 4.0},
|
||||
"needs_control_image": True,
|
||||
},
|
||||
"qwen_image_edit_plus": {
|
||||
"model": {"name_or_path": "Qwen/Qwen-Image-Edit-2509", "quantize": True, "quantize_te": True},
|
||||
"sample": {**IMG, "num_inference_steps": 20, "guidance_scale": 4.0},
|
||||
"needs_control_image": True,
|
||||
},
|
||||
# ---- legacy monolith archs (components adopted into v2 on load) ----
|
||||
"sd1": {
|
||||
"model": {"name_or_path": "stable-diffusion-v1-5/stable-diffusion-v1-5"},
|
||||
"sample": {"width": 512, "height": 512, "num_inference_steps": 20, "guidance_scale": 7.5, "seed": 42},
|
||||
},
|
||||
"sdxl": {
|
||||
"model": {"name_or_path": "stabilityai/stable-diffusion-xl-base-1.0"},
|
||||
"sample": {"width": 512, "height": 512, "num_inference_steps": 25, "guidance_scale": 6.0, "seed": 42},
|
||||
},
|
||||
"ace_step_15": {
|
||||
"model": {"name_or_path": "ostris/ace_step_1.5_ComfyUI_files/ace_step_1.5_base_aio.safetensors", "quantize": True, "quantize_te": True},
|
||||
"sample": {"width": 512, "height": 512, "num_inference_steps": 20, "guidance_scale": 4.0, "seed": 42},
|
||||
},
|
||||
"f-lite": {
|
||||
"model": {"name_or_path": "Freepik/F-Lite", "quantize": True, "quantize_te": True},
|
||||
"sample": {"width": 512, "height": 512, "num_inference_steps": 25, "guidance_scale": 4.0, "seed": 42},
|
||||
},
|
||||
}
|
||||
|
||||
SKIP_MARKERS = (
|
||||
"couldn't connect",
|
||||
"offline mode",
|
||||
"hf_hub_offline",
|
||||
"cannot find the requested files",
|
||||
"not found in cache",
|
||||
"localentrynotfounderror",
|
||||
"does not appear to have a file named",
|
||||
"404 client error",
|
||||
"entrynotfounderror",
|
||||
"gatedrepoerror",
|
||||
"cannot access gated repo",
|
||||
"repositorynotfounderror",
|
||||
)
|
||||
|
||||
|
||||
def classify_error(err: BaseException) -> str:
|
||||
text = f"{type(err).__name__}: {err}".lower()
|
||||
if any(m in text for m in SKIP_MARKERS):
|
||||
return "SKIP"
|
||||
if isinstance(err, FileNotFoundError):
|
||||
return "SKIP"
|
||||
return "FAIL"
|
||||
|
||||
|
||||
def run_one(arch: str, device: str, allow_download: bool) -> dict:
|
||||
entry = MODEL_TESTS[arch]
|
||||
out_dir = os.path.join(OUTPUT_ROOT, arch.replace("/", "_").replace(":", "_"))
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
for old in glob.glob(os.path.join(out_dir, "*")):
|
||||
os.remove(old)
|
||||
|
||||
from toolkit.config_modules import GenerateImageConfig, ModelConfig
|
||||
from toolkit.util.get_model import get_model_class
|
||||
|
||||
# quantization always tests the convrot8 backend (per-entry override wins)
|
||||
model_kwargs = dict(entry["model"])
|
||||
if model_kwargs.get("quantize"):
|
||||
model_kwargs.setdefault("qtype", "convrot8")
|
||||
if model_kwargs.get("quantize_te"):
|
||||
model_kwargs.setdefault("qtype_te", "convrot8")
|
||||
|
||||
model_config = ModelConfig(arch=arch, dtype="bf16", **model_kwargs)
|
||||
ModelClass = get_model_class(model_config)
|
||||
from toolkit.util.get_model import LEGACY_ARCHS
|
||||
|
||||
if (
|
||||
getattr(ModelClass, "arch", None) not in (arch, model_config.arch)
|
||||
and model_config.arch not in LEGACY_ARCHS
|
||||
):
|
||||
raise ValueError(
|
||||
f"arch {arch!r} resolved to {ModelClass.__name__} "
|
||||
f"(arch={getattr(ModelClass, 'arch', None)!r}) — registry mismatch"
|
||||
)
|
||||
|
||||
sampler = None
|
||||
if hasattr(ModelClass, "get_train_scheduler"):
|
||||
sampler = ModelClass.get_train_scheduler()
|
||||
else:
|
||||
# legacy monolith archs build their scheduler the way training does
|
||||
from toolkit.sampler import get_sampler
|
||||
|
||||
legacy_arch = "sd"
|
||||
if model_config.is_pixart:
|
||||
legacy_arch = "pixart"
|
||||
elif model_config.is_flux:
|
||||
legacy_arch = "flux"
|
||||
elif model_config.is_lumina2:
|
||||
legacy_arch = "lumina2"
|
||||
sampler = get_sampler(
|
||||
"ddpm",
|
||||
{
|
||||
"prediction_type": "v_prediction"
|
||||
if model_config.is_v_pred
|
||||
else "epsilon",
|
||||
},
|
||||
arch=legacy_arch,
|
||||
)
|
||||
|
||||
sd = ModelClass(
|
||||
device=device,
|
||||
model_config=model_config,
|
||||
dtype="bf16",
|
||||
noise_scheduler=sampler,
|
||||
)
|
||||
|
||||
# ---- per-stage speed / VRAM / CPU instrumentation + cProfile ----
|
||||
import cProfile
|
||||
|
||||
from testing.stage_profiler import StageProfiler, profile_top
|
||||
|
||||
prof = StageProfiler(device)
|
||||
if hasattr(sd, "print_and_status_update"):
|
||||
# every holder announces its stages through print_and_status_update;
|
||||
# each status line opens a new profiler stage
|
||||
_orig_status = sd.print_and_status_update
|
||||
|
||||
def _status_hook(msg, *a, **k):
|
||||
prof.stage(str(msg))
|
||||
return _orig_status(msg, *a, **k)
|
||||
|
||||
sd.print_and_status_update = _status_hook
|
||||
|
||||
prof.stage("load: init")
|
||||
load_profile = cProfile.Profile()
|
||||
load_profile.enable()
|
||||
t_load0 = time.perf_counter()
|
||||
sd.load_model()
|
||||
load_seconds = time.perf_counter() - t_load0
|
||||
load_profile.disable()
|
||||
load_profile.dump_stats(os.path.join(out_dir, "load.prof"))
|
||||
|
||||
sample_kwargs = dict(entry["sample"])
|
||||
if entry.get("needs_control_image"):
|
||||
# edit/kontext models require a control image; a flat gray input is fine
|
||||
from PIL import Image
|
||||
|
||||
ctrl_path = os.path.join(out_dir, ".ctrl.png")
|
||||
Image.new(
|
||||
"RGB", (sample_kwargs["width"], sample_kwargs["height"]), (128, 128, 128)
|
||||
).save(ctrl_path)
|
||||
sample_kwargs["ctrl_img"] = ctrl_path
|
||||
gen = GenerateImageConfig(
|
||||
prompt="a photo of a cat sitting on a wooden table",
|
||||
output_folder=out_dir,
|
||||
# the GenerateImageConfig default for output_ext is the Literal type
|
||||
# alias itself; real callers always pass one
|
||||
output_ext="png",
|
||||
**sample_kwargs,
|
||||
)
|
||||
gen_kwargs = {}
|
||||
if not hasattr(ModelClass, "get_train_scheduler"):
|
||||
# the legacy monolith takes the sampler NAME at generate time
|
||||
gen_kwargs["sampler"] = "ddpm"
|
||||
|
||||
prof.stage("generate")
|
||||
gen_profile = cProfile.Profile()
|
||||
gen_profile.enable()
|
||||
t_gen0 = time.perf_counter()
|
||||
sd.generate_images([gen], **gen_kwargs)
|
||||
gen_seconds = time.perf_counter() - t_gen0
|
||||
gen_profile.disable()
|
||||
gen_profile.dump_stats(os.path.join(out_dir, "gen.prof"))
|
||||
|
||||
# ---- 100% offload forward: attach full layer offloading to the DiT and
|
||||
# text encoder(s), then run a 1-step sample so the offload path (TE
|
||||
# encode + denoise + decode with every layer staged from cpu) is proven
|
||||
# for every arch ----
|
||||
import torch
|
||||
|
||||
from toolkit.memory_management import MemoryManager
|
||||
|
||||
prof.stage("offload 100%: attach")
|
||||
offload_targets = []
|
||||
model_module = getattr(sd, "model", None)
|
||||
if model_module is not None:
|
||||
subs = [
|
||||
getattr(model_module, name, None)
|
||||
for name in ("transformer_1", "transformer_2")
|
||||
]
|
||||
if all(s is not None for s in subs):
|
||||
# dual-DiT wrappers (wan22 14b): pipelines hold and .to() the
|
||||
# sub-transformers directly, so each needs its own manager
|
||||
offload_targets.extend(subs)
|
||||
else:
|
||||
offload_targets.append(model_module)
|
||||
tes = getattr(sd, "text_encoder", None)
|
||||
for te in tes if isinstance(tes, list) else [tes]:
|
||||
offload_targets.append(te)
|
||||
# auxiliary conditioning stacks where archs have them (ltx connectors,
|
||||
# i2v vision towers)
|
||||
offload_targets.append(getattr(sd, "image_encoder", None))
|
||||
offload_targets.append(getattr(getattr(sd, "pipeline", None), "connectors", None))
|
||||
attached = 0
|
||||
for m in offload_targets:
|
||||
if not isinstance(m, torch.nn.Module):
|
||||
continue
|
||||
if type(m).__name__.startswith("Fake"):
|
||||
continue
|
||||
if next(m.parameters(), None) is None:
|
||||
continue
|
||||
get_ignore = getattr(m, "get_offload_ignore_modules", None)
|
||||
ignore = get_ignore() if callable(get_ignore) else None
|
||||
MemoryManager.attach(
|
||||
m,
|
||||
torch.device(device),
|
||||
offload_percent=1.0,
|
||||
ignore_modules=list(ignore or []),
|
||||
)
|
||||
attached += 1
|
||||
|
||||
prof.stage("offload 100%: generate")
|
||||
offload_profile = cProfile.Profile()
|
||||
offload_profile.enable()
|
||||
t_off0 = time.perf_counter()
|
||||
# 2 steps, not 1: diffusers' shift_terminal stretch divides by
|
||||
# one_minus_z[-1], which is zero for a single step — NaN timesteps and a
|
||||
# NaN image would mask real numerical breakage in the offload path
|
||||
gen_offload = GenerateImageConfig(
|
||||
prompt="a photo of a cat sitting on a wooden table",
|
||||
output_folder=out_dir,
|
||||
output_ext="png",
|
||||
**{**sample_kwargs, "num_inference_steps": 2},
|
||||
)
|
||||
sd.generate_images([gen_offload], **gen_kwargs)
|
||||
offload_seconds = time.perf_counter() - t_off0
|
||||
offload_profile.disable()
|
||||
offload_profile.dump_stats(os.path.join(out_dir, "offload.prof"))
|
||||
|
||||
stages = prof.finish()
|
||||
|
||||
produced = [
|
||||
p
|
||||
for p in glob.glob(os.path.join(out_dir, "*"))
|
||||
if os.path.isfile(p)
|
||||
and os.path.getsize(p) > 1024
|
||||
and not p.endswith((".txt", ".prof", ".json"))
|
||||
]
|
||||
if not produced:
|
||||
raise RuntimeError(f"no output file produced in {out_dir}")
|
||||
|
||||
result = {
|
||||
"arch": arch,
|
||||
"status": "PASS",
|
||||
"outputs": produced,
|
||||
"qtype": model_config.qtype if model_kwargs.get("quantize") else None,
|
||||
"qtype_te": model_config.qtype_te if model_kwargs.get("quantize_te") else None,
|
||||
"load_seconds": round(load_seconds, 3),
|
||||
"gen_seconds": round(gen_seconds, 3),
|
||||
"offload_seconds": round(offload_seconds, 3),
|
||||
"offload_modules_attached": attached,
|
||||
"stages": stages,
|
||||
"profile": {
|
||||
"load_top": profile_top(load_profile),
|
||||
"gen_top": profile_top(gen_profile, limit=15),
|
||||
"offload_top": profile_top(offload_profile, limit=15),
|
||||
"load_prof": os.path.join(out_dir, "load.prof"),
|
||||
"gen_prof": os.path.join(out_dir, "gen.prof"),
|
||||
"offload_prof": os.path.join(out_dir, "offload.prof"),
|
||||
},
|
||||
"env": {
|
||||
"torch_num_threads": torch.get_num_threads(),
|
||||
"cpu_count": os.cpu_count(),
|
||||
"omp_num_threads": os.environ.get("OMP_NUM_THREADS"),
|
||||
},
|
||||
}
|
||||
with open(os.path.join(out_dir, "metrics.json"), "w") as f:
|
||||
json.dump(result, f, indent=2)
|
||||
return result
|
||||
|
||||
|
||||
def format_stage_table(stages) -> str:
|
||||
"""Aligned markdown table: stage column left-aligned, numbers right-aligned
|
||||
with fixed decimals so they line up in the terminal too."""
|
||||
headers = [
|
||||
"stage", "seconds", "vram peak GB", "vram resv GB", "vram end GB",
|
||||
"rss peak GB", "cpu avg %", "cpu max %", "threads",
|
||||
]
|
||||
|
||||
def fmt(value, decimals):
|
||||
if value is None or value == "-":
|
||||
return "-"
|
||||
return f"{value:.{decimals}f}"
|
||||
|
||||
rows = [
|
||||
[
|
||||
s["name"],
|
||||
fmt(s.get("seconds"), 2),
|
||||
fmt(s.get("vram_peak_gb", "-"), 2),
|
||||
fmt(s.get("vram_reserved_gb", "-"), 2),
|
||||
fmt(s.get("vram_end_gb", "-"), 2),
|
||||
fmt(s.get("rss_peak_gb"), 2),
|
||||
fmt(s.get("cpu_avg_pct"), 1),
|
||||
fmt(s.get("cpu_max_pct"), 1),
|
||||
str(s.get("threads_max", "-")),
|
||||
]
|
||||
for s in stages
|
||||
]
|
||||
|
||||
widths = [
|
||||
max(len(headers[i]), *(len(r[i]) for r in rows)) if rows else len(headers[i])
|
||||
for i in range(len(headers))
|
||||
]
|
||||
|
||||
def line(cells):
|
||||
out = []
|
||||
for i, cell in enumerate(cells):
|
||||
# stage name left-aligned, everything else right-aligned
|
||||
out.append(cell.ljust(widths[i]) if i == 0 else cell.rjust(widths[i]))
|
||||
return "| " + " | ".join(out) + " |"
|
||||
|
||||
sep = "|" + "|".join(
|
||||
(":" + "-" * (w + 1)) if i == 0 else ("-" * (w + 1) + ":")
|
||||
for i, w in enumerate(widths)
|
||||
) + "|"
|
||||
return "\n".join([line(headers), sep] + [line(r) for r in rows])
|
||||
|
||||
|
||||
def write_report(results, path):
|
||||
"""Aggregated markdown report: per-arch stage tables + cross-arch summary
|
||||
+ load-profile hotspots, for diagnosing load/quantization speedups and
|
||||
CPU usage."""
|
||||
|
||||
def _sum(stages, pred):
|
||||
return round(sum(s["seconds"] for s in stages if pred(s)), 1)
|
||||
|
||||
lines = ["# Model loading test report", ""]
|
||||
lines.append(
|
||||
"Per-arch stage timings, peak VRAM (torch allocated/reserved), peak "
|
||||
"process RSS, and CPU utilization (100 = one core, sampled at 50ms). "
|
||||
"Quantization is convrot8 across the board. Full cProfile dumps sit "
|
||||
"next to each arch's outputs (load.prof / gen.prof; inspect with "
|
||||
"`python -m pstats <file>` or snakeviz)."
|
||||
)
|
||||
lines.append("")
|
||||
|
||||
# ---- summary ----
|
||||
lines += ["## Summary", ""]
|
||||
lines += [
|
||||
"| arch | status | load s | quantize s | generate s | vram peak GB | rss peak GB | quant cpu avg % |",
|
||||
"|---|---|---|---|---|---|---|---|",
|
||||
]
|
||||
for r in results:
|
||||
if r["status"] != "PASS" or "stages" not in r:
|
||||
lines.append(
|
||||
f"| {r['arch']} | {r['status']} | - | - | - | - | - | - | "
|
||||
)
|
||||
continue
|
||||
stages = r["stages"]
|
||||
quant = [s for s in stages if "uantiz" in s["name"]]
|
||||
vram_peak = max((s.get("vram_peak_gb", 0) for s in stages), default=0)
|
||||
rss_peak = max((s.get("rss_peak_gb", 0) for s in stages), default=0)
|
||||
quant_cpu = (
|
||||
round(sum(s["cpu_avg_pct"] for s in quant) / len(quant), 1)
|
||||
if quant
|
||||
else "-"
|
||||
)
|
||||
lines.append(
|
||||
f"| {r['arch']} | PASS | {r['load_seconds']} "
|
||||
f"| {_sum(quant, lambda s: True)} | {r['gen_seconds']} "
|
||||
f"| {vram_peak} | {rss_peak} | {quant_cpu} |"
|
||||
)
|
||||
lines.append("")
|
||||
|
||||
# ---- per-arch detail ----
|
||||
for r in results:
|
||||
if r["status"] != "PASS" or "stages" not in r:
|
||||
lines += [f"## {r['arch']} — {r['status']}", "", r.get("error", ""), ""]
|
||||
continue
|
||||
lines += [f"## {r['arch']}", ""]
|
||||
lines += [
|
||||
f"load {r['load_seconds']}s, generate {r['gen_seconds']}s, "
|
||||
f"qtype {r.get('qtype')}, qtype_te {r.get('qtype_te')}, "
|
||||
f"torch threads {r['env']['torch_num_threads']}/{r['env']['cpu_count']} cores",
|
||||
"",
|
||||
]
|
||||
lines += [format_stage_table(r["stages"]), ""]
|
||||
top = r.get("profile", {}).get("load_top", [])[:12]
|
||||
if top:
|
||||
lines += ["Load hotspots (cumulative):", "", "```"]
|
||||
for row in top:
|
||||
lines.append(
|
||||
f"{row['cumtime']:>9.2f}s tot {row['tottime']:>8.2f}s "
|
||||
f"x{row['ncalls']:<8} {row['func']}"
|
||||
)
|
||||
lines += ["```", ""]
|
||||
|
||||
with open(path, "w") as f:
|
||||
f.write("\n".join(lines))
|
||||
return path
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--arch", type=str, default=None)
|
||||
parser.add_argument("--all", action="store_true")
|
||||
parser.add_argument("--list", action="store_true")
|
||||
parser.add_argument("--device", type=str, default="cuda:0")
|
||||
parser.add_argument("--allow-download", action="store_true")
|
||||
parser.add_argument("--json-result", type=str, default=None)
|
||||
parser.add_argument(
|
||||
"--report-only",
|
||||
action="store_true",
|
||||
help="rebuild report.md from each arch's saved metrics.json, no runs",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.list:
|
||||
for arch in MODEL_TESTS:
|
||||
print(arch)
|
||||
return
|
||||
|
||||
if args.report_only:
|
||||
results = []
|
||||
for arch in MODEL_TESTS:
|
||||
out_dir = os.path.join(
|
||||
OUTPUT_ROOT, arch.replace("/", "_").replace(":", "_")
|
||||
)
|
||||
metrics_path = os.path.join(out_dir, "metrics.json")
|
||||
if os.path.exists(metrics_path):
|
||||
with open(metrics_path) as f:
|
||||
results.append(json.load(f))
|
||||
else:
|
||||
results.append(
|
||||
{"arch": arch, "status": "SKIP", "error": "no metrics.json saved"}
|
||||
)
|
||||
report_path = write_report(results, os.path.join(OUTPUT_ROOT, "report.md"))
|
||||
print(f"report: {report_path}")
|
||||
return
|
||||
|
||||
os.environ.setdefault("CUDA_DEVICE_ORDER", "PCI_BUS_ID")
|
||||
if not args.allow_download:
|
||||
os.environ.setdefault("HF_HUB_OFFLINE", "1")
|
||||
|
||||
if args.arch is not None:
|
||||
if args.arch not in MODEL_TESTS:
|
||||
raise SystemExit(
|
||||
f"arch {args.arch!r} is not registered; --list shows options"
|
||||
)
|
||||
try:
|
||||
result = run_one(args.arch, args.device, args.allow_download)
|
||||
except BaseException as err:
|
||||
status = classify_error(err)
|
||||
result = {"arch": args.arch, "status": status, "error": f"{type(err).__name__}: {err}"}
|
||||
if status == "FAIL":
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
if args.json_result:
|
||||
with open(args.json_result, "w") as f:
|
||||
json.dump(result, f)
|
||||
if result["status"] == "PASS" and "stages" in result:
|
||||
print()
|
||||
print(format_stage_table(result["stages"]))
|
||||
print()
|
||||
print(f"[{result['status']}] {args.arch}" + (f" — {result.get('error', '')}" if result["status"] != "PASS" else ""))
|
||||
if result["status"] == "FAIL":
|
||||
sys.exit(1)
|
||||
return
|
||||
|
||||
if not args.all:
|
||||
parser.print_help()
|
||||
return
|
||||
|
||||
# --all: one subprocess per arch so every model fully unloads (clean CUDA
|
||||
# teardown) before the next loads
|
||||
results = []
|
||||
total = len(MODEL_TESTS)
|
||||
sweep_t0 = time.perf_counter()
|
||||
for i, arch in enumerate(MODEL_TESTS, start=1):
|
||||
# flush so tail -f shows headers/progress live when stdout is a file
|
||||
print(f"\n===== {arch} ({i}/{total}) =====", flush=True)
|
||||
result_path = os.path.join(OUTPUT_ROOT, f".{arch.replace('/', '_')}.result.json")
|
||||
cmd = [
|
||||
sys.executable,
|
||||
os.path.abspath(__file__),
|
||||
"--arch",
|
||||
arch,
|
||||
"--device",
|
||||
args.device,
|
||||
"--json-result",
|
||||
result_path,
|
||||
]
|
||||
if args.allow_download:
|
||||
cmd.append("--allow-download")
|
||||
proc = subprocess.run(cmd, cwd=TOOLKIT_ROOT)
|
||||
if os.path.exists(result_path):
|
||||
with open(result_path) as f:
|
||||
results.append(json.load(f))
|
||||
os.remove(result_path)
|
||||
else:
|
||||
results.append(
|
||||
{"arch": arch, "status": "FAIL", "error": f"subprocess died (exit {proc.returncode})"}
|
||||
)
|
||||
|
||||
# running tally so a long sweep always shows how far along it is
|
||||
tally = {"PASS": 0, "FAIL": 0, "SKIP": 0}
|
||||
for r in results:
|
||||
tally[r["status"]] = tally.get(r["status"], 0) + 1
|
||||
elapsed = time.perf_counter() - sweep_t0
|
||||
eta = (elapsed / i) * (total - i)
|
||||
print(
|
||||
f">>> progress: {i}/{total} ({i * 100 // total}%) — "
|
||||
f"{tally['PASS']} pass, {tally['FAIL']} fail, {tally['SKIP']} skip — "
|
||||
f"elapsed {elapsed / 60:.0f}m, eta ~{eta / 60:.0f}m",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
print("\n===== summary =====")
|
||||
counts = {"PASS": 0, "FAIL": 0, "SKIP": 0}
|
||||
for r in results:
|
||||
counts[r["status"]] = counts.get(r["status"], 0) + 1
|
||||
line = f"[{r['status']}] {r['arch']}"
|
||||
if r["status"] != "PASS":
|
||||
line += f" — {r.get('error', '')[:160]}"
|
||||
print(line)
|
||||
print(f"\n{counts['PASS']} passed, {counts['FAIL']} failed, {counts['SKIP']} skipped")
|
||||
|
||||
report_path = write_report(results, os.path.join(OUTPUT_ROOT, "report.md"))
|
||||
print(f"report: {report_path}")
|
||||
if counts["FAIL"]:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -46,6 +46,10 @@ def hacked_state_dict(self, *args, **kwargs):
|
||||
|
||||
# hacks the state dict so we can dequantize before saving
|
||||
def patch_dequantization_on_save(model):
|
||||
# idempotent: re-patching would capture the hacked state_dict as
|
||||
# orig_state_dict and recurse on save
|
||||
if getattr(model, "orig_state_dict", None) is not None:
|
||||
return
|
||||
model.orig_state_dict = model.state_dict
|
||||
model.state_dict = partial(hacked_state_dict, model)
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ from .manager_modules import (
|
||||
LinearLayerMemoryManager,
|
||||
ConvLayerMemoryManager,
|
||||
OstrisLinearLayerMemoryManager,
|
||||
EmbeddingLayerMemoryManager,
|
||||
_DEVICE_STATE,
|
||||
)
|
||||
import random
|
||||
@@ -92,12 +93,59 @@ class MemoryManager:
|
||||
module._mm_to = module.to
|
||||
module.to = module._memory_manager.memory_managed_to
|
||||
|
||||
# add ignore modules to unmanaged list
|
||||
# a fully offloaded module's parameters all live on cpu, which makes
|
||||
# ModelMixin.device (and pipelines deriving their execution device
|
||||
# from it) report "cpu" and plant latents/timesteps there. Report the
|
||||
# compute device instead via an in-place subclass (same class-swap
|
||||
# pattern as OstrisLinear/adopt_component); detach() restores it.
|
||||
try:
|
||||
module._mm_orig_class = module.__class__
|
||||
managed_cls = type(
|
||||
module.__class__.__name__,
|
||||
(module.__class__,),
|
||||
{
|
||||
"device": property(
|
||||
lambda self: self._memory_manager.process_device,
|
||||
lambda self, value: self.__dict__.__setitem__(
|
||||
"_mm_device_shadow", value
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
# transformers keys per-class registries (e.g. the hidden-states
|
||||
# capture specs) by str(model.__class__); make the subclass
|
||||
# stringify identically so those lookups still hit
|
||||
managed_cls.__module__ = module.__class__.__module__
|
||||
managed_cls.__qualname__ = module.__class__.__qualname__
|
||||
module.__class__ = managed_cls
|
||||
except TypeError:
|
||||
# exotic class layouts (__slots__ etc.): keep the original class
|
||||
module._mm_orig_class = None
|
||||
|
||||
# add ignore modules to unmanaged list; they must stay RESIDENT on the
|
||||
# compute device (fp32 tables, pad tokens) — a model attached while
|
||||
# parked on cpu would otherwise feed cpu tensors into gpu math
|
||||
for im in ignore_modules:
|
||||
module._memory_manager.unmanaged_modules.append(im)
|
||||
try:
|
||||
if isinstance(im, torch.Tensor):
|
||||
im.data = im.data.to(device)
|
||||
elif isinstance(im, torch.nn.Module):
|
||||
im.to(device)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# count ignore modules as processed
|
||||
modules_processed = [x for x in ignore_modules]
|
||||
|
||||
# weights tied to an embedding (lm_head <-> embed_tokens) must not be
|
||||
# managed: pinning the linear's weight to cpu strands the (unmanaged)
|
||||
# embedding that shares the same tensor
|
||||
embedding_weight_ptrs = {
|
||||
m.weight.data_ptr()
|
||||
for m in module.modules()
|
||||
if isinstance(m, torch.nn.Embedding)
|
||||
}
|
||||
# attach to all modules
|
||||
for name, sub_module in module.named_modules():
|
||||
for child_name, child_module in sub_module.named_modules():
|
||||
@@ -110,6 +158,12 @@ class MemoryManager:
|
||||
# randomly skip some modules
|
||||
if random.random() > offload_percent:
|
||||
skip = True
|
||||
if (
|
||||
not getattr(child_module, "is_ostris_quantized", False)
|
||||
and isinstance(getattr(child_module, "weight", None), torch.Tensor)
|
||||
and child_module.weight.data_ptr() in embedding_weight_ptrs
|
||||
):
|
||||
skip = True
|
||||
if skip:
|
||||
module._memory_manager.unmanaged_modules.append(child_module)
|
||||
else:
|
||||
@@ -158,12 +212,40 @@ class MemoryManager:
|
||||
)
|
||||
modules_processed.append(ara)
|
||||
modules_processed.append(child_module)
|
||||
elif (
|
||||
isinstance(child_module, torch.nn.Embedding)
|
||||
and child_module not in modules_processed
|
||||
and child_module.weight.numel()
|
||||
* child_module.weight.element_size()
|
||||
> 64 * 1024 * 1024
|
||||
):
|
||||
# (the cpu gather is autograd-transparent, so a trainable
|
||||
# embedding still gets grads — they just land on cpu)
|
||||
# large frozen vocab table: cpu-resident, rows gathered on
|
||||
# cpu (only the looked-up tokens cross the bus)
|
||||
EmbeddingLayerMemoryManager.attach(
|
||||
child_module, module._memory_manager
|
||||
)
|
||||
# the cpu move replaced weight.data, so a tied lm_head no
|
||||
# longer matches the (original-ptr) tied guard and gets
|
||||
# managed normally — correct: the shared weight now lives
|
||||
# on cpu, so the linear must bounce it
|
||||
modules_processed.append(child_module)
|
||||
elif child_module.__class__.__name__ in UNMANAGED_MODULES or any(
|
||||
inc in child_module.__class__.__name__
|
||||
for inc in UNMANAGED_MODULES_INCLUDES
|
||||
):
|
||||
# unmanaged
|
||||
module._memory_manager.unmanaged_modules.append(child_module)
|
||||
# unmanaged — but never re-list a module the nested walk
|
||||
# already managed (a managed Embedding landing here would
|
||||
# get hauled back to the gpu by memory_managed_to), and
|
||||
# don't append duplicates
|
||||
if (
|
||||
child_module not in modules_processed
|
||||
and not hasattr(child_module, "_layer_memory_manager")
|
||||
and child_module
|
||||
not in module._memory_manager.unmanaged_modules
|
||||
):
|
||||
module._memory_manager.unmanaged_modules.append(child_module)
|
||||
else:
|
||||
continue
|
||||
|
||||
@@ -179,6 +261,10 @@ class MemoryManager:
|
||||
if not hasattr(module, "_memory_manager"):
|
||||
return
|
||||
|
||||
if getattr(module, "_mm_orig_class", None) is not None:
|
||||
module.__class__ = module._mm_orig_class
|
||||
del module._mm_orig_class
|
||||
|
||||
for unmanaged in module._memory_manager.unmanaged_modules:
|
||||
try:
|
||||
if isinstance(unmanaged, torch.Tensor):
|
||||
|
||||
@@ -312,6 +312,12 @@ class _BouncingLinearFn(torch.autograd.Function):
|
||||
ctx.device = torch.device("cpu")
|
||||
return out.to(x.device)
|
||||
|
||||
if x.device != device:
|
||||
# a pipeline that derives its execution device from the module's
|
||||
# (fully offloaded -> "cpu") parameters can feed cpu activations;
|
||||
# compute happens on the staged device and the output stays there
|
||||
x = x.to(device, non_blocking=True)
|
||||
|
||||
state = _get_device_state(device)
|
||||
# the guard makes current_stream() (used by the staging helpers' event
|
||||
# waits/records) resolve to the process device; without it they hit
|
||||
@@ -461,6 +467,11 @@ class _BouncingConv2dFn(torch.autograd.Function):
|
||||
ctx.meta = ("cpu", stride, padding, dilation, groups, target_dtype)
|
||||
return out.to(x.device)
|
||||
|
||||
if x.device != device:
|
||||
# cpu activations from a fully-offloaded pipeline: see
|
||||
# _BouncingLinearFn.forward
|
||||
x = x.to(device, non_blocking=True)
|
||||
|
||||
state = _get_device_state(device)
|
||||
# device guard: see _BouncingLinearFn.forward
|
||||
with torch.cuda.device(device):
|
||||
@@ -631,6 +642,42 @@ class BaseLayerMemoryManager:
|
||||
param._is_memory_managed = True
|
||||
|
||||
|
||||
class EmbeddingLayerMemoryManager(BaseLayerMemoryManager):
|
||||
"""Offloads a (large, frozen) nn.Embedding by keeping the weight on cpu
|
||||
and doing the row gather THERE: instead of staging a multi-GB vocab table
|
||||
to the gpu, only the looked-up rows (tokens x dim, ~KBs) cross the bus.
|
||||
Lookups happen once per prompt, so the cpu gather is free."""
|
||||
|
||||
def __init__(self, module: nn.Module, manager: "MemoryManager"):
|
||||
super().__init__(module, manager)
|
||||
|
||||
# cpu-resident weight; no pinning — the weight never crosses the bus
|
||||
module.weight.data = module.weight.data.to("cpu")
|
||||
|
||||
self._original_forward = module.forward
|
||||
|
||||
def _mm_forward(input_ids, *args, **kwargs):
|
||||
if args or kwargs:
|
||||
return self._original_forward(input_ids, *args, **kwargs)
|
||||
out_device = (
|
||||
input_ids.device
|
||||
if input_ids.device.type == "cuda"
|
||||
else self.manager.process_device
|
||||
)
|
||||
out = F.embedding(
|
||||
input_ids.to("cpu"),
|
||||
self.module.weight,
|
||||
self.module.padding_idx,
|
||||
self.module.max_norm,
|
||||
self.module.norm_type,
|
||||
self.module.scale_grad_by_freq,
|
||||
self.module.sparse,
|
||||
)
|
||||
return out.to(out_device, non_blocking=True)
|
||||
|
||||
module.forward = _mm_forward
|
||||
|
||||
|
||||
class LinearLayerMemoryManager(BaseLayerMemoryManager):
|
||||
def __init__(
|
||||
self,
|
||||
@@ -739,6 +786,11 @@ class OstrisLinearLayerMemoryManager(BaseLayerMemoryManager):
|
||||
# already resident on device
|
||||
return self._original_forward(x)
|
||||
|
||||
if x.device != device:
|
||||
# cpu activations from a fully-offloaded pipeline: see
|
||||
# _BouncingLinearFn.forward
|
||||
x = x.to(device, non_blocking=True)
|
||||
|
||||
state = _get_device_state(device)
|
||||
d = state["depth"]
|
||||
idx = state["forward_clk"]
|
||||
|
||||
@@ -98,6 +98,9 @@ UNET_IN_CHANNELS = 4 # Stable Diffusion の in_channels は 4 で固定。XLも
|
||||
class BaseModel:
|
||||
# override these in child classes
|
||||
arch = None
|
||||
# rename LoRA keys transformer. <-> diffusion_model. (the ComfyUI-standard
|
||||
# prefix) on save/load
|
||||
lora_keys_use_comfy_prefix = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -1624,13 +1627,59 @@ class BaseModel:
|
||||
encoder.to(*args, **kwargs)
|
||||
else:
|
||||
self.text_encoder.to(*args, **kwargs)
|
||||
|
||||
|
||||
def component_load_kwargs(self, role: str = "transformer", dtype=None):
|
||||
"""kwargs for a v2 module's .load()/.aitk_post_load(), derived from
|
||||
model_config: qtype (with the accuracy recovery adapter recombined),
|
||||
offload fraction, devices, low_vram placement. Roles: "transformer",
|
||||
"te", "vae"."""
|
||||
mc = self.model_config
|
||||
qtype, offload = None, 0.0
|
||||
if role == "transformer":
|
||||
if mc.quantize:
|
||||
qtype = mc.qtype
|
||||
if mc.accuracy_recovery_adapter and "|" not in (qtype or ""):
|
||||
qtype = f"{qtype}|{mc.accuracy_recovery_adapter}"
|
||||
if mc.layer_offloading:
|
||||
offload = mc.layer_offloading_transformer_percent
|
||||
elif role == "te":
|
||||
if mc.quantize_te:
|
||||
qtype = mc.qtype_te
|
||||
if mc.layer_offloading:
|
||||
offload = mc.layer_offloading_text_encoder_percent
|
||||
if dtype is None:
|
||||
dtype = self.vae_torch_dtype if role == "vae" else self.torch_dtype
|
||||
device = self.te_device_torch if role == "te" else self.device_torch
|
||||
if mc.low_vram and role in ("transformer", "te"):
|
||||
device = "cpu"
|
||||
elif role == "vae":
|
||||
device = self.vae_device_torch
|
||||
return dict(
|
||||
qtype=qtype,
|
||||
offload=offload,
|
||||
dtype=dtype,
|
||||
device=device,
|
||||
quantize_device=self.device_torch,
|
||||
base_model=self,
|
||||
use_comfy_weights=mc.model_kwargs.get("use_comfy_weights", True),
|
||||
)
|
||||
|
||||
def convert_lora_weights_before_save(self, state_dict):
|
||||
# can be overridden in child classes to convert weights before saving
|
||||
if self.lora_keys_use_comfy_prefix:
|
||||
return {
|
||||
k.replace("transformer.", "diffusion_model."): v
|
||||
for k, v in state_dict.items()
|
||||
}
|
||||
return state_dict
|
||||
|
||||
|
||||
def convert_lora_weights_before_load(self, state_dict):
|
||||
# can be overridden in child classes to convert weights before loading
|
||||
if self.lora_keys_use_comfy_prefix:
|
||||
return {
|
||||
k.replace("diffusion_model.", "transformer."): v
|
||||
for k, v in state_dict.items()
|
||||
}
|
||||
return state_dict
|
||||
|
||||
def condition_noisy_latents(self, latents: torch.Tensor, batch:'DataLoaderBatchDTO'):
|
||||
|
||||
@@ -1,46 +1,29 @@
|
||||
from typing import List
|
||||
import torch
|
||||
from transformers import T5Tokenizer, UMT5EncoderModel
|
||||
|
||||
class PatchedT5Tokenizer(T5Tokenizer):
|
||||
def __init__(
|
||||
self,
|
||||
vocab: str | list[tuple[str, float]] | None = 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,
|
||||
)
|
||||
import torch
|
||||
|
||||
from toolkit.models.v2.text_encoders.umt5 import (
|
||||
PatchedT5Tokenizer,
|
||||
UMT5TextEncoder,
|
||||
)
|
||||
|
||||
|
||||
def get_umt5_encoder(
|
||||
model_path: str,
|
||||
tokenizer_subfolder: str = None,
|
||||
encoder_subfolder: str = None,
|
||||
torch_dtype: str = torch.bfloat16,
|
||||
comfy_files: List[str] = [
|
||||
"text_encoders/umt5_xxl_fp16.safetensors",
|
||||
"text_encoders/umt5_xxl_fp8_e4m3fn_scaled.safetensors",
|
||||
],
|
||||
) -> UMT5EncoderModel:
|
||||
"""
|
||||
Load the UMT5 encoder model from the specified path.
|
||||
"""
|
||||
tokenizer = PatchedT5Tokenizer.from_pretrained(model_path, subfolder=tokenizer_subfolder)
|
||||
# reserved for the comfy-weights flip (Phase 2); accepted for
|
||||
# signature compatibility, not consulted yet
|
||||
comfy_files: List[str] = None,
|
||||
):
|
||||
"""Load the UMT5 tokenizer + encoder. Thin compatibility wrapper around
|
||||
toolkit/models/v2/text_encoders/umt5.py."""
|
||||
tokenizer = UMT5TextEncoder.load_tokenizer(
|
||||
model_path, subfolder=tokenizer_subfolder or ""
|
||||
)
|
||||
print(f"Using {model_path} for UMT5 encoder.")
|
||||
text_encoder = UMT5EncoderModel.from_pretrained(
|
||||
model_path, subfolder=encoder_subfolder, torch_dtype=torch_dtype
|
||||
text_encoder = UMT5TextEncoder.load_model(
|
||||
model_path, dtype=torch_dtype, subfolder=encoder_subfolder or ""
|
||||
)
|
||||
return tokenizer, text_encoder
|
||||
@@ -0,0 +1,626 @@
|
||||
# v2 Model Module Restructure — Planning
|
||||
|
||||
## Goal
|
||||
|
||||
Every component the toolkit loads (DiTs/transformers, unets, text encoders, vision
|
||||
encoders, VAEs, audio VAEs) becomes a class extending one base module in
|
||||
`toolkit/models/v2`. `BaseModel` (`toolkit/models/base_model.py`) stays as the
|
||||
multimodal holder that each arch in `extensions_built_in/diffusion_models` extends —
|
||||
that layer is good. The layer below it is what gets unified: one loading entry point,
|
||||
one quantization path, one save path, shared component definitions instead of
|
||||
per-model-folder copies.
|
||||
|
||||
End state this enables:
|
||||
|
||||
- **Live server with model hot-swap**: a resident process where, when a generation or
|
||||
training run requests a different model, the unused components are dropped and the
|
||||
new ones loaded. Shared component classes (same TE/VAE reused across archs) make
|
||||
component-level reuse possible instead of full teardown/reload.
|
||||
- **Model loading test suite**: a test that loads each registered arch one at a time
|
||||
and runs an inference pass. Every model type gets added to this suite as it is
|
||||
migrated (see Testing below).
|
||||
- **Comfy-aligned weights**: weights live in the ComfyUI folder layout under
|
||||
`MODELS_PATH` (shareable with a ComfyUI install), download there when missing, and
|
||||
saves are comfy-format. Eventually defaults move to comfy / our own prequantized
|
||||
releases for everything.
|
||||
|
||||
## Current state (survey 2026-08-27)
|
||||
|
||||
Three generations of loading conventions coexist:
|
||||
|
||||
1. Legacy monolith `toolkit/stable_diffusion_model.py` (`is_flux` / `is_v3` branches);
|
||||
still the silent fallback in `toolkit/util/get_model.py` when an arch string
|
||||
doesn't match.
|
||||
2. `BaseModel` subclass per arch (35 registered classes), each with a hand-written
|
||||
`load_model()` / `save_model()`.
|
||||
3. `toolkit/models/v2/_mixin.py` (`OstrisModelMixin`) — the intended fix, currently
|
||||
used by one model (`v2/z_image.py` → z_image extension).
|
||||
|
||||
### Duplication highlights
|
||||
|
||||
- BFL KL autoencoder: full copies in `flux2/src/autoencoder.py` and
|
||||
`ideogram4/src/vae.py` (header says "Flux2 KL autoencoder").
|
||||
- Qwen3-VL text encoder loaded independently in qwen_image, nucleus_image, krea2,
|
||||
ideogram4, mageflow, minimax_h3 — from several different repo sources; krea2
|
||||
hand-patches the vision tower locally.
|
||||
- `Qwen3ForCausalLM` TE load: 3 verbatim line-for-line copies
|
||||
(`z_image/z_image.py:257`, `z_image/z_image_l2p_model.py:471`,
|
||||
`zeta_chroma/zeta_chroma_model.py:146`).
|
||||
- Flux1 VAE + T5 + CLIP trio loaded 4x from 2 different repos (chroma ×2,
|
||||
flux_kontext, legacy SD path).
|
||||
- Comfy-file resolver copy-pasted: `minimax_h3/minimax_h3.py:248`
|
||||
(`_resolve_comfy_file`) → `ltx2/ltx2.py:1254` ("mirrors MinimaxH3Model").
|
||||
- `AutoencoderKLQwenImage` latents mean/std handling triplicated (qwen_image,
|
||||
nucleus_image, krea2).
|
||||
- `transformer.` ↔ `diffusion_model.` LoRA key rename copy-pasted ~15x in
|
||||
`convert_lora_weights_before_save/load` overrides.
|
||||
- Fake CLIP/TE/config stubs redefined in ~5 places
|
||||
(canonical: `toolkit/models/FakeVAE.py`, `toolkit/unloader.py`).
|
||||
|
||||
### Inconsistency highlights
|
||||
|
||||
- **Quantization, 5 paths**: `quantize_model()` (block-streaming, ARA-aware — ~21
|
||||
users), raw `quantize()` (~10 users, no block streaming/excludes, but the only path
|
||||
honoring `quantize_kwargs`), hidream's hand-rolled block loop, the v2 mixin's own
|
||||
`quantize_`, and bare TE quantization everywhere.
|
||||
- **Known bugs**: ~9 sites quantize the TE with `qtype` instead of `qtype_te`
|
||||
(chroma ×2, flux2, flux_kontext, cogview4, wan21, legacy SD, ...);
|
||||
`toolkit/models/loaders/umt5.py` accepts a `comfy_files` param it never uses, so
|
||||
wan21's comfy-TE path is a silent no-op.
|
||||
- **Saving, 4 incompatible styles**: diffusers `save_pretrained` folders, flat
|
||||
safetensors, safetensors-inside-diffusers-folder hybrids, and z_image's
|
||||
loaded-format-dependent branch. Dequant-on-save done 3 ways; the
|
||||
`isinstance(v, QTensor)` variant (chroma, flux2, boogu_image, ideogram4) misses
|
||||
torchao and Ostris weights entirely. Every `save_pretrained` override ignores its
|
||||
`save_dtype` argument.
|
||||
- Registry: linear scan in `toolkit/util/get_model.py`, silent SD1 fallback on a
|
||||
typo'd arch, eager import of every model file at startup. Second unsynchronized
|
||||
registry in `ui/src/app/jobs/new/options.tsx`.
|
||||
|
||||
## Decisions (locked in)
|
||||
|
||||
1. **Save format = ComfyUI format.** Single-file safetensors in comfy key layout.
|
||||
Must support saving quantized — primarily convrot8 and nvfp4 (comfy_quant marker
|
||||
format, see `toolkit/util/comfy_quant_import.py`) — and plain bf16, all in comfy
|
||||
format. Loading stays backwards compatible: diffusers dirs, transformers repos,
|
||||
and single files all still digest through `load_model`; only saving standardizes
|
||||
on comfy.
|
||||
2. **v2 folder layout mirrors the comfy save path structure**:
|
||||
|
||||
```
|
||||
toolkit/models/v2/
|
||||
_mixin.py # base module (OstrisModelMixin, evolving)
|
||||
resolver.py # comfy-layout weight resolution (lift from minimax_h3)
|
||||
diffusion_models/ # one file per DiT/unet family
|
||||
text_encoders/ # qwen3_vl.py, qwen3.py, t5.py, clip.py, gemma.py, ...
|
||||
vae/ # flux_kl.py, qwen_image.py, wan.py, audio VAEs, ...
|
||||
vision_encoders/
|
||||
```
|
||||
|
||||
3. **Method names win from `BaseModel`**: `get_transformer_block_names` and
|
||||
`get_quantization_exclude_modules`. The mixin's `get_quantization_block_names`
|
||||
gets renamed to match; resolve the classmethod-vs-instance-method mismatch while
|
||||
doing so.
|
||||
4. **Loading policy**:
|
||||
- Per-model special handling is allowed via the hook methods.
|
||||
- If `name_or_path` is a diffusers/transformers source, load it with
|
||||
diffusers/transformers for now. **Step 1 is migrating every model to the v2
|
||||
module format and loader without breaking anything** — same weights, same
|
||||
sources, same results.
|
||||
- Each model declares a `comfy_weight_names` dict keyed per standard
|
||||
`name_or_path`. If the user points at a local folder or a non-standard repo,
|
||||
load it as-is. If `name_or_path` is the standard repo and we have matching
|
||||
comfy weight names, load those instead when any of them exist (locally under
|
||||
`MODELS_PATH` in comfy layout, or downloadable to there).
|
||||
- Eventually the default flips to comfy weights / our own prequantized releases
|
||||
for everything.
|
||||
|
||||
## Base module: what `OstrisModelMixin` still needs
|
||||
|
||||
The mixin already handles: diffusers dir / hub repo / local single file /
|
||||
`org/repo/file.safetensors`, key-conversion hooks on load and save, overridable
|
||||
backend hooks for transformers-lib models, block-wise quantize.
|
||||
|
||||
To add:
|
||||
|
||||
- [x] **Comfy weight spec + resolver.** `aitk_comfy_repo` / `aitk_comfy_weight_names`
|
||||
class attrs + `find_comfy_weights` (local-only until Phase 2); resolution chain
|
||||
generalized from `minimax_h3._resolve_comfy_file` into `v2/resolver.py`:
|
||||
explicit override → `MODELS_PATH` at the repo-relative comfy path → flat at
|
||||
root → recursive walk of the category folder → hub download **to the
|
||||
repo-relative path** (folder stays shareable with ComfyUI, no duplicate
|
||||
downloads).
|
||||
- [x] **Automatic prequantized import.** Single-file path sniffs `comfy_quant`
|
||||
markers and routes through `import_comfy_quantized_layers` before
|
||||
`load_state_dict`, including the OstrisLinear missing-key whitelist that
|
||||
minimax_h3 and ltx2 each hand-rolled.
|
||||
- [x] **One save path.** `save_model(path, dtype)`: dequantize via
|
||||
`dequantize_if_quantized` (honors dtype), run `convert_state_dict_on_save`,
|
||||
write single-file comfy-layout safetensors. (Quantized-storage saves —
|
||||
convrot8 / nvfp4 with comfy_quant markers — land with Phase 2; diffusers-folder
|
||||
save as an explicit flag still to add.)
|
||||
- [x] **Tokenizer/processor declaration** for text encoders
|
||||
(`aitk_tokenizer_repo`/`aitk_processor_repo` + `load_tokenizer`/`load_processor`).
|
||||
- [x] Rename quantization hooks to the `BaseModel` spellings (decision 3):
|
||||
`get_transformer_block_names` (classmethod on the module).
|
||||
|
||||
## Migration steps
|
||||
|
||||
Track progress here; check items off as they land.
|
||||
|
||||
### Phase 0 — foundation (done 2026-08-27)
|
||||
- [x] Evolve `_mixin.py` per the list above (comfy spec local-only until Phase 2)
|
||||
- [x] Create `v2/diffusion_models/`, `v2/text_encoders/`, `v2/vae/`,
|
||||
`v2/vision_encoders/`; move `v2/z_image.py` → `v2/diffusion_models/z_image.py`
|
||||
- [x] Lift the comfy resolver out of minimax_h3 into `v2/resolver.py`; point
|
||||
minimax_h3 and ltx2 at it (delete their copies)
|
||||
- [x] `BaseModel` default `convert_lora_weights_before_save/load` doing the
|
||||
`transformer.` ↔ `diffusion_model.` rename, gated on the class attr
|
||||
`lora_keys_use_comfy_prefix` (default False, so passthrough models keep
|
||||
their behavior); the ~18 identical overrides replaced with the flag.
|
||||
Custom conversions (anima, hidream_o1, ltx2, wan21) keep their overrides;
|
||||
ltx2's now composes with the flag via super().
|
||||
|
||||
### Phase 1 — migrate all models to v2 modules, no behavior change
|
||||
Every arch's components become v2 classes; if `name_or_path` is diffusers, it still
|
||||
loads via diffusers. Nothing about sources or outputs changes yet. Suggested order
|
||||
(worst duplication first), each including its loading test (see Testing):
|
||||
|
||||
- [x] `text_encoders/qwen3.py` — Qwen3TextEncoder + `OstrisTransformersMixin`
|
||||
backend + `BaseModel.prepare_text_encoder` policy helper; the 3 verbatim
|
||||
TE stanzas (z_image, z_image_l2p, zeta_chroma) replaced. Verified with
|
||||
real Z-Image weights (load + encode on GPU).
|
||||
- [x] `text_encoders/qwen3_vl.py` — Qwen3VLTextEncoder with
|
||||
`drop_vision_tower` / `patch_vision_patch_embed`; the 4 identical
|
||||
`patch_qwen_vl_patch_embed` copies (krea2, mageflow, boogu_image,
|
||||
Qwen3VLCaptioner) consolidated; TE loads migrated in krea2, mageflow,
|
||||
nucleus_image. Still on their own paths: ideogram4 (loads via AutoModel),
|
||||
minimax_h3 (custom truncated/prequantized comfy load — port later),
|
||||
qwen_image (Qwen2.5-VL, needs its own class)
|
||||
- [x] `text_encoders/t5.py`, `text_encoders/clip.py` — T5TextEncoder,
|
||||
CLIPTextEncoder, CLIPTextEncoderWithProjection; migrated chroma ×2,
|
||||
flux_kontext, f_light (T5 stanzas → `prepare_text_encoder`, fixing their
|
||||
`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
|
||||
- [x] krea2, ideogram4, mageflow — DiTs on the mixin via the `config=`
|
||||
passthrough (holder builds config from model_kwargs/holder state, mixin
|
||||
does build/markers/whitelist/casting; plain nn.Module classes now work
|
||||
with the default builder). krea2 + ideogram4 verified by harness;
|
||||
mageflow untestable while its repo 404s
|
||||
- [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)
|
||||
- [x] flux2 + zeta_chroma DiTs on the mixin via `config=` passthrough
|
||||
(flux2_klein_4b verified by harness). Every arch's DiT now loads
|
||||
through the mixin except: ltx2 family (one-file→two-modules split,
|
||||
helper delegated), anima (diffusers modular pipeline), ace_step
|
||||
(bundled single-file loader), z_image_l2p's local subclass, and the
|
||||
grandfathered legacy stable_diffusion_model archs
|
||||
- [x] minimax_h3 (+ ref2va) transformer ported to the mixin: config sniffed
|
||||
from the checkpoint via aitk_config_from_state_dict (adaln_t_table),
|
||||
marker attach + stored-precision load via the new
|
||||
`aitk_cast_on_load = False` knob. Verified on the real pruned convrot
|
||||
file: 200 ConvRot linears, pruned table detected, fp32/fp16/bf16 mix
|
||||
preserved, no meta leftovers. Its TE stays custom (50-layer truncation
|
||||
+ key_map). ltx2.5's `_load_quantized_module` now delegates to the
|
||||
mixin's whitelist/meta helper (~30 lines deleted); its full port is
|
||||
blocked on the one-comfy-file → transformer+connectors split, which
|
||||
doesn't fit the per-class single-file shape — revisit with the live
|
||||
server's component model
|
||||
- [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
|
||||
(`dequantize_if_quantized` everywhere), raw-`quantize()` → `quantize_model()`
|
||||
|
||||
### Phase 2 — comfy weights become the preferred source
|
||||
|
||||
Decisions:
|
||||
- Comfy weights come from the Comfy-Org hub repos (per-model repos, comfy
|
||||
layout nested under `split_files/` — stripped when placing files into
|
||||
MODELS_PATH). Repos ship several precision variants of each component.
|
||||
- **Selection preference: convrot8 > float8 mixed > float8 > bf16 > fp16**
|
||||
(`resolver.comfy_precision_rank`; nvfp4/unmarked rank last and are only
|
||||
used when explicitly listed). **Local-first**: the best-ranked LOCAL
|
||||
candidate wins; only when no candidate is local is the best-ranked one
|
||||
downloaded.
|
||||
- Per-model candidate lists (`aitk_comfy_weight_names`) hold only the
|
||||
variants the class can actually digest. Constraint discovered: comfy
|
||||
convrot/quantized files carry markers on the ORIGINAL module layout (e.g.
|
||||
z_image's fused attention.qkv) — diffusers-layout classes with split
|
||||
modules can't attach them until they grow fused-layout support; until then
|
||||
those models list bf16/fp8 variants only. (Vendored comfy-layout classes —
|
||||
minimax/ltx pattern — take convrot directly.)
|
||||
- The standard repo still supplies the config; local dirs and unregistered
|
||||
repos load as-is; `model_kwargs.use_comfy_weights: false` opts out.
|
||||
|
||||
- [x] Mechanism: `resolver.comfy_precision_rank` / `comfy_local_rel` /
|
||||
`resolve_comfy_candidates` + `OstrisModelMixin.resolve_comfy_weights`,
|
||||
integrated into `load_model` (comfy file preferred for registered
|
||||
standard repos, downloaded into the shared comfy layout)
|
||||
- [x] First wired model: z_image (Comfy-Org/z_image_turbo, bf16 candidate).
|
||||
Verified end-to-end against the real shared ComfyUI folder
|
||||
(MODELS_PATH=/mnt/Models/comfy_models): standard-repo name_or_path
|
||||
resolved to the locally-present comfy file and produced the identical
|
||||
generation to the diffusers-shards load
|
||||
- [x] qwen_image wired: its comfy files use the diffusers key layout
|
||||
directly — `fp8mixed` (float8-mixed, rank 1) attaches its 839
|
||||
`float8_e4m3fn` markers straight onto the class; candidates fp8mixed →
|
||||
fp8_e4m3fn (raw cast) → bf16. Verified with real weights: loaded the
|
||||
shared folder's local fp8 file (local-first, no download) and generated
|
||||
correctly. Holder skips re-quantization for prequantized checkpoints.
|
||||
- [x] New `float8_e4m3fn` Ostris backend (toolkit/util/float8_quant.py):
|
||||
ComfyUI's fp8 + per-tensor-scale storage with dequantized matmul, in
|
||||
get_ostris_quantizer + comfy import/export. Round-trip verified.
|
||||
- [x] wan family wired: comfy wan files (original key layout) convert via
|
||||
diffusers' own `convert_wan_transformer_to_diffusers` (rename-only, so
|
||||
quantized weight/scale keys ride along with their modules). Candidate
|
||||
keys support `(repo, subfolder)` tuples for wan2.2 A14B's dual DiTs
|
||||
(transformer = high noise, transformer_2 = low noise) and per-entry
|
||||
comfy-repo overrides ({"repo": ..., "files": [...]}) since wan2.1 and
|
||||
2.2 files live in different Comfy-Org repos. Wired: 2.2 TI2V-5B,
|
||||
T2V/I2V-A14B (fp8_scaled), 2.1 T2V 1.3B/14B, I2V 480P/720P. Verified
|
||||
with real weights: wan21 1.3B (downloaded comfy bf16) and wan22 5B
|
||||
(local comfy fp16) both load through the converter and generate video.
|
||||
Fix along the way: the mixin's meta build now uses accelerate
|
||||
init_empty_weights (params meta, buffers real) so init-computed
|
||||
non-persistent buffers like wan's rope tables materialize.
|
||||
- [x] Legacy ComfyUI scaled-fp8 support (`scaled_fp8` marker + per-layer
|
||||
fp8 weight / scalar scale_weight, e.g. every wan *_fp8_scaled file):
|
||||
imports onto the float8 backend; scale_input (activation quant) is
|
||||
dropped, matmuls run dequantized.
|
||||
- [x] wan comfy-format saves: `convert_state_dict_on_save` inverts diffusers'
|
||||
rename table (base/t2v/i2v; vace/animate excluded — their reverse
|
||||
mappings collide). Round-trip verified on both real comfy files
|
||||
(exact; the 2.1 file's legacy model.diffusion_model. prefix drops per
|
||||
the modern convention) and with real weights (load → save → 825-key
|
||||
original-layout file → reload bit-equal). wan21 + wan22_5b save one
|
||||
comfy file; wan22_14b saves the comfy-standard _high_noise/_low_noise
|
||||
pair instead of two diffusers folders.
|
||||
- [ ] Wire remaining archs' candidate lists (chroma/others as their key
|
||||
conversions are verified per file)
|
||||
- [x] Fused-layout quantized attach for diffusers-split classes:
|
||||
`split_fused_quantized_keys` / `fuse_split_quantized_keys`
|
||||
(comfy_quant_import) do exact out-dim row surgery on quantized comfy
|
||||
entries for all three formats (int8 rows+scales slice; fp8 scalar and
|
||||
nvfp4 per-tensor scales shared; nvfp4 block scales
|
||||
unswizzle→split→reswizzle). z_image's load/save converters use them, so
|
||||
its convrot8 candidate is live and top-ranked. Unit-verified exact both
|
||||
directions.
|
||||
- [x] Comfy-format save: `save_model` auto-keeps quantized storage
|
||||
(comfy_quant markers) for convrot8 / nvfp4 / convrotcomfyw4a4 layers via
|
||||
`toolkit/util/comfy_quant_export.py` (inverse of comfy_quant_import;
|
||||
nvfp4 nibbles re-swapped + scales re-swizzled to the cuBLAS tile
|
||||
layout), plain layers save at bf16; partially-exportable models fall
|
||||
back to dequantized. Round-trip verified: save → mixin reload → outputs
|
||||
match for convrot8, nvfp4, and plain layers.
|
||||
- [x] Save unification started: z_image and qwen_image holders now save
|
||||
comfy-format single files via the mixin regardless of how they loaded
|
||||
(z_image's dual-style branch deleted). Real round trip verified: the
|
||||
published z_image int8_convrot file loads (270 quantized linears,
|
||||
split-attach), resaves to the IDENTICAL 857-key comfy layout with
|
||||
bit-exact fused qkv weights/scales/markers, and the reload's quantized
|
||||
forward is bit-identical — toolkit saves are byte-compatible with
|
||||
ComfyUI.
|
||||
- [x] chroma + chroma_radiance saves flipped to the mixin (their class keys
|
||||
ARE the original layout) — also fixes their quanto-only dequant bug
|
||||
(torchao/Ostris weights now dequantize on save). Tiny-model round trip
|
||||
verified. Save flips so far: z_image, qwen_image, wan21, wan22_5b,
|
||||
wan22_14b (dual files), chroma ×2.
|
||||
- [ ] flux_kontext comfy wiring deferred: its Comfy-Org repo ships a single
|
||||
legacy-fp8 file in fused BFL layout — needs the flux fused-split
|
||||
conversion (split_fused_quantized_keys pattern + BFL↔diffusers maps)
|
||||
- [ ] Flip the remaining per-arch `save_model` overrides as each arch's
|
||||
save-side key conversion is in place
|
||||
- [ ] Publish/verify comfy repacks per model as they flip
|
||||
|
||||
### Phase 3 — live server
|
||||
- [ ] Component-level identity (which TE/VAE instances are shared between archs) so
|
||||
a model switch drops only what the next run doesn't need
|
||||
- [ ] Resident process: request comes in → diff requested components vs loaded →
|
||||
unload/load the difference
|
||||
- [ ] Legacy `stable_diffusion_model.py` archs: grandfather or port last
|
||||
|
||||
## 100% mixin coverage (2026-08-28)
|
||||
|
||||
Every component of every non-legacy arch now loads through OstrisModelMixin —
|
||||
DiTs, text encoders, VAEs, vision encoders, connectors/vocoders, and the
|
||||
custom cases that previously bypassed it:
|
||||
|
||||
- New wrapper classes: llama, gemma3 + gemma4, mistral3 (×2), qwen3 base,
|
||||
qwen3-vl base + text-only, wan VAE, diffusers flux2 KL VAE, CLIP vision
|
||||
(first vision encoder), cosmos DiT, anima text conditioner, the full ltx2
|
||||
set (transformer, video/audio VAEs, connectors, vocoders).
|
||||
- Existing-class swaps: omnigen2 (mllm + VAE), boogu (TE + VAE), klein TE,
|
||||
hidream llama, ideogram4 TE, prx TE.
|
||||
- Custom restructures: minimax video/audio VAEs and TE, MageVAE
|
||||
(deferred-load ctor), the shared flux2_kl AutoEncoder (small-decoder sniff
|
||||
as a class hook; flux2 + ideogram4 route through it), ace_step's bundle
|
||||
(per-component class loaders), anima (modular-pipeline load replaced with
|
||||
component-wise v2 loads + update_components), hidream_o1's Qwen3VL DiT,
|
||||
z_image_l2p rebased onto the v2 class, ltx2's converter builds v2 classes.
|
||||
- Mixin: kwargs flow through the single-file chain (component ctor args like
|
||||
MageVAE's sample_posterior).
|
||||
|
||||
Verified with real weights: ltx2.3 + ltx2.5 (full v2 family), anima,
|
||||
ace_step_15 (new harness entry), minimax VAEs, plus the standing harness
|
||||
coverage. The legacy monolith archs joined the system per the inference-engine
|
||||
goal ("send a job with any base_model, reload/unload components on the fly"):
|
||||
six new wrapper classes complete the component vocabulary (UNet2DCondition,
|
||||
SD3/PixArt ×2/AuraFlow/Lumina2 transformers, Gemma2), and
|
||||
`adopt_component` (in-place class swap, OstrisLinear-style) rebinds
|
||||
pipeline-loaded components onto their wrappers at the monolith's single
|
||||
post-load funnel — covering every legacy arch without touching its fifteen
|
||||
load branches, with all pipeline references staying valid. Verified: sd1
|
||||
(UNet/KLVAE/CLIPTextEncoder all mixin instances + generation) and sdxl
|
||||
(dual-CLIP adoption + 1024² generation); harness gained sd1/sdxl entries,
|
||||
a legacy scheduler fallback, and the sampler-name pass-through. Full
|
||||
monolith decomposition (per-arch v2 loading with comfy candidates) remains
|
||||
future work, but every resident component is now poolable by the engine.
|
||||
|
||||
## Unified load API (2026-08-28)
|
||||
|
||||
Loading policy moved out of the holders into the mixin. The surface:
|
||||
|
||||
- `ModelClassName.load(name_or_path, qtype=..., offload=..., dtype=...,
|
||||
device=..., ...)` — sourcing (`load_model`) + `aitk_post_load` in one call.
|
||||
- `module.aitk_post_load(**kwargs)` — the post-load half alone, for holders
|
||||
whose checkpoint sourcing is custom (state-dict surgery, combined files).
|
||||
Handles: qtype (incl. `"qtype|ara_path"` accuracy recovery adapters, with
|
||||
prequantized-skip via `aitk_is_quantized`), block-streamed quantization
|
||||
(`quantize_module` in toolkit/util/quantize.py), MemoryManager layer
|
||||
offloading with per-class `get_offload_ignore_modules()` hooks, and device
|
||||
placement (low_vram parks on cpu).
|
||||
- `BaseModel.component_load_kwargs(role)` derives the kwargs from
|
||||
model_config for roles "transformer" / "te" / "vae" (qtype/qtype_te, ARA
|
||||
recombination, offload percents, low_vram, te/vae devices).
|
||||
|
||||
All holders converted (transformer + TE quantize/offload/placement blocks
|
||||
deleted): krea2, z_image, qwen_image, chroma ×2, flux_kontext, f_light,
|
||||
nucleus, zeta_chroma, z_image_l2p, wan21 (+ subclasses), wan22_14b (dual:
|
||||
per-transformer load, dual-ARA branch kept), boogu, ernie, ideogram4,
|
||||
mageflow, prx_pixel, omnigen2, flux2 + klein, hidream + hidream_o1, ltx2 +
|
||||
ltx2.5 (prequantized ConvRot flagged via aitk_is_quantized), minimax_h3,
|
||||
anima (conditioner rides the transformer quantize flag at qtype_te),
|
||||
ace_step_15, example_model (template now teaches the new API).
|
||||
New `get_offload_ignore_modules` hooks: wan (scale_shift tables — also fixes
|
||||
wan21 offload which previously offloaded them), ltx2 (all four per-block
|
||||
tables), gemma3 (embed_tokens), gemma4 (embed_tokens + layer_scalar),
|
||||
ideogram4 (rotary inv_freq + input/cond projections), ernie (x_embedder),
|
||||
zeta ZImageDCT (pad tokens). New block-name hooks where only the holder had
|
||||
them: Flux2, MageFlow, ZImageDCT, Ideogram4Transformer2DModel (+ its exclude
|
||||
list on MageFlow). `prepare_text_encoder` remains only as the legacy-monolith
|
||||
path.
|
||||
|
||||
## Load/quantize optimization round 1 (2026-08-28)
|
||||
|
||||
Baseline: testing/.model_test_outputs/report_baseline.md (+ per-arch
|
||||
metrics_baseline.json). Profiling findings and fixes:
|
||||
|
||||
- The "slow quantization" in the baseline was mostly NOT quantize math
|
||||
(convrot8 on chroma's 57 blocks: 0.5s of GPU kernels). It was data
|
||||
movement: mmap-backed safetensors faulting pages in per-tensor cudaMemcpy
|
||||
order (cold cache on the 92%-full NVMe reads at ~0.32GB/s; the drive's
|
||||
sequential ceiling is ~435MB/s — cold loads are storage-bound), plus the
|
||||
old stream-quantize choreography crossing the bus three times (up, back
|
||||
into a fresh host copy, up again at final placement).
|
||||
- `quantize_module(keep_on_device=True)`: when the final home IS the
|
||||
quantize gpu (no offload/low_vram — aitk_post_load detects this), blocks
|
||||
stay on the gpu after quantizing and the extras pass runs there too.
|
||||
Weights cross the bus once; the model-sized host-RAM spike of the
|
||||
intermediate copy is gone (chroma RSS peak 25.3 -> 17.3GB, the rest is
|
||||
reclaimable page cache; flux2's 93GB spike class eliminated). Warm-cache
|
||||
chroma blocks: 46s -> 3.9s; wan22_5b quantize 10.6s -> 3.8s.
|
||||
- Offload/low_vram path keeps cpu residency but quantizes extras layer-by-
|
||||
layer via quantize_device instead of the all-cores cpu burst (was 350-650%
|
||||
cpu avg with 1600% spikes).
|
||||
- `quantize()` skips the quantize_device round-trip for same-qtype
|
||||
OstrisLinears (guaranteed no-op) and, for ostris backends, for non-Linear
|
||||
leaves (norms/embeddings were being ferried across the bus for nothing).
|
||||
- posix_fadvise(WILLNEED) readahead on single-file loads (mixin._readahead)
|
||||
— ~10-15% on cold reads, free when warm, no-op off POSIX.
|
||||
|
||||
Round 2 (same day):
|
||||
- ARA path (`attach_ara_and_quantize`) now quantizes each hijacked linear on
|
||||
the gpu (was: wherever it lived, historically the cpu) with the same
|
||||
keep_on_device policy; the uint8 extras pass follows suit. Verified with
|
||||
hidream + its uint3 ARA end to end.
|
||||
- `quantize_model` delegates its non-ARA branch to `quantize_module`
|
||||
(keep_on_device derived from model_config); dead `_quantize_model_blocks`
|
||||
removed; `patch_dequantization_on_save` made idempotent (double-patching
|
||||
would have recursed on save).
|
||||
- Local-first hub loads: `_local_first` wraps from_pretrained / load_config /
|
||||
AutoTokenizer / AutoProcessor — fully-cached repos load with
|
||||
local_files_only (13x on VAE metadata, 0.53s -> 0.04s; a failed local
|
||||
attempt is instant, so uncached repos fall through to the online path at
|
||||
zero cost). Tokenizer-load slowness was DIAGNOSED as these per-process hub
|
||||
etag HEADs, not the use_fast=False sites.
|
||||
|
||||
Round 3 — vram spike elimination (2026-08-28):
|
||||
- keep_on_device's extras pass had moved the whole unquantized remainder to
|
||||
the gpu in bf16 before quantizing (+7-10GB transient over final residency
|
||||
on big-extras models). Now `quantize(keep_on_quantize_device=True)`
|
||||
quantizes each extra layer on the gpu and leaves it — transient is one
|
||||
layer; non-quantized leftovers move only at final placement. Same for the
|
||||
ARA uint8 pass.
|
||||
- convrot8 `quantize_` chunked: takes the weight at stored dtype
|
||||
(wants_fp32_weight=False; ConvRotIntN keeps True) and rotates+quantizes in
|
||||
256MB row-chunks — a 152k-vocab TE projection's transient fell 7.6GB ->
|
||||
1.4GB. Sub-threshold layers keep the one-shot path, verified bit-identical.
|
||||
- qwen_image measured: standard load peak 35.3 -> 27.5GB (below the 28.0
|
||||
baseline; process peak now set by generation); low_vram load quantizes the
|
||||
whole 20B stack never holding >2.4GB gpu.
|
||||
|
||||
Ideas not yet done: pinned-buffer staged h2d (~+60% warm-upload bandwidth,
|
||||
9.5 vs 5.8GB/s measured), overlapping h2d with quantize kernels (small — the
|
||||
kernels are ~2% of the pass), wan22 low_vram generate choreography (165s of
|
||||
unload/reload per sample), holders' legacy "just to make sure" .to(device)
|
||||
lines that move quantized TEs to gpu even under low_vram (qwen's Preparing
|
||||
Model stage parks 7.6GB — policy question, not a quantize spike).
|
||||
|
||||
## Offload hardening + final certification (2026-08-28)
|
||||
|
||||
The harness gained a per-arch 100%-offload probe (attach MemoryManager at
|
||||
offload_percent=1.0 to the DiT + TEs + connectors/vision towers, then a
|
||||
2-step sample — 2 not 1: diffusers' shift_terminal stretch divides by zero
|
||||
at a single step). It flushed out and fixed, in the manager:
|
||||
- fully-offloaded modules now REPORT the compute device (class swap with
|
||||
stringification-stable __module__/__qualname__ — transformers keys its
|
||||
hidden-states capture registry by str(class)); bouncing ops also pull cpu
|
||||
activations to the staged device
|
||||
- weights tied to embeddings (lm_head <-> embed_tokens) are never pinned out
|
||||
from under the unmanaged embedding
|
||||
- ignore_modules are moved to the compute device at attach
|
||||
- the nested attach walk no longer double-lists managed modules as unmanaged
|
||||
(a managed Embedding was getting hauled back to gpu by pipeline.to)
|
||||
- NEW: bouncing embeddings — vocab tables >64MB stay cpu-resident with the
|
||||
row gather done ON cpu (only looked-up rows cross the bus); tied lm_heads
|
||||
fall through to normal bouncing
|
||||
- wan22's dual wrapper skips its whole-14B .to() swaps when memory-managed,
|
||||
and offload attaches per sub-transformer (pipelines hold those directly)
|
||||
|
||||
Measured offload-phase peaks: wan22_14b 14.7 -> 1.5GB, ltx2.5 14.0 ->
|
||||
11.8GB (remainder: video activations + resident VAEs/vocoder by design),
|
||||
most image archs 1-4GB vs 10-53GB resident. flux2's Mistral warnings
|
||||
silenced behavior-preservingly (tie_word_embeddings=False, explicit
|
||||
fix_mistral_regex=False, processor_kwargs — tokenization verified
|
||||
bit-identical). ConvRot skip notices gated to layers >=1M weights.
|
||||
|
||||
FINAL CERTIFICATION: full sweep, 33/34 PASS, 0 FAIL, 1 SKIP (mageflow
|
||||
upstream 404) — load + convrot8 quantize + generate + 100%-offload forward
|
||||
for every arch on the complete optimized stack.
|
||||
|
||||
## Testing
|
||||
|
||||
- [x] `testing/test_model_loading.py`: per-arch load + one small sample through
|
||||
the normal training-style flow (get_model_class → load_model →
|
||||
generate_images). `--arch X` runs one in-process; `--all` runs every
|
||||
registered arch in its own subprocess (full unload between archs).
|
||||
15 archs registered so far — add each model type as it migrates.
|
||||
- [x] Missing weights skip rather than fail: default is HF_HUB_OFFLINE=1 and
|
||||
hub/file errors classify as SKIP; `--allow-download` opts into fetching.
|
||||
(GPU + local-weights test, not CI-portable.)
|
||||
- [x] Final certification sweep (2026-08-27, post-polish): 14/14 runnable
|
||||
archs PASS — comfy-source loads (zimage convrot8, qwen fp8, wan ×2 +
|
||||
fp8 umt5 TE), all ported holder-config DiTs, and the migrated
|
||||
quantize_model paths (chroma, flux_kontext, f_light block-streamed) in
|
||||
one run; mageflow remains the upstream 404 skip. One regression caught
|
||||
and fixed: qwen's _load_single_file override needed the new config
|
||||
kwarg.
|
||||
- [x] Full sweep run 2026-08-27: 14/15 PASS (zimage, qwen_image, krea2,
|
||||
boogu_image, ernie_image, ideogram4, hidream_o1, anima, wan21, wan22_5b,
|
||||
chroma, flux_kontext, flux2_klein_4b, ltx2.3 — the quantized 22B ltx
|
||||
stack doesn't fit 32GB, needs the 96GB card). mageflow blocked
|
||||
upstream: microsoft/Mage-Flow-Base 404s on the hub (cached locally, so
|
||||
it runs offline — recheck whether the repo moved/went private).
|
||||
- [x] Registry carries realistic per-arch sample settings (native res, steps,
|
||||
CFG) so sweep outputs are visually verifiable, not just "a file
|
||||
exists". Verified: all 14 produce proper generations. Findings from
|
||||
the quality pass: boogu emits a black frame below native res at
|
||||
low-step/high-CFG (settings regime, present pre-restructure, not a
|
||||
migration bug); chroma's FakeCLIP hardcoded device 'cuda' broke any
|
||||
non-cuda:0 run (pre-existing, fixed — FakeCLIP now takes the real
|
||||
device); ideogram4's fp8 release renders its own "blocked by safety
|
||||
filter" card for a plain cat prompt (model behavior, not a bug —
|
||||
investigate its trigger).
|
||||
- [x] Round-trip verified for the first comfy-save arch: z_image convrot
|
||||
load → comfy save → identical key set + bit-exact quantized entries vs
|
||||
the published file → reload → bit-identical quantized forward. Extend
|
||||
per arch as saves flip.
|
||||
- [x] Full UI-default coverage (2026-08-27): the registry now holds all 31
|
||||
UI-facing archs, and 30/31 load + generate through the new stack with
|
||||
their real UI defaults (mageflow blocked upstream by its hub 404).
|
||||
This round verified the previously-untested tail with real weights:
|
||||
wan22_14b + i2v (comfy fp8 pairs via the legacy importer, candidate
|
||||
keys added for the ai-toolkit bf16 default repos), wan21_i2v, hidream,
|
||||
hidream_e1 (native 768² editing), nucleus, omnigen2, ltx2.5, flux2,
|
||||
flux2_klein_9b, prx_pixel, zeta_chroma, zimage_l2p, both qwen edit
|
||||
archs, f-lite. Fixes found by the run: ltx2.5's fp32 scale_shift
|
||||
tables promoted hidden states into bf16 linears under the diffusers
|
||||
class — fixed ComfyUI-style (toolkit/util/mixed_precision.py):
|
||||
per-op input casting hooks on every weighted module + the stored-fp32
|
||||
tensors pinned against parent .to(dtype) casts (device moves pass
|
||||
through), so the tables stay genuinely fp32 at sample time. The
|
||||
mechanism is general — any mixed-precision comfy checkpoint on a
|
||||
diffusers-class arch can use it; harness configs
|
||||
for e1 resolution and zeta/l2p extras_name_or_path corrected to match
|
||||
the UI defaults.
|
||||
- [ ] Each newly migrated model adds its test in the same PR as its migration.
|
||||
|
||||
## TODO / look at later
|
||||
|
||||
- [x] Quantize consolidation: `quantize_model` now honors `quantize_kwargs`
|
||||
(blocks + extras) and tolerates missing block names; the chroma ×2,
|
||||
flux_kontext, f_light, omnigen2 raw-quantize sites migrated onto it
|
||||
(gaining block streaming, excludes, ARA, dequant-on-save patching) with
|
||||
holder block names added. Remaining raw sites are legacy/extension
|
||||
(flex2, cogview4, stable_diffusion_model). The ARA uint8 hardcode
|
||||
stands — revisit if a non-uint8 ARA base is ever wanted.
|
||||
- [x] Last known `qtype_te` bugs fixed (flux2's Mistral TE, anima's
|
||||
text_conditioner) — 9/9 sites from the survey now correct outside the
|
||||
grandfathered legacy monolith (cogview4/legacy SD remain as-is).
|
||||
- [x] wan comfy-TE resolved for real: UMT5TextEncoder carries comfy
|
||||
candidates (fp8_e4m3fn_scaled via the legacy importer, fp16), files
|
||||
already in transformers key layout (spiece blob dropped, tied
|
||||
embed_tokens materialized). Verified: wan21 samples with the local
|
||||
comfy fp8 TE. The loaders/umt5.py `comfy_files` param stays as a
|
||||
no-op shim for old callers.
|
||||
- [x] Registry hardening: unknown archs now raise with the known-arch list
|
||||
(legacy monolith archs whitelisted via LEGACY_ARCHS). Still open: lazy
|
||||
per-arch imports; single source of truth shared with the UI's
|
||||
`options.tsx` model list.
|
||||
- [x] Stub dedup where identical: chroma_radiance imports FakeCLIP/FakeConfig
|
||||
from chroma_model. The other Fake* copies (hidream_o1, flux2, zeta)
|
||||
carry model-specific values — left in place.
|
||||
- [ ] Vendored upstream code (hidream/src, omnigen2/src, ltx2 converter's private
|
||||
comfy-quant parser): dedupe against toolkit utils where practical.
|
||||
+566
-17
@@ -9,7 +9,11 @@ digests any of:
|
||||
- a HuggingFace repo id ("org/repo")
|
||||
- a local single .safetensors file in the model's original key layout
|
||||
- a remote single .safetensors file ("org/repo/path/file.safetensors")
|
||||
plus optional automatic quantization of the loaded weights (`qtype=...`).
|
||||
plus optional automatic quantization of the loaded weights (`qtype=...`). A
|
||||
single-file checkpoint carrying ComfyUI `comfy_quant` markers loads with its
|
||||
pre-quantized layers attached to the toolkit's quantization backends
|
||||
(convrot8 / nvfp4) automatically. `save_model` writes a single-file
|
||||
.safetensors back in the original (comfy) key layout.
|
||||
|
||||
Model specific behavior lives in small overridable hooks (key conversion, config
|
||||
source, block names, backend loading), so subclassing for a new model usually means
|
||||
@@ -38,6 +42,24 @@ class BasicModel(ModelMixin, ConfigMixin, PeftAdapterMixin, FromOriginalModelMix
|
||||
pass
|
||||
|
||||
|
||||
def adopt_component(module: torch.nn.Module, wrapper_cls: type) -> torch.nn.Module:
|
||||
"""Rebind an already-loaded component instance onto its v2 wrapper class
|
||||
(in-place class swap, like the OstrisLinear conversion). Used when a
|
||||
component arrives from machinery that builds the base class directly
|
||||
(e.g. a diffusers pipeline load), so every resident component is a mixin
|
||||
instance the inference engine can pool and manage."""
|
||||
if isinstance(module, wrapper_cls):
|
||||
return module
|
||||
base = wrapper_cls.__mro__[1]
|
||||
if not isinstance(module, base):
|
||||
raise TypeError(
|
||||
f"cannot adopt {type(module).__name__} into {wrapper_cls.__name__} "
|
||||
f"(expected a {base.__name__})"
|
||||
)
|
||||
module.__class__ = wrapper_cls
|
||||
return module
|
||||
|
||||
|
||||
class OstrisModelMixin:
|
||||
# ---- per-model configuration, override in subclasses ----
|
||||
# subfolder that holds this model inside a diffusers style checkpoint
|
||||
@@ -47,6 +69,35 @@ class OstrisModelMixin:
|
||||
# .safetensors file and no config_path is given
|
||||
aitk_config_repo: Optional[str] = None
|
||||
|
||||
# ---- comfy weight sources ----
|
||||
# hub repo the comfy-format weight files are published in (Comfy-Org/...)
|
||||
aitk_comfy_repo: Optional[str] = None
|
||||
# standard name_or_path (hub repo id) -> repo-relative comfy weight file
|
||||
# candidates for this module: every precision variant the class can digest.
|
||||
# Selection ranks them convrot8 > float8 mixed > float8 > bf16 > fp16
|
||||
# (resolver.comfy_precision_rank); a locally-present candidate always wins
|
||||
# over a download.
|
||||
aitk_comfy_weight_names: Dict[str, List[str]] = {}
|
||||
|
||||
# ---- tokenizer/processor source, for text-encoder modules ----
|
||||
aitk_tokenizer_repo: Optional[str] = None
|
||||
aitk_tokenizer_subfolder: Optional[str] = None
|
||||
aitk_processor_repo: Optional[str] = None
|
||||
aitk_processor_subfolder: Optional[str] = None
|
||||
|
||||
# single-file loads without quant markers cast tensors to the requested
|
||||
# dtype; classes whose checkpoints carry a deliberate precision mix (e.g.
|
||||
# fp32 norms next to bf16 weights) set this False to load at stored
|
||||
# precision instead
|
||||
aitk_cast_on_load: bool = True
|
||||
|
||||
# pre-quantized (comfy marker) checkpoints normally load their
|
||||
# NON-quantized tensors at stored precision (the mix can be deliberate,
|
||||
# e.g. ltx2.5's fp32 tables). Classes whose forward assumes one uniform
|
||||
# dtype (wan: fp16 conv biases next to fp32 tables in the fp8 files) set
|
||||
# this True to cast those float tensors to the load dtype instead
|
||||
aitk_cast_quantized_load: bool = False
|
||||
|
||||
# ---- state set by the loader / quantizer ----
|
||||
aitk_is_quantized: bool = False
|
||||
aitk_qtype: Optional[str] = None
|
||||
@@ -68,7 +119,7 @@ class OstrisModelMixin:
|
||||
return state_dict
|
||||
|
||||
@classmethod
|
||||
def get_quantization_block_names(cls) -> Optional[List[str]]:
|
||||
def get_transformer_block_names(cls) -> Optional[List[str]]:
|
||||
"""Names (dotted paths allowed) of the repeated block lists to quantize one
|
||||
block at a time so the whole model never has to sit on the gpu at once."""
|
||||
return None
|
||||
@@ -78,28 +129,201 @@ class OstrisModelMixin:
|
||||
"""fnmatch patterns of sensitive modules to keep in full precision."""
|
||||
return None
|
||||
|
||||
def get_offload_ignore_modules(self) -> Optional[List]:
|
||||
"""Module/parameter objects layer offloading must keep resident (pad
|
||||
tokens, modulation tables, ...). Default: none."""
|
||||
return None
|
||||
|
||||
# ---- backend hooks: default to the diffusers ModelMixin API. transformers-lib
|
||||
# models (text encoders) override these three to use PreTrainedModel/AutoConfig.
|
||||
|
||||
@classmethod
|
||||
def aitk_from_pretrained(cls, path, subfolder=None, dtype=None, **kwargs):
|
||||
return cls.from_pretrained(
|
||||
path, subfolder=subfolder, torch_dtype=dtype, **kwargs
|
||||
return cls._local_first(
|
||||
cls.from_pretrained, path, subfolder=subfolder, torch_dtype=dtype, **kwargs
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def aitk_load_config(cls, path, subfolder=None):
|
||||
return cls.load_config(path, subfolder=subfolder)
|
||||
return cls._local_first(cls.load_config, path, subfolder=subfolder)
|
||||
|
||||
@staticmethod
|
||||
def _local_first(loader, *args, **kwargs):
|
||||
"""Serve fully-cached repos without touching the hub. Online
|
||||
from_pretrained does an etag HEAD round-trip per file (seconds of pure
|
||||
network latency on every warm load); a local_files_only attempt is
|
||||
instant when the cache can't satisfy it, so failing over to the
|
||||
online path costs nothing."""
|
||||
import os as _os
|
||||
|
||||
if _os.environ.get("HF_HUB_OFFLINE") == "1":
|
||||
return loader(*args, **kwargs)
|
||||
try:
|
||||
return loader(*args, local_files_only=True, **kwargs)
|
||||
except Exception:
|
||||
return loader(*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def aitk_from_config(cls, config):
|
||||
with torch.device("meta"):
|
||||
return cls.from_config(config)
|
||||
# params on meta (materialized by the state-dict assign), buffers real:
|
||||
# non-persistent buffers (e.g. rope tables) are computed at init and
|
||||
# never appear in checkpoints
|
||||
from accelerate import init_empty_weights
|
||||
|
||||
with init_empty_weights(include_buffers=False):
|
||||
if hasattr(cls, "from_config"):
|
||||
return cls.from_config(config)
|
||||
# plain nn.Module classes take their params/config object directly
|
||||
return cls(config)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# loading
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@classmethod
|
||||
def load(
|
||||
cls,
|
||||
name_or_path: str,
|
||||
qtype: Optional[str] = None,
|
||||
offload: float = 0.0,
|
||||
dtype: torch.dtype = torch.bfloat16,
|
||||
device: Optional[torch.device] = None,
|
||||
base_model=None,
|
||||
quantize_device: Optional[torch.device] = None,
|
||||
exclude_quant_modules: Optional[List[str]] = None,
|
||||
config_path: Optional[str] = None,
|
||||
config=None,
|
||||
subfolder: Optional[str] = None,
|
||||
use_comfy_weights: bool = True,
|
||||
**kwargs,
|
||||
):
|
||||
"""One-call component load. Resolves/downloads the source (comfy
|
||||
candidates included), builds the module, quantizes it, attaches layer
|
||||
offloading, and places it — everything a holder used to hand-roll.
|
||||
|
||||
qtype: quantization type; "type|path.safetensors" also attaches an
|
||||
accuracy recovery adapter (requires base_model for the network
|
||||
wiring). Pre-quantized checkpoints skip quantization automatically.
|
||||
offload: layer-offload fraction (0 disables); the class's
|
||||
get_offload_ignore_modules hook pins sensitive pieces.
|
||||
device: final placement. With offload active the memory manager owns
|
||||
staging, so no blanket device move happens.
|
||||
base_model: the holder — supplies status prints, quantize_kwargs, and
|
||||
ARA wiring when given.
|
||||
"""
|
||||
model = cls.load_model(
|
||||
name_or_path,
|
||||
dtype=dtype,
|
||||
config_path=config_path,
|
||||
config=config,
|
||||
subfolder=subfolder,
|
||||
use_comfy_weights=use_comfy_weights,
|
||||
**kwargs,
|
||||
)
|
||||
return model.aitk_post_load(
|
||||
qtype=qtype,
|
||||
offload=offload,
|
||||
dtype=dtype,
|
||||
device=device,
|
||||
base_model=base_model,
|
||||
quantize_device=quantize_device,
|
||||
exclude_quant_modules=exclude_quant_modules,
|
||||
)
|
||||
|
||||
def aitk_post_load(
|
||||
self,
|
||||
qtype: Optional[str] = None,
|
||||
offload: float = 0.0,
|
||||
dtype: torch.dtype = torch.bfloat16,
|
||||
device: Optional[torch.device] = None,
|
||||
base_model=None,
|
||||
quantize_device: Optional[torch.device] = None,
|
||||
exclude_quant_modules: Optional[List[str]] = None,
|
||||
**_load_only_kwargs, # tolerate component_load_kwargs() extras
|
||||
):
|
||||
"""The post-load half of `load` — quantize (incl. "qtype|ara"), attach
|
||||
layer offloading, place on device. Callable directly by holders whose
|
||||
checkpoint sourcing is custom but want the standard policy."""
|
||||
from toolkit.print import print_acc
|
||||
|
||||
status_fn = (
|
||||
base_model.print_and_status_update if base_model is not None else print_acc
|
||||
)
|
||||
|
||||
ara_path = None
|
||||
if qtype is not None and "|" in qtype:
|
||||
qtype, ara_path = qtype.split("|", 1)
|
||||
|
||||
if qtype and not getattr(self, "aitk_is_quantized", False):
|
||||
from toolkit.util.quantize import (
|
||||
attach_ara_and_quantize,
|
||||
quantize_module,
|
||||
)
|
||||
|
||||
exclude = list(exclude_quant_modules or []) + list(
|
||||
self.get_quantization_exclude_modules() or []
|
||||
)
|
||||
q_device = quantize_device if quantize_device is not None else device
|
||||
# final home is the quantize gpu (no offloading, no cpu
|
||||
# parking): quantized weights stay put — one bus crossing
|
||||
# instead of three, and no model-sized host-ram copy
|
||||
keep_on_device = (
|
||||
not offload
|
||||
and device is not None
|
||||
and q_device is not None
|
||||
and torch.device(device) == torch.device(q_device)
|
||||
and torch.device(device).type != "cpu"
|
||||
)
|
||||
if ara_path is not None:
|
||||
if base_model is None:
|
||||
raise ValueError(
|
||||
"an accuracy recovery adapter needs base_model= for the "
|
||||
"network wiring"
|
||||
)
|
||||
status_fn("Quantizing with accuracy recovery adapter")
|
||||
attach_ara_and_quantize(
|
||||
base_model,
|
||||
self,
|
||||
ara_path,
|
||||
exclude=exclude,
|
||||
device=q_device,
|
||||
keep_on_device=keep_on_device,
|
||||
)
|
||||
else:
|
||||
status_fn(f"Quantizing ({qtype})")
|
||||
quantize_module(
|
||||
self,
|
||||
qtype,
|
||||
device=q_device,
|
||||
dtype=dtype,
|
||||
block_names=self.get_transformer_block_names(),
|
||||
exclude=exclude,
|
||||
quantize_kwargs=(
|
||||
base_model.model_config.quantize_kwargs
|
||||
if base_model is not None
|
||||
else None
|
||||
),
|
||||
status_fn=status_fn,
|
||||
keep_on_device=keep_on_device,
|
||||
)
|
||||
self.aitk_is_quantized = True
|
||||
self.aitk_qtype = qtype
|
||||
elif qtype and getattr(self, "aitk_is_quantized", False):
|
||||
status_fn("Checkpoint is pre-quantized; skipping quantization")
|
||||
|
||||
if offload and offload > 0:
|
||||
from toolkit.memory_management import MemoryManager
|
||||
|
||||
MemoryManager.attach(
|
||||
self,
|
||||
device,
|
||||
offload_percent=offload,
|
||||
ignore_modules=list(self.get_offload_ignore_modules() or []),
|
||||
)
|
||||
elif device is not None:
|
||||
self.to(device)
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def load_model(
|
||||
cls,
|
||||
@@ -110,7 +334,9 @@ class OstrisModelMixin:
|
||||
quantize_device: Optional[torch.device] = None,
|
||||
exclude_quant_modules: Optional[List[str]] = None,
|
||||
config_path: Optional[str] = None,
|
||||
config=None,
|
||||
subfolder: Optional[str] = None,
|
||||
use_comfy_weights: bool = True,
|
||||
**kwargs,
|
||||
):
|
||||
"""Load a model universally from a given name or path.
|
||||
@@ -119,19 +345,47 @@ class OstrisModelMixin:
|
||||
single .safetensors file, or a remote single file
|
||||
("org/repo/file.safetensors").
|
||||
|
||||
When name_or_path is a standard hub repo this class has comfy weight
|
||||
candidates registered for (aitk_comfy_weight_names), the comfy file is
|
||||
the preferred source: the best-ranked local candidate, or the
|
||||
top-preference candidate downloaded into the comfy layout under
|
||||
MODELS_PATH. The standard repo still supplies the config. Local dirs
|
||||
and unregistered repos load as-is; use_comfy_weights=False opts out.
|
||||
|
||||
qtype: quantize the weights after loading. quantize_device: where to run the
|
||||
quantization math; blocks are moved there one at a time and returned to where
|
||||
they were.
|
||||
config_path: config source for single-file loads, overriding aitk_config_repo.
|
||||
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:
|
||||
subfolder = cls.aitk_subfolder
|
||||
elif subfolder == "":
|
||||
subfolder = None
|
||||
|
||||
if (
|
||||
use_comfy_weights
|
||||
and not name_or_path.endswith(".safetensors")
|
||||
and not os.path.exists(name_or_path)
|
||||
):
|
||||
comfy_path = cls.resolve_comfy_weights(name_or_path, subfolder=subfolder)
|
||||
if comfy_path is not None:
|
||||
if config_path is None:
|
||||
# the standard repo supplies the config for the comfy file
|
||||
config_path = name_or_path
|
||||
name_or_path = comfy_path
|
||||
|
||||
if name_or_path.endswith(".safetensors"):
|
||||
file_path = cls._resolve_single_file(name_or_path)
|
||||
model = cls._load_single_file(
|
||||
file_path, dtype=dtype, config_path=config_path, subfolder=subfolder
|
||||
file_path,
|
||||
dtype=dtype,
|
||||
config_path=config_path,
|
||||
config=config,
|
||||
subfolder=subfolder,
|
||||
**kwargs,
|
||||
)
|
||||
else:
|
||||
if os.path.isdir(name_or_path):
|
||||
@@ -188,28 +442,296 @@ class OstrisModelMixin:
|
||||
subfolder = None
|
||||
return cls.aitk_load_config(config_source, subfolder=subfolder)
|
||||
|
||||
@staticmethod
|
||||
def _readahead(file_path: str):
|
||||
"""Queue kernel readahead for the whole file. safetensors tensors are
|
||||
mmap-backed; without this a cold cache faults pages in per-tensor
|
||||
cudaMemcpy order at a fraction of the drive's sequential bandwidth."""
|
||||
if not hasattr(os, "posix_fadvise"):
|
||||
return # non-POSIX: mmap readahead heuristics only
|
||||
try:
|
||||
fd = os.open(file_path, os.O_RDONLY)
|
||||
try:
|
||||
os.posix_fadvise(fd, 0, 0, os.POSIX_FADV_WILLNEED)
|
||||
finally:
|
||||
os.close(fd)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def _load_single_file(
|
||||
cls,
|
||||
file_path: str,
|
||||
dtype: torch.dtype,
|
||||
config_path: Optional[str] = None,
|
||||
config=None,
|
||||
subfolder: Optional[str] = None,
|
||||
**kwargs,
|
||||
):
|
||||
config = cls._load_single_file_config(config_path, subfolder)
|
||||
|
||||
cls._readahead(file_path)
|
||||
state_dict = load_file(file_path)
|
||||
state_dict = cls.convert_state_dict_on_load(state_dict)
|
||||
for key, value in state_dict.items():
|
||||
state_dict[key] = value.to(dtype=dtype)
|
||||
return cls.load_from_state_dict(
|
||||
state_dict,
|
||||
dtype,
|
||||
config_path=config_path,
|
||||
config=config,
|
||||
subfolder=subfolder,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@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,
|
||||
config=None,
|
||||
subfolder: Optional[str] = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""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). ``config``, when
|
||||
given, is used directly (for models whose config comes from the
|
||||
holder, e.g. model_kwargs-driven archs)."""
|
||||
state_dict = cls.convert_state_dict_on_load(state_dict)
|
||||
has_quant_markers = "scaled_fp8" in state_dict or any(
|
||||
k.endswith(".comfy_quant") for k in state_dict
|
||||
)
|
||||
if config is None:
|
||||
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.load_state_dict(state_dict, assign=True)
|
||||
model.to(dtype=dtype)
|
||||
|
||||
if has_quant_markers:
|
||||
# pre-quantized comfy checkpoint: attach the quantized linears onto
|
||||
# the toolkit's backends, load the rest at its stored precision
|
||||
# (the checkpoint's bf16/fp16/fp32 mix is deliberate)
|
||||
from toolkit.util.comfy_quant_import import import_comfy_quantized_layers
|
||||
|
||||
state_dict, num_quantized = import_comfy_quantized_layers(
|
||||
model, state_dict, orig_dtype=dtype
|
||||
)
|
||||
if cls.aitk_cast_quantized_load:
|
||||
# uniform-dtype models: the leftover (non-quantized) float
|
||||
# tensors follow the load dtype, like the non-marker path
|
||||
for key, value in state_dict.items():
|
||||
if value.is_floating_point():
|
||||
state_dict[key] = value.to(dtype=dtype)
|
||||
cls._load_state_dict_with_quantized(model, state_dict)
|
||||
if cls.aitk_cast_quantized_load:
|
||||
# the importer assigns quantized-layer biases directly at
|
||||
# stored dtype; they must follow too (a stray fp16 bias also
|
||||
# makes ModelMixin.dtype — the pipelines' cast target — lie)
|
||||
from toolkit.util.ostris_quant import OstrisLinear
|
||||
|
||||
for m in model.modules():
|
||||
if isinstance(m, OstrisLinear) and m.bias is not None:
|
||||
m.bias.data = m.bias.data.to(dtype=dtype)
|
||||
model.aitk_is_quantized = True
|
||||
elif cls.aitk_cast_on_load:
|
||||
for key, value in state_dict.items():
|
||||
if value.is_floating_point():
|
||||
state_dict[key] = value.to(dtype=dtype)
|
||||
model.load_state_dict(state_dict, assign=True)
|
||||
model.to(dtype=dtype)
|
||||
else:
|
||||
# stored-precision load (the checkpoint's dtype mix is deliberate)
|
||||
model.load_state_dict(state_dict, assign=True)
|
||||
del state_dict
|
||||
flush()
|
||||
return model
|
||||
|
||||
@staticmethod
|
||||
def _load_state_dict_with_quantized(model, state_dict):
|
||||
"""Load a state dict onto a (meta-built) model whose quantized linears
|
||||
were already attached by import_comfy_quantized_layers: their weight
|
||||
(and importer-assigned bias) legitimately report as missing keys."""
|
||||
from toolkit.util.ostris_quant import OstrisLinear
|
||||
|
||||
result = model.load_state_dict(state_dict, assign=True, strict=False)
|
||||
quantized_param_keys = set()
|
||||
for name, m in model.named_modules():
|
||||
if isinstance(m, OstrisLinear):
|
||||
quantized_param_keys.add(f"{name}.weight")
|
||||
if m.bias is not None:
|
||||
quantized_param_keys.add(f"{name}.bias")
|
||||
bad_missing = [k for k in result.missing_keys if k not in quantized_param_keys]
|
||||
if bad_missing or result.unexpected_keys:
|
||||
raise ValueError(
|
||||
f"{type(model).__name__} load mismatch: missing {bad_missing[:8]}, "
|
||||
f"unexpected {result.unexpected_keys[:8]}"
|
||||
)
|
||||
leftover_meta = [n for n, p in model.named_parameters() if p.is_meta]
|
||||
if leftover_meta:
|
||||
raise ValueError(
|
||||
f"{type(model).__name__} load left meta parameters: "
|
||||
f"{leftover_meta[:8]}"
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# comfy weight sources
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@classmethod
|
||||
def resolve_comfy_weights(
|
||||
cls,
|
||||
name_or_path: str,
|
||||
subfolder: Optional[str] = None,
|
||||
local_only: bool = False,
|
||||
hf_token: Optional[str] = None,
|
||||
status_fn: Optional[callable] = None,
|
||||
) -> Optional[str]:
|
||||
"""The comfy-format weight file replacing a standard ``name_or_path``,
|
||||
or None when this class has none registered for it. Best-ranked local
|
||||
candidate wins; otherwise the top-preference candidate is downloaded
|
||||
to the comfy layout under MODELS_PATH (unless local_only).
|
||||
|
||||
Candidate keys may be plain repo ids or ``(repo_id, subfolder)``
|
||||
tuples for checkpoints holding several of this component (e.g.
|
||||
wan2.2's transformer / transformer_2)."""
|
||||
from toolkit.models.v2.resolver import resolve_comfy_candidates
|
||||
|
||||
candidates = None
|
||||
if subfolder is not None:
|
||||
candidates = cls.aitk_comfy_weight_names.get((name_or_path, subfolder))
|
||||
if candidates is None:
|
||||
candidates = cls.aitk_comfy_weight_names.get(name_or_path)
|
||||
repo_id = cls.aitk_comfy_repo
|
||||
if isinstance(candidates, dict):
|
||||
# entries may carry their own comfy repo ({"repo": ..., "files": [...]})
|
||||
repo_id = candidates.get("repo", repo_id)
|
||||
candidates = candidates.get("files")
|
||||
if not candidates or repo_id is None:
|
||||
return None
|
||||
return resolve_comfy_candidates(
|
||||
candidates,
|
||||
repo_id=repo_id,
|
||||
hf_token=hf_token,
|
||||
status_fn=status_fn,
|
||||
local_only=local_only,
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# tokenizer / processor (text-encoder modules)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@classmethod
|
||||
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
|
||||
|
||||
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")
|
||||
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 cls._local_first(
|
||||
AutoTokenizer.from_pretrained, source, subfolder=subfolder or "", **kwargs
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def load_processor(
|
||||
cls,
|
||||
name_or_path: Optional[str] = None,
|
||||
subfolder: Optional[str] = None,
|
||||
**kwargs,
|
||||
):
|
||||
from transformers import AutoProcessor
|
||||
|
||||
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")
|
||||
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 cls._local_first(
|
||||
AutoProcessor.from_pretrained, source, subfolder=subfolder or "", **kwargs
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# saving
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@torch.no_grad()
|
||||
def save_model(
|
||||
self,
|
||||
output_path: str,
|
||||
dtype: Optional[torch.dtype] = None,
|
||||
metadata: Optional[Dict[str, str]] = None,
|
||||
quantized: Optional[bool] = None,
|
||||
):
|
||||
"""Save as a single-file .safetensors in the model's original (comfy)
|
||||
key layout, via convert_state_dict_on_save.
|
||||
|
||||
quantized: None (auto) keeps quantized storage — comfy_quant markers,
|
||||
loadable by ComfyUI and by this class — when the model holds quantized
|
||||
layers whose backend has a comfy format (convrot8 / nvfp4 /
|
||||
convrotcomfyw4a4), and saves dequantized full precision otherwise.
|
||||
True forces quantized storage (raises if any quantized layer's backend
|
||||
has no comfy format); False forces a dequantized save. dtype, when
|
||||
given, casts the floating point (non-quantized) tensors."""
|
||||
from safetensors.torch import save_file
|
||||
from toolkit.util.quantize import dequantize_if_quantized
|
||||
|
||||
q_entries: Dict[str, torch.Tensor] = {}
|
||||
exported: List[str] = []
|
||||
if quantized is None or quantized:
|
||||
from toolkit.util.comfy_quant_export import export_comfy_quantized_layers
|
||||
|
||||
q_entries, exported, unexportable = export_comfy_quantized_layers(self)
|
||||
if unexportable:
|
||||
if quantized:
|
||||
raise ValueError(
|
||||
f"{type(self).__name__}: quantized layers without a comfy "
|
||||
f"storage format: {unexportable[:8]}"
|
||||
)
|
||||
# auto mode: a partially-quantized file would be inconsistent —
|
||||
# fall back to a fully dequantized save
|
||||
q_entries, exported = {}, []
|
||||
|
||||
skip_keys = {f"{name}.weight" for name in exported}
|
||||
state_dict = {}
|
||||
for key, value in self.state_dict().items():
|
||||
if key in skip_keys:
|
||||
continue
|
||||
value = dequantize_if_quantized(value)
|
||||
if dtype is not None and value.is_floating_point():
|
||||
value = value.to(dtype=dtype)
|
||||
state_dict[key] = value.detach().to("cpu").contiguous()
|
||||
state_dict.update(q_entries)
|
||||
state_dict = self.convert_state_dict_on_save(state_dict)
|
||||
|
||||
parent = os.path.dirname(output_path)
|
||||
if parent:
|
||||
os.makedirs(parent, exist_ok=True)
|
||||
save_file(state_dict, output_path, metadata=metadata)
|
||||
del state_dict
|
||||
flush()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# quantization
|
||||
# ------------------------------------------------------------------
|
||||
@@ -222,7 +744,7 @@ class OstrisModelMixin:
|
||||
exclude: Optional[List[str]] = None,
|
||||
):
|
||||
"""Quantize the model weights in place. When device is given, the repeated
|
||||
blocks (get_quantization_block_names) are moved there one at a time for the
|
||||
blocks (get_transformer_block_names) are moved there one at a time for the
|
||||
quantization math and returned to their original device, so the whole model
|
||||
never has to fit on the gpu in full precision."""
|
||||
from optimum.quanto import freeze
|
||||
@@ -238,7 +760,7 @@ class OstrisModelMixin:
|
||||
)
|
||||
|
||||
blocks: List[torch.nn.Module] = []
|
||||
for name in self.get_quantization_block_names() or []:
|
||||
for name in self.get_transformer_block_names() or []:
|
||||
# name may be a dotted path for models that nest their blocks
|
||||
block_list = self
|
||||
for part in name.split("."):
|
||||
@@ -270,3 +792,30 @@ class OstrisModelMixin:
|
||||
self.aitk_qtype = qtype
|
||||
flush()
|
||||
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._local_first(
|
||||
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 cls._local_first(
|
||||
AutoConfig.from_pretrained, path, subfolder=subfolder or ""
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def aitk_from_config(cls, config):
|
||||
from accelerate import init_empty_weights
|
||||
|
||||
with init_empty_weights(include_buffers=False):
|
||||
return cls(config)
|
||||
Whitespace-only changes.
@@ -0,0 +1,13 @@
|
||||
from diffusers import AuraFlowTransformer2DModel as DiffusersAuraFlowTransformer2DModel
|
||||
|
||||
from .._mixin import OstrisModelMixin
|
||||
|
||||
|
||||
class AuraFlowTransformer2DModel(
|
||||
DiffusersAuraFlowTransformer2DModel, OstrisModelMixin
|
||||
):
|
||||
aitk_subfolder = "transformer"
|
||||
|
||||
@classmethod
|
||||
def get_transformer_block_names(cls):
|
||||
return ["joint_transformer_blocks", "single_transformer_blocks"]
|
||||
@@ -0,0 +1,15 @@
|
||||
from diffusers.models import (
|
||||
CosmosTransformer3DModel as DiffusersCosmosTransformer3DModel,
|
||||
)
|
||||
|
||||
from .._mixin import OstrisModelMixin
|
||||
|
||||
|
||||
class CosmosTransformer3DModel(DiffusersCosmosTransformer3DModel, OstrisModelMixin):
|
||||
"""Cosmos video/image DiT (anima)."""
|
||||
|
||||
aitk_subfolder = "transformer"
|
||||
|
||||
@classmethod
|
||||
def get_transformer_block_names(cls):
|
||||
return ["transformer_blocks"]
|
||||
@@ -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,46 @@
|
||||
from diffusers.models.transformers import (
|
||||
LTX2VideoTransformer3DModel as DiffusersLTX2VideoTransformer3DModel,
|
||||
)
|
||||
from diffusers.pipelines.ltx2 import (
|
||||
LTX2TextConnectors as DiffusersLTX2TextConnectors,
|
||||
)
|
||||
from diffusers.pipelines.ltx2 import LTX2Vocoder as DiffusersLTX2Vocoder
|
||||
from diffusers.pipelines.ltx2 import (
|
||||
LTX2VocoderWithBWE as DiffusersLTX2VocoderWithBWE,
|
||||
)
|
||||
|
||||
from .._mixin import OstrisModelMixin
|
||||
|
||||
|
||||
class LTX2VideoTransformer3DModel(
|
||||
DiffusersLTX2VideoTransformer3DModel, OstrisModelMixin
|
||||
):
|
||||
aitk_subfolder = "transformer"
|
||||
|
||||
@classmethod
|
||||
def get_transformer_block_names(cls):
|
||||
return ["transformer_blocks"]
|
||||
|
||||
def get_offload_ignore_modules(self):
|
||||
# fp32 scale/shift tables (bare tensors) must stay resident
|
||||
mods = []
|
||||
for block in self.transformer_blocks:
|
||||
mods += [
|
||||
block.scale_shift_table,
|
||||
block.audio_scale_shift_table,
|
||||
block.video_a2v_cross_attn_scale_shift_table,
|
||||
block.audio_a2v_cross_attn_scale_shift_table,
|
||||
]
|
||||
return mods + [self.scale_shift_table, self.audio_scale_shift_table]
|
||||
|
||||
|
||||
class LTX2TextConnectors(DiffusersLTX2TextConnectors, OstrisModelMixin):
|
||||
aitk_subfolder = "connectors"
|
||||
|
||||
|
||||
class LTX2Vocoder(DiffusersLTX2Vocoder, OstrisModelMixin):
|
||||
aitk_subfolder = "vocoder"
|
||||
|
||||
|
||||
class LTX2VocoderWithBWE(DiffusersLTX2VocoderWithBWE, OstrisModelMixin):
|
||||
aitk_subfolder = "vocoder"
|
||||
@@ -0,0 +1,13 @@
|
||||
from diffusers import Lumina2Transformer2DModel as DiffusersLumina2Transformer2DModel
|
||||
|
||||
from .._mixin import OstrisModelMixin
|
||||
|
||||
|
||||
class Lumina2Transformer2DModel(
|
||||
DiffusersLumina2Transformer2DModel, OstrisModelMixin
|
||||
):
|
||||
aitk_subfolder = "transformer"
|
||||
|
||||
@classmethod
|
||||
def get_transformer_block_names(cls):
|
||||
return ["layers"]
|
||||
@@ -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,24 @@
|
||||
from diffusers import PixArtTransformer2DModel as DiffusersPixArtTransformer2DModel
|
||||
from diffusers import Transformer2DModel as DiffusersTransformer2DModel
|
||||
|
||||
from .._mixin import OstrisModelMixin
|
||||
|
||||
|
||||
class PixArtTransformer2DModel(
|
||||
DiffusersPixArtTransformer2DModel, OstrisModelMixin
|
||||
):
|
||||
aitk_subfolder = "transformer"
|
||||
|
||||
@classmethod
|
||||
def get_transformer_block_names(cls):
|
||||
return ["transformer_blocks"]
|
||||
|
||||
|
||||
class Transformer2DModel(DiffusersTransformer2DModel, OstrisModelMixin):
|
||||
"""The generic diffusers DiT the pixart sigma path loads."""
|
||||
|
||||
aitk_subfolder = "transformer"
|
||||
|
||||
@classmethod
|
||||
def get_transformer_block_names(cls):
|
||||
return ["transformer_blocks"]
|
||||
@@ -0,0 +1,58 @@
|
||||
from diffusers import (
|
||||
QwenImageTransformer2DModel as DiffusersQwenImageTransformer2DModel,
|
||||
)
|
||||
|
||||
from .._mixin import OstrisModelMixin
|
||||
|
||||
|
||||
class QwenImageTransformer2DModel(
|
||||
DiffusersQwenImageTransformer2DModel, OstrisModelMixin
|
||||
):
|
||||
aitk_subfolder = "transformer"
|
||||
aitk_config_repo = "Qwen/Qwen-Image"
|
||||
|
||||
aitk_comfy_repo = "Comfy-Org/Qwen-Image_ComfyUI"
|
||||
# the comfy files use the diffusers key layout directly (no conversion);
|
||||
# fp8mixed carries float8_e4m3fn comfy_quant markers that attach straight
|
||||
# onto this class's modules
|
||||
aitk_comfy_weight_names = {
|
||||
"Qwen/Qwen-Image": [
|
||||
"split_files/diffusion_models/qwen_image_fp8mixed.safetensors",
|
||||
# raw fp8 cast (no markers, diffusers keys) — loads via from_single_file
|
||||
"split_files/diffusion_models/qwen_image_fp8_e4m3fn.safetensors",
|
||||
"split_files/diffusion_models/qwen_image_bf16.safetensors",
|
||||
],
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_transformer_block_names(cls):
|
||||
return ["transformer_blocks"]
|
||||
|
||||
@classmethod
|
||||
def _load_single_file(
|
||||
cls, file_path, dtype, config_path=None, config=None, subfolder=None
|
||||
):
|
||||
from safetensors import safe_open
|
||||
|
||||
with safe_open(file_path, framework="pt") as f:
|
||||
has_markers = any(k.endswith(".comfy_quant") for k in f.keys())
|
||||
if has_markers:
|
||||
# comfy prequantized checkpoint (diffusers key layout): the mixin
|
||||
# path attaches the quantized layers
|
||||
return super()._load_single_file(
|
||||
file_path,
|
||||
dtype,
|
||||
config_path=config_path,
|
||||
config=config,
|
||||
subfolder=subfolder,
|
||||
)
|
||||
# other single-file checkpoints 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
|
||||
@@ -0,0 +1,11 @@
|
||||
from diffusers import SD3Transformer2DModel as DiffusersSD3Transformer2DModel
|
||||
|
||||
from .._mixin import OstrisModelMixin
|
||||
|
||||
|
||||
class SD3Transformer2DModel(DiffusersSD3Transformer2DModel, OstrisModelMixin):
|
||||
aitk_subfolder = "transformer"
|
||||
|
||||
@classmethod
|
||||
def get_transformer_block_names(cls):
|
||||
return ["transformer_blocks"]
|
||||
@@ -0,0 +1,13 @@
|
||||
from diffusers import UNet2DConditionModel as DiffusersUNet2DConditionModel
|
||||
|
||||
from .._mixin import OstrisModelMixin
|
||||
|
||||
|
||||
class UNet2DConditionModel(DiffusersUNet2DConditionModel, OstrisModelMixin):
|
||||
"""The SD1/SD2/SDXL-family UNet."""
|
||||
|
||||
aitk_subfolder = "unet"
|
||||
|
||||
@classmethod
|
||||
def get_transformer_block_names(cls):
|
||||
return ["down_blocks", "up_blocks"]
|
||||
@@ -0,0 +1,204 @@
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
from diffusers import WanTransformer3DModel as DiffusersWanTransformer3DModel
|
||||
|
||||
from .._mixin import OstrisModelMixin
|
||||
|
||||
|
||||
def patch_wan_patch_embedding(transformer) -> bool:
|
||||
"""Wan's patch_embedding is a Conv3d whose kernel == stride (non-overlapping
|
||||
patchify), 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 the forward for the exactly-equivalent F.linear (a GEMM),
|
||||
returning the conv's own [b, out, t', h', w'] layout so it stays a drop-in.
|
||||
The weight is read lazily so this survives later .to(device)/dtype moves."""
|
||||
conv = getattr(transformer, "patch_embedding", None)
|
||||
if not isinstance(conv, torch.nn.Conv3d):
|
||||
return False
|
||||
if (
|
||||
tuple(conv.kernel_size) != tuple(conv.stride)
|
||||
or any(p != 0 for p in conv.padding)
|
||||
or any(d != 1 for d in conv.dilation)
|
||||
or conv.groups != 1
|
||||
):
|
||||
return False
|
||||
|
||||
def fast_forward(x, _conv=conv):
|
||||
weight = _conv.weight
|
||||
out_c = weight.shape[0]
|
||||
kt, kh, kw = _conv.kernel_size
|
||||
b, c, t, h, w = x.shape
|
||||
tp, hp, wp = t // kt, h // kh, w // kw
|
||||
# patchify with (c, kt, kh, kw) fastest — matches weight.reshape below
|
||||
x = x.reshape(b, c, tp, kt, hp, kh, wp, kw)
|
||||
x = x.permute(0, 2, 4, 6, 1, 3, 5, 7).reshape(b, tp * hp * wp, -1)
|
||||
w2d = weight.reshape(out_c, -1)
|
||||
x = F.linear(x.to(w2d.dtype), w2d, _conv.bias)
|
||||
return x.transpose(1, 2).reshape(b, out_c, tp, hp, wp)
|
||||
|
||||
conv.forward = fast_forward
|
||||
return True
|
||||
|
||||
|
||||
class WanTransformer3DModel(DiffusersWanTransformer3DModel, OstrisModelMixin):
|
||||
"""Wan 2.1/2.2 video DiT (wan22 loads two of these into its dual wrapper)."""
|
||||
|
||||
aitk_subfolder = "transformer"
|
||||
|
||||
aitk_comfy_repo = "Comfy-Org/Wan_2.2_ComfyUI_Repackaged"
|
||||
# comfy wan files use the original key layout; convert_state_dict_on_load
|
||||
# renames them (pure substring renames, so the legacy scaled_fp8 weight /
|
||||
# scale keys ride along with their modules). wan2.2 A14B checkpoints hold
|
||||
# two DiTs, keyed by (repo, subfolder): transformer = high noise,
|
||||
# transformer_2 = low noise. wan2.1 entries override the comfy repo.
|
||||
aitk_comfy_weight_names = {
|
||||
"Wan-AI/Wan2.2-TI2V-5B-Diffusers": [
|
||||
"split_files/diffusion_models/wan2.2_ti2v_5B_fp16.safetensors",
|
||||
],
|
||||
("Wan-AI/Wan2.2-T2V-A14B-Diffusers", "transformer"): [
|
||||
"split_files/diffusion_models/wan2.2_t2v_high_noise_14B_fp8_scaled.safetensors",
|
||||
],
|
||||
("Wan-AI/Wan2.2-T2V-A14B-Diffusers", "transformer_2"): [
|
||||
"split_files/diffusion_models/wan2.2_t2v_low_noise_14B_fp8_scaled.safetensors",
|
||||
],
|
||||
("Wan-AI/Wan2.2-I2V-A14B-Diffusers", "transformer"): [
|
||||
"split_files/diffusion_models/wan2.2_i2v_high_noise_14B_fp8_scaled.safetensors",
|
||||
],
|
||||
("Wan-AI/Wan2.2-I2V-A14B-Diffusers", "transformer_2"): [
|
||||
"split_files/diffusion_models/wan2.2_i2v_low_noise_14B_fp8_scaled.safetensors",
|
||||
],
|
||||
# the UI defaults point at the ai-toolkit bf16 repacks; same comfy files
|
||||
("ai-toolkit/Wan2.2-T2V-A14B-Diffusers-bf16", "transformer"): [
|
||||
"split_files/diffusion_models/wan2.2_t2v_high_noise_14B_fp8_scaled.safetensors",
|
||||
],
|
||||
("ai-toolkit/Wan2.2-T2V-A14B-Diffusers-bf16", "transformer_2"): [
|
||||
"split_files/diffusion_models/wan2.2_t2v_low_noise_14B_fp8_scaled.safetensors",
|
||||
],
|
||||
("ai-toolkit/Wan2.2-I2V-A14B-Diffusers-bf16", "transformer"): [
|
||||
"split_files/diffusion_models/wan2.2_i2v_high_noise_14B_fp8_scaled.safetensors",
|
||||
],
|
||||
("ai-toolkit/Wan2.2-I2V-A14B-Diffusers-bf16", "transformer_2"): [
|
||||
"split_files/diffusion_models/wan2.2_i2v_low_noise_14B_fp8_scaled.safetensors",
|
||||
],
|
||||
"Wan-AI/Wan2.1-T2V-1.3B-Diffusers": {
|
||||
"repo": "Comfy-Org/Wan_2.1_ComfyUI_repackaged",
|
||||
"files": [
|
||||
"split_files/diffusion_models/wan2.1_t2v_1.3B_bf16.safetensors",
|
||||
"split_files/diffusion_models/wan2.1_t2v_1.3B_fp16.safetensors",
|
||||
],
|
||||
},
|
||||
"Wan-AI/Wan2.1-T2V-14B-Diffusers": {
|
||||
"repo": "Comfy-Org/Wan_2.1_ComfyUI_repackaged",
|
||||
"files": [
|
||||
"split_files/diffusion_models/wan2.1_t2v_14B_fp8_scaled.safetensors",
|
||||
"split_files/diffusion_models/wan2.1_t2v_14B_bf16.safetensors",
|
||||
"split_files/diffusion_models/wan2.1_t2v_14B_fp16.safetensors",
|
||||
],
|
||||
},
|
||||
"Wan-AI/Wan2.1-I2V-14B-480P-Diffusers": {
|
||||
"repo": "Comfy-Org/Wan_2.1_ComfyUI_repackaged",
|
||||
"files": [
|
||||
"split_files/diffusion_models/wan2.1_i2v_480p_14B_fp8_scaled.safetensors",
|
||||
"split_files/diffusion_models/wan2.1_i2v_480p_14B_bf16.safetensors",
|
||||
"split_files/diffusion_models/wan2.1_i2v_480p_14B_fp16.safetensors",
|
||||
],
|
||||
},
|
||||
"Wan-AI/Wan2.1-I2V-14B-720P-Diffusers": {
|
||||
"repo": "Comfy-Org/Wan_2.1_ComfyUI_repackaged",
|
||||
"files": [
|
||||
"split_files/diffusion_models/wan2.1_i2v_720p_14B_fp8_scaled.safetensors",
|
||||
"split_files/diffusion_models/wan2.1_i2v_720p_14B_bf16.safetensors",
|
||||
"split_files/diffusion_models/wan2.1_i2v_720p_14B_fp16.safetensors",
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
# the comfy fp8/scaled_fp8 wan files mix fp16 conv biases with fp32
|
||||
# tables; wan's forward assumes one uniform dtype, so cast the
|
||||
# non-quantized tensors to the load dtype (the old holder did this with a
|
||||
# blanket .to(device, dtype) after load)
|
||||
aitk_cast_quantized_load = True
|
||||
|
||||
@classmethod
|
||||
def get_transformer_block_names(cls):
|
||||
return ["blocks"]
|
||||
|
||||
def aitk_post_load(self, **kwargs):
|
||||
# slow bf16 Conv3d patchify -> equivalent GEMM, before quantize/offload
|
||||
patch_wan_patch_embedding(self)
|
||||
return super().aitk_post_load(**kwargs)
|
||||
|
||||
def get_offload_ignore_modules(self):
|
||||
# fp32 modulation tables must stay resident on the compute device
|
||||
return [self.scale_shift_table] + [
|
||||
block.scale_shift_table for block in self.blocks
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def convert_state_dict_on_load(cls, state_dict):
|
||||
# original/comfy wan keys -> diffusers layout via diffusers' own
|
||||
# converter (rename-only for the base/i2v/vace variants)
|
||||
is_original = any(
|
||||
".self_attn." in k
|
||||
or ".cross_attn." in k
|
||||
or k.startswith("model.diffusion_model.")
|
||||
for k in state_dict
|
||||
)
|
||||
if not is_original:
|
||||
return state_dict
|
||||
from diffusers.loaders.single_file_utils import (
|
||||
convert_wan_transformer_to_diffusers,
|
||||
)
|
||||
|
||||
return convert_wan_transformer_to_diffusers(dict(state_dict))
|
||||
|
||||
# inverse of diffusers' rename table, for the base/t2v/i2v variants (no
|
||||
# vace/animate: their extra mappings collide in reverse). Order matters:
|
||||
# longest/most-specific first, and the norm2/norm3 swap uses a placeholder.
|
||||
_SAVE_RENAMES = [
|
||||
("condition_embedder.time_embedder.linear_1", "time_embedding.0"),
|
||||
("condition_embedder.time_embedder.linear_2", "time_embedding.2"),
|
||||
("condition_embedder.text_embedder.linear_1", "text_embedding.0"),
|
||||
("condition_embedder.text_embedder.linear_2", "text_embedding.2"),
|
||||
("condition_embedder.time_proj", "time_projection.1"),
|
||||
("condition_embedder.image_embedder.norm1", "img_emb.proj.0"),
|
||||
("condition_embedder.image_embedder.ff.net.0.proj", "img_emb.proj.1"),
|
||||
("condition_embedder.image_embedder.ff.net.2", "img_emb.proj.3"),
|
||||
("condition_embedder.image_embedder.norm2", "img_emb.proj.4"),
|
||||
("ffn.net.0.proj", "ffn.0"),
|
||||
("ffn.net.2", "ffn.2"),
|
||||
(".norm_added_k.", ".norm_k_img."),
|
||||
(".add_k_proj.", ".k_img."),
|
||||
(".add_v_proj.", ".v_img."),
|
||||
(".to_out.0.", ".o."),
|
||||
(".to_q.", ".q."),
|
||||
(".to_k.", ".k."),
|
||||
(".to_v.", ".v."),
|
||||
("attn2", "cross_attn"),
|
||||
("attn1", "self_attn"),
|
||||
# norm2 <-> norm3 swap back
|
||||
("norm3", "norm__placeholder"),
|
||||
("norm2", "norm3"),
|
||||
("norm__placeholder", "norm2"),
|
||||
("proj_out", "head.head"),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def convert_state_dict_on_save(cls, state_dict):
|
||||
"""Diffusers layout back to the original/comfy wan key layout
|
||||
(rename-only, so quantized weight/scale/marker keys ride along)."""
|
||||
if any(".self_attn." in k or ".cross_attn." in k for k in state_dict):
|
||||
return state_dict # already original layout
|
||||
new_sd = {}
|
||||
for key, value in state_dict.items():
|
||||
k = key
|
||||
# scale_shift_table: blocks keep the name as `modulation`, the
|
||||
# top-level one belongs to the output head
|
||||
if k == "scale_shift_table":
|
||||
k = "head.modulation"
|
||||
elif k.endswith(".scale_shift_table"):
|
||||
k = k[: -len("scale_shift_table")] + "modulation"
|
||||
for src, dst in cls._SAVE_RENAMES:
|
||||
k = k.replace(src, dst)
|
||||
new_sd[k] = value
|
||||
return new_sd
|
||||
@@ -3,7 +3,7 @@ from diffusers.models.transformers import (
|
||||
ZImageTransformer2DModel as DiffusersZImageTransformer2DModel,
|
||||
)
|
||||
|
||||
from ._mixin import OstrisModelMixin
|
||||
from .._mixin import OstrisModelMixin
|
||||
|
||||
|
||||
class ZImageTransformer2DModel(DiffusersZImageTransformer2DModel, OstrisModelMixin):
|
||||
@@ -11,10 +11,24 @@ class ZImageTransformer2DModel(DiffusersZImageTransformer2DModel, OstrisModelMix
|
||||
# repo to pull the config from when loading a single-file checkpoint
|
||||
aitk_config_repo = "Tongyi-MAI/Z-Image-Turbo"
|
||||
|
||||
aitk_comfy_repo = "Comfy-Org/z_image_turbo"
|
||||
# the int8_convrot file marks the FUSED attention.qkv modules; the load
|
||||
# converter splits those entries exactly into to_q/to_k/to_v (row-sliced
|
||||
# qdata + scales), so it attaches onto this split layout directly
|
||||
aitk_comfy_weight_names = {
|
||||
"Tongyi-MAI/Z-Image-Turbo": [
|
||||
"split_files/diffusion_models/z_image_turbo_int8_convrot.safetensors",
|
||||
"split_files/diffusion_models/z_image_turbo_bf16.safetensors",
|
||||
],
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_quantization_block_names(cls):
|
||||
def get_transformer_block_names(cls):
|
||||
return ["layers"]
|
||||
|
||||
def get_offload_ignore_modules(self):
|
||||
return [self.x_pad_token, self.cap_pad_token]
|
||||
|
||||
@classmethod
|
||||
def get_quantization_exclude_modules(cls):
|
||||
# sensitive modules kept in full precision (fnmatch patterns on module
|
||||
@@ -36,6 +50,22 @@ class ZImageTransformer2DModel(DiffusersZImageTransformer2DModel, OstrisModelMix
|
||||
@classmethod
|
||||
def convert_state_dict_on_load(cls, state_dict):
|
||||
"""Convert a single-file Z-Image checkpoint to diffusers transformer keys."""
|
||||
from toolkit.util.comfy_quant_import import split_fused_quantized_keys
|
||||
|
||||
state_dict = dict(state_dict)
|
||||
# quantized fused qkv entries (comfy convrot/fp8/nvfp4 files) split
|
||||
# exactly into the three projections: row-consecutive weights/scales
|
||||
for marker_key in [
|
||||
k for k in list(state_dict) if k.endswith(".attention.qkv.comfy_quant")
|
||||
]:
|
||||
prefix = marker_key[: -len(".comfy_quant")]
|
||||
base = prefix[: -len(".qkv")]
|
||||
split_fused_quantized_keys(
|
||||
state_dict,
|
||||
prefix,
|
||||
[f"{base}.to_q", f"{base}.to_k", f"{base}.to_v"],
|
||||
)
|
||||
|
||||
new_sd = {}
|
||||
for key, value in state_dict.items():
|
||||
k = key
|
||||
@@ -47,9 +77,10 @@ class ZImageTransformer2DModel(DiffusersZImageTransformer2DModel, OstrisModelMix
|
||||
new_sd[prefix + ".attention.to_k.weight"] = k_proj
|
||||
new_sd[prefix + ".attention.to_v.weight"] = v
|
||||
continue
|
||||
k = k.replace(".attention.out.weight", ".attention.to_out.0.weight")
|
||||
k = k.replace(".attention.q_norm.weight", ".attention.norm_q.weight")
|
||||
k = k.replace(".attention.k_norm.weight", ".attention.norm_k.weight")
|
||||
# module-prefix renames so weight/bias/scale/marker keys all map
|
||||
k = k.replace(".attention.out.", ".attention.to_out.0.")
|
||||
k = k.replace(".attention.q_norm.", ".attention.norm_q.")
|
||||
k = k.replace(".attention.k_norm.", ".attention.norm_k.")
|
||||
if k.startswith("x_embedder."):
|
||||
k = "all_x_embedder.2-1." + k[len("x_embedder.") :]
|
||||
elif k.startswith("final_layer."):
|
||||
@@ -60,6 +91,20 @@ class ZImageTransformer2DModel(DiffusersZImageTransformer2DModel, OstrisModelMix
|
||||
@classmethod
|
||||
def convert_state_dict_on_save(cls, state_dict):
|
||||
"""Convert a diffusers transformer state dict back to the single-file layout."""
|
||||
from toolkit.util.comfy_quant_import import fuse_split_quantized_keys
|
||||
|
||||
state_dict = dict(state_dict)
|
||||
# quantized split projections fuse back into the single-file qkv entry
|
||||
for marker_key in [
|
||||
k for k in list(state_dict) if k.endswith(".attention.to_q.comfy_quant")
|
||||
]:
|
||||
base = marker_key[: -len(".to_q.comfy_quant")]
|
||||
fuse_split_quantized_keys(
|
||||
state_dict,
|
||||
[f"{base}.to_q", f"{base}.to_k", f"{base}.to_v"],
|
||||
f"{base}.qkv",
|
||||
)
|
||||
|
||||
new_sd = {}
|
||||
qkv_cache = {}
|
||||
for key, value in state_dict.items():
|
||||
@@ -90,9 +135,9 @@ class ZImageTransformer2DModel(DiffusersZImageTransformer2DModel, OstrisModelMix
|
||||
break
|
||||
if matched:
|
||||
continue
|
||||
k = k.replace(".attention.to_out.0.weight", ".attention.out.weight")
|
||||
k = k.replace(".attention.norm_q.weight", ".attention.q_norm.weight")
|
||||
k = k.replace(".attention.norm_k.weight", ".attention.k_norm.weight")
|
||||
k = k.replace(".attention.to_out.0.", ".attention.out.")
|
||||
k = k.replace(".attention.norm_q.", ".attention.q_norm.")
|
||||
k = k.replace(".attention.norm_k.", ".attention.k_norm.")
|
||||
if k.startswith("all_x_embedder.2-1."):
|
||||
k = "x_embedder." + k[len("all_x_embedder.2-1.") :]
|
||||
elif k.startswith("all_final_layer.2-1."):
|
||||
@@ -0,0 +1,201 @@
|
||||
"""ComfyUI-layout weight file resolution.
|
||||
|
||||
Weight files live under MODELS_PATH in ComfyUI's folder layout
|
||||
(diffusion_models/, text_encoders/, vae/, ...) so the folder is shareable with
|
||||
a ComfyUI install. Files are used in place when present and downloaded to
|
||||
exactly their repo-relative location only when missing, so nothing is ever
|
||||
duplicated on re-run.
|
||||
|
||||
Lifted from the minimax_h3 / ltx2.5 model implementations; those now call
|
||||
into here.
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import Callable, Iterable, Optional
|
||||
|
||||
from toolkit.paths import MODELS_PATH
|
||||
|
||||
|
||||
def comfy_precision_rank(filename: str) -> int:
|
||||
"""Load-preference rank for a comfy weight filename:
|
||||
convrot8 (0) > float8 mixed (1) > float8 (2) > bf16 (3) > fp16 (4) >
|
||||
anything else, e.g. nvfp4 or unmarked (5)."""
|
||||
name = os.path.basename(filename).lower()
|
||||
if "convrot" in name:
|
||||
return 0
|
||||
is_fp8 = "fp8" in name or "float8" in name or "e4m3" in name
|
||||
if is_fp8 and "mixed" in name:
|
||||
return 1
|
||||
if is_fp8:
|
||||
return 2
|
||||
if "bf16" in name:
|
||||
return 3
|
||||
if "fp16" in name:
|
||||
return 4
|
||||
return 5
|
||||
|
||||
|
||||
def comfy_local_rel(repo_rel: str) -> str:
|
||||
"""Repo file path -> ComfyUI models-folder path. Comfy-Org repos nest the
|
||||
comfy layout under a packaging prefix (split_files/, non_official/) that is
|
||||
not part of the shared models folder layout."""
|
||||
for prefix in ("split_files/", "non_official/"):
|
||||
if repo_rel.startswith(prefix):
|
||||
return repo_rel[len(prefix):]
|
||||
return repo_rel
|
||||
|
||||
|
||||
def resolve_comfy_candidates(
|
||||
candidates: Iterable[str],
|
||||
repo_id: str,
|
||||
hf_token: Optional[str] = None,
|
||||
status_fn: Optional[Callable[[str], None]] = None,
|
||||
local_only: bool = False,
|
||||
) -> Optional[str]:
|
||||
"""Pick the best comfy weight file among precision variants of one
|
||||
component (repo-relative paths, ranked by comfy_precision_rank then list
|
||||
order). The best-ranked LOCAL candidate wins; only when no candidate is
|
||||
local is the best-ranked one downloaded to its comfy-layout location
|
||||
under MODELS_PATH."""
|
||||
candidates = list(candidates)
|
||||
ordered = sorted(
|
||||
candidates, key=lambda c: (comfy_precision_rank(c), candidates.index(c))
|
||||
)
|
||||
for repo_rel in ordered:
|
||||
found = resolve_comfy_file(
|
||||
comfy_local_rel(repo_rel), repo_id, local_only=True
|
||||
)
|
||||
if found is not None:
|
||||
return found
|
||||
if local_only:
|
||||
return None
|
||||
|
||||
import huggingface_hub
|
||||
|
||||
best = ordered[0]
|
||||
local_rel = comfy_local_rel(best)
|
||||
if status_fn is not None:
|
||||
status_fn(f"Downloading {best} from {repo_id} into {MODELS_PATH}")
|
||||
path = huggingface_hub.hf_hub_download(
|
||||
repo_id=repo_id, filename=best, token=hf_token, local_dir=MODELS_PATH
|
||||
)
|
||||
target = os.path.join(MODELS_PATH, local_rel)
|
||||
if os.path.abspath(path) != os.path.abspath(target):
|
||||
# move out of the packaging prefix into the shared comfy layout
|
||||
os.makedirs(os.path.dirname(target), exist_ok=True)
|
||||
os.replace(path, target)
|
||||
return target
|
||||
return path
|
||||
|
||||
|
||||
def find_file_recursive(root_dir: str, filename: str) -> Optional[str]:
|
||||
"""First (breadth-stable, sorted) match of ``filename`` anywhere under
|
||||
``root_dir``."""
|
||||
if not os.path.isdir(root_dir):
|
||||
return None
|
||||
for dirpath, dirnames, filenames in os.walk(root_dir):
|
||||
dirnames.sort()
|
||||
if filename in filenames:
|
||||
return os.path.join(dirpath, filename)
|
||||
return None
|
||||
|
||||
|
||||
def repo_id_from_name_or_path(
|
||||
name_or_path: Optional[str], default: str
|
||||
) -> str:
|
||||
"""Treat a hub-style ``name_or_path`` ("org/repo") as a replacement comfy
|
||||
repo; anything local (or an explicit .safetensors file) keeps the
|
||||
default."""
|
||||
if (
|
||||
name_or_path
|
||||
and not os.path.exists(name_or_path)
|
||||
and not name_or_path.endswith(".safetensors")
|
||||
and "/" in name_or_path
|
||||
):
|
||||
return name_or_path
|
||||
return default
|
||||
|
||||
|
||||
def resolve_comfy_file(
|
||||
rel_path: str,
|
||||
repo_id: str,
|
||||
override_path: Optional[str] = None,
|
||||
extra_roots: Optional[Iterable[str]] = None,
|
||||
hf_token: Optional[str] = None,
|
||||
status_fn: Optional[Callable[[str], None]] = None,
|
||||
local_only: bool = False,
|
||||
) -> Optional[str]:
|
||||
"""Find a weight file at its local location, or download it there when
|
||||
(and only when) it is missing.
|
||||
|
||||
Search order: ``override_path`` (must exist), the repo-relative path under
|
||||
MODELS_PATH (and each of ``extra_roots``), the bare filename at each root,
|
||||
any subfolder of the category folder (recursive — e.g.
|
||||
diffusion_models/my_custom_sub/), then the hub — downloaded to the
|
||||
repo-relative path under MODELS_PATH. With ``local_only`` the hub is never
|
||||
touched and a miss returns None.
|
||||
"""
|
||||
if override_path is not None:
|
||||
if not os.path.exists(override_path):
|
||||
raise FileNotFoundError(
|
||||
f"Override path for {rel_path} does not exist: {override_path}"
|
||||
)
|
||||
return override_path
|
||||
|
||||
filename = os.path.basename(rel_path)
|
||||
category = os.path.dirname(rel_path)
|
||||
roots = [MODELS_PATH] + [r for r in (extra_roots or []) if os.path.isdir(r)]
|
||||
for root in roots:
|
||||
for rel in (rel_path, filename):
|
||||
candidate = os.path.join(root, rel)
|
||||
if os.path.exists(candidate):
|
||||
return candidate
|
||||
for root in roots:
|
||||
found = find_file_recursive(os.path.join(root, category), filename)
|
||||
if found is not None:
|
||||
return found
|
||||
|
||||
if local_only:
|
||||
return None
|
||||
|
||||
import huggingface_hub
|
||||
|
||||
if status_fn is not None:
|
||||
status_fn(f"Downloading {rel_path} from {repo_id} into {MODELS_PATH}")
|
||||
return huggingface_hub.hf_hub_download(
|
||||
repo_id=repo_id, filename=rel_path, token=hf_token, local_dir=MODELS_PATH
|
||||
)
|
||||
|
||||
|
||||
def resolve_named_file(
|
||||
path: str,
|
||||
component: str = "model",
|
||||
hf_token: Optional[str] = None,
|
||||
) -> str:
|
||||
"""Resolve an explicit .safetensors reference: a local file, a file already
|
||||
under MODELS_PATH, or an 'org/repo/path/file.safetensors' hub path
|
||||
(downloaded into the models folder at its repo-relative path)."""
|
||||
if os.path.exists(path):
|
||||
return path
|
||||
splits = path.split("/")
|
||||
if len(splits) < 3:
|
||||
raise ValueError(
|
||||
f"Invalid {component} path: {path}. Must be a local file or "
|
||||
"'org/repo/filename.safetensors' to download from the Hugging Face Hub."
|
||||
)
|
||||
rel_path = "/".join(splits[2:])
|
||||
for candidate in (
|
||||
os.path.join(MODELS_PATH, rel_path),
|
||||
os.path.join(MODELS_PATH, splits[-1]),
|
||||
):
|
||||
if os.path.exists(candidate):
|
||||
return candidate
|
||||
|
||||
import huggingface_hub
|
||||
|
||||
return huggingface_hub.hf_hub_download(
|
||||
repo_id="/".join(splits[:2]),
|
||||
filename=rel_path,
|
||||
token=hf_token,
|
||||
local_dir=MODELS_PATH,
|
||||
)
|
||||
Whitespace-only changes.
@@ -0,0 +1,9 @@
|
||||
from diffusers import AnimaTextConditioner as DiffusersAnimaTextConditioner
|
||||
|
||||
from .._mixin import OstrisModelMixin
|
||||
|
||||
|
||||
class AnimaTextConditioner(DiffusersAnimaTextConditioner, OstrisModelMixin):
|
||||
"""Anima's learned text conditioner (rides next to the Qwen3 encoder)."""
|
||||
|
||||
aitk_subfolder = "text_conditioner"
|
||||
@@ -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,14 @@
|
||||
from transformers import Gemma2Model
|
||||
|
||||
from .._mixin import OstrisTransformersMixin
|
||||
|
||||
|
||||
class Gemma2ModelEncoder(Gemma2Model, OstrisTransformersMixin):
|
||||
"""Gemma2 base model (lumina2's text encoder — what AutoModel resolves)."""
|
||||
|
||||
aitk_subfolder = "text_encoder"
|
||||
aitk_tokenizer_subfolder = "tokenizer"
|
||||
|
||||
@classmethod
|
||||
def get_transformer_block_names(cls):
|
||||
return ["layers"]
|
||||
@@ -0,0 +1,42 @@
|
||||
from transformers import Gemma3ForConditionalGeneration
|
||||
|
||||
from .._mixin import OstrisTransformersMixin
|
||||
|
||||
|
||||
class Gemma3TextEncoder(Gemma3ForConditionalGeneration, OstrisTransformersMixin):
|
||||
"""Gemma3 conditioning stack (ltx2 / ltx2.3)."""
|
||||
|
||||
aitk_subfolder = "text_encoder"
|
||||
aitk_tokenizer_subfolder = "tokenizer"
|
||||
|
||||
@classmethod
|
||||
def get_transformer_block_names(cls):
|
||||
# both layouts seen across transformers versions; missing paths skip
|
||||
return ["model.language_model.layers", "language_model.model.layers"]
|
||||
|
||||
def get_offload_ignore_modules(self):
|
||||
return [self.model.language_model.base_model.embed_tokens]
|
||||
|
||||
|
||||
try:
|
||||
from transformers.models.gemma4.modeling_gemma4 import Gemma4TextModel
|
||||
|
||||
class Gemma4TextEncoder(Gemma4TextModel, OstrisTransformersMixin):
|
||||
"""Gemma4 text decoder (ltx2.5's conditioning stack)."""
|
||||
|
||||
aitk_subfolder = "text_encoder"
|
||||
aitk_tokenizer_subfolder = "tokenizer"
|
||||
|
||||
@classmethod
|
||||
def get_transformer_block_names(cls):
|
||||
return ["layers"]
|
||||
|
||||
def get_offload_ignore_modules(self):
|
||||
# layer_scalar is a bare tensor buffer on each decoder layer; the
|
||||
# manager never enumerates it, so it must ride along explicitly
|
||||
return [self.embed_tokens] + [
|
||||
layer.layer_scalar for layer in self.layers
|
||||
]
|
||||
|
||||
except ImportError:
|
||||
Gemma4TextEncoder = None
|
||||
@@ -0,0 +1,14 @@
|
||||
from transformers import LlamaForCausalLM
|
||||
|
||||
from .._mixin import OstrisTransformersMixin
|
||||
|
||||
|
||||
class LlamaTextEncoder(LlamaForCausalLM, OstrisTransformersMixin):
|
||||
"""Llama causal-LM text encoder (hidream's text_encoder_4)."""
|
||||
|
||||
aitk_subfolder = "text_encoder_4"
|
||||
aitk_tokenizer_subfolder = "tokenizer_4"
|
||||
|
||||
@classmethod
|
||||
def get_transformer_block_names(cls):
|
||||
return ["model.layers"]
|
||||
@@ -0,0 +1,25 @@
|
||||
from transformers import Mistral3ForConditionalGeneration, Mistral3Model
|
||||
|
||||
from .._mixin import OstrisTransformersMixin
|
||||
|
||||
|
||||
class Mistral3TextEncoder(Mistral3ForConditionalGeneration, OstrisTransformersMixin):
|
||||
"""Mistral3 conditioning stack (flux2)."""
|
||||
|
||||
aitk_subfolder = "text_encoder"
|
||||
aitk_tokenizer_subfolder = "tokenizer"
|
||||
|
||||
@classmethod
|
||||
def get_transformer_block_names(cls):
|
||||
return ["model.language_model.layers", "language_model.model.layers"]
|
||||
|
||||
|
||||
class Mistral3ModelEncoder(Mistral3Model, OstrisTransformersMixin):
|
||||
"""The inner Mistral3 base model (ernie_image's text encoder)."""
|
||||
|
||||
aitk_subfolder = "text_encoder"
|
||||
aitk_tokenizer_subfolder = "tokenizer"
|
||||
|
||||
@classmethod
|
||||
def get_transformer_block_names(cls):
|
||||
return ["language_model.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
|
||||
@@ -0,0 +1,28 @@
|
||||
from transformers import Qwen3ForCausalLM, Qwen3Model
|
||||
|
||||
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"]
|
||||
|
||||
|
||||
class Qwen3ModelEncoder(Qwen3Model, OstrisTransformersMixin):
|
||||
"""The inner Qwen3 base model (anima's text encoder)."""
|
||||
|
||||
aitk_subfolder = "text_encoder"
|
||||
aitk_tokenizer_subfolder = "tokenizer"
|
||||
|
||||
@classmethod
|
||||
def get_transformer_block_names(cls):
|
||||
return ["layers"]
|
||||
@@ -0,0 +1,89 @@
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
from transformers import (
|
||||
Qwen3VLForConditionalGeneration,
|
||||
Qwen3VLModel,
|
||||
Qwen3VLTextModel,
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
class Qwen3VLModelEncoder(Qwen3VLModel, OstrisTransformersMixin):
|
||||
"""The inner Qwen3-VL base model (what AutoModel resolves): its
|
||||
last_hidden_state is the instruction feature boogu_image / ideogram4
|
||||
consume."""
|
||||
|
||||
aitk_subfolder = "text_encoder"
|
||||
aitk_tokenizer_subfolder = "tokenizer"
|
||||
|
||||
@classmethod
|
||||
def get_transformer_block_names(cls):
|
||||
return ["language_model.layers"]
|
||||
|
||||
def drop_vision_tower(self):
|
||||
if getattr(self, "visual", None) is not None:
|
||||
self.visual = None
|
||||
return self
|
||||
|
||||
def patch_vision_patch_embed(self) -> int:
|
||||
return patch_qwen_vl_patch_embed(self)
|
||||
|
||||
|
||||
class Qwen3VLTextOnlyEncoder(Qwen3VLTextModel, OstrisTransformersMixin):
|
||||
"""The Qwen3-VL text tower alone (prx_pixel)."""
|
||||
|
||||
aitk_subfolder = "text_encoder"
|
||||
aitk_tokenizer_subfolder = "tokenizer"
|
||||
|
||||
@classmethod
|
||||
def get_transformer_block_names(cls):
|
||||
return ["layers"]
|
||||
@@ -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"]
|
||||
@@ -0,0 +1,77 @@
|
||||
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"
|
||||
aitk_config_repo = "ai-toolkit/umt5_xxl_encoder"
|
||||
# comfy fp8 umt5 files store non-quantized tensors in a dtype mix; T5
|
||||
# assumes a uniform dtype, so follow the load dtype
|
||||
aitk_cast_quantized_load = True
|
||||
|
||||
aitk_comfy_repo = "Comfy-Org/Wan_2.1_ComfyUI_repackaged"
|
||||
# comfy umt5 files already use the transformers key layout; the
|
||||
# fp8_e4m3fn_scaled variant is the legacy scaled-fp8 format (handled by
|
||||
# the importer's float8 backend)
|
||||
aitk_comfy_weight_names = {
|
||||
"ai-toolkit/umt5_xxl_encoder": [
|
||||
"split_files/text_encoders/umt5_xxl_fp8_e4m3fn_scaled.safetensors",
|
||||
"split_files/text_encoders/umt5_xxl_fp16.safetensors",
|
||||
],
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_transformer_block_names(cls):
|
||||
return ["encoder.block"]
|
||||
|
||||
@classmethod
|
||||
def convert_state_dict_on_load(cls, state_dict):
|
||||
# drop the embedded sentencepiece blob and materialize the tied
|
||||
# embed_tokens reference
|
||||
state_dict = dict(state_dict)
|
||||
state_dict.pop("spiece_model", None)
|
||||
if (
|
||||
"encoder.embed_tokens.weight" not in state_dict
|
||||
and "shared.weight" in state_dict
|
||||
):
|
||||
state_dict["encoder.embed_tokens.weight"] = state_dict["shared.weight"]
|
||||
return state_dict
|
||||
|
||||
@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
|
||||
)
|
||||
Whitespace-only changes.
@@ -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"
|
||||
@@ -0,0 +1,10 @@
|
||||
from diffusers import AutoencoderKLFlux2
|
||||
|
||||
from .._mixin import OstrisModelMixin
|
||||
|
||||
|
||||
class Flux2KLVAE(AutoencoderKLFlux2, OstrisModelMixin):
|
||||
"""The diffusers Flux2 KL VAE (ernie_image; distinct from the hand-rolled
|
||||
BFL AutoEncoder in vae/flux2_kl.py that flux2/ideogram4 use)."""
|
||||
|
||||
aitk_subfolder = "vae"
|
||||
+104
-38
@@ -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
|
||||
|
||||
@@ -11,6 +20,8 @@ import torch.utils.checkpoint as ckpt
|
||||
from einops import rearrange
|
||||
from torch import Tensor, nn
|
||||
|
||||
from .._mixin import OstrisModelMixin
|
||||
|
||||
|
||||
@dataclass
|
||||
class AutoEncoderParams:
|
||||
@@ -22,6 +33,17 @@ class AutoEncoderParams:
|
||||
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)
|
||||
@@ -194,30 +216,30 @@ class Encoder(nn.Module):
|
||||
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]) # type: ignore[index, operator]
|
||||
if len(self.down[i_level].attn) > 0: # type: ignore[arg-type]
|
||||
h = ckpt.checkpoint(self.down[i_level].attn[i_block], h) # type: ignore[index, operator]
|
||||
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]) # type: ignore[index, operator]
|
||||
if len(self.down[i_level].attn) > 0: # type: ignore[arg-type]
|
||||
h = self.down[i_level].attn[i_block](h) # type: ignore[index, operator]
|
||||
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])) # type: ignore[operator]
|
||||
hs.append(ckpt.checkpoint(self.down[i_level].downsample, hs[-1]))
|
||||
else:
|
||||
hs.append(self.down[i_level].downsample(hs[-1])) # type: ignore[operator]
|
||||
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) # type: ignore[operator]
|
||||
h = ckpt.checkpoint(self.mid.attn_1, h) # type: ignore[operator]
|
||||
h = ckpt.checkpoint(self.mid.block_2, h) # type: ignore[operator]
|
||||
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) # type: ignore[operator]
|
||||
h = self.mid.attn_1(h) # type: ignore[operator]
|
||||
h = self.mid.block_2(h) # type: ignore[operator]
|
||||
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)
|
||||
@@ -300,13 +322,13 @@ class Decoder(nn.Module):
|
||||
|
||||
# middle
|
||||
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.attn_1, h) # type: ignore[operator]
|
||||
h = ckpt.checkpoint(self.mid.block_2, h) # type: ignore[operator]
|
||||
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) # type: ignore[operator]
|
||||
h = self.mid.attn_1(h) # type: ignore[operator]
|
||||
h = self.mid.block_2(h) # type: ignore[operator]
|
||||
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)
|
||||
@@ -314,18 +336,18 @@ class Decoder(nn.Module):
|
||||
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) # type: ignore[index, operator]
|
||||
if len(self.up[i_level].attn) > 0: # type: ignore[arg-type]
|
||||
h = ckpt.checkpoint(self.up[i_level].attn[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:
|
||||
h = ckpt.checkpoint(self.up[i_level].attn[i_block], h)
|
||||
else:
|
||||
h = self.up[i_level].block[i_block](h) # type: ignore[index, operator]
|
||||
if len(self.up[i_level].attn) > 0: # type: ignore[arg-type]
|
||||
h = self.up[i_level].attn[i_block](h) # type: ignore[index, operator]
|
||||
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) # type: ignore[operator]
|
||||
h = ckpt.checkpoint(self.up[i_level].upsample, h)
|
||||
else:
|
||||
h = self.up[i_level].upsample(h) # type: ignore[operator]
|
||||
h = self.up[i_level].upsample(h)
|
||||
|
||||
# end
|
||||
h = self.norm_out(h)
|
||||
@@ -334,7 +356,14 @@ class Decoder(nn.Module):
|
||||
return h
|
||||
|
||||
|
||||
class AutoEncoder(nn.Module):
|
||||
class AutoEncoder(nn.Module, OstrisModelMixin):
|
||||
@classmethod
|
||||
def aitk_config_from_state_dict(cls, state_dict):
|
||||
# small decoder builds report 96ch at the first decoder block
|
||||
if state_dict["decoder.up.0.block.0.conv1.bias"].shape[0] == 96:
|
||||
return AutoEncoderSmallDecoderParams()
|
||||
return AutoEncoderParams()
|
||||
|
||||
def __init__(self, params: AutoEncoderParams):
|
||||
super().__init__()
|
||||
self.params = params
|
||||
@@ -346,10 +375,13 @@ class AutoEncoder(nn.Module):
|
||||
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=params.ch,
|
||||
ch=decoder_ch,
|
||||
out_ch=params.out_ch,
|
||||
ch_mult=params.ch_mult,
|
||||
num_res_blocks=params.num_res_blocks,
|
||||
@@ -369,7 +401,7 @@ class AutoEncoder(nn.Module):
|
||||
self._gradient_checkpointing = False
|
||||
|
||||
@property
|
||||
def gradient_checkpointing(self) -> bool:
|
||||
def gradient_checkpointing(self):
|
||||
return self._gradient_checkpointing
|
||||
|
||||
@gradient_checkpointing.setter
|
||||
@@ -378,18 +410,52 @@ class AutoEncoder(nn.Module):
|
||||
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()
|
||||
|
||||
@property
|
||||
def device(self) -> torch.device:
|
||||
return next(self.parameters()).device
|
||||
def normalize(self, z):
|
||||
self.bn.eval()
|
||||
return self.bn(z)
|
||||
|
||||
@property
|
||||
def dtype(self) -> torch.dtype:
|
||||
return next(self.parameters()).dtype
|
||||
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
|
||||
|
||||
|
||||
_NUM_RESOLUTIONS = 4
|
||||
@@ -0,0 +1,16 @@
|
||||
from diffusers.models.autoencoders import (
|
||||
AutoencoderKLLTX2Audio as DiffusersAutoencoderKLLTX2Audio,
|
||||
)
|
||||
from diffusers.models.autoencoders import (
|
||||
AutoencoderKLLTX2Video as DiffusersAutoencoderKLLTX2Video,
|
||||
)
|
||||
|
||||
from .._mixin import OstrisModelMixin
|
||||
|
||||
|
||||
class LTX2VideoVAE(DiffusersAutoencoderKLLTX2Video, OstrisModelMixin):
|
||||
aitk_subfolder = "vae"
|
||||
|
||||
|
||||
class LTX2AudioVAE(DiffusersAutoencoderKLLTX2Audio, OstrisModelMixin):
|
||||
aitk_subfolder = "audio_vae"
|
||||
@@ -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)
|
||||
@@ -0,0 +1,9 @@
|
||||
from diffusers import AutoencoderKLWan
|
||||
|
||||
from .._mixin import OstrisModelMixin
|
||||
|
||||
|
||||
class WanVAE(AutoencoderKLWan, OstrisModelMixin):
|
||||
"""The wan 2.1/2.2 causal video VAE."""
|
||||
|
||||
aitk_subfolder = "vae"
|
||||
Whitespace-only changes.
Loaded 100 of 114 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user