Skip to content

Commit 917faef

Browse files
Support PID 1.5 models. (Comfy-Org#14894)
1 parent 8b099de commit 917faef

4 files changed

Lines changed: 140 additions & 17 deletions

File tree

comfy/ldm/pixeldit/model.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,9 @@ def _pre_patch_block(self, s, i, **kwargs):
197197
"""Hook for subclasses to inject per-block state into the patch stream (e.g. PiD's LQ gate)."""
198198
return s
199199

200+
def _pre_pixel_blocks(self, s, **kwargs):
201+
return s
202+
200203
def _forward(self, x, timesteps, context=None, attention_mask=None, transformer_options={}, **kwargs):
201204
H_orig, W_orig = x.shape[2], x.shape[3]
202205
x = comfy.ldm.common_dit.pad_to_patch_size(x, (self.patch_size, self.patch_size))
@@ -226,6 +229,7 @@ def _forward(self, x, timesteps, context=None, attention_mask=None, transformer_
226229
s, y_emb = blk(s, y_emb, condition, pos_img, pos_txt, None, transformer_options=transformer_options)
227230
s = F.silu(t_emb + s)
228231

232+
s = self._pre_pixel_blocks(s, **kwargs)
229233
s_cond = s.view(B * L, self.hidden_size)
230234
x_pixels = self.pixel_embedder(x, patch_size=self.patch_size)
231235
for blk in self.pixel_blocks:

comfy/ldm/pixeldit/pid.py

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,15 @@
1313
from .modules import precompute_freqs_cis_2d
1414

1515

16-
class SigmaAwareGatePerTokenPerDim(nn.Module):
16+
class SigmaAwareGate(nn.Module):
1717
"""gate = sigmoid(content_proj(cat[x, lq]) - exp(log_alpha) * sigma); out = x + gate * lq.
1818
1919
Trained init gives ~0.88 gate at sigma=0, ~0.05 at sigma=1.
2020
"""
2121

22-
def __init__(self, dim: int, dtype=None, device=None, operations=None):
22+
def __init__(self, dim: int, per_token: bool = False, dtype=None, device=None, operations=None):
2323
super().__init__()
24-
self.content_proj = operations.Linear(dim * 2, dim, dtype=dtype, device=device)
24+
self.content_proj = operations.Linear(dim * 2, 1 if per_token else dim, dtype=dtype, device=device)
2525
self.log_alpha = nn.Parameter(torch.empty((), dtype=dtype, device=device))
2626

2727
def forward(self, x: torch.Tensor, lq: torch.Tensor, sigma: torch.Tensor) -> torch.Tensor:
@@ -36,15 +36,15 @@ def forward(self, x: torch.Tensor, lq: torch.Tensor, sigma: torch.Tensor) -> tor
3636
class ResBlock(nn.Module):
3737
"""Pre-activation ResNet block: GN -> SiLU -> Conv -> GN -> SiLU -> Conv + skip."""
3838

39-
def __init__(self, channels: int, num_groups: int = 4, dtype=None, device=None, operations=None):
39+
def __init__(self, channels: int, num_groups: int = 4, conv_padding_mode: str = "zeros", dtype=None, device=None, operations=None):
4040
super().__init__()
4141
self.block = nn.Sequential(
4242
operations.GroupNorm(num_groups, channels, dtype=dtype, device=device),
4343
nn.SiLU(),
44-
operations.Conv2d(channels, channels, kernel_size=3, padding=1, dtype=dtype, device=device),
44+
operations.Conv2d(channels, channels, kernel_size=3, padding=1, padding_mode=conv_padding_mode, dtype=dtype, device=device),
4545
operations.GroupNorm(num_groups, channels, dtype=dtype, device=device),
4646
nn.SiLU(),
47-
operations.Conv2d(channels, channels, kernel_size=3, padding=1, dtype=dtype, device=device),
47+
operations.Conv2d(channels, channels, kernel_size=3, padding=1, padding_mode=conv_padding_mode, dtype=dtype, device=device),
4848
)
4949

5050
def forward(self, x: torch.Tensor) -> torch.Tensor:
@@ -62,9 +62,13 @@ def __init__(
6262
patch_size: int = 16,
6363
sr_scale: int = 4,
6464
latent_spatial_down_factor: int = 8,
65+
latent_unpatchify_factor: int = 1,
6566
num_res_blocks: int = 4,
6667
num_outputs: int = 7,
6768
interval: int = 2,
69+
conv_padding_mode: str = "zeros",
70+
gate_per_token: bool = False,
71+
pit_output: bool = False,
6872
dtype=None, device=None, operations=None,
6973
):
7074
super().__init__()
@@ -74,34 +78,38 @@ def __init__(
7478
self.patch_size = patch_size
7579
self.sr_scale = sr_scale
7680
self.latent_spatial_down_factor = latent_spatial_down_factor
81+
self.latent_unpatchify_factor = latent_unpatchify_factor
7782
self.num_outputs = num_outputs
7883
self.interval = interval
7984

80-
z_to_patch_ratio = (sr_scale * latent_spatial_down_factor) / patch_size
85+
effective_latent_channels = latent_channels // (latent_unpatchify_factor * latent_unpatchify_factor)
86+
effective_spatial_down_factor = latent_spatial_down_factor // latent_unpatchify_factor
87+
z_to_patch_ratio = (sr_scale * effective_spatial_down_factor) / patch_size
8188
self.z_to_patch_ratio = z_to_patch_ratio
8289
if z_to_patch_ratio >= 1:
8390
self.latent_fold_factor = 0
84-
latent_proj_in_ch = latent_channels
91+
latent_proj_in_ch = effective_latent_channels
8592
else:
8693
fold_factor = int(1 / z_to_patch_ratio)
8794
assert fold_factor * z_to_patch_ratio == 1.0
8895
self.latent_fold_factor = fold_factor
89-
latent_proj_in_ch = latent_channels * fold_factor * fold_factor
96+
latent_proj_in_ch = effective_latent_channels * fold_factor * fold_factor
9097

9198
layers = [
92-
operations.Conv2d(latent_proj_in_ch, hidden_dim, kernel_size=3, padding=1, dtype=dtype, device=device),
99+
operations.Conv2d(latent_proj_in_ch, hidden_dim, kernel_size=3, padding=1, padding_mode=conv_padding_mode, dtype=dtype, device=device),
93100
nn.SiLU(),
94-
operations.Conv2d(hidden_dim, hidden_dim, kernel_size=3, padding=1, dtype=dtype, device=device),
101+
operations.Conv2d(hidden_dim, hidden_dim, kernel_size=3, padding=1, padding_mode=conv_padding_mode, dtype=dtype, device=device),
95102
]
96103
for _ in range(num_res_blocks):
97-
layers.append(ResBlock(hidden_dim, dtype=dtype, device=device, operations=operations))
104+
layers.append(ResBlock(hidden_dim, conv_padding_mode=conv_padding_mode, dtype=dtype, device=device, operations=operations))
98105
self.latent_proj = nn.Sequential(*layers)
99106

100107
self.output_heads = nn.ModuleList(
101108
[operations.Linear(hidden_dim, out_dim, dtype=dtype, device=device) for _ in range(num_outputs)]
102109
)
110+
self.pit_head = operations.Linear(hidden_dim, out_dim, dtype=dtype, device=device) if pit_output else None
103111
self.gate_modules = nn.ModuleList(
104-
[SigmaAwareGatePerTokenPerDim(out_dim, dtype=dtype, device=device, operations=operations)
112+
[SigmaAwareGate(out_dim, per_token=gate_per_token, dtype=dtype, device=device, operations=operations)
105113
for _ in range(num_outputs)]
106114
)
107115

@@ -115,6 +123,11 @@ def gate(self, x: torch.Tensor, lq_feature: torch.Tensor, sigma: torch.Tensor, o
115123
return self.gate_modules[out_idx](x, lq_feature, sigma)
116124

117125
def _align_latent_to_patch_grid(self, lq_latent: torch.Tensor, pH: int, pW: int) -> torch.Tensor:
126+
f = self.latent_unpatchify_factor
127+
if f > 1:
128+
B, C, H, W = lq_latent.shape
129+
lq_latent = lq_latent.reshape(B, C // (f * f), f, f, H, W)
130+
lq_latent = lq_latent.permute(0, 1, 4, 2, 5, 3).reshape(B, C // (f * f), H * f, W * f)
118131
B, z_dim = lq_latent.shape[:2]
119132
if self.z_to_patch_ratio >= 1:
120133
if lq_latent.shape[2] != pH or lq_latent.shape[3] != pW:
@@ -134,7 +147,10 @@ def forward(self, lq_latent: torch.Tensor, target_pH: int, target_pW: int) -> Li
134147
feat = self._align_latent_to_patch_grid(lq_latent, target_pH, target_pW)
135148
B, C, H, W = feat.shape
136149
tokens = feat.permute(0, 2, 3, 1).contiguous().view(B, H * W, C)
137-
return [head(tokens) for head in self.output_heads]
150+
outputs = [head(tokens) for head in self.output_heads]
151+
if self.pit_head is not None:
152+
outputs.append(self.pit_head(tokens))
153+
return outputs
138154

139155

140156
class PidNet(PixDiT_T2I):
@@ -148,6 +164,10 @@ def __init__(
148164
lq_interval: int = 2,
149165
sr_scale: int = 4,
150166
latent_spatial_down_factor: int = 8,
167+
lq_latent_unpatchify_factor: int = 1,
168+
lq_conv_padding_mode: str = "zeros",
169+
lq_gate_per_token: bool = False,
170+
pit_lq_inject: bool = False,
151171
rope_ref_h: int = 1024, # NTK ref resolution in PIXEL units: 1024px / patch=16 -> grid_ref=64.
152172
rope_ref_w: int = 1024,
153173
image_model=None,
@@ -165,6 +185,8 @@ def _pit_rope_fn(head_dim, h, w, device=None, dtype=torch.float32, **rope_opts):
165185
for blk in self.pixel_blocks:
166186
blk._rope_fn = _pit_rope_fn
167187

188+
self.pit_lq_inject = pit_lq_inject
189+
168190
num_lq_outputs = (self.patch_depth + lq_interval - 1) // lq_interval
169191
self.lq_proj = LQProjection2D(
170192
latent_channels=lq_latent_channels,
@@ -173,13 +195,20 @@ def _pit_rope_fn(head_dim, h, w, device=None, dtype=torch.float32, **rope_opts):
173195
patch_size=self.patch_size,
174196
sr_scale=sr_scale,
175197
latent_spatial_down_factor=latent_spatial_down_factor,
198+
latent_unpatchify_factor=lq_latent_unpatchify_factor,
176199
num_res_blocks=lq_num_res_blocks,
177200
num_outputs=num_lq_outputs,
178201
interval=lq_interval,
202+
conv_padding_mode=lq_conv_padding_mode,
203+
gate_per_token=lq_gate_per_token,
204+
pit_output=pit_lq_inject,
179205
dtype=dtype,
180206
device=device,
181207
operations=operations,
182208
)
209+
self.pit_lq_gate = SigmaAwareGate(
210+
self.hidden_size, per_token=lq_gate_per_token, dtype=dtype, device=device, operations=operations
211+
) if pit_lq_inject else None
183212

184213
def _fetch_patch_pos(self, height, width, device, dtype, **rope_opts):
185214
return precompute_freqs_cis_2d(
@@ -197,6 +226,11 @@ def _pre_patch_block(self, s, i, pid_lq_features, pid_degrade_sigma, **kwargs):
197226
return s
198227
return self.lq_proj.gate(s, pid_lq_features[out_idx], pid_degrade_sigma, out_idx)
199228

229+
def _pre_pixel_blocks(self, s, pid_pit_lq_feature=None, pid_degrade_sigma=None, **kwargs):
230+
if pid_pit_lq_feature is None:
231+
return s
232+
return self.pit_lq_gate(s, pid_pit_lq_feature, pid_degrade_sigma)
233+
200234
def _forward(self, x, timesteps, context=None, attention_mask=None, transformer_options={}, lq_latent=None, degrade_sigma=None, **kwargs):
201235
if lq_latent is None:
202236
raise ValueError("PidNet requires lq_latent — attach via PiDConditioning")
@@ -216,12 +250,14 @@ def _forward(self, x, timesteps, context=None, attention_mask=None, transformer_
216250
degrade_sigma = degrade_sigma.expand(B).contiguous()
217251

218252
lq_features = self.lq_proj(lq_latent=lq_latent.to(x), target_pH=Hs, target_pW=Ws)
253+
pit_lq_feature = lq_features.pop() if self.pit_lq_inject else None
219254

220255
return super()._forward(
221256
x, timesteps,
222257
context=context, attention_mask=attention_mask,
223258
transformer_options=transformer_options,
224259
pid_lq_features=lq_features,
260+
pid_pit_lq_feature=pit_lq_feature,
225261
pid_degrade_sigma=degrade_sigma,
226262
**kwargs,
227263
)

comfy/model_detection.py

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -470,15 +470,46 @@ def detect_unet_config(state_dict, key_prefix, metadata=None):
470470
# PiD (Pixel Diffusion Decoder). Must check BEFORE plain PixelDiT_T2I.
471471
_lq_w_key = '{}lq_proj.latent_proj.0.weight'.format(key_prefix)
472472
if _lq_w_key in state_dict_keys:
473-
in_ch = int(state_dict[_lq_w_key].shape[1])
473+
latent_proj_in_channels = int(state_dict[_lq_w_key].shape[1])
474+
hidden_dim = int(state_dict[_lq_w_key].shape[0])
474475
_gate_prefix = '{}lq_proj.gate_modules.'.format(key_prefix)
475476
num_gates = len({k[len(_gate_prefix):].split('.')[0]
476477
for k in state_dict_keys if k.startswith(_gate_prefix)})
478+
pid_v1_5 = '{}lq_proj.pit_head.weight'.format(key_prefix) in state_dict_keys
477479
dit_config = {"image_model": "pid",
478-
"lq_latent_channels": in_ch,
479-
"latent_spatial_down_factor": 16 if in_ch >= 64 else 8}
480+
"lq_hidden_dim": hidden_dim}
480481
if num_gates > 0:
481482
dit_config["lq_interval"] = (14 + num_gates - 1) // num_gates
483+
if pid_v1_5:
484+
pid_v1_5_variants = {
485+
16: { # Flux and QwenImage
486+
"lq_latent_channels": 16,
487+
"latent_spatial_down_factor": 8,
488+
"lq_latent_unpatchify_factor": 1,
489+
},
490+
32: { # Flux2 after 2x latent unpatchify
491+
"lq_latent_channels": 128,
492+
"latent_spatial_down_factor": 16,
493+
"lq_latent_unpatchify_factor": 2,
494+
},
495+
}
496+
variant = pid_v1_5_variants.get(latent_proj_in_channels)
497+
if variant is None:
498+
raise ValueError(f"Unsupported PiD v1.5 latent projection with {latent_proj_in_channels} input channels")
499+
gate_weight = state_dict['{}lq_proj.gate_modules.0.content_proj.weight'.format(key_prefix)]
500+
dit_config.update(variant)
501+
dit_config.update({
502+
"lq_conv_padding_mode": "replicate",
503+
"lq_gate_per_token": gate_weight.shape[0] == 1,
504+
"pit_lq_inject": True,
505+
"rope_ref_h": 2048,
506+
"rope_ref_w": 2048,
507+
})
508+
else:
509+
dit_config.update({
510+
"lq_latent_channels": latent_proj_in_channels,
511+
"latent_spatial_down_factor": 16 if latent_proj_in_channels >= 64 else 8,
512+
})
482513
return dit_config
483514

484515
if '{}core.pixel_embedder.proj.weight'.format(key_prefix) in state_dict_keys: # PixelDiT T2I

tests-unit/comfy_test/model_detection_test.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,21 @@ def _make_seedvr2_3b_shared_mm_sd():
9797
}
9898

9999

100+
def _make_pid_v1_5_sd(latent_proj_channels=16):
101+
sd = {
102+
"pixel_embedder.proj.weight": torch.empty(16, 3, device="meta"),
103+
"lq_proj.latent_proj.0.weight": torch.empty(1024, latent_proj_channels, 3, 3, device="meta"),
104+
"lq_proj.pit_head.weight": torch.empty(1536, 1024, device="meta"),
105+
"lq_proj.gate_modules.0.content_proj.weight": torch.empty(1, 3072, device="meta"),
106+
"pixel_blocks.0.attn.q_norm.weight": torch.empty(72, device="meta"),
107+
"pixel_blocks.0.adaLN_modulation.0.weight": torch.empty(24576, 1536, device="meta"),
108+
"pixel_blocks.0.adaLN_modulation.0.bias": torch.empty(24576, device="meta"),
109+
}
110+
for i in range(7):
111+
sd[f"lq_proj.gate_modules.{i}.log_alpha"] = torch.empty((), device="meta")
112+
return sd
113+
114+
100115
def _add_model_diffusion_prefix(sd):
101116
return {f"model.diffusion_model.{k}": v for k, v in sd.items()}
102117

@@ -206,6 +221,43 @@ def test_seedvr2_model_match_accepts_full_checkpoint_prefix(self):
206221

207222
assert type(model_config_from_unet(sd, "model.diffusion_model.")).__name__ == "SeedVR2"
208223

224+
def test_pid_v1_5_detection(self):
225+
sd = _make_pid_v1_5_sd()
226+
unet_config = detect_unet_config(sd, "")
227+
228+
assert unet_config == {
229+
"image_model": "pid",
230+
"lq_latent_channels": 16,
231+
"lq_hidden_dim": 1024,
232+
"latent_spatial_down_factor": 8,
233+
"lq_interval": 2,
234+
"lq_latent_unpatchify_factor": 1,
235+
"lq_conv_padding_mode": "replicate",
236+
"lq_gate_per_token": True,
237+
"pit_lq_inject": True,
238+
"rope_ref_h": 2048,
239+
"rope_ref_w": 2048,
240+
}
241+
assert type(model_config_from_unet_config(unet_config, sd)).__name__ == "PiD"
242+
243+
def test_pid_v1_5_flux2_detection(self):
244+
unet_config = detect_unet_config(_make_pid_v1_5_sd(latent_proj_channels=32), "")
245+
246+
assert unet_config["lq_latent_channels"] == 128
247+
assert unet_config["latent_spatial_down_factor"] == 16
248+
assert unet_config["lq_latent_unpatchify_factor"] == 2
249+
250+
def test_pid_v1_5_pixel_adaln_conversion(self):
251+
sd = _make_pid_v1_5_sd()
252+
model_config = model_config_from_unet_config(detect_unet_config(sd, ""), sd)
253+
processed = model_config.process_unet_state_dict(sd)
254+
255+
assert processed["pixel_blocks.0.attn.q_norm.weight"].shape == (72,)
256+
assert processed["pixel_blocks.0.adaLN_modulation_msa.weight"].shape == (12288, 1536)
257+
assert processed["pixel_blocks.0.adaLN_modulation_mlp.weight"].shape == (12288, 1536)
258+
assert processed["pixel_blocks.0.adaLN_modulation_msa.bias"].shape == (12288,)
259+
assert processed["pixel_blocks.0.adaLN_modulation_mlp.bias"].shape == (12288,)
260+
209261
def test_unet_config_and_required_keys_combination_is_unique(self):
210262
"""Each model in the registry must have a unique combination of
211263
``unet_config`` and ``required_keys``. If two models share the same

0 commit comments

Comments
 (0)