File size: 17,634 Bytes
bc50ea0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 | """Schedule native MiniMax H3 reference-video influence during denoising.
H3 packs native Ref2VA picture/video/audio blocks into the same transformer
sequence as the target streams. A reference video is therefore available to
every DiT block at every sampling evaluation; it is not reduced to a pose map.
That is useful for exact motion transfer, but it can also carry source color,
background, framing, and texture farther into the render than desired.
This experimental MODEL patch leaves the full reference video present during
early denoising, then applies a half-cosine fade to the value rows belonging to
native ``video`` and ``video_audio`` reference blocks. Native still-picture
rows, Qwen presentation tokens, reference audio, continuation guides, and the
target streams are unchanged.
The patch mutates H3's freshly cloned V tensor in place before the active
attention backend consumes it. It therefore adds no sequence-sized mask and
no second attention pass. An existing optimized-attention override (including
SolAttn or Comfy Kitchen) remains the executor after this small value gate.
"""
from __future__ import annotations
import logging
import math
from typing import Any, Iterable
import torch
_LOG = logging.getLogger("minimax_h3_context_loop.reference_video_fade")
_WRAPPER_KEY = "h3_context_loop_reference_video_fade"
_MODEL_OPTION_MARKER = "h3_context_loop_reference_video_fade_recipe"
_STATE_MARKER = "h3_context_loop_reference_video_fade_state"
REFERENCE_VIDEO_FADE_PRESETS = {
"full": (1.0, 1.0),
"balanced": (0.67, 0.20),
"freer": (0.50, 0.15),
"early_only": (0.50, 0.0),
}
def _schedule_values(sigmas: Any) -> tuple[float, ...]:
"""Return finite non-negative scheduler levels in descending order."""
if torch.is_tensor(sigmas):
values: Iterable[Any] = sigmas.detach().float().reshape(-1).cpu()
else:
values = sigmas or ()
normalized = []
for value in values:
number = float(value)
if math.isfinite(number) and number >= 0.0:
normalized.append(number)
return tuple(sorted(set(normalized), reverse=True))
def schedule_progress(current_sigma: float, sigmas: Any) -> float | None:
"""Map an absolute sigma onto normalized progress through a full schedule."""
schedule = _schedule_values(sigmas)
if len(schedule) < 2:
return None
current = float(current_sigma)
if not math.isfinite(current):
return None
if current >= schedule[0]:
return 0.0
if current <= schedule[-1]:
return 1.0
denominator = float(len(schedule) - 1)
for index, (high, low) in enumerate(zip(schedule, schedule[1:])):
if high >= current >= low:
width = high - low
fraction = 0.0 if width <= 0.0 else (high - current) / width
return max(0.0, min(1.0, (index + fraction) / denominator))
return None
def reference_strength(
progress: float,
fade_start: float,
end_strength: float,
) -> float:
"""Half-cosine reference strength from full influence to the end value."""
start = float(fade_start)
end = float(end_strength)
if not 0.0 <= start <= 1.0:
raise ValueError("reference_video_fade: fade_start must be in [0, 1].")
if not 0.0 <= end <= 1.0:
raise ValueError(
"reference_video_fade: end_strength must be in [0, 1].")
position = max(0.0, min(1.0, float(progress)))
if position <= start or start >= 1.0:
return 1.0
u = (position - start) / (1.0 - start)
return end + (1.0 - end) * 0.5 * (1.0 + math.cos(math.pi * u))
def reference_video_ranges(
payload: Any,
) -> tuple[tuple[tuple[int, int], ...], int]:
"""Resolve packed row ranges for native video refs, excluding pictures."""
if not isinstance(payload, dict):
return (), 0
layout = payload.get("layout")
segments = tuple(getattr(layout, "segments", ()) or ())
if not segments:
return (), 0
refs = tuple(payload.get("refs") or ())
visual_refs = tuple(
ref for ref in refs
if isinstance(ref, dict)
and str(ref.get("kind") or "") in ("image", "video", "video_audio")
)
visual_segments = tuple(
(int(start), int(stop))
for start, stop, kind in segments
if str(kind) == "ref_img"
)
# A mismatch means an unknown core/fork changed row packing. Refuse to
# guess: fading a picture or target range would be worse than a no-op.
if len(visual_refs) != len(visual_segments):
return (), int(segments[-1][1])
ranges = tuple(
segment for ref, segment in zip(visual_refs, visual_segments)
if str(ref.get("kind") or "") in ("video", "video_audio")
)
return ranges, int(segments[-1][1])
class _ReferenceVideoFadeState:
"""Per-cloned-model schedule and packed-reference attention state."""
def __init__(
self,
fade_start: float,
end_strength: float,
schedule_override: Any = None,
):
# Validate eagerly, including the full-strength boundary case.
reference_strength(1.0, fade_start, end_strength)
self.fade_start = float(fade_start)
self.end_strength = float(end_strength)
self.schedule_override = _schedule_values(schedule_override)
self.current_strength = 1.0
self.current_progress = 0.0
self.current_sigma: float | None = None
self.reference_ranges: tuple[tuple[int, int], ...] = ()
self.sequence_length = 0
self.gated_attention_calls = 0
self._warned_schedule = False
self._warned_layout = False
self._reported_active = False
def _update(
self,
timestep: Any,
transformer_options: dict[str, Any],
payload: Any,
) -> None:
timestep_value = float(
torch.as_tensor(timestep).detach().float().reshape(-1)[0])
# H3 BaseModel supplies model_sampling.timestep(sigma) = sigma * 1000.
sigma = timestep_value / 1000.0
schedule = (
self.schedule_override
or _schedule_values(transformer_options.get("sample_sigmas", ()))
or _schedule_values(transformer_options.get("sigmas", ()))
)
progress = schedule_progress(sigma, schedule)
if progress is None:
# Normal Comfy sampling exposes sample_sigmas. This fallback keeps
# an unusual custom sampler usable while making its limitation
# visible instead of silently restarting a split schedule.
progress = max(0.0, min(1.0, 1.0 - sigma))
if not self._warned_schedule:
_LOG.warning(
"H3 Reference Video Fade received no usable sigma "
"schedule; falling back to 1-sigma progress. Connect the "
"original full_sigmas for split or custom sampling.")
self._warned_schedule = True
ranges, sequence_length = reference_video_ranges(payload)
has_video_ref = (
isinstance(payload, dict)
and any(
isinstance(ref, dict)
and str(ref.get("kind") or "") in ("video", "video_audio")
for ref in (payload.get("refs") or ()))
)
if not ranges and has_video_ref:
if not self._warned_layout:
_LOG.warning(
"H3 Reference Video Fade found no safely identifiable "
"native video-reference rows. The patch is a no-op for "
"this conditioning payload.")
self._warned_layout = True
self.current_sigma = sigma
self.current_progress = progress
self.current_strength = reference_strength(
progress, self.fade_start, self.end_strength)
self.reference_ranges = ranges
self.sequence_length = sequence_length
if ranges and not self._reported_active:
_LOG.info(
"H3 Reference Video Fade active on %d native video block(s); "
"full through %.0f%% of denoising, then cosine fade to %.0f%%",
len(ranges), self.fade_start * 100.0,
self.end_strength * 100.0)
self._reported_active = True
def diffusion_model_wrapper(
self,
executor,
x,
timestep,
context,
transformer_options=None,
minimax_payload=None,
**kwargs,
):
options = transformer_options if isinstance(
transformer_options, dict) else {}
self._update(timestep, options, minimax_payload)
if minimax_payload is not None:
kwargs["minimax_payload"] = minimax_payload
return executor(
x, timestep, context, transformer_options=options, **kwargs)
def make_attention_override(self, previous_override=None):
state = self
def override(
original,
q,
k,
v,
heads,
mask=None,
attn_precision=None,
skip_reshape=False,
skip_output_reshape=False,
**kwargs,
):
strength = float(state.current_strength)
ranges = state.reference_ranges
sequence_length = int(state.sequence_length)
if (
strength < 1.0 - 1e-7
and ranges
and skip_reshape
and torch.is_tensor(q)
and torch.is_tensor(k)
and torch.is_tensor(v)
and q.ndim == k.ndim == v.ndim == 4
and int(q.shape[-2]) == sequence_length
and int(k.shape[-2]) == sequence_length
and int(v.shape[-2]) == sequence_length
):
# H3 creates V with v.clone() immediately before attention, so
# these small slices are single-owner scratch memory. In-place
# scaling avoids another sequence-sized allocation per block.
for start, stop in ranges:
v[..., int(start):int(stop), :].mul_(strength)
state.gated_attention_calls += 1
call_kwargs = dict(
mask=mask,
attn_precision=attn_precision,
skip_reshape=skip_reshape,
skip_output_reshape=skip_output_reshape,
**kwargs,
)
if previous_override is not None:
return previous_override(
original, q, k, v, heads, **call_kwargs)
return original(q, k, v, heads, **call_kwargs)
return override
def resolve_reference_video_fade_preset(
preset: str,
custom_fade_start: float,
custom_end_strength: float,
) -> tuple[float, float]:
name = str(preset or "balanced")
if name == "custom":
values = (float(custom_fade_start), float(custom_end_strength))
elif name in REFERENCE_VIDEO_FADE_PRESETS:
values = REFERENCE_VIDEO_FADE_PRESETS[name]
else:
raise ValueError(
"Unknown H3 Reference Video Fade preset %r." % name)
reference_strength(1.0, values[0], values[1])
return values
def install_reference_video_fade_model(
model: Any,
fade_start: float,
end_strength: float,
schedule_override: Any = None,
):
"""Clone one H3 model and install the schedule-aware reference gate."""
reference_strength(1.0, fade_start, end_strength)
if float(fade_start) >= 1.0 and float(end_strength) >= 1.0:
return model
if model is None or not callable(getattr(model, "clone", None)):
raise ValueError(
"H3 Reference Video Fade requires a ComfyUI MODEL input.")
inner = getattr(model, "model", None)
model_type = str(getattr(getattr(inner, "model_type", None), "name", ""))
if model_type != "FLOW_AV" and inner.__class__.__name__ != "MiniMaxH3":
raise ValueError(
"H3 Reference Video Fade only supports MiniMax H3 AV models.")
patched = model.clone()
options = getattr(patched, "model_options", None)
if not isinstance(options, dict):
raise ValueError(
"H3 Reference Video Fade received a MODEL without model options.")
if _MODEL_OPTION_MARKER in options:
raise ValueError(
"H3 Reference Video Fade is already installed on this MODEL. "
"Use one fade patch per model branch.")
if not callable(getattr(patched, "add_wrapper_with_key", None)):
raise RuntimeError(
"H3 Reference Video Fade requires current ComfyUI model wrappers.")
from comfy.patcher_extension import WrappersMP
transformer_options = dict(options.get("transformer_options") or {})
previous_override = transformer_options.get(
"optimized_attention_override")
state = _ReferenceVideoFadeState(
fade_start,
end_strength,
schedule_override=schedule_override,
)
transformer_options["optimized_attention_override"] = (
state.make_attention_override(previous_override))
options["transformer_options"] = transformer_options
patched.add_wrapper_with_key(
WrappersMP.DIFFUSION_MODEL,
_WRAPPER_KEY,
state.diffusion_model_wrapper,
)
options[_MODEL_OPTION_MARKER] = {
"version": 1,
"fade_start": float(fade_start),
"end_strength": float(end_strength),
"full_schedule_values": len(state.schedule_override),
"scope": "native_video_and_video_audio_value_rows",
}
options[_STATE_MARKER] = state
_LOG.info(
"H3 Reference Video Fade installed: start %.3f, end %.3f, %s; "
"existing attention override %s",
float(fade_start), float(end_strength),
("%d full-schedule values" % len(state.schedule_override)
if state.schedule_override else "sampler-provided schedule"),
"chained" if previous_override is not None else "not present",
)
return patched
class MiniMaxH3ReferenceVideoFadeModelPatch:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"model": ("MODEL", {
"tooltip": "MiniMax H3 AV MODEL. Place this after any "
"backend/model patches and route its output "
"through every sampler stage."}),
"preset": ([
"full", "balanced", "freer", "early_only", "custom",
], {
"default": "balanced",
"tooltip": "full: unchanged. balanced: full through 67%, "
"fade to 20%. freer: full through 50%, fade to "
"15%. early_only: full through 50%, fade to "
"zero. custom uses the two expert widgets."}),
"custom_fade_start": ("FLOAT", {
"default": 0.67, "min": 0.0, "max": 1.0,
"step": 0.01,
"tooltip": "Custom only. Fraction of the COMPLETE "
"denoising schedule that keeps native video "
"references at full strength."}),
"custom_end_strength": ("FLOAT", {
"default": 0.20, "min": 0.0, "max": 1.0,
"step": 0.01,
"tooltip": "Custom only. Native video-reference value "
"strength at the end of denoising."}),
},
"optional": {
"full_sigmas": ("SIGMAS", {
"tooltip": "Original COMPLETE sigma schedule before any "
"split. Required for split-sigma sampling so "
"stage two continues the same fade instead of "
"starting a new local curve. Use the same "
"schedule on every switched model branch."}),
},
}
RETURN_TYPES = ("MODEL",)
RETURN_NAMES = ("model",)
OUTPUT_TOOLTIPS = (
"H3 MODEL with an opt-in native reference-video denoising fade. "
"Still-picture refs, Qwen presentation, reference audio, continuation "
"guides, and target streams remain unchanged.",
)
FUNCTION = "patch"
CATEGORY = "conditioning/minimax/context_loop/experimental"
DESCRIPTION = (
"Experimental low-memory Ref2VA freedom control. H3 still receives "
"the complete native reference video, but its attention value "
"contribution fades late in denoising. This targets every native "
"video/video+audio reference block and leaves still pictures intact. "
"It composes with an existing SolAttn or Comfy Kitchen attention "
"override. For sigma splits, connect the original full schedule."
)
def patch(
self,
model,
preset,
custom_fade_start,
custom_end_strength,
full_sigmas=None,
):
fade_start, end_strength = resolve_reference_video_fade_preset(
preset, custom_fade_start, custom_end_strength)
return (install_reference_video_fade_model(
model,
fade_start,
end_strength,
schedule_override=full_sigmas,
),)
NODE_CLASS_MAPPINGS = {
"MiniMaxH3ReferenceVideoFadeModelPatch": (
MiniMaxH3ReferenceVideoFadeModelPatch),
}
NODE_DISPLAY_NAME_MAPPINGS = {
"MiniMaxH3ReferenceVideoFadeModelPatch": (
"MiniMax H3 Reference Video Fade (Experimental)"),
}
|