diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a37986d0..0786f4eb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -119,6 +119,7 @@ Plus quite a lot more, see full [changelog](https://github.com/vladmandic/automa - torchsde: handle obsolete dependency - upscaler: avoid unnecessary multi-pass - ideogram4: fix callback + - xyzgrid: fix seed/prompt/negative handling, thanks @QuintusAntonius ## Update for 2026-08-07 diff --git a/TODO.md b/TODO.md index 29e7d692b..5442387b0 100644 --- a/TODO.md +++ b/TODO.md @@ -3,7 +3,8 @@ ## Short-term - Update LTX wiki, @CalamitousFelicitousness -- Lora: new handler, @CalamitousFelicitousness +- LoRA: new handler, @CalamitousFelicitousness +- LoRA: native loader for MiniMax-H3 - Productize benchmark tool, @CalamitousFelicitousness - Inpaint: https://discord.com/channels/1101998836328697867/1130536562422186044/1506850651035144322, @vladmandic - Control tab verify overrides handling, @vladmandic @@ -33,9 +34,7 @@ - Video models: use Networks/Reference instead of custom - UI Lite vs Expert mode - Expand custom VAE support -- Refactor: remove obsolete code: - - Remove `olive-ai` - +- Remove obsolete code: `olive-ai` ### OnHold diff --git a/modules/lora/extra_networks_lora.py b/modules/lora/extra_networks_lora.py index bc07d61d0..35605e95e 100644 --- a/modules/lora/extra_networks_lora.py +++ b/modules/lora/extra_networks_lora.py @@ -226,7 +226,7 @@ class ExtraNetworkLora(extra_networks.ExtraNetwork): if debug: import sys fn = f'{sys._getframe(2).f_code.co_name}:{sys._getframe(1).f_code.co_name}' # pylint: disable=protected-access - debug_log(f'Network load: type=LoRA include={include} exclude={exclude} method={load_method} reason={load_reason} requested={requested} fn={fn}') + debug_log(f'Network load: type=LoRA include={include} exclude={exclude} method={load_method} reason="{load_reason}" requested={requested} fn={fn}') if load_method == 'diffusers': has_changed, reason = self.changed(requested) @@ -267,7 +267,7 @@ class ExtraNetworkLora(extra_networks.ExtraNetwork): prompt(p) if has_changed and len(include) == 0: # print only once actual_method = 'native' if any(len(n.modules) > 0 for n in l.loaded_networks) else load_method - log.info(f'Network load: type=LoRA networks={[n.name for n in l.loaded_networks]} load={load_method}({load_reason}) method={actual_method} mode={"fuse" if shared.opts.lora_fuse_native else "backup"} te={te_multipliers} unet={unet_multipliers} time={l.timer.summary} reason={reason}') + log.info(f'Network load: type=LoRA networks={[n.name for n in l.loaded_networks]} load={load_method}({load_reason}) method={actual_method} mode={"fuse" if shared.opts.lora_fuse_native else "backup"} te={te_multipliers} unet={unet_multipliers} time={l.timer.summary} reason="{reason}"') def deactivate(self, p, force=False): if len(lora_diffusers.diffuser_loaded) > 0 and (shared.opts.lora_force_reload or force): diff --git a/modules/minimax/minimax_chunking.py b/modules/minimax/minimax_chunking.py new file mode 100644 index 000000000..0b7c055d2 --- /dev/null +++ b/modules/minimax/minimax_chunking.py @@ -0,0 +1,78 @@ +from contextlib import contextmanager +import torch +import torch.nn.functional as F + + +orig_sdpa = F.scaled_dot_product_attention + + +def safe_slice_mask(mask, start, end): + if mask is None: + return None + ndim = mask.ndim + if ndim == 2: + return mask[start:end, :] + elif ndim == 3: + return mask[:, start:end, :] + elif ndim >= 4: + return mask[..., start:end, :] + return mask + + +def chunked_sdpa(query, key, value, attn_mask=None, dropout_p=0.0, is_causal=False, scale=None, chunk_size=512, **kwargs): + """ + Drop-in replacement for F.scaled_dot_product_attention + """ + N_q = query.shape[2] + if (chunk_size == 0) or (N_q <= chunk_size): # fallback if disabled or sequence length is already smaller than chunk size + return orig_sdpa( + query, key, value, + attn_mask=attn_mask, + dropout_p=dropout_p, + is_causal=is_causal, + scale=scale, + **kwargs, + ) + out_chunks = [] + for start in range(0, N_q, chunk_size): # process query sequence in chunks against all Keys/Values + end = min(start + chunk_size, N_q) + q_chunk = query[:, :, start:end, :] + chunk_mask = safe_slice_mask(attn_mask, start, end) + out_chunk = orig_sdpa( + q_chunk, key, value, + attn_mask=chunk_mask, + dropout_p=dropout_p, + is_causal=is_causal, + scale=scale, + **kwargs, + ) + out_chunks.append(out_chunk) + return torch.cat(out_chunks, dim=2) + + +@contextmanager +def minimax_attention(chunk_size=0): + """ + Context manager to safely monkey-patch PyTorch's SDPA function + Value 0 bypasses the patch entirely and uses native SDPA + Values are 64-2048 aligned to multiples of 64. Lower values reduce VRAM usage at the cost of speed + """ + global orig_sdpa # pylint: disable=global-statement + orig_sdpa = F.scaled_dot_product_attention + chunk_size = max(64, (chunk_size // 64) * 64) # sanitize and align chunk_size to a multiple of 64 (minimum 64) + + def patched_sdpa(query, key, value, attn_mask=None, dropout_p=0.0, is_causal=False, scale=None, **kwargs): + return chunked_sdpa( + query, key, value, + attn_mask=attn_mask, + dropout_p=dropout_p, + is_causal=is_causal, + scale=scale, + chunk_size=chunk_size, + **kwargs, + ) + F.scaled_dot_product_attention = patched_sdpa + try: + yield + finally: + F.scaled_dot_product_attention = orig_sdpa diff --git a/modules/minimax/minimax_video.py b/modules/minimax/minimax_video.py index bbdf78470..50701bcf8 100644 --- a/modules/minimax/minimax_video.py +++ b/modules/minimax/minimax_video.py @@ -1,3 +1,4 @@ +import os import time from PIL import Image import numpy as np @@ -137,7 +138,15 @@ def generate(task_id, _ui_state, p.task_args.update(task_args) _processed: processing.Processed = scripts_manager.scripts_video.run(p, *args) - processed = processing.process_images(p) + + if os.environ.get("SD_MINIMAX_CHUNK", None) is not None: + from modules.minimax.minimax_chunking import minimax_attention + chunk_size = int(os.environ.get("SD_MINIMAX_CHUNK", 0)) + log.debug(f'Video: engine="{engine}" model="{model}" chunking=True size={chunk_size}') + with minimax_attention(chunk_size=chunk_size): + processed = processing.process_images(p) + else: + processed = processing.process_images(p) sd_models.offload_ondemand(shared.sd_model, reason='finish', force=True) # force offload all loaded modules to cpu devices.torch_gc(force=True) # free gpu memory before saving video diff --git a/scripts/xyz/xyz_grid_shared.py b/scripts/xyz/xyz_grid_shared.py index fd441143d..b90d37848 100644 --- a/scripts/xyz/xyz_grid_shared.py +++ b/scripts/xyz/xyz_grid_shared.py @@ -40,12 +40,20 @@ def apply_task_args(p, x, xs): def apply_processing(p, x, xs): for section in x.split(';'): - k, v = section.split('=') + k, v = section.split('=', 1) k, v = k.strip(), v.strip() if v.replace('.','',1).isdigit(): v = float(v) if '.' in v else int(v) found = 'existing' if hasattr(p, k) else 'new' setattr(p, k, v) + if k == 'prompt': + p.all_prompts = None + elif k == 'negative_prompt': + p.all_negative_prompts = None + elif k == 'seed': + p.all_seeds = None + elif k == 'subseed': + p.all_subseeds = None log.debug(f'XYZ grid apply processing-arg: type={found} {k}={type(v)}:{v} ') diff --git a/wiki b/wiki index b974e5084..eff72c57f 160000 --- a/wiki +++ b/wiki @@ -1 +1 @@ -Subproject commit b974e50847dccceb55154d1834ac0b40e18c5487 +Subproject commit eff72c57fe723d43129721e33b244d824e6dc674