mirror of
https://github.com/Comfy-Org/ComfyUI.git
synced 2026-09-21 05:27:57 -05:00
Merge branch 'master' into update-comfyui-embedded-docs-0.5.12-20260918-171917
This commit is contained in:
@@ -928,6 +928,59 @@ class HunyuanImage21(LatentFormat):
|
||||
|
||||
latent_rgb_factors_bias = [0.0007, -0.0256, -0.0206]
|
||||
|
||||
class QwenImage21(LatentFormat):
|
||||
latent_channels = 64
|
||||
latent_dimensions = 2
|
||||
spacial_downscale_ratio = 16
|
||||
|
||||
latent_rgb_factors = [
|
||||
[-0.0158, -0.0115, -0.0174], [ 0.0030, 0.0120, 0.0027], [ 0.0637, 0.0470, -0.0127], [ 0.0360, 0.0661, -0.0030],
|
||||
[ 0.0159, 0.0181, 0.0082], [ 0.0132, 0.0326, 0.0169], [ 0.0191, 0.0261, 0.0136], [-0.0146, -0.0276, -0.0361],
|
||||
[ 0.0187, -0.0024, -0.0072], [-0.1059, -0.0090, 0.0350], [-0.0195, -0.0226, -0.0138], [-0.0295, 0.0024, -0.0215],
|
||||
[ 0.0191, -0.0393, -0.0001], [-0.0144, -0.0166, -0.0272], [ 0.0389, 0.0430, 0.0445], [-0.0153, -0.0336, 0.0031],
|
||||
[ 0.0339, 0.0122, 0.0220], [-0.0136, -0.0078, -0.0120], [-0.0340, -0.0282, -0.0245], [-0.0133, -0.0176, -0.0133],
|
||||
[ 0.0109, -0.0087, 0.0096], [-0.0010, 0.0044, 0.0016], [ 0.0301, 0.0053, 0.0361], [-0.0281, -0.0205, -0.0032],
|
||||
[-0.0725, 0.0002, 0.0160], [-0.0036, 0.0158, 0.0807], [ 0.0087, 0.0040, -0.0053], [-0.0260, 0.0183, -0.0077],
|
||||
[-0.0039, -0.0035, -0.0107], [-0.0026, 0.0172, 0.0237], [ 0.0088, 0.0078, 0.0078], [-0.0087, -0.0310, -0.0122],
|
||||
[-0.0027, 0.0018, 0.0094], [-0.0064, 0.0292, -0.0256], [ 0.0594, 0.1049, 0.1180], [ 0.0103, -0.0103, -0.0026],
|
||||
[-0.0091, 0.0025, -0.0015], [ 0.0178, 0.0243, 0.0292], [-0.0063, -0.0012, 0.0202], [ 0.0452, 0.0246, 0.0143],
|
||||
[ 0.0149, 0.0270, 0.0052], [ 0.1484, 0.0801, 0.0804], [-0.0120, 0.0040, 0.0010], [ 0.0181, 0.0051, -0.0021],
|
||||
[ 0.0132, 0.0050, 0.0019], [ 0.0291, 0.0020, 0.0092], [ 0.0066, -0.0410, -0.1314], [-0.1153, -0.0629, -0.0802],
|
||||
[ 0.0258, 0.0378, 0.0298], [ 0.0375, 0.1139, 0.0468], [-0.0142, -0.0126, -0.0276], [ 0.0339, 0.0153, 0.0138],
|
||||
[ 0.0346, 0.0211, 0.0267], [ 0.0369, -0.0431, -0.0993], [-0.0052, -0.0092, 0.0056], [-0.0279, 0.0410, -0.0357],
|
||||
[ 0.0036, 0.0017, -0.0083], [-0.0441, -0.0367, -0.0454], [-0.0001, -0.0092, -0.0001], [-0.0222, -0.0183, -0.0051],
|
||||
[ 0.0039, 0.0053, -0.0184], [-0.0094, -0.0075, -0.0143], [-0.0066, -0.0088, -0.0063], [ 0.0220, 0.0074, 0.0100],
|
||||
]
|
||||
latent_rgb_factors_bias = [-0.1228, -0.1869, -0.3083]
|
||||
|
||||
def __init__(self):
|
||||
self.latents_mean = torch.tensor([
|
||||
0.5126, 0.7721, -0.0631, 1.3506, -0.7855, -2.1025, -0.3458, 1.3722,
|
||||
1.8873, -1.7177, -0.6510, 0.2732, 0.7562, -0.6163, -1.0277, 3.8363,
|
||||
2.0210, 0.0472, 0.9320, 2.0087, 2.4954, -0.1391, -1.4249, 1.8464,
|
||||
-0.5236, 1.2826, 3.7046, -1.3035, 2.7286, -1.4518, -1.9036, -1.9955,
|
||||
-0.0342, -1.0265, -0.7636, 3.0555, 0.0746, -3.0751, -0.1076, 1.7376,
|
||||
-1.0914, -1.9435, -0.2784, -1.3680, 0.4809, -0.4433, 0.3764, 0.5729,
|
||||
-2.0595, 1.0960, -1.3260, -2.0211, -5.0179, 0.5275, 4.0162, 1.8505,
|
||||
0.3026, 1.9373, 1.4937, 0.2632, 0.5547, -1.7121, -0.1562, 0.0304,
|
||||
]).view(1, self.latent_channels, 1, 1)
|
||||
self.latents_std = torch.tensor([
|
||||
3.2001, 3.2936, 3.4321, 3.0091, 3.1061, 4.0379, 4.0705, 3.7910,
|
||||
3.0785, 3.6500, 3.9308, 3.0904, 2.8778, 3.7675, 3.7320, 5.0756,
|
||||
3.2864, 4.0397, 3.1317, 4.0443, 2.9249, 3.9454, 3.0988, 4.2489,
|
||||
3.4896, 3.8513, 3.9323, 3.4719, 3.7498, 4.2830, 3.5694, 4.2467,
|
||||
3.9037, 3.2947, 5.0770, 3.5075, 3.2700, 3.4767, 2.8063, 5.1125,
|
||||
3.5327, 4.7833, 3.1286, 4.1819, 3.8527, 3.8312, 3.5605, 4.3875,
|
||||
3.9624, 4.0168, 3.5643, 4.0550, 5.5614, 4.2963, 4.4080, 3.4959,
|
||||
3.8747, 3.7608, 3.5735, 3.1490, 3.7662, 3.6746, 3.4563, 3.8161,
|
||||
]).view(1, self.latent_channels, 1, 1)
|
||||
|
||||
def process_in(self, latent):
|
||||
return (latent - self.latents_mean.to(latent.device, latent.dtype)) / self.latents_std.to(latent.device, latent.dtype)
|
||||
|
||||
def process_out(self, latent):
|
||||
return latent * self.latents_std.to(latent.device, latent.dtype) + self.latents_mean.to(latent.device, latent.dtype)
|
||||
|
||||
class HunyuanImage21Refiner(LatentFormat):
|
||||
latent_channels = 64
|
||||
latent_dimensions = 3
|
||||
|
||||
@@ -0,0 +1,353 @@
|
||||
# https://github.com/huggingface/diffusers (Apache 2.0) Qwen-Image 2.1
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
|
||||
import comfy.model_management
|
||||
import comfy.ops
|
||||
import comfy.patcher_extension
|
||||
import comfy.quant_ops
|
||||
import comfy.rmsnorm
|
||||
from comfy.ldm.flux.layers import EmbedND, timestep_embedding
|
||||
from comfy.ldm.flux.math import apply_rope1
|
||||
from comfy.ldm.lightricks.model import TimestepEmbedding
|
||||
from comfy.ldm.modules.attention import optimized_attention
|
||||
from comfy.ldm.wan.model_animate2 import PoseBranchCache
|
||||
|
||||
|
||||
class ZeroCenteredRMSNorm(nn.Module):
|
||||
# stored weight is scale - 1, applied in fp32
|
||||
def __init__(self, dim, eps=1e-6, dtype=None, device=None):
|
||||
super().__init__()
|
||||
self.weight = nn.Parameter(torch.empty(dim, dtype=dtype, device=device))
|
||||
self.eps = eps
|
||||
|
||||
def forward(self, x):
|
||||
w = comfy.model_management.cast_to(self.weight, dtype=torch.float32, device=x.device) + 1.0
|
||||
return comfy.rmsnorm.rms_norm(x.float(), w, self.eps).to(x.dtype)
|
||||
|
||||
|
||||
class TextProjection(nn.Module):
|
||||
def __init__(self, in_dim, hidden_size, eps=1e-6, dtype=None, device=None, operations=None):
|
||||
super().__init__()
|
||||
self.text_norm = ZeroCenteredRMSNorm(in_dim, eps=eps, dtype=dtype, device=device)
|
||||
self.in_layer = operations.Linear(in_dim, hidden_size, bias=False, dtype=dtype, device=device)
|
||||
self.out_layer = operations.Linear(hidden_size, hidden_size, bias=False, dtype=dtype, device=device)
|
||||
|
||||
def forward(self, x):
|
||||
return self.out_layer(F.gelu(self.in_layer(self.text_norm(x)), approximate="tanh"))
|
||||
|
||||
|
||||
class TimestepProjEmbeddings(nn.Module):
|
||||
def __init__(self, embedding_dim, dtype=None, device=None, operations=None):
|
||||
super().__init__()
|
||||
self.timestep_embedder = TimestepEmbedding(in_channels=256, time_embed_dim=embedding_dim, sample_proj_bias=False, dtype=dtype, device=device, operations=operations)
|
||||
|
||||
def forward(self, timestep, dtype):
|
||||
return self.timestep_embedder(timestep_embedding(timestep.float(), 256).to(dtype))
|
||||
|
||||
|
||||
class SwiGLUFeedForward(nn.Module):
|
||||
def __init__(self, dim, hidden_dim, fused=True, dtype=None, device=None, operations=None):
|
||||
super().__init__()
|
||||
self.fused = fused
|
||||
if fused:
|
||||
# [gate; up] in one GEMM, the SiLU-gate folded into the down projection's input quantizer
|
||||
self.gate_up = operations.Linear(dim, 2 * hidden_dim, bias=False, dtype=dtype, device=device)
|
||||
else:
|
||||
self.proj = operations.Linear(dim, hidden_dim, bias=False, dtype=dtype, device=device)
|
||||
self.gate_layer = operations.Linear(dim, hidden_dim, bias=False, dtype=dtype, device=device)
|
||||
self.out = operations.Linear(hidden_dim, dim, bias=False, dtype=dtype, device=device)
|
||||
|
||||
def forward(self, x):
|
||||
if self.fused:
|
||||
return comfy.ops.linear_input_act(self.out, self.gate_up(x), "swiglu")
|
||||
return self.out(F.silu(self.gate_layer(x)) * self.proj(x))
|
||||
|
||||
|
||||
class Attention(nn.Module):
|
||||
def __init__(self, dim, heads, dim_head, eps=1e-6, dtype=None, device=None, operations=None):
|
||||
super().__init__()
|
||||
self.heads = heads
|
||||
inner_dim = heads * dim_head
|
||||
self.to_q = operations.Linear(dim, inner_dim, bias=False, dtype=dtype, device=device)
|
||||
self.to_k = operations.Linear(dim, inner_dim, bias=False, dtype=dtype, device=device)
|
||||
self.to_v = operations.Linear(dim, inner_dim, bias=False, dtype=dtype, device=device)
|
||||
self.to_out = nn.ModuleList([operations.Linear(inner_dim, dim, bias=False, dtype=dtype, device=device)])
|
||||
self.norm_q = operations.RMSNorm(dim_head, eps=eps, dtype=dtype, device=device)
|
||||
self.norm_k = operations.RMSNorm(dim_head, eps=eps, dtype=dtype, device=device)
|
||||
|
||||
def forward(self, x, pe, attn_fn, prefix_len, transformer_options={}):
|
||||
# (B, N, H, D) throughout: no transposes, the rope table is laid out to match
|
||||
B, N, _ = x.shape
|
||||
q = self.to_q(x).view(B, N, self.heads, -1)
|
||||
k = self.to_k(x).view(B, N, self.heads, -1)
|
||||
v = self.to_v(x).view(B, N, self.heads, -1)
|
||||
patches = transformer_options.get("patches", {}).get("attn1_patch", [])
|
||||
if comfy.model_management.in_training or patches:
|
||||
q, k = self.norm_q(q), self.norm_k(k)
|
||||
if patches:
|
||||
# patches see the Qwen-Image convention: (B, H, N, D) before rope, rope table (1, 1, N, ...), target image rows in img_slice
|
||||
q, k, v, pe = q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2), pe.transpose(1, 2)
|
||||
extra_options = {**transformer_options, "img_slice": [prefix_len, N]}
|
||||
for p in patches:
|
||||
out = p(q, k, v, pe=pe, attn_mask=None, extra_options=extra_options)
|
||||
q, k, v, pe = out.get("q", q), out.get("k", k), out.get("v", v), out.get("pe", pe)
|
||||
q, k, v, pe = q.transpose(1, 2).contiguous(), k.transpose(1, 2).contiguous(), v.transpose(1, 2).contiguous(), pe.transpose(1, 2).contiguous()
|
||||
q = apply_rope1(q, pe)
|
||||
k = apply_rope1(k, pe)
|
||||
else:
|
||||
q_scale, _, q_stream = comfy.ops.cast_bias_weight(self.norm_q, q, offloadable=True)
|
||||
k_scale, _, k_stream = comfy.ops.cast_bias_weight(self.norm_k, k, offloadable=True)
|
||||
q, k = comfy.quant_ops.ck.rms_rope(q, k, pe, q_scale, k_scale, self.norm_q.eps)
|
||||
comfy.ops.uncast_bias_weight(self.norm_q, q_scale, None, q_stream)
|
||||
comfy.ops.uncast_bias_weight(self.norm_k, k_scale, None, k_stream)
|
||||
return self.to_out[0](attn_fn(q, k, v, self.heads))
|
||||
|
||||
|
||||
def _split_rows(p):
|
||||
# shared modulation rows: (t = 0 row for text and references, sampled-t rows for the target)
|
||||
return p[-1:].unsqueeze(1), p[:-1].unsqueeze(1)
|
||||
|
||||
|
||||
def _modulated_norm(norm, x, scale, prefix_len, zero):
|
||||
# LayerNorm * (1 + scale), fused over every row with the target scale; the prefix rows are then redone with the t = 0 scale
|
||||
s_prefix, s_target = scale
|
||||
if comfy.model_management.in_training:
|
||||
out = norm(x)
|
||||
return torch.cat([out[:, :prefix_len] * (1 + s_prefix), out[:, prefix_len:] * (1 + s_target)], dim=1)
|
||||
out = comfy.quant_ops.ck.adaln(x, s_target, zero, norm.eps)
|
||||
if prefix_len:
|
||||
out[:, :prefix_len] = comfy.quant_ops.ck.adaln(x[:, :prefix_len], s_prefix, zero, norm.eps)
|
||||
return out
|
||||
|
||||
|
||||
def _gated_residual(x, y, gate, prefix_len):
|
||||
g_prefix, g_target = gate
|
||||
out = torch.addcmul(x, y, g_target)
|
||||
if prefix_len:
|
||||
out[:, :prefix_len] = torch.addcmul(x[:, :prefix_len], y[:, :prefix_len], g_prefix)
|
||||
return out
|
||||
|
||||
|
||||
class QwenImage21TransformerBlock(nn.Module):
|
||||
def __init__(self, dim, num_attention_heads, attention_head_dim, mlp_ratio=3, eps=1e-6, fused_mlp=True, dtype=None, device=None, operations=None):
|
||||
super().__init__()
|
||||
self.img_norm1 = operations.LayerNorm(dim, elementwise_affine=False, eps=eps, dtype=dtype, device=device)
|
||||
self.attn = Attention(dim, num_attention_heads, attention_head_dim, eps=eps, dtype=dtype, device=device, operations=operations)
|
||||
self.img_norm2 = operations.LayerNorm(dim, elementwise_affine=False, eps=eps, dtype=dtype, device=device)
|
||||
self.img_mlp = SwiGLUFeedForward(dim, dim * mlp_ratio, fused=fused_mlp, dtype=dtype, device=device, operations=operations)
|
||||
|
||||
def forward(self, x, mod, pe, attn_fn, prefix_len, transformer_options={}):
|
||||
scale1, gate1, scale2, gate2, zero = mod
|
||||
x = _gated_residual(x, self.attn(_modulated_norm(self.img_norm1, x, scale1, prefix_len, zero), pe, attn_fn, prefix_len, transformer_options), gate1, prefix_len)
|
||||
x = _gated_residual(x, self.img_mlp(_modulated_norm(self.img_norm2, x, scale2, prefix_len, zero)), gate2, prefix_len)
|
||||
if x.dtype == torch.float16:
|
||||
x = x.clip(-65504, 65504)
|
||||
return x
|
||||
|
||||
|
||||
class LastLayer(nn.Module):
|
||||
# scale only, no shift
|
||||
def __init__(self, dim, eps=1e-6, dtype=None, device=None, operations=None):
|
||||
super().__init__()
|
||||
self.linear = operations.Linear(dim, dim, bias=False, dtype=dtype, device=device)
|
||||
self.norm = operations.LayerNorm(dim, eps, elementwise_affine=False, dtype=dtype, device=device)
|
||||
|
||||
def forward(self, x, temb):
|
||||
scale = self.linear(F.silu(temb)).unsqueeze(1)
|
||||
if comfy.model_management.in_training:
|
||||
return self.norm(x) * (1 + scale)
|
||||
return comfy.quant_ops.ck.adaln(x, scale, torch.zeros_like(scale[:1]), self.norm.eps)
|
||||
|
||||
|
||||
def block_causal_attention(segments, transformer_options={}, cache=None, block_index=0, prefix_len=0):
|
||||
# segments: (start, end, mask); text segments get a causal mask, image blocks attend to everything before their end
|
||||
def attn(q, k, v, heads):
|
||||
if cache is not None:
|
||||
# K and V stacked on dim 1 so batch stays first and quantized rows are per token and head
|
||||
cache.put(block_index, torch.stack([k[:, :prefix_len], v[:, :prefix_len]], dim=1))
|
||||
outs = [optimized_attention(q[:, start:end].flatten(2), k[:, :end].flatten(2), v[:, :end].flatten(2), heads, mask=mask, transformer_options=transformer_options)
|
||||
for start, end, mask in segments]
|
||||
return torch.cat(outs, dim=1) if len(outs) > 1 else outs[0]
|
||||
return attn
|
||||
|
||||
|
||||
def prefix_cached_attention(prefix_k, prefix_v, transformer_options={}):
|
||||
# target-only queries: block-causal reduces to full attention over [cached prefix, target]
|
||||
def attn(q, k, v, heads):
|
||||
return optimized_attention(q.flatten(2), torch.cat([prefix_k, k], dim=1).flatten(2), torch.cat([prefix_v, v], dim=1).flatten(2), heads, transformer_options=transformer_options)
|
||||
return attn
|
||||
|
||||
|
||||
def prefix_cache_key(x, context, refs, slots):
|
||||
# one fp32 tensor per batch row: lengths and slots, then the prompt embedding and reference latents
|
||||
# the target shape is part of it because reference rope ids are centred on the target
|
||||
header = [context.shape[1], len(refs)] + list(x.shape[-2:]) + list(slots) + [s for r in refs for s in r.shape[-2:]]
|
||||
header = torch.tensor(header, dtype=torch.float32, device=context.device).expand(context.shape[0], -1)
|
||||
return torch.cat([header, context.float().flatten(1)] + [r.float().flatten(1) for r in refs], dim=1)
|
||||
|
||||
|
||||
class QwenImage21Transformer2DModel(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
in_channels=64,
|
||||
out_channels=64,
|
||||
num_layers=32,
|
||||
attention_head_dim=128,
|
||||
num_attention_heads=32,
|
||||
context_in_dim=4096,
|
||||
mlp_ratio=3,
|
||||
axes_dims_rope=(16, 56, 56),
|
||||
eps=1e-6,
|
||||
fused_mlp=True,
|
||||
image_model=None,
|
||||
dtype=None,
|
||||
device=None,
|
||||
operations=None,
|
||||
):
|
||||
super().__init__()
|
||||
self.dtype = dtype
|
||||
self.out_channels = out_channels
|
||||
self.inner_dim = num_attention_heads * attention_head_dim
|
||||
|
||||
self.pe_embedder = EmbedND(dim=attention_head_dim, theta=10000, axes_dim=list(axes_dims_rope))
|
||||
self.time_text_embed = TimestepProjEmbeddings(self.inner_dim, dtype=dtype, device=device, operations=operations)
|
||||
self.txt_in = TextProjection(context_in_dim, self.inner_dim, eps=eps, dtype=dtype, device=device, operations=operations)
|
||||
self.img_in = operations.Linear(in_channels, self.inner_dim, bias=False, dtype=dtype, device=device)
|
||||
|
||||
# one modulation shared by every block
|
||||
self.modulation = nn.Sequential(nn.SiLU(), operations.Linear(self.inner_dim, 4 * self.inner_dim, bias=False, dtype=dtype, device=device))
|
||||
|
||||
self.transformer_blocks = nn.ModuleList([
|
||||
QwenImage21TransformerBlock(self.inner_dim, num_attention_heads, attention_head_dim, mlp_ratio=mlp_ratio, eps=eps, fused_mlp=fused_mlp, dtype=dtype, device=device, operations=operations)
|
||||
for _ in range(num_layers)
|
||||
])
|
||||
|
||||
self.norm_out = LastLayer(self.inner_dim, eps=eps, dtype=dtype, device=device, operations=operations)
|
||||
self.proj_out = operations.Linear(self.inner_dim, out_channels, bias=False, dtype=dtype, device=device)
|
||||
|
||||
# text + reference K/V are step-independent (t = 0 modulation, causal prefix), cached for one sampling run
|
||||
self.prefix_cache = None
|
||||
self.prefix_cache_enabled = False
|
||||
|
||||
def reset_prefix_cache(self, enabled):
|
||||
if self.prefix_cache is not None:
|
||||
self.prefix_cache.free()
|
||||
self.prefix_cache = None
|
||||
self.prefix_cache_enabled = enabled
|
||||
|
||||
def select_prefix_cache(self, key, cache_bytes, device, options):
|
||||
# returns (cache with the slot to read or fill selected, whether the slot is filled), or (None, False) to recompute
|
||||
if options.get("device") == "off":
|
||||
return None, False
|
||||
cache = self.prefix_cache
|
||||
if cache is not None and cache.select(key, create=False):
|
||||
return cache, cache.filled(len(self.transformer_blocks))
|
||||
dtype = options.get("dtype", "default")
|
||||
cache_bytes //= {"int8": 2, "int4": 4}.get(dtype, 1)
|
||||
if cache is None:
|
||||
store = options.get("device", "auto")
|
||||
if store == "auto":
|
||||
# spare VRAM first, then pinned RAM prefetched behind compute; room for cond and uncond
|
||||
store = next((d for d in (device, torch.device("cpu")) if comfy.model_management.get_free_memory(d) > 4 * cache_bytes), None)
|
||||
if store is None:
|
||||
return None, False
|
||||
else:
|
||||
store = device if store == "gpu" else torch.device("cpu")
|
||||
cache = self.prefix_cache = PoseBranchCache(store_device=store, dtype=dtype)
|
||||
if comfy.model_management.get_free_memory(cache.store_device) < 2 * cache_bytes:
|
||||
# no room for this slot: recompute rather than evict the other cond's slot every step
|
||||
return None, False
|
||||
cache.select(key)
|
||||
return cache, False
|
||||
|
||||
def build_sequence(self, x, context, ref_latents, image_slots):
|
||||
# text with each reference image spliced in at its slot, target image last
|
||||
txt = self.txt_in(context)
|
||||
slots = (image_slots + [txt.shape[1]] * len(ref_latents))[:len(ref_latents)]
|
||||
bounds = [0] + slots + [txt.shape[1]]
|
||||
|
||||
parts, ids, segments = [], [], []
|
||||
pos, length = 0, 0
|
||||
for (start, end), img in zip(zip(bounds[:-1], bounds[1:]), ref_latents + [x]):
|
||||
n = end - start
|
||||
if n > 0:
|
||||
parts.append(txt[:, start:end])
|
||||
ids.append(torch.arange(pos, pos + n, device=x.device, dtype=torch.float32).unsqueeze(1).expand(n, 3))
|
||||
segments.append((length, length + n, torch.ones((n, length + n), dtype=torch.bool, device=x.device).tril(length)))
|
||||
pos += n
|
||||
length += n
|
||||
h, w = img.shape[-2:]
|
||||
parts.append(self.img_in(img.flatten(2).transpose(1, 2)))
|
||||
# half a token where a reference grid has the other parity, so it centres on the target
|
||||
hh = torch.arange(h, device=x.device, dtype=torch.float32) - (h - h // 2) + 0.5 * (h % 2 - x.shape[-2] % 2)
|
||||
ww = torch.arange(w, device=x.device, dtype=torch.float32) - (w - w // 2) + 0.5 * (w % 2 - x.shape[-1] % 2)
|
||||
ids.append(torch.stack([torch.full((h, w), pos, device=x.device, dtype=torch.float32), hh[:, None].expand(h, w), ww[None, :].expand(h, w)], dim=-1).flatten(0, 1))
|
||||
segments.append((length, length + h * w, None))
|
||||
pos += max(h, w)
|
||||
length += h * w
|
||||
|
||||
# (1, N, 1, ...): the layout the fused rms_rope wants for (B, N, H, D) queries
|
||||
pe = self.pe_embedder(torch.cat(ids, dim=0).unsqueeze(0)).transpose(1, 2).contiguous()
|
||||
return torch.cat(parts, dim=1), pe, segments
|
||||
|
||||
def forward(self, x, timestep, context, ref_latents=None, image_slots=None, transformer_options={}, **kwargs):
|
||||
return comfy.patcher_extension.WrapperExecutor.new_class_executor(
|
||||
self._forward,
|
||||
self,
|
||||
comfy.patcher_extension.get_all_wrappers(comfy.patcher_extension.WrappersMP.DIFFUSION_MODEL, transformer_options)
|
||||
).execute(x, timestep, context, ref_latents, image_slots, transformer_options, **kwargs)
|
||||
|
||||
def _forward(self, x, timesteps, context, ref_latents=None, image_slots=None, transformer_options={}, **kwargs):
|
||||
B, C, H, W = x.shape
|
||||
dtype = x.dtype
|
||||
ref_latents = list(ref_latents or [])
|
||||
image_slots = list(image_slots or [])
|
||||
|
||||
hidden_states, pe, segments = self.build_sequence(x, context, ref_latents, image_slots)
|
||||
prefix_len = hidden_states.shape[1] - H * W
|
||||
patches = transformer_options.get("patches", {})
|
||||
for p in patches.get("post_input", []):
|
||||
out = p({"img": hidden_states, "pe": pe, "transformer_options": transformer_options})
|
||||
hidden_states, pe = out["img"], out.get("pe", pe)
|
||||
|
||||
# pipeline rounds t*1000 and t to the compute dtype; text and reference tokens modulate from t = 0
|
||||
t = ((timesteps * 1000).to(dtype) / 1000).to(dtype)
|
||||
temb = self.time_text_embed(torch.cat([t, t.new_zeros(1)]), dtype)
|
||||
scale1, gate1, scale2, gate2 = self.modulation(temb).chunk(4, dim=-1)
|
||||
mod = (_split_rows(scale1), _split_rows(gate1.tanh()), _split_rows(scale2), _split_rows(gate2.tanh()), torch.zeros_like(scale1[:1, None]))
|
||||
|
||||
blocks_replace = transformer_options.get("patches_replace", {}).get("dit", {})
|
||||
cache, cached = None, False
|
||||
# a cached step runs target rows only, so anything hooked into a block would see a different sequence from step 2
|
||||
hooked = blocks_replace or patches.get("post_input") or patches.get("single_block") or patches.get("attn1_patch")
|
||||
if self.prefix_cache_enabled and prefix_len > 0 and not hooked:
|
||||
key = prefix_cache_key(x, context, ref_latents, image_slots)
|
||||
cache_bytes = 2 * len(self.transformer_blocks) * B * prefix_len * self.inner_dim * hidden_states.element_size()
|
||||
cache, cached = self.select_prefix_cache(key, cache_bytes, x.device, transformer_options.get("qwen_image21_cache", {}))
|
||||
if cached:
|
||||
hidden_states, pe, prefix_len = hidden_states[:, prefix_len:], pe[:, prefix_len:], 0
|
||||
|
||||
transformer_options["total_blocks"] = len(self.transformer_blocks)
|
||||
transformer_options["block_type"] = "single"
|
||||
for i, block in enumerate(self.transformer_blocks):
|
||||
transformer_options["block_index"] = i
|
||||
if cached:
|
||||
prefix_k, prefix_v = cache.take(i, x.device, dtype, B).unbind(1)
|
||||
cache.prefetch(i + 1, x.device, dtype) # queue the next block before the compute it should overlap
|
||||
attn_fn = prefix_cached_attention(prefix_k, prefix_v, transformer_options)
|
||||
else:
|
||||
attn_fn = block_causal_attention(segments, transformer_options, cache, i, prefix_len)
|
||||
if ("single_block", i) in blocks_replace:
|
||||
def block_wrap(args):
|
||||
return {"img": block(args["img"], mod, args["pe"], attn_fn, prefix_len, args["transformer_options"])}
|
||||
hidden_states = blocks_replace[("single_block", i)]({"img": hidden_states, "vec": temb, "pe": pe, "transformer_options": transformer_options}, {"original_block": block_wrap})["img"]
|
||||
else:
|
||||
hidden_states = block(hidden_states, mod, pe, attn_fn, prefix_len, transformer_options)
|
||||
for p in patches.get("single_block", []):
|
||||
hidden_states = p({"img": hidden_states, "x": x, "block_index": i, "transformer_options": transformer_options})["img"]
|
||||
|
||||
hidden_states = self.norm_out(hidden_states[:, prefix_len:], temb[:-1])
|
||||
hidden_states = self.proj_out(hidden_states)
|
||||
return hidden_states.transpose(1, 2).reshape(B, self.out_channels, H, W)
|
||||
@@ -116,7 +116,8 @@ class PoseBranchCache:
|
||||
Caching the block input rather than its K/V halves the memory; reprojecting K/V on read
|
||||
costs ~4% of re-running the block. One slot per distinct pose sequence, so under
|
||||
context windows each window keeps its own; least recently used slots are evicted when
|
||||
the store device runs low on memory. Created and freed by WanAnimate2Cache.
|
||||
the store device runs low on memory. Created and freed by WanAnimate2Cache; Qwen-Image 2.1
|
||||
also stores its prefix K/V in it.
|
||||
"""
|
||||
|
||||
CONVROT_GROUPSIZE = 256
|
||||
@@ -129,21 +130,22 @@ class PoseBranchCache:
|
||||
self._pending = {}
|
||||
self._staging = {}
|
||||
|
||||
def select(self, pose_latents):
|
||||
def select(self, k, create=True):
|
||||
# select runs at a forward boundary: an interrupted forward can leave copies in flight that a different slot's forward would then mistake for its own
|
||||
if self._pending:
|
||||
for t, stream in self._pending.values():
|
||||
if stream is not None:
|
||||
stream.synchronize()
|
||||
self._pending = {}
|
||||
# keyed on batch element 0, so a cond batch size change mid-run stays valid
|
||||
k = pose_latents[:1]
|
||||
for s in self.slots:
|
||||
if s["key"].shape == k.shape and torch.equal(s["key"], k.to(s["key"].device)):
|
||||
self.slots.remove(s)
|
||||
self.slots.append(s)
|
||||
self.slot = s
|
||||
return
|
||||
return True
|
||||
self.slot = None
|
||||
if not create:
|
||||
return False
|
||||
# cache what fits: a filled slot is the size estimate for the next one, and least recently used slots make room when the store device runs low
|
||||
est = max((self._slot_bytes(s) for s in self.slots), default=0) * 1.5
|
||||
while self.slots and comfy.model_management.get_free_memory(self.store_device) < est:
|
||||
@@ -169,8 +171,7 @@ class PoseBranchCache:
|
||||
def filled(self, num_blocks):
|
||||
return self.slot is not None and len(self.slot["blocks"]) == num_blocks
|
||||
|
||||
def put(self, i, x_pose):
|
||||
t = x_pose[:1]
|
||||
def put(self, i, t):
|
||||
params = None
|
||||
if self.dtype in ("int8", "int4"):
|
||||
# convrot is what lets low-bit survive the ~125x per-channel outliers here, and over a [tokens, dim] view per-row scale means per-token. The kernels want 2D and a power-of-4 group that divides dim.
|
||||
@@ -294,7 +295,7 @@ class WanAnimate2Model(WanModel):
|
||||
|
||||
cache = transformer_options.get("animate2_cache", None) if apply_pose else None
|
||||
if cache is not None:
|
||||
cache.select(pose_latents)
|
||||
cache.select(pose_latents[:1]) # keyed on batch element 0, so a cond batch size change mid-run stays valid
|
||||
cached = cache is not None and cache.filled(len(self.blocks))
|
||||
|
||||
x_pose = None
|
||||
@@ -342,7 +343,7 @@ class WanAnimate2Model(WanModel):
|
||||
# pose-only prepass, to avoid inflating dynamic VRAM calibration when using multiple context windows
|
||||
for i, block in enumerate(self.blocks):
|
||||
transformer_options["block_index"] = i
|
||||
cache.put(i, x_pose)
|
||||
cache.put(i, x_pose[:1])
|
||||
x_pose = block.forward_pose(x_pose, e0_pose, freqs_pose, context_pose, context_img_len=context_img_len_pose, transformer_options=transformer_options)[0]
|
||||
x_pose = None
|
||||
cached = True
|
||||
@@ -365,7 +366,7 @@ class WanAnimate2Model(WanModel):
|
||||
del x_pose_in
|
||||
else:
|
||||
if cache is not None:
|
||||
cache.put(i, x_pose)
|
||||
cache.put(i, x_pose[:1])
|
||||
# runs even under a block replace: its state has to reach block i+1
|
||||
x_pose, k_pose, v_pose = block.forward_pose(x_pose, e0_pose, freqs_pose, context_pose, context_img_len=context_img_len_pose, transformer_options=transformer_options)
|
||||
if v_pose is not None and pose_strength != 1.0:
|
||||
|
||||
+85
-24
@@ -1,6 +1,7 @@
|
||||
# original version: https://github.com/Wan-Video/Wan2.2/blob/main/wan/modules/vae2_2.py
|
||||
# Copyright 2024-2025 The Alibaba Wan Team Authors. All rights reserved.
|
||||
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
@@ -15,7 +16,7 @@ CACHE_T = 2
|
||||
|
||||
class Resample(nn.Module):
|
||||
|
||||
def __init__(self, dim, mode):
|
||||
def __init__(self, dim, mode, temporal_kernel=3):
|
||||
assert mode in (
|
||||
"none",
|
||||
"upsample2d",
|
||||
@@ -40,7 +41,7 @@ class Resample(nn.Module):
|
||||
# ops.Conv2d(dim, dim//2, 3, padding=1)
|
||||
)
|
||||
self.time_conv = CausalConv3d(
|
||||
dim, dim * 2, (3, 1, 1), padding=(1, 0, 0))
|
||||
dim, dim * 2, (temporal_kernel, 1, 1), padding=(temporal_kernel // 2, 0, 0))
|
||||
elif mode == "downsample2d":
|
||||
self.resample = nn.Sequential(
|
||||
nn.ZeroPad2d((0, 1, 0, 1)),
|
||||
@@ -50,7 +51,7 @@ class Resample(nn.Module):
|
||||
nn.ZeroPad2d((0, 1, 0, 1)),
|
||||
ops.Conv2d(dim, dim, 3, stride=(2, 2)))
|
||||
self.time_conv = CausalConv3d(
|
||||
dim, dim, (3, 1, 1), stride=(2, 1, 1), padding=(0, 0, 0))
|
||||
dim, dim, (temporal_kernel, 1, 1), stride=(2, 1, 1), padding=(0, 0, 0))
|
||||
else:
|
||||
self.resample = nn.Identity()
|
||||
|
||||
@@ -96,7 +97,10 @@ class Resample(nn.Module):
|
||||
x = x.reshape(b, c, t * 2, h, w)
|
||||
t = x.shape[2]
|
||||
x = rearrange(x, "b c t h w -> (b t) c h w")
|
||||
x = self.resample(x)
|
||||
if feat_cache is None and self.mode in ("upsample2d", "upsample3d"):
|
||||
x = strip_apply(self.resample, x, scale=2)
|
||||
else:
|
||||
x = self.resample(x)
|
||||
x = rearrange(x, "(b t) c h w -> b c t h w", t=t)
|
||||
|
||||
if self.mode == "downsample3d":
|
||||
@@ -114,9 +118,38 @@ class Resample(nn.Module):
|
||||
return x
|
||||
|
||||
|
||||
STRIP_ELEMS = 2 ** 24
|
||||
|
||||
|
||||
def strip_apply(fn, x, scale=1, halo=1, out=None):
|
||||
# strips of rows bound cudnn's conv workspace, a halo row per 3x3 conv keeps them exact
|
||||
n = -(-x.numel() * scale * scale // STRIP_ELEMS)
|
||||
if n <= 1 and out is None:
|
||||
return fn(x)
|
||||
add = out is not None
|
||||
size = x.shape[-2]
|
||||
step = -(-size // n)
|
||||
for a in range(0, size, step):
|
||||
b = min(size, a + step)
|
||||
lo = max(0, a - halo)
|
||||
y = fn(x.narrow(-2, lo, min(size, b + halo) - lo)).narrow(-2, (a - lo) * scale, (b - a) * scale)
|
||||
if out is None:
|
||||
out = y.new_empty(*y.shape[:-2], size * scale, y.shape[-1])
|
||||
dst = out.narrow(-2, a * scale, (b - a) * scale)
|
||||
if add:
|
||||
dst.add_(y)
|
||||
else:
|
||||
dst.copy_(y)
|
||||
return out
|
||||
|
||||
|
||||
def conv3x3(in_dim, out_dim, temporal_kernel=3):
|
||||
return CausalConv3d(in_dim, out_dim, (temporal_kernel, 3, 3), padding=(temporal_kernel // 2, 1, 1))
|
||||
|
||||
|
||||
class ResidualBlock(nn.Module):
|
||||
|
||||
def __init__(self, in_dim, out_dim, dropout=0.0):
|
||||
def __init__(self, in_dim, out_dim, dropout=0.0, temporal_kernel=3):
|
||||
super().__init__()
|
||||
self.in_dim = in_dim
|
||||
self.out_dim = out_dim
|
||||
@@ -125,17 +158,20 @@ class ResidualBlock(nn.Module):
|
||||
self.residual = nn.Sequential(
|
||||
RMS_norm(in_dim, images=False),
|
||||
nn.SiLU(),
|
||||
CausalConv3d(in_dim, out_dim, 3, padding=1),
|
||||
conv3x3(in_dim, out_dim, temporal_kernel),
|
||||
RMS_norm(out_dim, images=False),
|
||||
nn.SiLU(),
|
||||
nn.Dropout(dropout),
|
||||
CausalConv3d(out_dim, out_dim, 3, padding=1),
|
||||
conv3x3(out_dim, out_dim, temporal_kernel),
|
||||
)
|
||||
self.shortcut = (
|
||||
CausalConv3d(in_dim, out_dim, 1)
|
||||
if in_dim != out_dim else nn.Identity())
|
||||
|
||||
def forward(self, x, feat_cache=None, feat_idx=[0]):
|
||||
if feat_cache is None:
|
||||
# single image: the whole block runs in strips so its intermediates never exist at full size
|
||||
return strip_apply(lambda s: self.residual(s).add_(self.shortcut(s)), x, halo=2)
|
||||
old_x = x
|
||||
for layer in self.residual:
|
||||
if isinstance(layer, CausalConv3d) and feat_cache is not None:
|
||||
@@ -302,7 +338,8 @@ class Down_ResidualBlock(nn.Module):
|
||||
dropout,
|
||||
mult,
|
||||
temperal_downsample=False,
|
||||
down_flag=False):
|
||||
down_flag=False,
|
||||
temporal_kernel=3):
|
||||
super().__init__()
|
||||
|
||||
# Shortcut path with downsample
|
||||
@@ -316,13 +353,13 @@ class Down_ResidualBlock(nn.Module):
|
||||
# Main path with residual blocks and downsample
|
||||
downsamples = []
|
||||
for _ in range(mult):
|
||||
downsamples.append(ResidualBlock(in_dim, out_dim, dropout))
|
||||
downsamples.append(ResidualBlock(in_dim, out_dim, dropout, temporal_kernel=temporal_kernel))
|
||||
in_dim = out_dim
|
||||
|
||||
# Add the final downsample block
|
||||
if down_flag:
|
||||
mode = "downsample3d" if temperal_downsample else "downsample2d"
|
||||
downsamples.append(Resample(out_dim, mode=mode))
|
||||
downsamples.append(Resample(out_dim, mode=mode, temporal_kernel=temporal_kernel))
|
||||
|
||||
self.downsamples = nn.Sequential(*downsamples)
|
||||
|
||||
@@ -342,7 +379,8 @@ class Up_ResidualBlock(nn.Module):
|
||||
dropout,
|
||||
mult,
|
||||
temperal_upsample=False,
|
||||
up_flag=False):
|
||||
up_flag=False,
|
||||
temporal_kernel=3):
|
||||
super().__init__()
|
||||
# Shortcut path with upsample
|
||||
if up_flag:
|
||||
@@ -358,13 +396,13 @@ class Up_ResidualBlock(nn.Module):
|
||||
# Main path with residual blocks and upsample
|
||||
upsamples = []
|
||||
for _ in range(mult):
|
||||
upsamples.append(ResidualBlock(in_dim, out_dim, dropout))
|
||||
upsamples.append(ResidualBlock(in_dim, out_dim, dropout, temporal_kernel=temporal_kernel))
|
||||
in_dim = out_dim
|
||||
|
||||
# Add the final upsample block
|
||||
if up_flag:
|
||||
mode = "upsample3d" if temperal_upsample else "upsample2d"
|
||||
upsamples.append(Resample(out_dim, mode=mode))
|
||||
upsamples.append(Resample(out_dim, mode=mode, temporal_kernel=temporal_kernel))
|
||||
|
||||
self.upsamples = nn.Sequential(*upsamples)
|
||||
|
||||
@@ -373,6 +411,8 @@ class Up_ResidualBlock(nn.Module):
|
||||
for module in self.upsamples:
|
||||
x_main = module(x_main, feat_cache, feat_idx)
|
||||
if self.avg_shortcut is not None:
|
||||
if feat_cache is None:
|
||||
return strip_apply(lambda s: self.avg_shortcut(s, first_chunk), x, scale=self.avg_shortcut.factor_s, halo=0, out=x_main)
|
||||
x_shortcut = self.avg_shortcut(x, first_chunk)
|
||||
return x_main + x_shortcut
|
||||
else:
|
||||
@@ -390,6 +430,8 @@ class Encoder3d(nn.Module):
|
||||
attn_scales=[],
|
||||
temperal_downsample=[True, True, False],
|
||||
dropout=0.0,
|
||||
in_channels=12,
|
||||
temporal_kernel=3,
|
||||
):
|
||||
super().__init__()
|
||||
self.dim = dim
|
||||
@@ -404,7 +446,7 @@ class Encoder3d(nn.Module):
|
||||
scale = 1.0
|
||||
|
||||
# init block
|
||||
self.conv1 = CausalConv3d(12, dims[0], 3, padding=1)
|
||||
self.conv1 = conv3x3(in_channels, dims[0], temporal_kernel)
|
||||
|
||||
# downsample blocks
|
||||
downsamples = []
|
||||
@@ -420,22 +462,23 @@ class Encoder3d(nn.Module):
|
||||
mult=num_res_blocks,
|
||||
temperal_downsample=t_down_flag,
|
||||
down_flag=i != len(dim_mult) - 1,
|
||||
temporal_kernel=temporal_kernel,
|
||||
))
|
||||
scale /= 2.0
|
||||
self.downsamples = nn.Sequential(*downsamples)
|
||||
|
||||
# middle blocks
|
||||
self.middle = nn.Sequential(
|
||||
ResidualBlock(out_dim, out_dim, dropout),
|
||||
ResidualBlock(out_dim, out_dim, dropout, temporal_kernel=temporal_kernel),
|
||||
AttentionBlock(out_dim),
|
||||
ResidualBlock(out_dim, out_dim, dropout),
|
||||
ResidualBlock(out_dim, out_dim, dropout, temporal_kernel=temporal_kernel),
|
||||
)
|
||||
|
||||
# # output blocks
|
||||
self.head = nn.Sequential(
|
||||
RMS_norm(out_dim, images=False),
|
||||
nn.SiLU(),
|
||||
CausalConv3d(out_dim, z_dim, 3, padding=1),
|
||||
conv3x3(out_dim, z_dim, temporal_kernel),
|
||||
)
|
||||
|
||||
def forward(self, x, feat_cache=None, feat_idx=[0]):
|
||||
@@ -506,6 +549,8 @@ class Decoder3d(nn.Module):
|
||||
attn_scales=[],
|
||||
temperal_upsample=[False, True, True],
|
||||
dropout=0.0,
|
||||
out_channels=12,
|
||||
temporal_kernel=3,
|
||||
):
|
||||
super().__init__()
|
||||
self.dim = dim
|
||||
@@ -518,13 +563,13 @@ class Decoder3d(nn.Module):
|
||||
# dimensions
|
||||
dims = [dim * u for u in [dim_mult[-1]] + dim_mult[::-1]]
|
||||
# init block
|
||||
self.conv1 = CausalConv3d(z_dim, dims[0], 3, padding=1)
|
||||
self.conv1 = conv3x3(z_dim, dims[0], temporal_kernel)
|
||||
|
||||
# middle blocks
|
||||
self.middle = nn.Sequential(
|
||||
ResidualBlock(dims[0], dims[0], dropout),
|
||||
ResidualBlock(dims[0], dims[0], dropout, temporal_kernel=temporal_kernel),
|
||||
AttentionBlock(dims[0]),
|
||||
ResidualBlock(dims[0], dims[0], dropout),
|
||||
ResidualBlock(dims[0], dims[0], dropout, temporal_kernel=temporal_kernel),
|
||||
)
|
||||
|
||||
# upsample blocks
|
||||
@@ -540,6 +585,7 @@ class Decoder3d(nn.Module):
|
||||
mult=num_res_blocks + 1,
|
||||
temperal_upsample=t_up_flag,
|
||||
up_flag=i != len(dim_mult) - 1,
|
||||
temporal_kernel=temporal_kernel,
|
||||
))
|
||||
self.upsamples = nn.Sequential(*upsamples)
|
||||
|
||||
@@ -547,7 +593,7 @@ class Decoder3d(nn.Module):
|
||||
self.head = nn.Sequential(
|
||||
RMS_norm(out_dim, images=False),
|
||||
nn.SiLU(),
|
||||
CausalConv3d(out_dim, 12, 3, padding=1),
|
||||
conv3x3(out_dim, out_channels, temporal_kernel),
|
||||
)
|
||||
|
||||
def forward(self, x, feat_cache=None, feat_idx=[0], first_chunk=False):
|
||||
@@ -580,7 +626,7 @@ class Decoder3d(nn.Module):
|
||||
if feat_cache is not None:
|
||||
x = layer(x, feat_cache, feat_idx, first_chunk)
|
||||
else:
|
||||
x = layer(x)
|
||||
x = layer(x, first_chunk=first_chunk)
|
||||
|
||||
## head
|
||||
for layer in self.head:
|
||||
@@ -624,6 +670,9 @@ class WanVAE(nn.Module):
|
||||
attn_scales=[],
|
||||
temperal_downsample=[True, True, False],
|
||||
dropout=0.0,
|
||||
image_channels=3,
|
||||
patch_size=2,
|
||||
temporal_kernel=3,
|
||||
):
|
||||
super().__init__()
|
||||
self.dim = dim
|
||||
@@ -633,6 +682,7 @@ class WanVAE(nn.Module):
|
||||
self.attn_scales = attn_scales
|
||||
self.temperal_downsample = temperal_downsample
|
||||
self.temperal_upsample = temperal_downsample[::-1]
|
||||
self.patch_size = patch_size
|
||||
|
||||
# modules
|
||||
self.encoder = Encoder3d(
|
||||
@@ -643,6 +693,8 @@ class WanVAE(nn.Module):
|
||||
attn_scales,
|
||||
self.temperal_downsample,
|
||||
dropout,
|
||||
in_channels=image_channels * patch_size * patch_size,
|
||||
temporal_kernel=temporal_kernel,
|
||||
)
|
||||
self.conv1 = CausalConv3d(z_dim * 2, z_dim * 2, 1)
|
||||
self.conv2 = CausalConv3d(z_dim, z_dim, 1)
|
||||
@@ -654,12 +706,18 @@ class WanVAE(nn.Module):
|
||||
attn_scales,
|
||||
self.temperal_upsample,
|
||||
dropout,
|
||||
out_channels=image_channels * patch_size * patch_size,
|
||||
temporal_kernel=temporal_kernel,
|
||||
)
|
||||
|
||||
def encode(self, x):
|
||||
if x.ndim == 4:
|
||||
# single image: no temporal cache, which would keep every conv input alive for the whole pass
|
||||
x = patchify(x.unsqueeze(2), patch_size=self.patch_size)
|
||||
return self.conv1(self.encoder(x)).chunk(2, dim=1)[0].squeeze(2)
|
||||
conv_idx = [0]
|
||||
feat_map = [None] * count_conv3d(self.encoder)
|
||||
x = patchify(x, patch_size=2)
|
||||
x = patchify(x, patch_size=self.patch_size)
|
||||
t = x.shape[2]
|
||||
iter_ = 1 + (t - 1) // 4
|
||||
for i in range(iter_):
|
||||
@@ -681,6 +739,9 @@ class WanVAE(nn.Module):
|
||||
return mu
|
||||
|
||||
def decode(self, z):
|
||||
if z.ndim == 4:
|
||||
out = self.decoder(self.conv2(z.unsqueeze(2)), first_chunk=True)
|
||||
return unpatchify(out, patch_size=self.patch_size).squeeze(2)
|
||||
conv_idx = [0]
|
||||
feat_map = [None] * count_conv3d(self.decoder)
|
||||
iter_ = z.shape[2]
|
||||
@@ -701,7 +762,7 @@ class WanVAE(nn.Module):
|
||||
feat_idx=conv_idx,
|
||||
)
|
||||
out = torch.cat([out, out_], 2)
|
||||
out = unpatchify(out, patch_size=2)
|
||||
out = unpatchify(out, patch_size=self.patch_size)
|
||||
return out
|
||||
|
||||
def reparameterize(self, mu, log_var):
|
||||
|
||||
+10
-5
@@ -327,11 +327,16 @@ def model_lora_keys_unet(model, key_map={}):
|
||||
for k in sdk:
|
||||
if k.startswith("diffusion_model.") and k.endswith(".weight"): #QwenImage lora format
|
||||
key_lora = k[len("diffusion_model."):-len(".weight")]
|
||||
# Direct mapping for transformer_blocks format (QwenImage LoRA format)
|
||||
key_map["{}".format(key_lora)] = k
|
||||
# Support transformer prefix format
|
||||
key_map["transformer.{}".format(key_lora)] = k
|
||||
key_map["lycoris_{}".format(key_lora.replace(".", "_"))] = k #SimpleTuner lycoris format
|
||||
targets = [(key_lora, k)]
|
||||
if key_lora.endswith(".img_mlp.gate_up"): # Qwen Image 2.1 fuses gate_layer/proj at load; LoRAs address the halves
|
||||
half = sd[k].shape[0] // 2
|
||||
targets = [(key_lora.replace(".gate_up", ".gate_layer"), (k, (0, 0, half))), (key_lora.replace(".gate_up", ".proj"), (k, (0, half, half)))]
|
||||
for key_lora, to in targets:
|
||||
# Direct mapping for transformer_blocks format (QwenImage LoRA format)
|
||||
key_map["{}".format(key_lora)] = to
|
||||
# Support transformer prefix format
|
||||
key_map["transformer.{}".format(key_lora)] = to
|
||||
key_map["lycoris_{}".format(key_lora.replace(".", "_"))] = to #SimpleTuner lycoris format
|
||||
|
||||
if isinstance(model, comfy.model_base.Krea2):
|
||||
diffusers_keys = comfy.utils.krea2_to_diffusers(model.model_config.unet_config, output_prefix="diffusion_model.")
|
||||
|
||||
+24
-6
@@ -63,6 +63,7 @@ import comfy.ldm.omnigen.omnigen2
|
||||
import comfy.ldm.seedvr.model
|
||||
import comfy.ldm.boogu.model
|
||||
import comfy.ldm.qwen_image.model
|
||||
import comfy.ldm.qwen_image21.model
|
||||
import comfy.ldm.mage_flow.model
|
||||
import comfy.ldm.joyimage.model
|
||||
import comfy.ldm.ideogram4.model
|
||||
@@ -2644,7 +2645,8 @@ class QwenImage(BaseModel):
|
||||
out = {}
|
||||
ref_latents = kwargs.get("reference_latents", None)
|
||||
if ref_latents is not None:
|
||||
out['ref_latents'] = list([1, 16, sum(map(lambda a: math.prod(a.size()), ref_latents)) // 16])
|
||||
c = self.latent_format.latent_channels
|
||||
out['ref_latents'] = list([1, c, sum(map(lambda a: math.prod(a.size()), ref_latents)) // c])
|
||||
return out
|
||||
|
||||
class MageFlow(QwenImage):
|
||||
@@ -2655,11 +2657,27 @@ class MageFlow(QwenImage):
|
||||
# Mage runs in bf16 and rounds its timestep frequency table to the timestep dtype, keep that on fp32 devices.
|
||||
return timestep.to(torch.bfloat16)
|
||||
|
||||
def extra_conds_shapes(self, **kwargs):
|
||||
out = {}
|
||||
ref_latents = kwargs.get("reference_latents", None)
|
||||
if ref_latents is not None:
|
||||
out['ref_latents'] = list([1, 128, sum(map(lambda a: math.prod(a.size()), ref_latents)) // 128])
|
||||
class QwenImage21(QwenImage):
|
||||
def __init__(self, model_config, model_type=ModelType.FLUX, device=None):
|
||||
super().__init__(model_config, model_type, device=device, unet_model=comfy.ldm.qwen_image21.model.QwenImage21Transformer2DModel)
|
||||
|
||||
@property
|
||||
def current_patcher(self):
|
||||
return self._current_patcher
|
||||
|
||||
@current_patcher.setter
|
||||
def current_patcher(self, patcher):
|
||||
# set by pre_run / cleanup: the prefix K/V cache lives for one sampling run, off when hooks can repatch weights mid-run
|
||||
self._current_patcher = patcher
|
||||
diffusion_model = getattr(self, "diffusion_model", None)
|
||||
if diffusion_model is not None:
|
||||
diffusion_model.reset_prefix_cache(patcher is not None and len(patcher.hook_patches) == 0)
|
||||
|
||||
def extra_conds(self, **kwargs):
|
||||
out = super().extra_conds(**kwargs)
|
||||
image_slots = kwargs.get("image_slots", None)
|
||||
if image_slots is not None:
|
||||
out['image_slots'] = comfy.conds.CONDConstant(image_slots)
|
||||
return out
|
||||
|
||||
class JoyImage(BaseModel):
|
||||
|
||||
@@ -959,6 +959,27 @@ def detect_unet_config(state_dict, key_prefix, metadata=None):
|
||||
dit_config["num_layers"] = count_blocks(state_dict_keys, '{}transformer_blocks.'.format(key_prefix) + '{}.')
|
||||
return dit_config
|
||||
|
||||
qwen_image21_keys = ['txt_in.text_norm.weight', 'modulation.1.weight', 'transformer_blocks.0.attn.norm_q.weight', 'img_in.weight', 'proj_out.weight']
|
||||
if all('{}{}'.format(key_prefix, k) in state_dict_keys for k in qwen_image21_keys) and any('{}transformer_blocks.0.img_mlp.{}.weight'.format(key_prefix, k) in state_dict_keys for k in ('gate_up', 'proj')): # Qwen Image 2.1
|
||||
dit_config = {}
|
||||
dit_config["image_model"] = "qwen_image21"
|
||||
head_dim = state_dict['{}transformer_blocks.0.attn.norm_q.weight'.format(key_prefix)].shape[0]
|
||||
inner_dim = state_dict['{}img_in.weight'.format(key_prefix)].shape[0]
|
||||
dit_config["in_channels"] = state_dict['{}img_in.weight'.format(key_prefix)].shape[1]
|
||||
dit_config["out_channels"] = state_dict['{}proj_out.weight'.format(key_prefix)].shape[0]
|
||||
dit_config["num_layers"] = count_blocks(state_dict_keys, '{}transformer_blocks.'.format(key_prefix) + '{}.')
|
||||
dit_config["attention_head_dim"] = head_dim
|
||||
dit_config["num_attention_heads"] = inner_dim // head_dim
|
||||
dit_config["context_in_dim"] = state_dict['{}txt_in.text_norm.weight'.format(key_prefix)].shape[0]
|
||||
# gate and up projections fuse into one GEMM when their rows can be concatenated: plain weights or per-row scales; a comfy-saved file is already fused
|
||||
gate_up = state_dict.get('{}transformer_blocks.0.img_mlp.gate_up.weight'.format(key_prefix), None)
|
||||
if gate_up is not None:
|
||||
dit_config["mlp_ratio"] = gate_up.shape[0] // 2 // inner_dim
|
||||
else:
|
||||
dit_config["mlp_ratio"] = state_dict['{}transformer_blocks.0.img_mlp.proj.weight'.format(key_prefix)].shape[0] // inner_dim
|
||||
dit_config["fused_mlp"] = gate_up is not None
|
||||
return dit_config
|
||||
|
||||
if '{}txt_norm.weight'.format(key_prefix) in state_dict_keys: # Qwen Image
|
||||
dit_config = {}
|
||||
dit_config["image_model"] = "qwen_image"
|
||||
|
||||
+27
-4
@@ -64,6 +64,7 @@ import comfy.text_encoders.hidream
|
||||
import comfy.text_encoders.ace
|
||||
import comfy.text_encoders.omnigen2
|
||||
import comfy.text_encoders.qwen_image
|
||||
import comfy.text_encoders.qwen_image21
|
||||
import comfy.text_encoders.hunyuan_image
|
||||
import comfy.text_encoders.z_image
|
||||
import comfy.text_encoders.krea2
|
||||
@@ -382,6 +383,8 @@ class CLIP:
|
||||
o = self.cond_stage_model.encode_token_weights(tokens)
|
||||
cond, pooled = o[:2]
|
||||
pooled_dict = {"pooled_output": pooled}
|
||||
if len(o) > 2:
|
||||
pooled_dict.update(o[2])
|
||||
# add clip_start_percent and clip_end_percent in pooled
|
||||
pooled_dict["clip_start_percent"] = t_range[0]
|
||||
pooled_dict["clip_end_percent"] = t_range[1]
|
||||
@@ -826,7 +829,20 @@ class VAE:
|
||||
self.memory_used_encode = lambda shape, dtype: (50 * (round((shape[2] + 7) / 8) * 8) * shape[3] * shape[4]) * model_management.dtype_size(dtype)
|
||||
self.working_dtypes = [torch.bfloat16, torch.float32]
|
||||
elif "decoder.middle.0.residual.0.gamma" in sd:
|
||||
if "decoder.upsamples.0.upsamples.0.residual.2.weight" in sd: # Wan 2.2 VAE
|
||||
wan22_layout = "decoder.upsamples.0.upsamples.0.residual.2.weight" in sd
|
||||
head = sd.get("decoder.head.2.weight", None)
|
||||
if wan22_layout and head is not None and head.ndim == 5 and head.shape[2] == 1: # Qwen Image 2.1 VAE: Wan 2.2 layout, temporal kernel 1, no patchify, RGBA
|
||||
self.upscale_ratio = 16
|
||||
self.downscale_ratio = 16
|
||||
self.latent_channels = 64
|
||||
self.output_channels = sd["decoder.head.2.weight"].shape[0]
|
||||
self.pad_channel_value = 1.0 # opaque alpha for RGB input
|
||||
ddconfig = {"dim": sd["encoder.conv1.weight"].shape[0], "dec_dim": sd["decoder.head.0.gamma"].shape[0], "z_dim": self.latent_channels, "dim_mult": [1, 2, 4, 8, 8], "num_res_blocks": 2, "attn_scales": [], "temperal_downsample": [False, True, True, True], "dropout": 0.0, "image_channels": self.output_channels, "patch_size": 1, "temporal_kernel": 1}
|
||||
self.first_stage_model = comfy.ldm.wan.vae2_2.WanVAE(**ddconfig)
|
||||
self.working_dtypes = [torch.bfloat16, torch.float16, torch.float32]
|
||||
self.memory_used_encode = lambda shape, dtype: 600 * shape[2] * shape[3] * model_management.dtype_size(dtype)
|
||||
self.memory_used_decode = lambda shape, dtype: 900 * shape[2] * shape[3] * (16 * 16) * model_management.dtype_size(dtype)
|
||||
elif wan22_layout: # Wan 2.2 VAE
|
||||
self.upscale_ratio = (lambda a: max(0, a * 4 - 3), 16, 16)
|
||||
self.upscale_index_formula = (4, 16, 16)
|
||||
self.downscale_ratio = (lambda a: max(0, math.floor((a + 3) / 4)), 16, 16)
|
||||
@@ -1095,6 +1111,9 @@ class VAE:
|
||||
self.patcher = mp(self.first_stage_model, load_device=self.device, offload_device=offload_device, fast_disk=fast_disk)
|
||||
|
||||
m, u = self.first_stage_model.load_state_dict(sd, strict=False, assign=self.patcher.is_dynamic())
|
||||
if not self.patcher.is_dynamic():
|
||||
# Lazy parameters only exist after loading the state dict.
|
||||
self.first_stage_model.to(self.vae_dtype)
|
||||
if len(m) > 0:
|
||||
logging.warning("Missing VAE keys {}".format(m))
|
||||
|
||||
@@ -1150,9 +1169,9 @@ class VAE:
|
||||
|
||||
decode_fn = lambda a: self.first_stage_model.decode(a.to(self.vae_dtype).to(self.device)).to(dtype=self.vae_output_dtype())
|
||||
output = self.process_output(
|
||||
(comfy.utils.tiled_scale(samples, decode_fn, tile_x // 2, tile_y * 2, overlap, upscale_amount = self.upscale_ratio, output_device=self.output_device, pbar = pbar) +
|
||||
comfy.utils.tiled_scale(samples, decode_fn, tile_x * 2, tile_y // 2, overlap, upscale_amount = self.upscale_ratio, output_device=self.output_device, pbar = pbar) +
|
||||
comfy.utils.tiled_scale(samples, decode_fn, tile_x, tile_y, overlap, upscale_amount = self.upscale_ratio, output_device=self.output_device, pbar = pbar))
|
||||
(comfy.utils.tiled_scale(samples, decode_fn, tile_x // 2, tile_y * 2, overlap, upscale_amount = self.upscale_ratio, out_channels=self.output_channels, output_device=self.output_device, pbar = pbar) +
|
||||
comfy.utils.tiled_scale(samples, decode_fn, tile_x * 2, tile_y // 2, overlap, upscale_amount = self.upscale_ratio, out_channels=self.output_channels, output_device=self.output_device, pbar = pbar) +
|
||||
comfy.utils.tiled_scale(samples, decode_fn, tile_x, tile_y, overlap, upscale_amount = self.upscale_ratio, out_channels=self.output_channels, output_device=self.output_device, pbar = pbar))
|
||||
/ 3.0)
|
||||
return output
|
||||
|
||||
@@ -1933,6 +1952,10 @@ def load_text_encoder_state_dicts(state_dicts=[], embedding_directory=None, clip
|
||||
clip_data[0] = comfy.utils.state_dict_prefix_replace(clip_data[0], {"model.language_model.": "model.", "model.visual.": "visual.", "lm_head.": "model.lm_head."})
|
||||
clip_target.clip = comfy.text_encoders.joyimage.te(**llama_detect(clip_data))
|
||||
clip_target.tokenizer = comfy.text_encoders.joyimage.JoyImageTokenizer
|
||||
elif clip_type == CLIPType.QWEN_IMAGE and te_model == TEModel.QWEN3VL_8B: # Qwen-Image 2.1: full Qwen3-VL-8B, last hidden state, image slots spliced by the DiT.
|
||||
clip_data[0] = comfy.utils.state_dict_prefix_replace(clip_data[0], {"model.language_model.": "model.", "model.visual.": "visual.", "lm_head.": "model.lm_head."})
|
||||
clip_target.clip = comfy.text_encoders.qwen_image21.te(**llama_detect(clip_data))
|
||||
clip_target.tokenizer = comfy.text_encoders.qwen_image21.QwenImage21Tokenizer
|
||||
elif clip_type in (CLIPType.FLUX, CLIPType.FLUX2): # Flux2 Klein reuses the Qwen3-VL LM (3-layer tap -> 12288); visual unused.
|
||||
klein_model_type = "qwen3_8b" if te_model == TEModel.QWEN3VL_8B else "qwen3_4b"
|
||||
clip_target.clip = comfy.text_encoders.flux.klein_te(**llama_detect(clip_data), model_type=klein_model_type)
|
||||
|
||||
@@ -23,6 +23,7 @@ import comfy.text_encoders.wan
|
||||
import comfy.text_encoders.ace
|
||||
import comfy.text_encoders.omnigen2
|
||||
import comfy.text_encoders.qwen_image
|
||||
import comfy.text_encoders.qwen_image21
|
||||
import comfy.text_encoders.hunyuan_image
|
||||
import comfy.text_encoders.kandinsky5
|
||||
import comfy.text_encoders.z_image
|
||||
@@ -2051,6 +2052,35 @@ class QwenImage(supported_models_base.BASE):
|
||||
hunyuan_detect = comfy.text_encoders.hunyuan_video.llama_detect(state_dict, "{}qwen25_7b.transformer.".format(pref))
|
||||
return supported_models_base.ClipTarget(comfy.text_encoders.qwen_image.QwenImageTokenizer, comfy.text_encoders.qwen_image.te(**hunyuan_detect))
|
||||
|
||||
class QwenImage21(supported_models_base.BASE):
|
||||
unet_config = {
|
||||
"image_model": "qwen_image21",
|
||||
}
|
||||
|
||||
# scheduler mu at 1024x1024 (base 0.5 @ 256 tokens, max 0.9 @ 8192)
|
||||
sampling_settings = {
|
||||
"multiplier": 1.0,
|
||||
"shift": 0.69,
|
||||
}
|
||||
|
||||
memory_usage_factor = 6.0
|
||||
|
||||
unet_extra_config = {}
|
||||
latent_format = latent_formats.QwenImage21
|
||||
|
||||
supported_inference_dtypes = [torch.bfloat16, torch.float32]
|
||||
|
||||
vae_key_prefix = ["vae."]
|
||||
text_encoder_key_prefix = ["text_encoders."]
|
||||
|
||||
def get_model(self, state_dict, prefix="", device=None):
|
||||
return model_base.QwenImage21(self, device=device)
|
||||
|
||||
def clip_target(self, state_dict={}):
|
||||
pref = self.text_encoder_key_prefix[0]
|
||||
hunyuan_detect = comfy.text_encoders.hunyuan_video.llama_detect(state_dict, "{}qwen3vl_8b.transformer.".format(pref))
|
||||
return supported_models_base.ClipTarget(comfy.text_encoders.qwen_image21.QwenImage21Tokenizer, comfy.text_encoders.qwen_image21.te(**hunyuan_detect))
|
||||
|
||||
class JoyImage(supported_models_base.BASE):
|
||||
unet_config = {
|
||||
"image_model": "joyimage",
|
||||
@@ -2610,6 +2640,7 @@ models = [
|
||||
Boogu,
|
||||
MageFlow,
|
||||
QwenImage,
|
||||
QwenImage21,
|
||||
JoyImage,
|
||||
Ideogram4,
|
||||
Krea2,
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import numbers
|
||||
|
||||
import torch
|
||||
|
||||
import comfy.text_encoders.qwen3vl
|
||||
from comfy import sd1_clip
|
||||
|
||||
VISION_BLOCK = "<|vision_start|><|image_pad|><|vision_end|>"
|
||||
SYSTEM_PROMPT = "<|im_start|>system\nComprehend and analyze the provided prompt.<|im_end|>\n"
|
||||
T2I_TEMPLATE = SYSTEM_PROMPT + "<|im_start|>user\n{}<|im_end|>\n<|im_start|>assistant\n"
|
||||
|
||||
|
||||
class QwenImage21Tokenizer(comfy.text_encoders.qwen3vl.Qwen3VLTokenizer):
|
||||
def __init__(self, embedding_directory=None, tokenizer_data={}):
|
||||
super().__init__(embedding_directory=embedding_directory, tokenizer_data=tokenizer_data, model_type="qwen3vl_8b")
|
||||
self.llama_template = T2I_TEMPLATE
|
||||
|
||||
def tokenize_with_weights(self, text, return_word_ids=False, llama_template=None, images=[], prevent_empty_text=False, thinking=True, keep_vision=False, **kwargs):
|
||||
image = kwargs.get("image", None)
|
||||
if image is not None and len(images) == 0:
|
||||
images = [image[i:i + 1] for i in range(image.shape[0])]
|
||||
if llama_template is None and len(images) > 0:
|
||||
refs = " ".join("<image{}>{}".format(i + 1, VISION_BLOCK) for i in range(len(images)))
|
||||
llama_template = T2I_TEMPLATE.replace("{}", refs + "{}", 1)
|
||||
out = super().tokenize_with_weights(text, return_word_ids=return_word_ids, llama_template=llama_template, images=images, prevent_empty_text=prevent_empty_text, thinking=thinking, **kwargs)
|
||||
out["keep_vision"] = keep_vision
|
||||
return out
|
||||
|
||||
|
||||
class QwenImage21Qwen3VLClipModel(comfy.text_encoders.qwen3vl.Qwen3VLClipModel):
|
||||
def __init__(self, device="cpu", dtype=None, attention_mask=True, model_options={}):
|
||||
super().__init__(device=device, dtype=dtype, attention_mask=attention_mask, model_options=model_options, model_type="qwen3vl_8b")
|
||||
# last layer without the final RMSNorm: transformers 4.57 hidden_states[-1], which Qwen's results are tuned to (5.x norms it)
|
||||
self.layer_norm_hidden_state = False
|
||||
self.image_spans = []
|
||||
|
||||
def process_tokens(self, tokens, device):
|
||||
embeds, attention_mask, num_tokens, embeds_info = super().process_tokens(tokens, device)
|
||||
self.image_spans = [(e["index"], e["size"]) for e in embeds_info if e["type"] == "image"]
|
||||
return embeds, attention_mask, num_tokens, embeds_info
|
||||
|
||||
|
||||
class QwenImage21TEModel(sd1_clip.SD1ClipModel):
|
||||
def __init__(self, device="cpu", dtype=None, model_options={}):
|
||||
super().__init__(device=device, dtype=dtype, name="qwen3vl_8b", clip_model=QwenImage21Qwen3VLClipModel, model_options=model_options)
|
||||
|
||||
def encode_token_weights(self, token_weight_pairs):
|
||||
out, pooled, extra = super().encode_token_weights(token_weight_pairs)
|
||||
tokens = [t[0] for t in token_weight_pairs["qwen3vl_8b"][0]]
|
||||
image_spans = getattr(self, self.clip).image_spans
|
||||
|
||||
# drop the system turn, everything before the second <|im_start|>; positions shift by each image expanded before it
|
||||
im_starts, offset, spans = [], 0, iter(image_spans)
|
||||
for i, t in enumerate(tokens):
|
||||
if isinstance(t, numbers.Integral):
|
||||
if t == 151644:
|
||||
im_starts.append(i + offset)
|
||||
elif isinstance(t, dict) and t.get("type") == "image": # a textual embedding is a bare tensor and has no span
|
||||
offset += next(spans, (0, 1))[1] - 1
|
||||
keep = torch.ones(out.shape[1], dtype=torch.bool)
|
||||
keep[:im_starts[1] if len(im_starts) > 1 else 0] = False
|
||||
|
||||
# vision tokens are replaced by reference latents in the DiT: drop them and record where each image goes
|
||||
# with no latents coming (no vae) they stay, and the image conditions through the text encoder alone
|
||||
slots = []
|
||||
if not token_weight_pairs.get("keep_vision", False):
|
||||
for start, size in image_spans:
|
||||
keep[start:start + size] = False
|
||||
slots.append(int(keep[:start].sum()))
|
||||
|
||||
out = out[:, keep.to(out.device)]
|
||||
extra["attention_mask"] = extra["attention_mask"][:, keep.to(extra["attention_mask"].device)]
|
||||
if extra["attention_mask"].sum() == torch.numel(extra["attention_mask"]):
|
||||
extra.pop("attention_mask")
|
||||
if len(slots) > 0:
|
||||
extra["image_slots"] = slots
|
||||
return out, pooled, extra
|
||||
|
||||
|
||||
def te(dtype_llama=None, llama_quantization_metadata=None):
|
||||
class QwenImage21TEModel_(QwenImage21TEModel):
|
||||
def __init__(self, device="cpu", dtype=None, model_options={}):
|
||||
if dtype_llama is not None:
|
||||
dtype = dtype_llama
|
||||
if llama_quantization_metadata is not None:
|
||||
model_options = model_options.copy()
|
||||
model_options["quantization_metadata"] = llama_quantization_metadata
|
||||
super().__init__(device=device, dtype=dtype, model_options=model_options)
|
||||
return QwenImage21TEModel_
|
||||
@@ -108,6 +108,108 @@ class TextEncodeQwenImageEditPlus(io.ComfyNode):
|
||||
return io.NodeOutput(conditioning)
|
||||
|
||||
|
||||
class TextEncodeQwenImage21(io.ComfyNode):
|
||||
@classmethod
|
||||
def define_schema(cls):
|
||||
return io.Schema(
|
||||
node_id="TextEncodeQwenImage21",
|
||||
display_name="Text Encode Qwen Image 2.1",
|
||||
category="model/conditioning/qwen image",
|
||||
inputs=[
|
||||
io.Clip.Input("clip"),
|
||||
io.String.Input("prompt", multiline=True, dynamic_prompts=True),
|
||||
io.String.Input("negative_prompt", multiline=True, dynamic_prompts=True),
|
||||
io.Vae.Input("vae", optional=True),
|
||||
io.Int.Input("resolution", default=1024, min=0, max=4096, step=32,
|
||||
tooltip="Reference images are resized to about resolution x resolution pixels, at multiples of 32, preserving aspect ratio. 0 keeps each reference at its own size, rounded to a multiple of 32. "),
|
||||
io.Autogrow.Input(
|
||||
"images",
|
||||
template=io.Autogrow.TemplateNames(
|
||||
io.Image.Input("image"),
|
||||
names=[f"image_{i}" for i in range(1, 17)],
|
||||
min=0,
|
||||
),
|
||||
tooltip="Reference images, seen by the text encoder and spliced into the sequence as VAE latents.",
|
||||
),
|
||||
],
|
||||
outputs=[
|
||||
io.Conditioning.Output(display_name="positive"),
|
||||
io.Conditioning.Output(display_name="negative"),
|
||||
io.Latent.Output(display_name="latent",
|
||||
tooltip="Empty latent on the first reference image's size, to match with sampling as any other size shifts the edit."),
|
||||
],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def execute(cls, clip, prompt, negative_prompt, vae=None, resolution=1024, images: io.Autogrow.Type = None) -> io.NodeOutput:
|
||||
ref_latents = []
|
||||
images_vl = []
|
||||
images = images or {}
|
||||
latent_w = latent_h = resolution or 1024
|
||||
for name in sorted(images, key=lambda n: int(n.rsplit("_", 1)[-1])):
|
||||
image = images[name]
|
||||
if image is None:
|
||||
continue
|
||||
# same resize for the text encoder and the VAE, so every vision slot covers 2x2 latents; one image per input
|
||||
samples = image[:1].movedim(-1, 1)
|
||||
if resolution > 0:
|
||||
ratio = samples.shape[3] / samples.shape[2]
|
||||
width = round(math.sqrt(resolution * resolution * ratio) / 32) * 32
|
||||
height = round(math.sqrt(resolution * resolution / ratio) / 32) * 32
|
||||
else:
|
||||
width, height = round(samples.shape[3] / 32) * 32, round(samples.shape[2] / 32) * 32
|
||||
width, height = max(32, width), max(32, height)
|
||||
if (width, height) == (samples.shape[3], samples.shape[2]):
|
||||
s = image[:1]
|
||||
else:
|
||||
s = comfy.utils.common_upscale(samples, width, height, "lanczos", "disabled").movedim(1, -1)
|
||||
if not images_vl:
|
||||
latent_w, latent_h = width, height
|
||||
rgb = s[:, :, :, :3]
|
||||
if s.shape[-1] > 3:
|
||||
rgb = rgb * s[:, :, :, 3:] + (1.0 - s[:, :, :, 3:]) # the vision tower sees alpha over white, the vae keeps all four
|
||||
images_vl.append(rgb)
|
||||
if vae is not None:
|
||||
ref_latents.append(vae.encode(s))
|
||||
|
||||
keep_vision = len(ref_latents) == 0
|
||||
positive = clip.encode_from_tokens_scheduled(clip.tokenize(prompt, images=images_vl, keep_vision=keep_vision, prevent_empty_text=True))
|
||||
negative = clip.encode_from_tokens_scheduled(clip.tokenize(negative_prompt, images=images_vl, keep_vision=keep_vision, prevent_empty_text=True))
|
||||
if len(ref_latents) > 0:
|
||||
positive = node_helpers.conditioning_set_values(positive, {"reference_latents": ref_latents}, append=True)
|
||||
negative = node_helpers.conditioning_set_values(negative, {"reference_latents": ref_latents}, append=True)
|
||||
latent = torch.zeros([1, 64, latent_h // 16, latent_w // 16], device=comfy.model_management.intermediate_device())
|
||||
return io.NodeOutput(positive, negative, {"samples": latent})
|
||||
|
||||
|
||||
class QwenImage21Cache(io.ComfyNode):
|
||||
@classmethod
|
||||
def define_schema(cls):
|
||||
return io.Schema(
|
||||
node_id="QwenImage21Cache",
|
||||
display_name="Qwen Image 2.1 Cache",
|
||||
category="model/conditioning/qwen image",
|
||||
description=(
|
||||
"Allows setting the KV cache device and quantization, by default the model uses the auto -option. Quantization potentially speeds up edit workflows when memory starved "
|
||||
),
|
||||
inputs=[
|
||||
io.Model.Input("model"),
|
||||
io.Combo.Input("device", options=["auto", "gpu", "cpu", "off"], default="auto",
|
||||
tooltip="auto uses spare VRAM, then RAM. cpu (RAM) is prefetched behind compute and costs little speed. off recomputes the prefix every step, which is slower but is the one way to rule the cache out."),
|
||||
io.Combo.Input("dtype", options=["default", "int8", "int4"], default="default",
|
||||
tooltip="Storage precision. default is lossless. int8 halves the cache at about bf16 accuracy, int4 quarters it but roughly doubles the per-step error."),
|
||||
],
|
||||
outputs=[io.Model.Output()],
|
||||
is_experimental=True,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def execute(cls, model, device, dtype) -> io.NodeOutput:
|
||||
m = model.clone()
|
||||
m.model_options["transformer_options"]["qwen_image21_cache"] = {"device": device, "dtype": dtype}
|
||||
return io.NodeOutput(m)
|
||||
|
||||
|
||||
class EmptyQwenImageLayeredLatentImage(io.ComfyNode):
|
||||
@classmethod
|
||||
def define_schema(cls):
|
||||
@@ -138,6 +240,8 @@ class QwenExtension(ComfyExtension):
|
||||
return [
|
||||
TextEncodeQwenImageEdit,
|
||||
TextEncodeQwenImageEditPlus,
|
||||
TextEncodeQwenImage21,
|
||||
QwenImage21Cache,
|
||||
EmptyQwenImageLayeredLatentImage,
|
||||
]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user