diff --git a/CHANGELOG.md b/CHANGELOG.md index e3416144b..0786f4eb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Change Log for SD.Next -## Highlights for 2026-08-23 +## Highlights for 2026-08-24 Time for a new release, this is a larger one! Main focus is improving video workflows which also brings full support for new [MiniMax H3](https://vladmandic.github.io/sdnext-docs/MiniMax) and [LTXVideo-2.5](https://vladmandic.github.io/sdnext-docs/LTX) @@ -17,7 +17,7 @@ Plus quite a lot more, see full [changelog](https://github.com/vladmandic/automa [Home](https://vladmandic.github.io/sdnext/) | [ChangeLog](https://github.com/vladmandic/automatic/blob/master/CHANGELOG.md) | [Docs](https://vladmandic.github.io/sdnext-docs/) | [Discord](https://discord.com/invite/sd-next-federal-batch-inspectors-1101998836328697867) | [Sponsor](https://github.com/sponsors/vladmandic) -## Details for 2026-08-23 +## Details for 2026-08-24 - **Models** - [MiniMax H3](https://huggingface.co/MiniMaxAI/MiniMax-H3) available in *base* and *ref* variants @@ -118,6 +118,8 @@ Plus quite a lot more, see full [changelog](https://github.com/vladmandic/automa - krea2: fallback to base pipeline/transformer for nunchaku-lite - 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/extensions-builtin/sdnq b/extensions-builtin/sdnq index 738613a62..b99d9af15 160000 --- a/extensions-builtin/sdnq +++ b/extensions-builtin/sdnq @@ -1 +1 @@ -Subproject commit 738613a62cfa278b9b6312dd3755879c3671ee62 +Subproject commit b99d9af158a81fdd713dcbe31c9eb36f2332e54c diff --git a/modules/api/process.py b/modules/api/process.py index 0bed6557b..19b712ad6 100644 --- a/modules/api/process.py +++ b/modules/api/process.py @@ -219,10 +219,13 @@ class APIProcess: seed = req.seed or -1 seed = processing_helpers.get_fixed_seed(seed) prompt = '' - from modules.scripts_manager import scripts_txt2img + from modules.scripts_manager import scripts_control default_model = 'google/gemma-3-4b-it' if req.type == 'image' else 'google/gemma-3-1b-it' model = default_model if req.model is None or len(req.model) < 4 else req.model - instance = [s for s in scripts_txt2img.scripts if 'prompt_enhance.py' in s.filename][0] + instance = [s for s in scripts_control.scripts if 'prompt_enhance_ext.py' in s.filename] + if len(instance) == 0: + raise HTTPException(status_code=500, detail="Prompt enhancement script not found") + instance = instance[0] prompt = instance.enhance( model=model, prompt=req.prompt, diff --git a/modules/attention/backends/sdnq.py b/modules/attention/backends/sdnq.py index 61fc14bbf..8ae3b86df 100644 --- a/modules/attention/backends/sdnq.py +++ b/modules/attention/backends/sdnq.py @@ -11,18 +11,19 @@ def prepare(platform: Platform, original): # pylint: disable=unused-argument 'smooth_k': shared.opts.sdnq_attention_smooth_k, 'use_hadamard': shared.opts.sdnq_attention_use_hadamard, 'hadamard_group_size': shared.opts.sdnq_attention_hadamard_group_size, + 'quantize_fp32': shared.opts.sdnq_attention_quantize_fp32, 'use_fp16_accum': shared.opts.sdnq_attention_use_fp16_accum, } def call(query, key, value, attn_mask, dropout_p, is_causal, scale, enable_gqa): # pylint: disable=unused-argument return sdnq_triton_atten(query=query, key=key, value=value, attn_mask=attn_mask, is_causal=is_causal, scale=scale, enable_gqa=enable_gqa, **options) - log.debug(f'Torch attention: type="SDNQ attention" matmul={options["matmul_dtype"]}:{options["pv_matmul_dtype"]} smooth={options["smooth_k"]} hadamard={options["use_hadamard"]} fp16_accum={options["use_fp16_accum"]}') + log.debug(f'Torch attention: type="SDNQ attention" matmul={options["matmul_dtype"]}:{options["pv_matmul_dtype"]} smooth={options["smooth_k"]} hadamard={options["use_hadamard"]} quantize_fp32={options["quantize_fp32"]} fp16_accum={options["use_fp16_accum"]}') return call backend = AttentionBackend( name='sdnq', label='SDNQ attention', priority=60, prepare=prepare, constraints=Constraints(min_tokens=32, min_long_side=512, min_heads=2), # sequences of 512 or fewer are text encoders, single-head calls the vae - options=('sdnq_attention_matmul_type', 'sdnq_attention_pv_matmul_type', 'sdnq_attention_smooth_k', 'sdnq_attention_use_hadamard', 'sdnq_attention_hadamard_group_size', 'sdnq_attention_use_fp16_accum'), + options=('sdnq_attention_matmul_type', 'sdnq_attention_pv_matmul_type', 'sdnq_attention_smooth_k', 'sdnq_attention_use_hadamard', 'sdnq_attention_hadamard_group_size', 'sdnq_attention_quantize_fp32', 'sdnq_attention_use_fp16_accum'), ) 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_ui.py b/modules/minimax/minimax_ui.py index bd03d2c7c..314628b06 100644 --- a/modules/minimax/minimax_ui.py +++ b/modules/minimax/minimax_ui.py @@ -26,7 +26,7 @@ def create_ui(prompt, _negative, styles, overrides, script_inputs, mp4_fps, mp4_ width, height = ui_sections.create_resolution_inputs('minimax', default_width=1024, default_height=576, step=32) with gr.Row(): steps = gr.Slider(minimum=2, maximum=100, step=1, label="MiniMax Steps", elem_id='minimax_steps', value=30) - frames = gr.Slider(label='MiniMax Frames', minimum=22, maximum=345, step=17, value=107, elem_id='minimax_frames') + frames = gr.Slider(label='MiniMax Frames', minimum=22, maximum=362, step=17, value=124, elem_id='minimax_frames') with gr.Row(): video_shift = gr.Slider(minimum=8.0, maximum=16.0, step=0.1, label="MiniMax Video Shift", elem_id='minimax_video_shift', value=12) audio_shift = gr.Slider(minimum=1.5, maximum=6.0, step=0.1, label="MiniMax Audio Shift", elem_id='minimax_audio_shift', value=3) 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/modules/processing.py b/modules/processing.py index 0e919cd23..743a973f5 100644 --- a/modules/processing.py +++ b/modules/processing.py @@ -247,6 +247,10 @@ def process_images(p: StableDiffusionProcessing) -> Processed | None: if k == 'sd_vae': sd_vae.reload_vae_weights() timer.process.record('post') + + if os.environ.get('SD_UNLOAD_MODEL', None) is not None: + sd_models.unload_model_weights() + return results @@ -424,10 +428,11 @@ def print_stats(): log.debug(f'Processed: memory={memstats.memory_stats()}') if devices.triton_ok: - from modules.timer_sdnq import update_sdnq_attention_timers - update_sdnq_attention_timers() + # from modules.timer_sdnq import update_sdnq_attention_timers + # update_sdnq_attention_timers() if timer.autotune.get_total() > 0.1: - log.debug(f'Processed: autotune={timer.autotune.dct(min_time=0)}') + log.debug(f'Processed: autotune={timer.autotune.dct(min_time=0, no_total=True)}') + timer.autotune.reset() from modules.sd_models_compile import update_compile_times update_compile_times() diff --git a/modules/processing_vae.py b/modules/processing_vae.py index bd80c4d1f..778407fd6 100644 --- a/modules/processing_vae.py +++ b/modules/processing_vae.py @@ -141,7 +141,10 @@ def full_vae_decode(latents, model): log_debug(f'VAE config: {model.vae.config}') try: with devices.inference_context(): - decoded = model.vae.decode(latents, return_dict=False)[0] + decoded = model.vae.decode(latents, return_dict=False) + if decoded is None or len(decoded) == 0: + raise RuntimeError('returned no data') + decoded = decoded[0] except Exception as e: log.error(f'VAE decode: {e}') if 'out of memory' not in str(e) and 'no data' not in str(e): diff --git a/modules/sd_hijack_triton.py b/modules/sd_hijack_triton.py index da4190298..28bb6530a 100644 --- a/modules/sd_hijack_triton.py +++ b/modules/sd_hijack_triton.py @@ -1,9 +1,12 @@ +import time import math +from typing import Any from modules.logger import log, get_console +from modules.timer import autotune installed = False -status = {'session': None, 'pending': None, 'reported': set()} +status: dict[str, Any] = {'session': None, 'pending': None, 'reported': set(), 'run_id': 0} slow_compile_seconds = 1.0 @@ -47,19 +50,43 @@ def shape_key(key): successive sweeps of the same kernel are different shapes, not a repeating loop.""" if key is None: return 'unknown' - text = ','.join(str(k) for k in key) if isinstance(key, (tuple, list)) else str(key) + if isinstance(key, (tuple, list)): + parts = [] + for k in key: + if isinstance(k, str) and k.startswith('torch.'): + continue + parts.append(str(k)) + if len(parts) > 6: + text = '...' + ','.join(parts[-6:]) + else: + text = ','.join(parts) + else: + text = str(key) return text if len(text) <= 64 else text[:61] + '...' -def start_progress(name: str, total: int): +def start_progress(name: str, total: int, shape: str | None = None): """Console bar for the sweep, matching how model and file loading report elsewhere. Returns (progress, task) or (None, None) when there is no console to draw on.""" console = get_console() if console is None: return None, None import rich.progress as rp - progress = rp.Progress(rp.TextColumn('[cyan]Autotune'), rp.BarColumn(), rp.TaskProgressColumn(), rp.TimeRemainingColumn(), rp.TimeElapsedColumn(), rp.TextColumn('[yellow]{task.description}'), console=console, transient=True) - task = progress.add_task(description=f'kernel={name}', total=total) + progress = rp.Progress( + rp.TextColumn('[cyan]Autotune'), + rp.BarColumn(), + rp.TaskProgressColumn(), + rp.TextColumn('[green]{task.completed}/{task.total}'), + rp.TimeRemainingColumn(), + rp.TimeElapsedColumn(), + rp.TextColumn('[yellow]{task.description}'), + rp.TextColumn('[blue]{task.fields[shape]}'), + console=console, + transient=True, + ) + task = progress.add_task(description=f'kernel={name}', total=total, shape=(f'shape={shape}' if shape is not None else '')) + progress.ts = time.time() + progress.kernel = name progress.start() return progress, task @@ -69,55 +96,42 @@ def stop_progress(session): task = session.get('task', None) if session is not None else None if progress is not None: try: - progress.remove_task(task) progress.stop() except Exception as e: log.debug(f'Kernel autotune: report error: {e}') + try: + if task is not None: + autotune.add(progress.kernel, time.time() - progress.ts) + autotune.add('_shapes', 1) + progress.remove_task(task) + except Exception as e: + log.debug(f'Kernel autotune: report error: {e}') session['progress'] = None def bench_hook(orig): def wrapped(self, *args, config, **meta): - from modules import shared - """ - if shared.state.textinfo != 'Autotune kernel': - _textinfo = shared.state.textinfo - shared.state.textinfo = 'Autotune kernel' - else: - _textinfo = None - """ try: - session = status['session'] - if session is None or session['owner'] is not self: - stop_progress(session) # a sweep that never reported completion must not leave its bar drawing - prev = session['prev'] if session is not None else shared.state.textinfo # chained sweeps inherit the pre-tuning text, so an abandoned one cannot leave its own label behind - progress, task = start_progress(kernel_name(getattr(self, 'base_fn', None)), len(self.configs)) - session = {'owner': self, 'count': 0, 'total': len(self.configs), 'name': kernel_name(getattr(self, 'base_fn', None)), 'prev': prev, 'compile_us': 0, 'progress': progress, 'task': task} - status['session'] = session - session['count'] += 1 - if session['progress'] is not None and session['task'] is not None: - session['progress'].update(session['task'], completed=session['count'], description=f'kernel={session["name"]} {session["count"]}/{session["total"]}') - if session["count"] >= session["total"]: - stop_progress(session) + session: dict[str, Any] | None = status.get('session', None) + current_run_id = status.get('run_id') + if session is not None and session.get('owner') is self and session.get('run_id') == current_run_id: + session['count'] += 1 + if session['count'] > session['total']: + session['total'] = session['count'] + if session['progress'] is not None and session['task'] is not None: + session['progress'].update(session['task'], completed=session['count'], description=f'kernel={session["name"]}') except Exception as e: log.debug(f'Kernel autotune: report error: {e}') - res = orig(self, *args, config=config, **meta) - """ - if _textinfo is not None: - shared.state.textinfo = _textinfo - """ - return res + return orig(self, *args, config=config, **meta) return wrapped def make_autotune_listener(prior): def listener(*, fn=None, key=None, best_config=None, configs_timings=None, duration=None, cache_hit=False, **kwargs): try: - from modules import shared session = status['session'] if session is not None: stop_progress(session) - shared.state.textinfo = session['prev'] status['session'] = None name = kernel_name(fn) shape = shape_key(key) @@ -141,24 +155,68 @@ def run_hook(orig): """Report the register use of the config a sweep just chose. n_regs and n_spills are filled in when the driver loads the binary, so they exist only once the kernel has run, not at compile.""" def wrapped(self, *args, **kwargs): - res = orig(self, *args, **kwargs) - pending = status.get('pending', None) - if pending is not None: - status['pending'] = None - name, config = pending - try: - regs, spills = getattr(res, 'n_regs', None), getattr(res, 'n_spills', None) - if spills: - seen = f'{name}:{config}:{spills}' - if seen not in status['reported']: # the same kernel tunes once per shape, so warn on each distinct config only - status['reported'].add(seen) - # the sweep line carries the config too, but it is debug and filtered out at default level - log.warning(f'Kernel autotune: kernel={name} register spill config="{config}" regs={regs} spills={spills}') - elif regs is not None: - log.debug(f'Kernel autotune: kernel={name} regs={regs} spills=0') - except Exception as e: - log.debug(f'Kernel autotune: report error: {e}') - return res + session = status['session'] + if session is not None: + stop_progress(session) + status['session'] = None + status['run_id'] = status.get('run_id', 0) + 1 + run_id = status['run_id'] + + try: + self.nargs = dict(zip(self.arg_names, args)) + all_args = {**self.nargs, **kwargs} + _args = {k: v for (k, v) in all_args.items() if k in self.arg_names} + key = tuple(_args[k] for k in self.keys if k in _args) + for arg in _args.values(): + if hasattr(arg, 'dtype'): + key += (str(arg.dtype),) + needs_benchmark = len(self.configs) > 1 and key not in self.cache + if needs_benchmark: + try: + total = len(self.prune_configs(kwargs)) + except Exception: + total = len(self.configs) + shape = shape_key(key) + progress, task = start_progress(kernel_name(getattr(self, 'base_fn', None)), total, shape=shape) + session = { + 'owner': self, + 'run_id': run_id, + 'count': 0, + 'total': total, + 'name': kernel_name(getattr(self, 'base_fn', None)), + 'shape': shape, + 'compile_us': 0, + 'progress': progress, + 'task': task, + } + status['session'] = session + except Exception as e: + log.debug(f'Kernel autotune: report error: {e}') + + res = None + try: + res = orig(self, *args, **kwargs) + return res + finally: + pending = status.get('pending', None) + if pending is not None and res is not None: + status['pending'] = None + name, config = pending + try: + regs, spills = getattr(res, 'n_regs', None), getattr(res, 'n_spills', None) + if spills: + seen = f'{name}:{config}:{spills}' + if seen not in status['reported']: + status['reported'].add(seen) + log.warning(f'Kernel autotune: kernel={name} register spill config="{config}" regs={regs} spills={spills}') + elif regs is not None: + log.debug(f'Kernel autotune: kernel={name} regs={regs} spills=0') + except Exception as e: + log.debug(f'Kernel autotune: report error: {e}') + session = status.get('session', None) + if session is not None and session.get('owner') is self and session.get('run_id') == run_id: + stop_progress(session) + status['session'] = None return wrapped @@ -174,7 +232,7 @@ def make_compile_listener(prior): name = kernel_name(getattr(src, 'fn', None)) if name == 'unknown' and isinstance(metadata, dict): name = str(metadata.get('name', 'unknown')) - log.info(f'Kernel compile: kernel={name} time={total_s:.2f}') + log.debug(f'Kernel compile: kernel={name} time={total_s:.2f}') except Exception as e: log.debug(f'Kernel compile: report error: {e}') if prior is not None: @@ -201,7 +259,7 @@ def install(): log.debug(f'Kernel autotune: {e}') return try: - knobs.autotuning.listener = make_autotune_listener(getattr(knobs.autotuning, 'listener', None)) + knobs.autotuning.listener = make_autotune_listener(getattr(knobs.autotuning, 'listener', None)) # this is not actually invoked by triton knobs.compilation.listener = make_compile_listener(getattr(knobs.compilation, 'listener', None)) Autotuner._bench = bench_hook(Autotuner._bench) # pylint: disable=protected-access Autotuner.run = run_hook(Autotuner.run) diff --git a/modules/ui_definitions.py b/modules/ui_definitions.py index 552ca6d97..44a4d8bb4 100644 --- a/modules/ui_definitions.py +++ b/modules/ui_definitions.py @@ -257,6 +257,7 @@ def create_settings(cmd_opts): "sdnq_attention_smooth_k": OptionInfo(True, "SDNQ Attention use Smooth K", gr.Checkbox), "sdnq_attention_use_hadamard": OptionInfo(False, "SDNQ Attention use Hadamard", gr.Checkbox), "sdnq_attention_use_fp16_accum": OptionInfo(False, "SDNQ Attention use FP16 Accumulation", gr.Checkbox), + "sdnq_attention_quantize_fp32": OptionInfo(True, "SDNQ Attention Quantize FP32", gr.Checkbox), "sdnq_attention_matmul_type": OptionInfo("enabled", "SDNQ Attention MatMul type", gr.Radio, {"choices": sdnq_matmul_modes}), "sdnq_attention_pv_matmul_type": OptionInfo("disabled", "SDNQ Attention PV MatMul type", gr.Radio, {"choices": sdnq_matmul_modes}), "sdnq_attention_hadamard_group_size": OptionInfo(256, "SDNQ Attention Hadamard Group Size", gr.Slider, {"minimum": 4, "maximum": 1024, "step": 1}), diff --git a/modules/video_models/models_def.py b/modules/video_models/models_def.py index 75075c483..9bd08ff88 100644 --- a/modules/video_models/models_def.py +++ b/modules/video_models/models_def.py @@ -773,7 +773,7 @@ try: if m.name == 'None': continue total += 1 - log.info(f'Networks: type="video" engines={len(models)} models={total} errors={errors} time={t1 - t0:.2f}') + log.debug(f'Networks: type="video" engines={len(models)} models={total} errors={errors} time={t1 - t0:.2f}') except Exception as e: models = {} log.error(f'Networks: type="video" {e}') diff --git a/pipelines/ideogram/ideogram4.py b/pipelines/ideogram/ideogram4.py index d6ec40816..f58118751 100644 --- a/pipelines/ideogram/ideogram4.py +++ b/pipelines/ideogram/ideogram4.py @@ -741,7 +741,9 @@ class Ideogram4Pipeline(DiffusionPipeline): latents = self.scheduler.step(-v, t, latents, return_dict=False)[0] if callback_on_step_end is not None: - callback_kwargs = {k: locals()[k] for k in callback_on_step_end_tensor_inputs} + callback_kwargs = {} + for k in callback_on_step_end_tensor_inputs: + callback_kwargs[k] = locals()[k] callback_outputs = callback_on_step_end(self, i, t, callback_kwargs) latents = callback_outputs.pop("latents", latents) 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/ui/locale/locale_en.json b/ui/locale/locale_en.json index 3019dfb1f..c8fedeb30 100644 --- a/ui/locale/locale_en.json +++ b/ui/locale/locale_en.json @@ -1465,7 +1465,8 @@ {"id":"","label":"SDNQ Attention use FP16 Accumulation","localized":"","hint":"Accumulates the floating point matmuls in fp16 rather than fp32. Some tensor cores run fp16 accumulation at a higher rate than fp32, and on those the kernel is cheaper for it.
Operands are pre-scaled to keep products inside the fp16 range, which covers ordinary activations with less headroom than fp32 leaves.

Reaches the parts of the kernel that run in floating point. An int8 matmul accumulates in int32 and is unaffected, so at the default SDNQ Attention MatMul type this applies to the probability-value matmul alone.

Disabled by default.","ui":"settings_cuda"}, {"id":"","label":"SDNQ Attention MatMul type","localized":"","hint":"Precision the query-key matmul is computed in, the first of the two matmuls in attention.

enabled selects int8, and int8 and uint8 reach the same kernel.
float16 and float8_e4m3fn take the floating point path. fp8 needs a GPU with fp8 tensor cores and fails on hardware without them rather than falling back.
disabled leaves queries and keys in the model's own precision, which also idles SDNQ Attention use Smooth K and SDNQ Attention use Hadamard.

Default enabled.","ui":"settings_cuda"}, {"id":"","label":"SDNQ Attention PV MatMul type","localized":"","hint":"Precision the probability-value matmul is computed in, the second of the two matmuls in attention. Choices match SDNQ Attention MatMul type.

Quantizing this one as well takes out the floating point work the first setting leaves behind, and it is the more delicate of the two: its inputs are already normalized probabilities, and the small ones among them carry the fine detail.
disabled keeps this matmul in the model's own precision.

Default disabled.","ui":"settings_cuda"}, - {"id":"","label":"SDNQ Attention Hadamard Group Size","localized":"","hint":"Width of the Hadamard rotation in channels. Wider groups mix more channels together and spread outliers further.

Clamped to the head dimension of the running model, rounded down to a power of two that divides it. On a model with 64 or 128 channels per head the upper part of this range resolves to that head dimension rather than to the number shown. Rotation is skipped below 4.
Applies while SDNQ Attention use Hadamard is enabled.

Default 256.","ui":"settings_cuda"} + {"id":"","label":"SDNQ Attention Hadamard Group Size","localized":"","hint":"Width of the Hadamard rotation in channels. Wider groups mix more channels together and spread outliers further.

Clamped to the head dimension of the running model, rounded down to a power of two that divides it. On a model with 64 or 128 channels per head the upper part of this range resolves to that head dimension rather than to the number shown. Rotation is skipped below 4.
Applies while SDNQ Attention use Hadamard is enabled.

Default 256.","ui":"settings_cuda"}, + {"id":"","label":"SDNQ Attention Quantize FP32","localized":"","hint":"Upcasts queries, keys and values to fp32 for the quantization step, meaning the mean subtraction, scale and rounding that produce the low precision operands. The matmuls themselves are unaffected, and the kernel applies the scales in fp32 either way.
Turned off, that arithmetic runs in the model's own precision. bf16 carries eight mantissa bits, so a scale derived in it is coarser than one derived in fp32, and SDNQ Attention use Smooth K loses the most from it, since a mean across the whole sequence is exactly the kind of sum that wants the extra bits.

Whether the upcast costs anything depends on how the GPU runs fp32 vector work against fp16 and bf16. NVIDIA and AMD run them at the same rate here, so there is nothing to save; Intel runs fp32 slower and takes a noticeable hit.

Enabled by default.","ui":"settings_cuda"} ], "t": [ {"id":"txt2img_nav","label":"T2I","localized":"","hint":"Create image from text
Legacy interface that mimics original text-to-image interface and behavior"}, diff --git a/wiki b/wiki index b974e5084..eff72c57f 160000 --- a/wiki +++ b/wiki @@ -1 +1 @@ -Subproject commit b974e50847dccceb55154d1834ac0b40e18c5487 +Subproject commit eff72c57fe723d43129721e33b244d824e6dc674