darask0's picture
Claude Opus 5.5
Fix rapid-moss on ComfyUI with comfy-aimdo ("aimdo memory compile error")
c88f33b
Raw History Blame Contribute Delete
14.3 kB
"""Few-step MOSS-Image adapters (rapid-moss DMAD 4-step, lodestones 8-step DMD) for ComfyUI.
Works on native ComfyUI MOSS-Image models and the OpenMOSS HF loader.
An adapter is a folder in models/rapid_moss/ holding lora.safetensors, time_embedding.safetensors and
dataspace.safetensors. The node returns a patched MODEL plus the data-space SAMPLER and SIGMAS for
SamplerCustomAdvanced (use BasicGuider: no CFG, no negative prompt).
"""
import inspect
import math
import os
import comfy.patcher_extension
import comfy.samplers
import folder_paths
import torch
from safetensors.torch import load_file
from torch.nn import functional as F
FOLDER = 'rapid_moss'
folder_paths.add_model_folder_path(FOLDER, os.path.join(folder_paths.models_dir, FOLDER))
FILES = ('lora.safetensors', 'time_embedding.safetensors', 'dataspace.safetensors')
PROJ = ('q', 'k', 'v', 'o', 'ffn_up_gate', 'ffn_down')
TIME = ('time_embedding.0.weight', 'time_embedding.0.bias', 'time_embedding.2.weight', 'time_embedding.2.bias',
'shared_block_modulation.bias', 'video_head_modulation.bias')
DM = 'diffusion_model.'
WRAPPER_KEY = 'rapid_moss'
def _adapter_dirs():
found = {}
for root in folder_paths.get_folder_paths(FOLDER):
if os.path.isdir(root):
for name in sorted(os.listdir(root)):
path = os.path.join(root, name)
if name not in found and all(os.path.isfile(os.path.join(path, f)) for f in FILES):
found[name] = path
return found
class _DeviceCopy:
"""Tensors kept on the CPU, copied once to the device that first needs them."""
def __init__(self, *tensors):
self.cpu = tensors
self.device = None
self.copies = None
def get(self, device):
if device != self.device:
self.copies = [t.to(device) for t in self.cpu]
self.device = device
return self.copies
# ---- OpenMOSS HF loader: patch the modules of the diffusers transformer ----
class _FP32Linear(torch.nn.Module):
"""fp32 linear outside ComfyUI's weight management. `weight` is a plain attribute so the MOSS
forward (which casts its input to `.weight.dtype`) runs this path in fp32."""
def __init__(self, weight, bias):
super().__init__()
self.weight = weight
self.tensors = _DeviceCopy(weight, bias)
def forward(self, x):
weight, bias = self.tensors.get(x.device)
return F.linear(x.float(), weight, bias)
def _lora_forward(module, down, up, scale):
"""Unmerged fp32 LoRA: the delta is below bf16 rounding, so it must not be merged into the weight."""
class_forward = type(module).forward
tensors = _DeviceCopy(down, up)
def forward(x, *args, **kwargs):
out = class_forward(module, x, *args, **kwargs)
d, u = tensors.get(x.device)
return out + (((x.float() @ d.t()) @ u.t()) * scale).to(out.dtype)
return forward
def _patch_hf(patched, lora, time_path, strength):
patched.add_object_patch(DM + 'time_embedding', torch.nn.Sequential(
_FP32Linear(time_path['time_embedding.0.weight'].float(), time_path['time_embedding.0.bias'].float()),
torch.nn.SiLU(),
_FP32Linear(time_path['time_embedding.2.weight'].float(), time_path['time_embedding.2.bias'].float())))
for name in ('shared_block_modulation', 'video_head_modulation'):
weight = patched.get_model_object(DM + name).weight.detach().to('cpu', torch.float32)
patched.add_object_patch(DM + name, _FP32Linear(weight, time_path[name + '.bias'].float()))
for i in range(len(patched.get_model_object(DM + 'layers'))):
for proj in PROJ:
target = f'{DM}layers.{i}.proj.{proj}'
down = lora[target + '.lora_down.weight'].float()
up = lora[target + '.lora_up.weight'].float()
scale = strength * float(lora[target + '.alpha']) / down.shape[0]
patched.add_object_patch(target + '.forward', _lora_forward(patched.get_model_object(target), down, up, scale))
# ---- native ComfyUI MOSS-Image: the whole forward as a DIFFUSION_MODEL wrapper ----
def _timestep_embedding(timestep, dim=256):
"""The native sinusoidal embedding (timestep * 1000, cos | sin) in float64, never rounded to bf16."""
half = dim // 2
freqs = torch.exp(-math.log(10000) * torch.arange(half, dtype=torch.float64, device=timestep.device) / half)
phase = (timestep.double() * 1000.0)[:, None] * freqs
return torch.cat((phase.cos(), phase.sin()), dim=-1).float()
def _fp32_linear(module, x, bias):
"""The base modulation weight in fp32 with the adapter's bias (ComfyUI casts or loads the weight)."""
import comfy.ops
weight, base_bias, stream = comfy.ops.cast_bias_weight(module, x, dtype=torch.float32, offloadable=True)
out = F.linear(x, weight, bias)
comfy.ops.uncast_bias_weight(module, weight, base_bias, stream)
return out
def _lin(module, x, lora):
out = module(x)
down, up = lora
return out + F.linear(F.linear(x.to(down.dtype), down), up).to(out.dtype)
class _NativeForward:
"""MOSS-Image forward with the adapter: fp32 time path, modulation and residual stream, unmerged LoRA
(as rapid-moss trains and samples it). ComfyUI's memory compiler (comfy-aimdo) records the allocations of
each block and replays them; a tensor allocated in one block and freed in a later one aborts it
("aimdo memory compile error"). So, like the native blocks, every block updates a residual allocated before
the compiled region in place, and the adapter tensors reach the device before that region."""
def __init__(self, lora, time_path, strength, num_layers, lora_dtype=torch.bfloat16):
self.time = _DeviceCopy(*(time_path[k].float() for k in TIME))
self.lora = []
for i in range(num_layers):
tensors = []
for proj in PROJ:
prefix = f'{DM}layers.{i}.proj.{proj}'
down = lora[prefix + '.lora_down.weight']
scale = strength * float(lora[prefix + '.alpha']) / down.shape[0]
tensors += [down.to(lora_dtype), (lora[prefix + '.lora_up.weight'].float() * scale).to(lora_dtype)]
self.lora.append(_DeviceCopy(*tensors))
def __call__(self, executor, x, timestep, context, transformer_options={}, **kwargs):
import comfy.model_prefetch
from comfy.ldm.moss_image.model import modulated_rms_norm
dm = executor.class_obj
B, _, H, W = x.shape
dtype, device = x.dtype, x.device
transformer_options = transformer_options.copy()
if 'transformer_options' in inspect.signature(dm.rope).parameters:
pe = dm.rope(H, W, context.shape[1], device, transformer_options)
else:
pe = dm.rope(H, W, context.shape[1], device)
t0_w, t0_b, t2_w, t2_b, mod_bias, head_bias = self.time.get(device)
lora = [layer.get(device) for layer in self.lora]
vec = F.linear(F.silu(F.linear(_timestep_embedding(timestep), t0_w, t0_b)), t2_w, t2_b)
mod = _fp32_linear(dm.shared_block_modulation, vec, mod_bias).unflatten(-1, (6, -1)).unsqueeze(2).unbind(1)
head_shift, head_scale = _fp32_linear(dm.video_head_modulation, vec, head_bias).unflatten(-1, (2, -1)).unsqueeze(2).unbind(1)
txt = dm.text_embedder(context, pe[:, H * W:], transformer_options)
# fp32 residual, allocated outside the memory compiler region; the blocks update it in place
x = torch.cat((dm.video_embedder(x.flatten(2).transpose(1, 2)), txt), dim=1).float()
del txt
blocks_replace = transformer_options.get('patches_replace', {}).get('dit', {})
transformer_options['total_blocks'] = len(dm.layers)
transformer_options['block_type'] = 'single'
prefetch_queue = comfy.model_prefetch.make_prefetch_queue(list(dm.layers), device, transformer_options)
comfy.model_prefetch.malloc_graph_begin(device)
for i, block in enumerate(dm.layers):
comfy.model_prefetch.prefetch_queue_pop(prefetch_queue, device, block, dtype, malloc_scope='block')
transformer_options['block_index'] = i
if ('single_block', i) in blocks_replace:
def block_wrap(args, block=block, layer=lora[i]):
return {'img': self.block(block, layer, args['img'], args['mod'], args['pe'], dtype, args['transformer_options'])}
x = blocks_replace[('single_block', i)]({'img': x, 'vec': vec, 'mod': mod, 'pe': pe, 'transformer_options': transformer_options},
{'original_block': block_wrap})['img']
else:
self.block(block, lora[i], x, mod, pe, dtype, transformer_options)
comfy.model_prefetch.prefetch_queue_pop(prefetch_queue, device, None, malloc_scope='block')
comfy.model_prefetch.malloc_graph_end()
y = modulated_rms_norm(x[:, :H * W], dm.final_norm_video, head_scale, head_shift).to(dtype)
return dm.final_linear_video(y).transpose(1, 2).reshape(B, dm.latent_channels, H, W)
@staticmethod
def block(block, lora, x, mod, pe, dtype, transformer_options):
from comfy.ldm.moss_image.model import attention, modulated_rms_norm, situ
q_lora, k_lora, v_lora, o_lora, up_lora, down_lora = zip(lora[0::2], lora[1::2])
shift_a, scale_a, gate_a, shift_f, scale_f, gate_f = mod
y = modulated_rms_norm(x, block.norm1, scale_a, shift_a).to(dtype)
if block.fused_qkv:
q, k, v = block.proj['qkv'](y).chunk(3, dim=-1)
else:
q, k, v = block.proj['q'](y), block.proj['k'](y), block.proj['v'](y)
y_l = y.to(q_lora[0].dtype)
q, k, v = (out + F.linear(F.linear(y_l, down), up).to(out.dtype) for out, (down, up) in zip((q, k, v), (q_lora, k_lora, v_lora)))
out = attention(q, k, v, block.q_norm, block.k_norm, pe, block.num_heads, block.comfy_attention, transformer_options)
del q, k, v, y_l
x.addcmul_(_lin(block.proj['o'], out, o_lora), gate_a)
del out
y = modulated_rms_norm(x, block.norm2, scale_f, shift_f).to(dtype)
hidden, gate = _lin(block.proj['ffn_up_gate'], y, up_lora).chunk(2, dim=-1)
x.addcmul_(_lin(block.proj['ffn_down'], situ(hidden, gate), down_lora), gate_f)
return x
def _is_native(patched):
try:
from comfy.ldm.moss_image.model import MOSSImage
except ImportError:
return False
return isinstance(patched.get_model_object(DM[:-1]), MOSSImage)
@torch.no_grad()
def sample_dataspace(model, x, sigmas, extra_args=None, callback=None, disable=None, Q=None, codes=None):
"""x0 = 0; for r: x = z + Q x0, v = net(x, code_r), x0 = z - v (one noise z, no schedule, no CFG)."""
extra_args = {} if extra_args is None else extra_args
z = x.float() # sigmas[0] == 1, so the sampler receives pure noise
mix = Q.to(z.device, torch.float32)
x0 = torch.zeros_like(z)
s_in = z.new_ones([z.shape[0]])
for r in range(len(sigmas) - 1):
x_in = z + torch.einsum('oc,bchw->bohw', mix, x0)
s = float(codes[r]) / 1000.0 # flow model: timestep = sigma * 1000 = code
denoised = model(x_in, s * s_in, **extra_args).float() # = x_in - s * v
x0 = z - (x_in - denoised) / s
if callback is not None:
callback({'x': x_in, 'i': r, 'sigma': sigmas[r], 'sigma_hat': sigmas[r], 'denoised': x0})
return x0
class RapidMOSSAdapter:
@classmethod
def INPUT_TYPES(cls):
names = list(_adapter_dirs()) or ['(no adapter folder in models/rapid_moss)']
return {'required': {
'model': ('MODEL',),
'adapter': (names,),
'steps': ('INT', {'default': 4, 'min': 1, 'max': 8,
'tooltip': 'Network evaluations; up to the adapter step-code count (4 for rapid-moss, 8 for lodestones).'}),
'lora_strength': ('FLOAT', {'default': 1.0, 'min': 0.0, 'max': 2.0, 'step': 0.05}),
}}
RETURN_TYPES = ('MODEL', 'SAMPLER', 'SIGMAS')
RETURN_NAMES = ('model', 'sampler', 'sigmas')
FUNCTION = 'apply'
CATEGORY = 'MOSS-Image/rapid-moss'
DESCRIPTION = ('Few-step MOSS-Image: patches the MOSS-Image MODEL with a data-space adapter (unmerged LoRA, '
'fp32 time path) and returns the matching SAMPLER and SIGMAS for SamplerCustomAdvanced + BasicGuider.')
def apply(self, model, adapter, steps, lora_strength):
dirs = _adapter_dirs()
if adapter not in dirs:
raise FileNotFoundError(f'Put {", ".join(FILES)} in a folder under models/{FOLDER}/ (got {adapter!r}).')
path = dirs[adapter]
lora = load_file(os.path.join(path, 'lora.safetensors'))
time_path = load_file(os.path.join(path, 'time_embedding.safetensors'))
data = load_file(os.path.join(path, 'dataspace.safetensors'))
codes = data['step_codes'].float()
if steps > len(codes):
raise ValueError(f'{adapter} has {len(codes)} step codes; steps must be <= {len(codes)}.')
patched = model.clone()
base = patched.get_model_object(DM + 'shared_block_modulation')
if base.weight.dtype not in (torch.bfloat16, torch.float16, torch.float32):
raise ValueError('Load MOSS-Image with weight_dtype default/bf16/fp32; fp8 weights are not supported here.')
if _is_native(patched):
patched.remove_wrappers_with_key(comfy.patcher_extension.WrappersMP.DIFFUSION_MODEL, WRAPPER_KEY)
patched.add_wrapper_with_key(comfy.patcher_extension.WrappersMP.DIFFUSION_MODEL, WRAPPER_KEY, _NativeForward(
lora, time_path, lora_strength, len(patched.get_model_object(DM + 'layers'))))
else:
_patch_hf(patched, lora, time_path, lora_strength)
sampler = comfy.samplers.KSAMPLER(sample_dataspace, extra_options={'Q': data['latent_mix.Q'].float(), 'codes': codes})
sigmas = torch.cat((codes[:steps] / 1000.0, torch.zeros(1)))
return (patched, sampler, sigmas)
NODE_CLASS_MAPPINGS = {'RapidMOSSAdapter': RapidMOSSAdapter}
NODE_DISPLAY_NAME_MAPPINGS = {'RapidMOSSAdapter': 'MOSS-Image Few-Step Adapter (rapid-moss)'}