Files
LUCAS M MORGANandCursor d005858eb2 Add MetaView ComfyUI nodes with fp8 load parity and Qwen schedule.
Fix denoise divergence by synthesizing full_precision_matrix_mult comfy_quant markers (bf16 dequant matmul, matching the reference --no_fp8_mm path), keep schedule as a pack-local MetaViewSigmas node for SamplerCustom, and ship DA3 conditioning plus docs/workflow for 8-step CFG-1 novel-view synthesis.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-24 11:54:59 -05:00

707 lines
32 KiB
Python

"""MetaView DiT for ComfyUI.
A grafted Qwen-Image-Edit transformer for novel-view synthesis. The base transformer is
Qwen-Image-Edit (diffusers/comfy naming); the graft adds:
* ``_3D_in``: Linear(6144 -> 3072) projecting a DA3 3D feature into the transformer width.
* per-block ``prope_attn`` (MetaViewSelfAttention3D): a parallel self-attention branch that
jointly attends image tokens + 3D-feature tokens, both PRoPE positional-encoded; its output
is added to the main image attention output.
* PRoPE rotary encoding (Cameras as Relative Positional Encoding).
The math is ported faithfully from ``C:\\repos\\MetaView\\src\\MetaView_dit.py`` and
``MetaView_pipeline.py`` (``model_fn_MetaView``), but built on comfy ``operations`` so weights
stream/cast through comfy's native machinery and fp8 layers execute on comfy's quantized op path.
See ``docs/metaview_contract.md`` for the conditioning contract.
"""
import json
import logging
import torch
import torch.nn as nn
import torch.nn.functional as F
from einops import rearrange
import comfy.ops
import comfy.conds
import comfy.utils
import comfy.model_base
import comfy.model_patcher
import comfy.model_management
import comfy.supported_models_base
import comfy.latent_formats
from comfy.model_base import ModelType
from comfy.ldm.qwen_image.model import (
QwenImageTransformer2DModel,
QwenTimestepProjEmbeddings,
LastLayer,
)
try: # loaded as part of the node pack (package-relative)
from .metaview.prope import PropeDotProductAttention
from .metaview.qwen_rope import QwenEmbedRope, apply_rotary_emb_qwen, approximate_gelu
except ImportError: # loaded as a top-level module (standalone tests)
from metaview.prope import PropeDotProductAttention
from metaview.qwen_rope import QwenEmbedRope, apply_rotary_emb_qwen, approximate_gelu
try:
import folder_paths
except ImportError: # not available outside comfy (unit tests that don't need the node)
folder_paths = None
LOGGER = logging.getLogger(__name__)
# Fixed architecture constants for this artifact (metaview_fp8_scaled_v1).
_DIM = 3072
_NUM_HEADS = 24
_HEAD_DIM = 128
_NUM_LAYERS = 60
_JOINT_DIM = 3584 # Qwen2.5-VL text hidden dim (txt_norm / txt_in input)
_3D_DIM = 6144 # DA3 feature channel dim (_3D_in input)
_ADD_IN_DIM = 3072 # projected 3D-feature width
_PATCH = 2
_IN_CHANNELS = 64 # 16 latent channels * patch(2) * patch(2)
_OUT_CHANNELS = 16
# PRoPE dimension arrangement. The MetaView reference demo (src/inference.py:86,
# src/inference_lowvram.py) uses the 4-term arrangement [64, 20, 20, 24] with the
# DEPTH channel active (len == 4 -> depth term), and feeds the DA3 depth map into
# PRoPE precompute. NOTE: MetaViewPipeline's signature default [16, 56, 56] is
# overridden by the demo and is NOT what the released weights were trained with.
_PROPE_DIM_ARRANGE = [64, 20, 20, 24]
_PROPE_FREQ_BASE = 10000.0
# ---------------------------------------------------------------------------
# attention helper (matches diffsynth qwen_image_flash_attention non-flash path)
# ---------------------------------------------------------------------------
def _attention(q, k, v, num_heads, attn_mask=None):
# q,k,v: [b, heads, seq, head_dim]
x = F.scaled_dot_product_attention(q, k, v, attn_mask=attn_mask)
return rearrange(x, "b n s d -> b s (n d)", n=num_heads)
# ---------------------------------------------------------------------------
# feed forward (diffsynth QwenFeedForward: ApproximateGELU = x*sigmoid(1.702x))
# ---------------------------------------------------------------------------
class _ProjGELU(nn.Module):
"""FeedForward.net[0] — holds the ``.proj`` Linear then the sigmoid-GELU activation."""
def __init__(self, dim_in, dim_out, dtype, device, operations):
super().__init__()
self.proj = operations.Linear(dim_in, dim_out, bias=True, dtype=dtype, device=device)
def forward(self, x):
return approximate_gelu(self.proj(x))
class MetaViewFeedForward(nn.Module):
def __init__(self, dim, dim_out, dtype, device, operations):
super().__init__()
inner = dim * 4
self.net = nn.ModuleList([
_ProjGELU(dim, inner, dtype, device, operations), # net.0 (.proj)
nn.Dropout(0.0), # net.1
operations.Linear(inner, dim_out, bias=True, dtype=dtype, device=device), # net.2
])
def forward(self, x):
for m in self.net:
x = m(x)
return x
# ---------------------------------------------------------------------------
# main double-stream attention (diffsynth QwenDoubleStreamAttention)
# In MetaView's shipped config this branch is invoked with prope=None, so image tokens receive
# the standard Qwen 2D RoPE (via image_rotary_emb) exactly like base Qwen-Image-Edit.
# ---------------------------------------------------------------------------
class QwenDoubleStreamAttention(nn.Module):
def __init__(self, dim, num_heads, head_dim, dtype, device, operations):
super().__init__()
self.num_heads = num_heads
self.head_dim = head_dim
self.to_q = operations.Linear(dim, dim, dtype=dtype, device=device)
self.to_k = operations.Linear(dim, dim, dtype=dtype, device=device)
self.to_v = operations.Linear(dim, dim, dtype=dtype, device=device)
self.norm_q = operations.RMSNorm(head_dim, eps=1e-6, elementwise_affine=True, dtype=dtype, device=device)
self.norm_k = operations.RMSNorm(head_dim, eps=1e-6, elementwise_affine=True, dtype=dtype, device=device)
self.add_q_proj = operations.Linear(dim, dim, dtype=dtype, device=device)
self.add_k_proj = operations.Linear(dim, dim, dtype=dtype, device=device)
self.add_v_proj = operations.Linear(dim, dim, dtype=dtype, device=device)
self.norm_added_q = operations.RMSNorm(head_dim, eps=1e-6, elementwise_affine=True, dtype=dtype, device=device)
self.norm_added_k = operations.RMSNorm(head_dim, eps=1e-6, elementwise_affine=True, dtype=dtype, device=device)
self.to_out = nn.Sequential(operations.Linear(dim, dim, dtype=dtype, device=device))
self.to_add_out = operations.Linear(dim, dim, dtype=dtype, device=device)
def forward(self, image, text, image_rotary_emb=None, attention_mask=None, prope=None):
img_q, img_k, img_v = self.to_q(image), self.to_k(image), self.to_v(image)
txt_q, txt_k, txt_v = self.add_q_proj(text), self.add_k_proj(text), self.add_v_proj(text)
seq_txt = txt_q.shape[1]
img_q = rearrange(img_q, "b s (h d) -> b h s d", h=self.num_heads)
img_k = rearrange(img_k, "b s (h d) -> b h s d", h=self.num_heads)
img_v = rearrange(img_v, "b s (h d) -> b h s d", h=self.num_heads)
txt_q = rearrange(txt_q, "b s (h d) -> b h s d", h=self.num_heads)
txt_k = rearrange(txt_k, "b s (h d) -> b h s d", h=self.num_heads)
txt_v = rearrange(txt_v, "b s (h d) -> b h s d", h=self.num_heads)
img_q, img_k = self.norm_q(img_q), self.norm_k(img_k)
txt_q, txt_k = self.norm_added_q(txt_q), self.norm_added_k(txt_k)
if prope is not None and image_rotary_emb is not None:
_, txt_freqs = image_rotary_emb
txt_q = apply_rotary_emb_qwen(txt_q, txt_freqs)
txt_k = apply_rotary_emb_qwen(txt_k, txt_freqs)
img_q = prope._apply_to_q(img_q)
img_k = prope._apply_to_kv(img_k)
elif image_rotary_emb is not None:
img_freqs, txt_freqs = image_rotary_emb
img_q = apply_rotary_emb_qwen(img_q, img_freqs)
img_k = apply_rotary_emb_qwen(img_k, img_freqs)
txt_q = apply_rotary_emb_qwen(txt_q, txt_freqs)
txt_k = apply_rotary_emb_qwen(txt_k, txt_freqs)
joint_q = torch.cat([txt_q, img_q], dim=2)
joint_k = torch.cat([txt_k, img_k], dim=2)
joint_v = torch.cat([txt_v, img_v], dim=2)
out = _attention(joint_q, joint_k, joint_v, self.num_heads, attn_mask=attention_mask).to(joint_q.dtype)
txt_out = out[:, :seq_txt, :]
img_out = out[:, seq_txt:, :]
img_out = self.to_out[0](img_out)
txt_out = self.to_add_out(txt_out)
return img_out, txt_out
# ---------------------------------------------------------------------------
# grafted parallel 3D attention (diffsynth MetaViewSelfAttention3D)
# ---------------------------------------------------------------------------
class MetaViewSelfAttention3D(nn.Module):
def __init__(self, dim_a, dim_b, num_heads, head_dim, merge_3D, dtype, device, operations):
super().__init__()
self.merge_3D = merge_3D
self.num_heads = num_heads
self.head_dim = head_dim
self.to_q = operations.Linear(dim_a, dim_a, dtype=dtype, device=device)
self.to_k = operations.Linear(dim_a, dim_a, dtype=dtype, device=device)
self.to_v = operations.Linear(dim_a, dim_a, dtype=dtype, device=device)
self.to_q_3D = operations.Linear(dim_b, dim_a, dtype=dtype, device=device)
self.to_k_3D = operations.Linear(dim_b, dim_a, dtype=dtype, device=device)
self.to_v_3D = operations.Linear(dim_b, dim_a, dtype=dtype, device=device)
self.norm_q = operations.RMSNorm(head_dim, eps=1e-6, elementwise_affine=True, dtype=dtype, device=device)
self.norm_k = operations.RMSNorm(head_dim, eps=1e-6, elementwise_affine=True, dtype=dtype, device=device)
self.norm_added_q = operations.RMSNorm(head_dim, eps=1e-6, elementwise_affine=True, dtype=dtype, device=device)
self.norm_added_k = operations.RMSNorm(head_dim, eps=1e-6, elementwise_affine=True, dtype=dtype, device=device)
self.to_out = nn.Sequential(operations.Linear(dim_a, dim_a, dtype=dtype, device=device))
def forward(self, image, feat_3D, attention_mask=None, prope=None, add_prope=None):
img_q, img_k, img_v = self.to_q(image), self.to_k(image), self.to_v(image)
_3D_q, _3D_k, _3D_v = self.to_q_3D(feat_3D), self.to_k_3D(feat_3D), self.to_v_3D(feat_3D)
seq_img = img_q.shape[1]
img_q = rearrange(img_q, "b s (h d) -> b h s d", h=self.num_heads)
img_k = rearrange(img_k, "b s (h d) -> b h s d", h=self.num_heads)
img_v = rearrange(img_v, "b s (h d) -> b h s d", h=self.num_heads)
img_q, img_k = self.norm_q(img_q), self.norm_k(img_k)
_3D_q = rearrange(_3D_q, "b s (h d) -> b h s d", h=self.num_heads)
_3D_k = rearrange(_3D_k, "b s (h d) -> b h s d", h=self.num_heads)
_3D_v = rearrange(_3D_v, "b s (h d) -> b h s d", h=self.num_heads)
_3D_q, _3D_k = self.norm_added_q(_3D_q), self.norm_added_k(_3D_k)
if prope is not None:
img_q = prope._apply_to_q(img_q)
img_k = prope._apply_to_kv(img_k)
img_v = prope._apply_to_kv(img_v)
_3D_q = add_prope._apply_to_q(_3D_q)
_3D_k = add_prope._apply_to_kv(_3D_k)
_3D_v = add_prope._apply_to_kv(_3D_v)
joint_q = torch.cat([img_q, _3D_q], dim=2)
joint_k = torch.cat([img_k, _3D_k], dim=2)
joint_v = torch.cat([img_v, _3D_v], dim=2)
num_heads = img_q.shape[1]
out = _attention(joint_q, joint_k, joint_v, num_heads, attn_mask=attention_mask).to(img_q.dtype)
img_out = out[:, :seq_img, :]
_3D_out = out[:, seq_img:, :]
if prope is not None:
img_out = rearrange(img_out, "b s (n d) -> b n s d", n=num_heads)
img_out = prope._apply_to_o(img_out)
img_out = rearrange(img_out, "b n s d -> b s (n d)", n=num_heads)
img_out = self.to_out(img_out)
if self.merge_3D:
if add_prope is not None:
_3D_out = rearrange(_3D_out, "b s (n d) -> b n s d", n=num_heads)
_3D_out = add_prope._apply_to_o(_3D_out)
_3D_out = rearrange(_3D_out, "b n s d -> b s (n d)", n=num_heads)
_3D_out = self.to_out(_3D_out)
return img_out, _3D_out
return img_out, None
# ---------------------------------------------------------------------------
# grafted transformer block (diffsynth MetaViewTransformerBlock, merge_3D=True)
# ---------------------------------------------------------------------------
class MetaViewTransformerBlock(nn.Module):
def __init__(self, dim, num_heads, head_dim, add_in_dim, merge_3D, eps, dtype, device, operations):
super().__init__()
self.merge_3D = merge_3D
self.img_mod = nn.Sequential(
nn.SiLU(),
operations.Linear(dim, 6 * dim, bias=True, dtype=dtype, device=device),
)
self.img_norm1 = operations.LayerNorm(dim, elementwise_affine=False, eps=eps, dtype=dtype, device=device)
self.attn = QwenDoubleStreamAttention(dim, num_heads, head_dim, dtype, device, operations)
self.prope_attn = MetaViewSelfAttention3D(dim, add_in_dim, num_heads, head_dim, merge_3D, dtype, device, operations)
self.img_norm2 = operations.LayerNorm(dim, elementwise_affine=False, eps=eps, dtype=dtype, device=device)
self.img_mlp = MetaViewFeedForward(dim, dim, dtype, device, operations)
self.txt_mod = nn.Sequential(
nn.SiLU(),
operations.Linear(dim, 6 * dim, bias=True, dtype=dtype, device=device),
)
self.txt_norm1 = operations.LayerNorm(dim, elementwise_affine=False, eps=eps, dtype=dtype, device=device)
self.txt_norm2 = operations.LayerNorm(dim, elementwise_affine=False, eps=eps, dtype=dtype, device=device)
self.txt_mlp = MetaViewFeedForward(dim, dim, dtype, device, operations)
@staticmethod
def _modulate(x, mod_params):
shift, scale, gate = mod_params.chunk(3, dim=-1)
return x * (1 + scale.unsqueeze(1)) + shift.unsqueeze(1), gate.unsqueeze(1)
def forward(self, image, text, temb, image_rotary_emb=None, attention_mask=None,
prope=None, add_prope=None, add_attn=True, feat_3D=None):
img_mod_attn, img_mod_mlp = self.img_mod(temb).chunk(2, dim=-1)
txt_mod_attn, txt_mod_mlp = self.txt_mod(temb).chunk(2, dim=-1)
img_normed = self.img_norm1(image)
img_modulated, img_gate = self._modulate(img_normed, img_mod_attn)
txt_normed = self.txt_norm1(text)
txt_modulated, txt_gate = self._modulate(txt_normed, txt_mod_attn)
if self.merge_3D:
feat_3D_modulated = self.img_norm1(feat_3D)
else:
feat_3D_modulated = feat_3D
_3D_prope_out = None
if add_attn and prope is not None:
img_prope_out, _3D_prope_out = self.prope_attn(
image=img_modulated, feat_3D=feat_3D_modulated,
attention_mask=attention_mask, prope=prope, add_prope=add_prope,
)
img_attn_out, txt_attn_out = self.attn(
image=img_modulated, text=txt_modulated,
image_rotary_emb=image_rotary_emb, attention_mask=attention_mask, prope=None,
)
img_attn_out = img_attn_out + img_prope_out
else:
img_attn_out, txt_attn_out = self.attn(
image=img_modulated, text=txt_modulated,
image_rotary_emb=image_rotary_emb, attention_mask=attention_mask, prope=prope,
)
image = image + img_gate * img_attn_out
text = text + txt_gate * txt_attn_out
img_normed_2 = self.img_norm2(image)
img_modulated_2, img_gate_2 = self._modulate(img_normed_2, img_mod_mlp)
txt_normed_2 = self.txt_norm2(text)
txt_modulated_2, txt_gate_2 = self._modulate(txt_normed_2, txt_mod_mlp)
image = image + img_gate_2 * self.img_mlp(img_modulated_2)
text = text + txt_gate_2 * self.txt_mlp(txt_modulated_2)
if self.merge_3D:
feat_3D = feat_3D + _3D_prope_out
feat_3D = feat_3D + self.img_mlp(self.img_norm2(feat_3D))
return text, image, feat_3D
return text, image, feat_3D
# ---------------------------------------------------------------------------
# full grafted DiT
# ---------------------------------------------------------------------------
class MetaViewDiT(QwenImageTransformer2DModel):
"""Grafted Qwen-Image-Edit transformer. Subclasses comfy's QwenImageTransformer2DModel to
inherit its forward()/patcher-wrapper plumbing, but rebuilds the module tree with the
MetaView graft and overrides _forward with the MetaView denoise math."""
def __init__(self, image_model=None, dtype=None, device=None, operations=None, **kwargs):
nn.Module.__init__(self)
self.dtype = dtype
self.patch_size = _PATCH
self.in_channels = _IN_CHANNELS
self.out_channels = _OUT_CHANNELS
self.inner_dim = _DIM
self.num_heads = _NUM_HEADS
self.head_dim = _HEAD_DIM
self.merge_3D = True
# Text-stream RoPE (image stream is handled by PRoPE / Qwen rope via this too).
self.pos_embed = QwenEmbedRope(theta=10000, axes_dim=[16, 56, 56], scale_rope=True)
# Base Qwen-Image-Edit modules (names match the artifact keys exactly).
self.time_text_embed = QwenTimestepProjEmbeddings(
embedding_dim=self.inner_dim, pooled_projection_dim=768,
dtype=dtype, device=device, operations=operations)
self.txt_norm = operations.RMSNorm(_JOINT_DIM, eps=1e-6, dtype=dtype, device=device)
self.img_in = operations.Linear(_IN_CHANNELS, self.inner_dim, dtype=dtype, device=device)
self.txt_in = operations.Linear(_JOINT_DIM, self.inner_dim, dtype=dtype, device=device)
self.transformer_blocks = nn.ModuleList([
MetaViewTransformerBlock(
dim=self.inner_dim, num_heads=self.num_heads, head_dim=self.head_dim,
add_in_dim=_ADD_IN_DIM, merge_3D=True, eps=1e-6,
dtype=dtype, device=device, operations=operations)
for _ in range(_NUM_LAYERS)
])
self.norm_out = LastLayer(self.inner_dim, self.inner_dim, dtype=dtype, device=device, operations=operations)
self.proj_out = operations.Linear(self.inner_dim, _PATCH * _PATCH * self.out_channels, bias=True, dtype=dtype, device=device)
# Grafted 3D-feature input projection.
self._3D_in = operations.Linear(_3D_DIM, _ADD_IN_DIM, dtype=dtype, device=device)
# ---- helpers -----------------------------------------------------------
def _patchify(self, latent):
# latent [b, 16, H_lat, W_lat] -> tokens [b, (H_lat/2 * W_lat/2), 64]
H = latent.shape[2] // _PATCH
W = latent.shape[3] // _PATCH
return rearrange(latent, "b c (H P) (W Q) -> b (H W) (c P Q)", H=H, W=W, P=_PATCH, Q=_PATCH), H, W
def _build_prope(self, patches_x, patches_y, viewmats, ks, depth=None):
p = PropeDotProductAttention(
head_dim=self.head_dim, patches_x=patches_x, patches_y=patches_y,
image_width=patches_x * 16, image_height=patches_y * 16,
freq_base=_PROPE_FREQ_BASE, dim_arrange=_PROPE_DIM_ARRANGE,
).to(viewmats.device)
p._precompute_and_cache_apply_fns(viewmats, ks, depth)
return p
# ---- main forward ------------------------------------------------------
def _forward(self, x, timesteps, context, attention_mask=None, ref_latents=None,
additional_t_cond=None, transformer_options={}, control=None,
metaview=None, **kwargs):
orig_ndim = x.ndim
if orig_ndim == 5: # comfy passes [b, C, T=1, H, W] for image models
x = x[:, :, 0]
image, patches_y, patches_x = self._patchify(x)
image_seq_len = image.shape[1]
img_shapes = [(1, patches_y, patches_x)]
if ref_latents is not None and len(ref_latents) > 0:
edit = ref_latents[0]
if edit.ndim == 5:
edit = edit[:, :, 0]
edit_tokens, e_H, e_W = self._patchify(edit.to(x.dtype))
image = torch.cat([image, edit_tokens], dim=1)
img_shapes.append((1, e_H, e_W))
# text sequence lengths
if attention_mask is not None:
txt_seq_lens = attention_mask.sum(dim=1).tolist()
else:
txt_seq_lens = [context.shape[1]] * context.shape[0]
image = self.img_in(image)
temb = self.time_text_embed(timesteps, image, additional_t_cond)
text = self.txt_in(self.txt_norm(context))
image_rotary_emb = self.pos_embed(img_shapes, txt_seq_lens, device=x.device)
# ---- build PRoPE from cameras -------------------------------------
prope = None
add_prope = None
feat_3D = None
if metaview is not None and metaview.get("viewmats") is not None and metaview.get("ks") is not None:
viewmats = metaview["viewmats"]
ks = metaview["ks"]
if viewmats.ndim == 3:
viewmats = viewmats.unsqueeze(0)
if ks.ndim == 3:
ks = ks.unsqueeze(0)
viewmats = viewmats.to(device=x.device, dtype=torch.float32)
ks = ks.to(device=x.device, dtype=torch.float32)
# Depth enters PRoPE (dim_arrange has 4 terms -> depth channel active),
# bilinearly resized to the token grid, exactly like the reference
# (MetaView_pipeline.py model_fn: F.interpolate to (h//16, w//16)).
depth = metaview.get("depth")
if depth is not None:
if depth.ndim == 3: # [n_cams, H, W] -> [1, n_cams, H, W]
depth = depth.unsqueeze(0)
depth = depth.to(device=x.device, dtype=torch.float32)
depth = F.interpolate(depth, size=(patches_y, patches_x),
mode="bilinear", align_corners=False)
prope = self._build_prope(patches_x, patches_y, viewmats, ks, depth)
feat = metaview.get("feat3d")
if feat is not None:
if feat.ndim == 3:
feat = feat.unsqueeze(0)
feat = feat.to(device=x.device, dtype=image.dtype)
feat_3D = rearrange(feat, "b h w d -> b (h w) d")
feat_3D = self._3D_in(feat_3D)
# add_PRoPE uses the source camera only (index 1), incl. its depth
add_prope = self._build_prope(
patches_x, patches_y, viewmats[:, 1:2], ks[:, 1:2],
depth[:, 1:2] if depth is not None else None)
use_graft = prope is not None and feat_3D is not None
for block in self.transformer_blocks:
text, image, feat_3D = block(
image=image, text=text, temb=temb,
image_rotary_emb=image_rotary_emb, attention_mask=None,
prope=prope, add_prope=add_prope, add_attn=use_graft, feat_3D=feat_3D,
)
image = self.norm_out(image, temb)
image = self.proj_out(image)
image = image[:, :image_seq_len]
out = rearrange(image, "b (H W) (c P Q) -> b c (H P) (W Q)",
H=patches_y, W=patches_x, P=_PATCH, Q=_PATCH)
if orig_ndim == 5:
out = out.unsqueeze(2)
return out
# ---------------------------------------------------------------------------
# state-dict preparation: synthesize comfy_quant markers for fp8 layers
# ---------------------------------------------------------------------------
def prepare_metaview_state_dict(sd):
"""The artifact stores fp8 weights + ``<layer>.weight_scale`` companions but no per-layer
``comfy_quant`` markers. Comfy's modern quantized load path (MixedPrecisionOps) keys on the
marker, so synthesize one for every fp8 layer. Values are untouched (marker only).
``full_precision_matrix_mult: true`` matches Comfy-Org's scaled-fp8 Qwen convention and the
MetaView reference ``--no_fp8_mm`` path: dequant weight to bf16, run F.linear. Without it,
MixedPrecisionOps quantizes activations to fp8 every matmul and the denoise trajectory
diverges into noise relative to the working reference.
"""
marker = torch.tensor(
list(json.dumps({
"format": "float8_e4m3fn",
"full_precision_matrix_mult": True,
}).encode("utf-8")),
dtype=torch.uint8,
)
fp8_layers = []
for k, v in sd.items():
if k.endswith(".weight") and getattr(v, "dtype", None) == torch.float8_e4m3fn:
layer = k[: -len(".weight")]
if f"{layer}.weight_scale" in sd:
fp8_layers.append(layer)
for layer in fp8_layers:
sd[f"{layer}.comfy_quant"] = marker.clone()
return sd, len(fp8_layers)
# ---------------------------------------------------------------------------
# comfy model config + BaseModel subclass
# ---------------------------------------------------------------------------
# Default mu for the training pixel budget 960x528 → patch grid 60x33 = 1980 tokens.
# diffsynth FlowMatchScheduler._calculate_shift_qwen_image(1980) ≈ 0.5869.
_QWEN_DEFAULT_MU = (0.9 - 0.5) / (8192 - 256) * 1980 + (0.5 - (0.9 - 0.5) / (8192 - 256) * 256)
_QWEN_SHIFT_TERMINAL = 0.02
def calculate_qwen_image_mu(image_seq_len, base_seq_len=256, max_seq_len=8192,
base_shift=0.5, max_shift=0.9):
"""Match diffsynth FlowMatchScheduler._calculate_shift_qwen_image."""
m = (max_shift - base_shift) / (max_seq_len - base_seq_len)
b = base_shift - m * base_seq_len
return float(image_seq_len * m + b)
def qwen_image_sigmas(steps, mu=_QWEN_DEFAULT_MU, shift_terminal=_QWEN_SHIFT_TERMINAL):
"""Exact diffsynth Qwen-Image FlowMatch schedule (incl. terminal shift to 0.02).
Pack-local helper — used by the MetaViewSigmas node. Does not register into
Comfy's global scheduler table.
"""
import math
sigmas = torch.linspace(1.0, 0.0, int(steps) + 1)[:-1]
sigmas = math.exp(mu) / (math.exp(mu) + (1.0 / sigmas - 1.0))
one_minus_z = 1.0 - sigmas
scale_factor = one_minus_z[-1] / (1.0 - shift_terminal)
sigmas = 1.0 - (one_minus_z / scale_factor)
return torch.cat([sigmas, sigmas.new_zeros(1)])
class MetaViewModel(comfy.model_base.QwenImage):
"""Reuses QwenImage's ref-latent/attention-mask extra_conds and adds the MetaView bundle."""
def __init__(self, model_config, model_type=ModelType.FLUX, device=None):
# ModelType.FLUX, exactly like comfy's native model_base.QwenImage: the sampler feeds
# sigma in [0,1] and QwenTimestepProjEmbeddings' Timesteps(scale=1000) applies the
# x1000 internally (comfy/ldm/qwen_image/model.py). The reference MetaView_pipeline is
# equivalent: it divides its [0,1000] timesteps by 1000 before its own scale=1000
# embedder. ModelType.FLOW would double-scale the timestep and break denoising.
#
# Pair with MetaViewSigmas (exact Qwen-Image schedule) + stock SamplerCustom; do not
# rely on KSampler's named schedulers for the terminal-shift schedule.
comfy.model_base.BaseModel.__init__(
self, model_config, model_type, device=device, unet_model=MetaViewDiT)
self.memory_usage_factor_conds = ("ref_latents",)
def extra_conds(self, **kwargs):
out = super().extra_conds(**kwargs)
mv = {}
for src, dst in (("metaview_viewmats", "viewmats"),
("metaview_ks", "ks"),
("metaview_feat3d", "feat3d"),
("metaview_depth", "depth")):
v = kwargs.get(src, None)
if v is not None:
mv[dst] = v
if mv:
# CONDConstant (a plain dict) bypasses _apply_model's automatic bf16 cast so the
# float32 camera matrices survive intact.
out["metaview"] = comfy.conds.CONDConstant(mv)
return out
class MetaViewConfig(comfy.supported_models_base.BASE):
unet_config = {"image_model": "metaview"}
unet_extra_config = {}
# Default mu for 960x528; MetaViewSigmas recomputes from width/height when provided.
sampling_settings = {"multiplier": 1.0, "shift": _QWEN_DEFAULT_MU}
memory_usage_factor = 1.8
latent_format = comfy.latent_formats.Wan21
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 MetaViewModel(self, device=device)
def _metaview_unet_config():
return {"image_model": "metaview"}
def load_metaview_model(ckpt_path):
"""Load the MetaView artifact and return a ComfyUI ModelPatcher (MODEL)."""
sd = comfy.utils.load_torch_file(ckpt_path, safe_load=True)
sd, n_fp8 = prepare_metaview_state_dict(sd)
LOGGER.info("MetaView: synthesized comfy_quant markers for %d fp8 layers", n_fp8)
parameters = comfy.utils.calculate_parameters(sd)
load_device = comfy.model_management.get_torch_device()
offload_device = comfy.model_management.unet_offload_device()
model_config = MetaViewConfig(_metaview_unet_config())
model_config.quant_config = {"mixed_ops": True} # routes pick_operations -> mixed_precision_ops
unet_dtype = comfy.model_management.unet_dtype(
model_params=parameters,
supported_dtypes=model_config.supported_inference_dtypes,
weight_dtype=None,
)
manual_cast_dtype = comfy.model_management.unet_manual_cast(
None, load_device, model_config.supported_inference_dtypes)
if manual_cast_dtype is None:
manual_cast_dtype = torch.bfloat16 # ensure a real compute dtype for MixedPrecisionOps
model_config.set_inference_dtype(unet_dtype, manual_cast_dtype)
model = model_config.get_model(sd, "")
model_patcher = comfy.model_patcher.ModelPatcher(
model, load_device=load_device, offload_device=offload_device)
if not comfy.model_management.is_device_cpu(offload_device):
model.to(offload_device)
model.load_model_weights(sd, "")
left = list(sd.keys())
if left:
LOGGER.warning("MetaView: %d leftover state-dict keys after load: %s", len(left), left[:8])
return model_patcher
# ---------------------------------------------------------------------------
# loader + schedule nodes (pack-local; no Comfy globals mutated)
# ---------------------------------------------------------------------------
class MetaViewModelLoader:
@classmethod
def INPUT_TYPES(cls):
models = folder_paths.get_filename_list("diffusion_models") if folder_paths else []
return {"required": {"model_name": (models,)}}
RETURN_TYPES = ("MODEL",)
RETURN_NAMES = ("model",)
FUNCTION = "load"
CATEGORY = "EnviralDesign/MetaView"
TITLE = "MetaView Model Loader"
def load(self, model_name):
path = folder_paths.get_full_path("diffusion_models", model_name)
if path is None:
raise FileNotFoundError(f"MetaView model '{model_name}' not found in diffusion_models")
return (load_metaview_model(path),)
class MetaViewSigmas:
"""Emit the exact Qwen-Image / MetaView FlowMatch sigma schedule as SIGMAS.
Wire into stock ``SamplerCustom`` / ``SamplerCustomAdvanced`` (not KSampler's
named-scheduler dropdown). Pass the MetaView3DConditioning LATENT so mu is
derived from the real generation grid; width/height widgets are fallbacks.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"steps": ("INT", {"default": 8, "min": 1, "max": 10000}),
},
"optional": {
"latent": ("LATENT", {"tooltip": "Prefer this: mu from latent HxW (*8 → pixels)."}),
"width": ("INT", {"default": 960, "min": 16, "max": 8192, "step": 16}),
"height": ("INT", {"default": 528, "min": 16, "max": 8192, "step": 16}),
},
}
RETURN_TYPES = ("SIGMAS",)
RETURN_NAMES = ("sigmas",)
FUNCTION = "build"
CATEGORY = "EnviralDesign/MetaView"
TITLE = "MetaView Sigmas"
def build(self, steps, latent=None, width=960, height=528):
if latent is not None and "samples" in latent:
# Wan/Qwen VAE: spatial /8. Patch grid for mu is /16 → latent_h/2 * latent_w/2.
samples = latent["samples"]
lat_h, lat_w = int(samples.shape[-2]), int(samples.shape[-1])
height, width = lat_h * 8, lat_w * 8
mu = calculate_qwen_image_mu((height // 16) * (width // 16))
return (qwen_image_sigmas(steps, mu=mu),)
NODE_CLASS_MAPPINGS = {
"MetaViewModelLoader": MetaViewModelLoader,
"MetaViewSigmas": MetaViewSigmas,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"MetaViewModelLoader": "MetaView Model Loader",
"MetaViewSigmas": "MetaView Sigmas",
}