1 Commits
Author SHA1 Message Date
lodestone-rock 4ea0cac774 init flux lite distillation 2025-06-22 16:44:05 +07:00
3 changed files with 436 additions and 2 deletions
+120 -1
View File
@@ -1,7 +1,7 @@
import torch
import torch.nn as nn
from safetensors.torch import safe_open
from .model import FluxParams, FluxMod
from .model import FluxParams, FluxMod, FluxLiteMod
from .layers import Approximator
import comfy
import comfy.model_patcher
@@ -250,3 +250,122 @@ def load_flux_mod(model_path, timestep_guidance_path=None, linear_dtypes=torch.b
offload_device = offload_device,
)
return model_patcher
def load_flux_lite_mod(model_path, timestep_guidance_path=None, linear_dtypes=torch.bfloat16, lite_patch_path=None, is_gguf=False):
if is_gguf:
ensure_gguf()
# just load safetensors here
state_dict = load_selected_keys(model_path, {"mod", "time_in", "guidance_in", "vector_in"}, is_gguf=is_gguf)
if timestep_guidance_path is None:
# Chroma mode - we expect the timestep guidance to be bundled under
# the key distilled_guidance_layer.
if lite_patch_path is not None:
raise ValueError("Internal error: lite patch specified in Chroma loader mode")
timestep_state_dict = {
k[25:]: v
for k, v in state_dict.items()
if k.startswith("distilled_guidance_layer.")
}
if not timestep_state_dict:
raise RuntimeError("Could not find distilled guidance layer in Chroma model")
n_layers = 0
for key in timestep_state_dict:
keysplit = key.split(".", 2)
if (
len(keysplit) == 3 and
keysplit[0] == "norms" and
keysplit[1].isnumeric() and
keysplit[2] == "scale"
):
n_layers = max(n_layers, int(keysplit[1]) + 1)
del state_dict[f"distilled_guidance_layer.{key}"]
if n_layers == 0:
raise RuntimeError("Could not determine number of distilled guidance layers in Chroma model")
else:
timestep_state_dict = comfy.utils.load_torch_file(timestep_guidance_path)
n_layers = 5
# if "v3" in timestep_guidance_path:
# n_layers = 6
# elif "v2" in timestep_guidance_path:
# n_layers = 5
# else:
# n_layers = 4
param_count = sum(x.numel() for x in state_dict.values())
unet_dtype = torch.bfloat16
load_device = model_management.get_torch_device()
offload_device = model_management.unet_offload_device()
params=FluxParams(
in_channels=64,
vec_in_dim=768,
context_in_dim=4096,
hidden_size=3072,
mlp_ratio=4.0,
num_heads=24,
depth=8,
depth_single_blocks=38,
axes_dim=[16, 56, 56],
theta=10_000,
qkv_bias=True,
guidance_embed=False,
)
model_conf = ExternalFlux()
model_class = ChromaFluxModel if timestep_guidance_path is None else ExternalFluxModel
model = model_class(
model_conf,
model_type=comfy.model_base.ModelType.FLUX,
device=model_management.get_torch_device()
)
unet_config = model_conf.unet_config
if is_gguf:
operations = model_conf.custom_operations = gguf.ops.GGMLOps()
modelpatcher_class = gguf.nodes.GGUFModelPatcher
else:
modelpatcher_class = comfy.model_patcher.ModelPatcher
if model_conf.custom_operations is None:
fp8 = model_conf.optimizations.get("fp8", model_conf.scaled_fp8 is not None)
operations = comfy.ops.pick_operations(
unet_config.get("dtype"),
model.manual_cast_dtype,
fp8_optimizations=fp8,
scaled_fp8=model_conf.scaled_fp8,
)
else:
operations = model_conf.custom_operations
model.diffusion_model = FluxLiteMod(params=params, dtype=unet_dtype, operations=operations)
model.diffusion_model.load_state_dict(state_dict)
# if lite_patch_path is not None:
# model.diffusion_model.lite = True
# for _ in range(5,16):
# del model.diffusion_model.double_blocks[5]
# model.diffusion_model.double_blocks[4].load_state_dict(comfy.utils.load_torch_file(lite_patch_path))
model.diffusion_model.distilled_guidance_layer = Approximator(32, 3072, 5120, n_layers, operations=operations)
model.diffusion_model.distilled_guidance_layer.load_state_dict(timestep_state_dict)
model.diffusion_model.dtype = unet_dtype
model.diffusion_model.eval()
if not is_gguf:
model.diffusion_model.to(unet_dtype)
# we cast to fp8 for mixed matmul ops but omit the picky and sensitive layers
cast_layers(model.diffusion_model, nn.Linear, dtype=linear_dtypes, exclude_keywords={"img_in", "final_layer", "scale"})
if model_management.force_channels_last():
model.diffusion_model.to(memory_format=torch.channels_last)
model_patcher = modelpatcher_class(
model,
load_device = load_device,
offload_device = offload_device,
)
return model_patcher
+254
View File
@@ -326,3 +326,257 @@ class FluxMod(nn.Module):
txt_ids = torch.zeros((bs, context.shape[1], 3), device=x.device, dtype=x.dtype)
out = self.forward_orig(img, img_ids, context, txt_ids, timestep, guidance, control, transformer_options)
return rearrange(out, "b (h w) (c ph pw) -> b c (h ph) (w pw)", h=h_len, w=w_len, ph=2, pw=2)[:,:,:h,:w]
class FluxLiteMod(nn.Module):
"""
Transformer model for flow matching on sequences.
"""
def __init__(self, params: FluxParams, image_model=None, final_layer=True, dtype=None, device=None, operations=nn, **kwargs):
super().__init__()
self.dtype = dtype
self.params = params
self.in_channels = params.in_channels
self.out_channels = self.in_channels
if params.hidden_size % params.num_heads != 0:
raise ValueError(
f"Hidden size {params.hidden_size} must be divisible by num_heads {params.num_heads}"
)
pe_dim = params.hidden_size // params.num_heads
if sum(params.axes_dim) != pe_dim:
raise ValueError(f"Got {params.axes_dim} but expected positional dim {pe_dim}")
self.hidden_size = params.hidden_size
self.num_heads = params.num_heads
self.pe_embedder = EmbedND(dim=pe_dim, theta=params.theta, axes_dim=params.axes_dim)
self.img_in = operations.Linear(self.in_channels, self.hidden_size, bias=True, dtype=dtype, device=device)
self.txt_in = operations.Linear(params.context_in_dim, self.hidden_size, dtype=dtype, device=device)
# set as nn identity for now, will overwrite it later.
self.distilled_guidance_layer = nn.Identity()
self.double_blocks = nn.ModuleList(
[
DoubleStreamBlock(
self.hidden_size,
self.num_heads,
mlp_ratio=params.mlp_ratio,
qkv_bias=params.qkv_bias,
dtype=dtype, device=device, operations=operations
)
for _ in range(params.depth)
]
)
self.single_blocks = nn.ModuleList(
[
SingleStreamBlock(self.hidden_size, self.num_heads, mlp_ratio=params.mlp_ratio, dtype=dtype, device=device, operations=operations)
for _ in range(params.depth_single_blocks)
]
)
if final_layer:
self.final_layer = LastLayer(self.hidden_size, 1, self.out_channels, dtype=dtype, device=device, operations=operations)
self.skip_mmdit = []
self.skip_dit = []
@staticmethod
def distribute_modulations(tensor: torch.Tensor, single_block_count: int = 38, double_blocks_count: int = 8):
"""
Distributes slices of the tensor into the block_dict as ModulationOut objects.
Args:
tensor (torch.Tensor): Input tensor with shape [batch_size, vectors, dim].
"""
batch_size, vectors, dim = tensor.shape
block_dict = {}
# HARD CODED VALUES! lookup table for the generated vectors
# Add 38 single mod blocks
for i in range(single_block_count):
key = f"single_blocks.{i}.modulation.lin"
block_dict[key] = None
# Add 19 image double blocks
for i in range(double_blocks_count):
key = f"double_blocks.{i}.img_mod.lin"
block_dict[key] = None
# Add 19 text double blocks
for i in range(double_blocks_count):
key = f"double_blocks.{i}.txt_mod.lin"
block_dict[key] = None
# Add the final layer
block_dict["final_layer.adaLN_modulation.1"] = None
idx = 0 # Index to keep track of the vector slices
for key in block_dict.keys():
if "single_blocks" in key:
# Single block: 1 ModulationOut
block_dict[key] = ModulationOut(
shift=tensor[:, idx:idx+1, :],
scale=tensor[:, idx+1:idx+2, :],
gate=tensor[:, idx+2:idx+3, :]
)
idx += 3 # Advance by 3 vectors
elif "img_mod" in key:
# Double block: List of 2 ModulationOut
double_block = []
for _ in range(2): # Create 2 ModulationOut objects
double_block.append(
ModulationOut(
shift=tensor[:, idx:idx+1, :],
scale=tensor[:, idx+1:idx+2, :],
gate=tensor[:, idx+2:idx+3, :]
)
)
idx += 3 # Advance by 3 vectors per ModulationOut
block_dict[key] = double_block
elif "txt_mod" in key:
# Double block: List of 2 ModulationOut
double_block = []
for _ in range(2): # Create 2 ModulationOut objects
double_block.append(
ModulationOut(
shift=tensor[:, idx:idx+1, :],
scale=tensor[:, idx+1:idx+2, :],
gate=tensor[:, idx+2:idx+3, :]
)
)
idx += 3 # Advance by 3 vectors per ModulationOut
block_dict[key] = double_block
elif "final_layer" in key:
# Final layer: 1 ModulationOut
block_dict[key] = [
tensor[:, idx:idx+1, :],
tensor[:, idx+1:idx+2, :],
]
idx += 2 # Advance by 2 vectors
return block_dict
def forward_orig(
self,
img: Tensor,
img_ids: Tensor,
txt: Tensor,
txt_ids: Tensor,
timesteps: Tensor,
guidance: Tensor = None,
control=None,
transformer_options={},
) -> Tensor:
patches_replace = transformer_options.get("patches_replace", {})
if img.ndim != 3 or txt.ndim != 3:
raise ValueError("Input img and txt tensors must have 3 dimensions.")
# running on sequences img
img = self.img_in(img)
# distilled vector guidance
device = img.device
dtype = img.dtype
mod_index_length = 212
distill_timestep = timestep_embedding(timesteps.detach().clone(), 8).to(device=device, dtype=dtype)
# guidance = guidance *
distil_guidance = timestep_embedding(guidance.detach().clone(), 8).to(device=device, dtype=dtype)
# get all modulation index
modulation_index = timestep_embedding(torch.arange(mod_index_length), 16).to(device=device, dtype=dtype)
# we need to broadcast the modulation index here so each batch has all of the index
modulation_index = modulation_index.unsqueeze(0).repeat(img.shape[0], 1, 1)
# and we need to broadcast timestep and guidance along too
timestep_guidance = torch.cat([distill_timestep, distil_guidance], dim=1).unsqueeze(1).repeat(1, mod_index_length, 1)
# then and only then we could concatenate it together
input_vec = torch.cat([timestep_guidance, modulation_index], dim=-1)
mod_vectors = self.distilled_guidance_layer(input_vec)
mod_vectors_dict = self.distribute_modulations(mod_vectors, 38, 8)
txt = self.txt_in(txt)
ids = torch.cat((txt_ids, img_ids), dim=1)
pe = self.pe_embedder(ids)
blocks_replace = patches_replace.get("dit", {})
for i, block in enumerate(self.double_blocks):
if i not in self.skip_mmdit:
guidance_index = i
img_mod = mod_vectors_dict[f"double_blocks.{guidance_index}.img_mod.lin"]
txt_mod = mod_vectors_dict[f"double_blocks.{guidance_index}.txt_mod.lin"]
double_mod = [img_mod, txt_mod]
if ("double_block", i) in blocks_replace:
def block_wrap(args):
out = {}
out["img"], out["txt"] = block(img=args["img"], txt=args["txt"], pe=args["pe"], distill_vec=args["distill_vec"])
return out
out = blocks_replace[("double_block", i)]({"img": img, "txt": txt, "pe": pe, "distill_vec": double_mod}, {"original_block": block_wrap})
txt = out["txt"]
img = out["img"]
else:
img, txt = block(img=img, txt=txt, pe=pe, distill_vec=double_mod)
if control is not None: # Controlnet
control_i = control.get("input")
if i < len(control_i):
add = control_i[i]
if add is not None:
img += add
img = torch.cat((txt, img), 1)
for i, block in enumerate(self.single_blocks):
if i not in self.skip_dit:
single_mod = mod_vectors_dict[f"single_blocks.{i}.modulation.lin"]
if ("single_block", i) in blocks_replace:
def block_wrap(args):
out = {}
out["img"] = block(args["img"], pe=args["pe"], distill_vec=args["distill_vec"])
return out
out = blocks_replace[("single_block", i)]({"img": img, "pe": pe, "distill_vec": single_mod}, {"original_block": block_wrap})
img = out["img"]
else:
img = block(img, pe=pe, distill_vec=single_mod)
if control is not None: # Controlnet
control_o = control.get("output")
if i < len(control_o):
add = control_o[i]
if add is not None:
img[:, txt.shape[1] :, ...] += add
img = img[:, txt.shape[1] :, ...]
final_mod = mod_vectors_dict["final_layer.adaLN_modulation.1"]
img = self.final_layer(img, distill_vec=final_mod) # (N, T, patch_size ** 2 * out_channels)
return img
def forward(self, x, timestep, context, guidance, control=None, transformer_options={}, **kwargs):
bs, c, h, w = x.shape
patch_size = 2
x = comfy.ldm.common_dit.pad_to_patch_size(x, (patch_size, patch_size))
img = rearrange(x, "b c (h ph) (w pw) -> b (h w) (c ph pw)", ph=patch_size, pw=patch_size)
h_len = ((h + (patch_size // 2)) // patch_size)
w_len = ((w + (patch_size // 2)) // patch_size)
img_ids = torch.zeros((h_len, w_len, 3), device=x.device, dtype=x.dtype)
img_ids[:, :, 1] = img_ids[:, :, 1] + torch.linspace(0, h_len - 1, steps=h_len, device=x.device, dtype=x.dtype).unsqueeze(1)
img_ids[:, :, 2] = img_ids[:, :, 2] + torch.linspace(0, w_len - 1, steps=w_len, device=x.device, dtype=x.dtype).unsqueeze(0)
img_ids = repeat(img_ids, "h w c -> b (h w) c", b=bs)
txt_ids = torch.zeros((bs, context.shape[1], 3), device=x.device, dtype=x.dtype)
out = self.forward_orig(img, img_ids, context, txt_ids, timestep, guidance, control, transformer_options)
return rearrange(out, "b (h w) (c ph pw) -> b c (h ph) (w pw)", h=h_len, w=w_len, ph=2, pw=2)[:,:,:h,:w]
+62 -1
View File
@@ -4,7 +4,7 @@ import json
import torch
import folder_paths
from .loader import load_flux_mod
from .loader import load_flux_mod, load_flux_lite_mod
from .sampler import common_ksampler
import comfy.samplers
import comfy.cli_args
@@ -79,6 +79,66 @@ class FluxModDiffusionLoader:
return (flux_mod,)
class FluxLiteModDiffusionLoader:
NodeId = "FluxLiteModDiffusionLoader"
NodeName = "FluxLiteMod Unified Model Loader"
@classmethod
def INPUT_TYPES(s):
patches = ["None"] + folder_paths.get_filename_list("diffusion_models")
checkpoint_paths = folder_paths.get_filename_list("diffusion_models")
if "unet_gguf" in folder_paths.folder_names_and_paths:
checkpoint_paths = checkpoint_paths + folder_paths.get_filename_list(
"unet_gguf"
)
return {
"required": {
"unet_name": (checkpoint_paths, {"tooltip": "This loads both .safetensors and .GGUF models if ComfyUI-GGUF is installed."}),
"guidance_name": (patches,),
"quant_mode": (
["bf16", "float8_e4m3fn (8 bit)", "float8_e5m2 (also 8 bit)"],
),
}
}
RETURN_TYPES = ("MODEL",)
RETURN_NAMES = ("model",)
FUNCTION = "load_unet"
CATEGORY = "advanced/FluxMod"
def load_unet(
self, *, unet_name, quant_mode, guidance_name=None, lite_patch_unet_name=None
):
dtypes = {
"bf16": torch.bfloat16,
"float8_e4m3fn (8 bit)": torch.float8_e4m3fn,
"float8_e5m2 (also 8 bit)": torch.float8_e5m2,
}
is_gguf = unet_name.lower().endswith(".gguf")
unet_path = folder_paths.get_full_path(
"unet_gguf" if is_gguf else "diffusion_models", unet_name
)
if guidance_name is not None:
guidance_path = folder_paths.get_full_path(
"diffusion_models", guidance_name
)
else:
guidance_path = None
if lite_patch_unet_name is not None:
lite_patch_unet_name = folder_paths.get_full_path(
"diffusion_models", lite_patch_unet_name
)
flux_mod = load_flux_lite_mod(
model_path=unet_path,
timestep_guidance_path=guidance_path,
linear_dtypes=dtypes[quant_mode],
lite_patch_path=lite_patch_unet_name,
is_gguf=is_gguf,
)
return (flux_mod,)
class FluxModDiffusionLoaderMini(FluxModDiffusionLoader):
NodeId = "FluxModDiffusionLoaderMini"
NodeName = "FluxMod Mini Unified Model Loader"
@@ -420,6 +480,7 @@ class FluxModSamplerWrapperNode:
node_list = [
# loaders
FluxModDiffusionLoader,
FluxLiteModDiffusionLoader,
FluxModDiffusionLoaderMini,
ChromaDiffusionLoader,
# Sampler