Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
155 lines
7.1 KiB
Python
155 lines
7.1 KiB
Python
# Copyright (c) 2026 — ComfyUI-BerniniR. Apache-2.0.
|
|
"""Loop de muestreo de Bernini-R (port de GEN_Wanx22.sample).
|
|
|
|
Reúne: scheduler UniPC, ensamblado de combos de stream con source_id, switch
|
|
dual-expert por VALOR de timestep, los 7 modos de guía y el offload secuencial.
|
|
"""
|
|
from typing import List, Optional
|
|
|
|
import torch
|
|
|
|
from . import constants as C
|
|
from . import latents as L
|
|
from .model import BerniniRenderer, Stream
|
|
from .guidance import MomentumBuffer, normalized_guidance, normalized_guidance_chain
|
|
|
|
|
|
def build_scheduler(num_inference_steps, shift=C.SHIFT, use_unipc=True,
|
|
base_scheduler_dir=None, device="cpu"):
|
|
"""UniPCMultistepScheduler con flow_shift = shift (=3.0 efectivo)."""
|
|
from diffusers import UniPCMultistepScheduler
|
|
if base_scheduler_dir:
|
|
sch = UniPCMultistepScheduler.from_pretrained(base_scheduler_dir, flow_shift=shift)
|
|
else:
|
|
sch = UniPCMultistepScheduler(prediction_type="flow_prediction",
|
|
use_flow_sigmas=True, flow_shift=shift)
|
|
sch.set_timesteps(num_inference_steps, device=device)
|
|
return sch
|
|
|
|
|
|
class BerniniSampler:
|
|
def __init__(self, renderer: BerniniRenderer, guidance_mode: str,
|
|
params: dict, offload_experts: bool = True):
|
|
self.r = renderer
|
|
self.mode = guidance_mode
|
|
self.p = {**C.DEFAULTS, **params}
|
|
self.offload = offload_experts
|
|
|
|
# -- offload / block-swap: colocación del experto activo en GPU ------------
|
|
def _activate(self, expert, device):
|
|
# gestiona la colocación cuando hay offload O block-swap (block_swap>0 implica
|
|
# mover solo lo residente; el inactivo va full-CPU). Sin ninguno de los dos, el
|
|
# loader ya dejó ambos expertos en GPU -> nada que hacer.
|
|
bs = getattr(expert, "block_swap", 0)
|
|
if self.r.low is None or not (self.offload or bs):
|
|
return
|
|
other = self.r.low if expert is self.r.high else self.r.high
|
|
if other is not None:
|
|
other.to_idle()
|
|
torch.cuda.empty_cache()
|
|
expert.to_active(device)
|
|
|
|
def _apg_sigma(self, t_idx):
|
|
# sigmas[t_idx] = nivel de ruido al INICIO del paso actual. Equivale a
|
|
# la lógica step_index de Bernini (tras t_idx steps, step_index==t_idx),
|
|
# pero sin la ambigüedad de leer step_index antes de scheduler.step().
|
|
return self.scheduler.sigmas[t_idx].to(torch.float64)
|
|
|
|
@torch.no_grad()
|
|
def sample(self, video_latents: List[torch.Tensor], image_latents: List[torch.Tensor],
|
|
text_pos: torch.Tensor, text_neg: torch.Tensor,
|
|
target_shape, device, seed: int = 0,
|
|
base_scheduler_dir: Optional[str] = None) -> torch.Tensor:
|
|
p = self.p
|
|
self.scheduler = build_scheduler(p["num_inference_steps"], shift=C.SHIFT,
|
|
base_scheduler_dir=base_scheduler_dir, device=device)
|
|
timesteps = self.scheduler.timesteps.to(device)
|
|
|
|
gen = torch.Generator(device=device).manual_seed(int(seed))
|
|
noisy = L.make_noise(target_shape, device=device, dtype=torch.float32, generator=gen) # packed
|
|
|
|
# --- streams de condición (constantes en el tiempo) ------------------
|
|
sid = 1
|
|
v_cond, vi_cond = [], []
|
|
for idx, vid in enumerate(video_latents or []):
|
|
st = Stream(vid, sid); sid += 1
|
|
if idx == 0:
|
|
v_cond.append(st)
|
|
vi_cond.append(st)
|
|
i_cond = []
|
|
sid_img = 1
|
|
for img in (image_latents or []):
|
|
vi_cond.append(Stream(img, sid)); sid += 1
|
|
i_cond.append(Stream(img, sid_img)); sid_img += 1
|
|
|
|
# --- omegas (mutables: se escalan al cambiar a low-noise) ------------
|
|
oV, oI, oTI = float(p["omega_V"]), float(p["omega_I"]), float(p["omega_TI"])
|
|
eta = float(p["eta"]); momentum = float(p["momentum"])
|
|
nt = p["norm_threshold"]
|
|
nt = list(nt) if isinstance(nt, (list, tuple)) else [nt, nt, nt]
|
|
while len(nt) < 3:
|
|
nt.append(nt[-1])
|
|
mb1 = MomentumBuffer(momentum); mb2 = MomentumBuffer(momentum); mb = MomentumBuffer(momentum)
|
|
|
|
switched = False
|
|
boundary = self.r.boundary_timestep()
|
|
|
|
for t_idx, t in enumerate(timesteps):
|
|
tv = float(t)
|
|
expert = self.r.expert_for_timestep(tv)
|
|
if (not switched) and tv < boundary and self.r.low is not None:
|
|
expert = self.r.low
|
|
oV *= float(p["omega_scale"]); oI *= float(p["omega_scale"]); oTI *= float(p["omega_scale"])
|
|
switched = True
|
|
self._activate(expert, device)
|
|
t_tensor = t.reshape(1).to(device)
|
|
|
|
noisy_spatial = L.to_spatial(noisy, target_shape).to(expert.dtype)
|
|
target = Stream(noisy_spatial, 0, is_target=True)
|
|
none_c = [target]
|
|
V_c = v_cond + [target]
|
|
I_c = i_cond + [target]
|
|
VI_c = vi_cond + [target]
|
|
|
|
def fwd(streams, text):
|
|
return expert.forward_streams(streams, text, t_tensor).float()
|
|
|
|
mode = self.mode
|
|
if mode == "rv2v":
|
|
eu = fwd(none_c, text_neg); eV = fwd(V_c, text_neg)
|
|
eVI = fwd(VI_c, text_neg); eVTI = fwd(VI_c, text_pos)
|
|
noise_pred = eu + oV * (eV - eu) + oI * (eVI - eV) + oTI * (eVTI - eVI)
|
|
elif mode == "v2v":
|
|
eu = fwd(VI_c, text_neg); eVTI = fwd(VI_c, text_pos)
|
|
noise_pred = eu + oTI * (eVTI - eu)
|
|
elif mode == "v2v_chain":
|
|
eu = fwd(none_c, text_neg); eV = fwd(V_c, text_neg); eVTI = fwd(VI_c, text_pos)
|
|
noise_pred = eu + oV * (eV - eu) + oTI * (eVTI - eV)
|
|
elif mode == "t2v":
|
|
eu = fwd(none_c, text_neg); eT = fwd(none_c, text_pos)
|
|
noise_pred = eu + oTI * (eT - eu)
|
|
elif mode in ("r2v_apg", "v2v_apg", "t2v_apg"):
|
|
sigma = self._apg_sigma(t_idx)
|
|
noisy_s = L.to_spatial(noisy, target_shape).double()
|
|
|
|
def to_x(eps_packed):
|
|
return noisy_s - sigma * L.to_spatial(eps_packed, target_shape).double()
|
|
|
|
if mode == "r2v_apg":
|
|
eu = fwd(none_c, text_neg); eI = fwd(I_c, text_neg); eTI = fwd(I_c, text_pos)
|
|
x_guided = normalized_guidance_chain(
|
|
to_x(eu), [to_x(eI), to_x(eTI)], [oI, oTI], [mb1, mb2], eta, [nt[0], nt[1]])
|
|
elif mode == "v2v_apg":
|
|
eu = fwd(VI_c, text_neg); eVTI = fwd(VI_c, text_pos)
|
|
x_guided = normalized_guidance(to_x(eVTI), to_x(eu), oTI, mb, eta, nt[0])
|
|
else: # t2v_apg
|
|
eu = fwd(none_c, text_neg); eT = fwd(none_c, text_pos)
|
|
x_guided = normalized_guidance(to_x(eT), to_x(eu), oTI, mb, eta, nt[0])
|
|
noise_pred = L.to_packed(((noisy_s - x_guided) / sigma).to(noisy.dtype), target_shape)
|
|
else:
|
|
raise ValueError(f"guidance_mode desconocido: {mode}")
|
|
|
|
noisy = self.scheduler.step(noise_pred.to(noisy.dtype), t, noisy, return_dict=False)[0]
|
|
|
|
return L.to_spatial(noisy, target_shape) # latente final espacial [1,16,T,H,W]
|