diff --git a/CHANGELOG.md b/CHANGELOG.md index 47e1f166b..733b6c447 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Change Log for SD.Next -## Update for 2026-08-13 +## Update for 2026-08-14 - **Models** - [MiniMax H3](https://huggingface.co/MiniMaxAI/MiniMax-H3) in *base* and *ref* variants @@ -14,6 +14,7 @@ *note*: LTX-2.5 is a [gated model](https://vladmandic.github.io/sdnext-docs/Gated/) *note*: image conditioning is now re-compressed to match what the models were trained on, at CRF 18 for LTX-2.5 and 33 for earlier LTX-2.x versions + see [MiniMax wiki page](wiki/MiniMax) for details and usage instructions - **Detailer**: Pretty much *detailer.next* :) Detailer detection models were traditionally *YOLO* models, but now we can also use: - [Facebook-SAM3](https://huggingface.co/facebook/sam3) hybrid promptable concept segmentation and detection network @@ -38,8 +39,8 @@ - add `SD_DIFFUSERS_DEBUG` and `SD_TRANSFORMERS_DEBUG` env variables to trace diffusers and transformers internal operations - **API** - full support for video generation using api - *note*: video api uses async workflow where you submit request and then later download the result new endpoints: `/sdapi/v1/video`, `/sdapi/v1/video/models`, `/sdapi/v1/video/file` + *note*: video api uses async workflow where you submit request and then later download the result - **Removed** - remove DirectML support latest release was over 2 years ago and is not compatible with modern frameworks @@ -57,6 +58,8 @@ - ltx: reload the latent upsampler when the model or its repo changes - video: take the audio sample rate from the loaded vocoder - video: keep the shared text encoder out of the registry rows + - processing stats reporting + - image metadata handle correct image index ## Update for 2026-08-07 diff --git a/data/reference-quantized.json b/data/reference-quantized.json index aca805c79..d9ccfbc70 100644 --- a/data/reference-quantized.json +++ b/data/reference-quantized.json @@ -84,7 +84,7 @@ "preview": "MiniMaxAI--MiniMax-H3.jpg", "desc": "Quantization of MiniMaxAI/MiniMax-H3 using SDNQ: dynamic 4-bit uint. Video with synchronized audio; in image tabs the model runs in experimental still mode.", "extras": "sampler: Default", - "size": 47.33, + "size": 64.80, "date": "2026 August" }, "MiniMaxAI MiniMax-H3 sdnq-uint4 Ref2VA": { @@ -93,24 +93,24 @@ "subfolder": "ref2va", "desc": "Quantization of MiniMaxAI/MiniMax-H3 using SDNQ: dynamic 4-bit uint. Video with synchronized audio; in image tabs the model runs in experimental still mode.", "extras": "sampler: Default", - "size": 47.33, + "size": 64.80, "date": "2026 August" }, "MiniMaxAI MiniMax-H3 Pruned sdnq-uint4": { "path": "OzzyGT/MiniMax_H3_sdnq_4bit_pruned", - "preview": "MiniMaxAI--MiniMax-H3.jpg", + "preview": "OzzyGT--MiniMax_H3_sdnq_4bit_pruned.jpg", "desc": "Quantization of MiniMaxAI/MiniMax-H3 using SDNQ: dynamic 4-bit uint. Video with synchronized audio; in image tabs the model runs in experimental still mode.", "extras": "sampler: Default", - "size": 47.33, + "size": 23.70, "date": "2026 August" }, "MiniMaxAI MiniMax-H3 Pruned sdnq-uint4 Ref2VA": { "path": "OzzyGT/MiniMax_H3_sdnq_4bit_pruned", - "preview": "MiniMaxAI--MiniMax-H3.jpg", + "preview": "OzzyGT--MiniMax_H3_sdnq_4bit_pruned.jpg", "subfolder": "ref2va", "desc": "Quantization of MiniMaxAI/MiniMax-H3 using SDNQ: dynamic 4-bit uint. Video with synchronized audio; in image tabs the model runs in experimental still mode.", "extras": "sampler: Default", - "size": 47.33, + "size": 23.70, "date": "2026 August" }, "Z-Image-Turbo sdnq-svd-uint4": { diff --git a/extensions-builtin/sdnq b/extensions-builtin/sdnq index 46fe58736..d19656260 160000 --- a/extensions-builtin/sdnq +++ b/extensions-builtin/sdnq @@ -1 +1 @@ -Subproject commit 46fe58736ad454362b35d45d9f34103c09ce7d09 +Subproject commit d19656260d32be0636301665e4204ea9dbbd2f92 diff --git a/models/Reference/OzzyGT--MiniMax_H3_sdnq_4bit_pruned.jpg b/models/Reference/OzzyGT--MiniMax_H3_sdnq_4bit_pruned.jpg new file mode 100644 index 000000000..fc6c6fa6c Binary files /dev/null and b/models/Reference/OzzyGT--MiniMax_H3_sdnq_4bit_pruned.jpg differ diff --git a/modules/devices.py b/modules/devices.py index c6e792145..114b1623a 100644 --- a/modules/devices.py +++ b/modules/devices.py @@ -143,7 +143,10 @@ def get_gpu_info(): } elif backend == 'cuda' or backend == 'zluda': return { - 'device': f'{torch.cuda.get_device_name(torch.cuda.current_device())} n={torch.cuda.device_count()} arch={torch.cuda.get_arch_list()[-1]} capability={torch.cuda.get_device_capability(device)}', + 'device': f'"{torch.cuda.get_device_name(torch.cuda.current_device())}"', + 'count': torch.cuda.device_count(), + 'arch': torch.cuda.get_arch_list()[-1], + 'capability': torch.cuda.get_device_capability(torch.cuda.current_device()), 'cuda': torch.version.cuda, 'cudnn': torch.backends.cudnn.version(), 'driver': get_driver(), diff --git a/modules/loader.py b/modules/loader.py index 275afadae..14aea1c11 100644 --- a/modules/loader.py +++ b/modules/loader.py @@ -11,6 +11,7 @@ from modules import timer, errors from modules.logger import log +log.info('Initializing: packages') initialized = False errors.install() logging.getLogger("DeepSpeed").disabled = True diff --git a/modules/processing.py b/modules/processing.py index b5cd149e7..59982495c 100644 --- a/modules/processing.py +++ b/modules/processing.py @@ -417,7 +417,7 @@ def process_samples(p: StableDiffusionProcessing, samples): def print_stats(): - log.debug(f'Processed: timers={timer.process.dct()}') + log.debug(f'Processed: timers={timer.process.dct(no_total=True)}') log.debug(f'Processed: memory={memstats.memory_stats()}') if devices.triton_ok: @@ -428,13 +428,14 @@ def print_stats(): from modules.sd_models_compile import update_compile_times update_compile_times() - dynamo_dct = timer.dynamo.dct(min_time=0.5, no_total=True) + dynamo_dct = timer.dynamo.dct(min_time=1.0, no_total=True) timer.dynamo.reset() if dynamo_dct: log.debug(f'Processed: dynamo={dynamo_dct}') def process_images_inner(p: StableDiffusionProcessing) -> Processed: + t0 = time.time() if type(p.prompt) == list: assert len(p.prompt) > 0 else: @@ -453,7 +454,7 @@ def process_images_inner(p: StableDiffusionProcessing) -> Processed: jobid = shared.state.begin('Process') shared.state.batch_count = p.n_iter with devices.inference_context(): - t0 = time.time() + t1 = time.time() if not hasattr(p, 'skip_init'): p.init(p.all_prompts, p.all_seeds, p.all_subseeds) debug(f'Processing inner: args={vars(p)}') @@ -553,7 +554,8 @@ def process_images_inner(p: StableDiffusionProcessing) -> Processed: shared.sd_model.restore_pipeline() shared.sd_model = sd_models.set_diffuser_pipe(shared.sd_model, sd_models.DiffusersTaskType.TEXT_2_IMAGE) - t1 = time.time() + t2 = time.time() + timer.process.add('process', t2 - t1) p.color_corrections = None index_of_first_image = 0 @@ -588,9 +590,12 @@ def process_images_inner(p: StableDiffusionProcessing) -> Processed: p.scripts.postprocess(p, results) timer.process.record('post') p.ops = list(set(p.ops)) + t3 = time.time() + timer.process.add('wall', t3 - t0) if not p.disable_extra_networks: - log.info(f'Processed: images={len(output_images)} its={(p.steps * len(output_images)) / (t1 - t0):.3f} ops={p.ops}') + its = (p.steps * len(output_images)) / (t2 - t1) + log.info(f'Processed: images={len(output_images)} its={its:.3f} ops={p.ops}') print_stats() if shared.cmd_opts.lowvram or shared.cmd_opts.medvram: diff --git a/modules/processing_info.py b/modules/processing_info.py index 8e6ce4f97..ad87e6177 100644 --- a/modules/processing_info.py +++ b/modules/processing_info.py @@ -23,6 +23,7 @@ def create_infotext(p: StableDiffusionProcessing, all_prompts=None, all_seeds=No if not hasattr(shared.sd_model, 'sd_checkpoint_info'): return '' + job_size = p.n_iter * p.batch_size if index is None: index = position_in_batch + iteration * p.batch_size if all_prompts is None: @@ -209,7 +210,13 @@ def create_infotext(p: StableDiffusionProcessing, all_prompts=None, all_seeds=No if shared.sd_model_type == 'h1': args['LLM'] = None if shared.opts.model_h1_llama_repo == 'Default' else shared.opts.model_h1_llama_repo - args.update(p.extra_generation_params) + # args.update(p.extra_generation_params) + for k, v in p.extra_generation_params.items(): + if isinstance(v, (list, tuple)) and (job_size > index) and (len(v) > 1) and (len(v) == job_size): # likely a per-job param + args[k] = v[index] + else: + args[k] = v + for k, v in args.copy().items(): if v is None: del args[k] diff --git a/modules/processing_vae.py b/modules/processing_vae.py index 0d18e875d..03c1161d2 100644 --- a/modules/processing_vae.py +++ b/modules/processing_vae.py @@ -90,11 +90,10 @@ def full_vae_decode(latents, model): if debug: devices.torch_gc(force=True) shared.mem_mon.reset() - if shared.opts.diffusers_offload_mode != "sequential": sd_models.move_model(model.vae, devices.device) - sd_models.set_vae_options(model, vae=None, op='decode') + # sd_models.set_vae_options(model, vae=None, op='decode') # set during model load upcast = (model.vae.dtype == torch.float16) and (getattr(model.vae.config, 'force_upcast', False) or shared.opts.no_half_vae) if upcast: if hasattr(model, 'upcast_vae'): # this is done by diffusers automatically if output_type != 'latent' diff --git a/modules/sd_models.py b/modules/sd_models.py index a393652fd..6df77d0d4 100644 --- a/modules/sd_models.py +++ b/modules/sd_models.py @@ -12,7 +12,7 @@ import torch import huggingface_hub as hf from modules.logger import log from modules import timer, paths, shared, modelloader, devices, script_callbacks, sd_vae, sd_unet, errors, sd_models_compile, sd_detect, model_quant, sd_hijack_te, sd_hijack_vae, sd_hijack_accelerate, sd_hijack_safetensors, sd_hijack_transformers, sd_hijack_hfhub, attention -from modules.memstats import memory_stats +from modules.memstats import memory_stats, gpu_stats from modules.shared_helpers import walk_files from modules.modeldata import model_data from modules.sd_checkpoint import CheckpointInfo, select_checkpoint, list_models, checkpoint_titles, get_closest_checkpoint_match, update_model_hashes, write_metadata, checkpoints_list # pylint: disable=unused-import @@ -149,7 +149,8 @@ def set_vae_options(sd_model, vae=None, op:str='model', quiet:bool=False): ops['upcast'] = True sd_model.vqvae.to(torch.float32) # vqvae is producing nans in fp16 if not quiet and len(ops) > 0: - log.quiet(quiet, f'Setting {op}: component=vae {ops}') + fn = f'{sys._getframe(2).f_code.co_name}:{sys._getframe(1).f_code.co_name}' # pylint: disable=protected-access + log.quiet(quiet, f'Setting {op}: component=vae {ops} fn={fn}') def set_diffuser_options(sd_model, vae=None, op:str='model', offload:bool=True, quiet:bool=False): @@ -1685,3 +1686,10 @@ def list_hfcache(): checkpoint = CheckpointInfo(filename=f.path, name=path_to_repo(f.name), model_type='hfcache') checkpoints.append(checkpoint) return checkpoints + + +def warn_group_offload(min_vram: int = 0): + vram = gpu_stats() + vram = round(vram['total'] if "total" in vram else 0) + if (0 < vram < min_vram) and (shared.opts.diffusers_offload_mode in ['none', 'balanced', 'model']): + log.warning(f'Load model: vram={vram} min={min_vram} offload={shared.opts.diffusers_offload_mode} recommended=group reason="insufficient vram"') diff --git a/modules/sd_offload.py b/modules/sd_offload.py index 0b8089a64..8bdb6fd48 100644 --- a/modules/sd_offload.py +++ b/modules/sd_offload.py @@ -796,10 +796,10 @@ def apply_balanced_offload(sd_model=None, exclude: list[str] | None = None, forc report_model_stats(module_name, module) set_accelerate(sd_model) - t = time.time() - t0 - process_timer.add('offload', t) + t1 = time.time() + process_timer.add('offload', t1 - t0) fn = f'{sys._getframe(2).f_code.co_name}:{sys._getframe(1).f_code.co_name}' # pylint: disable=protected-access - debug_move(f'Apply offload: time={t:.2f} type=balanced fn={fn}') + debug_move(f'Apply offload: time={t1 - t0:.2f} type=balanced fn={fn}') if not cached: log.info(f'Model class={sd_model.__class__.__name__} modules={len(offload_hook_instance.offload_map)} size={offload_hook_instance.model_size():.3f}') return sd_model diff --git a/modules/shared_defaults.py b/modules/shared_defaults.py index c18a59e24..0530fb6a3 100644 --- a/modules/shared_defaults.py +++ b/modules/shared_defaults.py @@ -60,7 +60,7 @@ def get_default_modes(cmd_opts, mem_stat): default_sdp_override_options = ['Dynamic attention'] if devices.get_optimal_device_name() != "cpu": - os.environ.setdefault('SDNQ_USE_OPENVINO_MM', '0') + os.environ.setdefault('SDNQ_USE_OPENVINO_MM', '0') # TODO sdnq openvino: this is too late as sdnq already initialized it return ( default_offload_mode, diff --git a/modules/styles.py b/modules/styles.py index 9fd1e77cf..05837c4cd 100644 --- a/modules/styles.py +++ b/modules/styles.py @@ -123,6 +123,7 @@ def apply_file_wildcards(prompt, replaced = None, not_found = None, recursion=0, not_found = [] if replaced is None: replaced = [] + def check_wildcard_files(prompt, wildcard, files, file_only=True): trimmed = wildcard.replace('\\', os.path.sep).replace('/', os.path.sep).strip().lower() for file in files: @@ -144,8 +145,6 @@ def apply_file_wildcards(prompt, replaced = None, not_found = None, recursion=0, prompt = prompt.replace(f"__{wildcard}__", choice, 1) log.debug(f'Apply wildcard: select="{wildcard}" choice="{choice}" file="{file}" choices={len(lines)}') replaced.append(wildcard) - if p is not None: - p.extra_generation_params['Wildcards'] = p.extra_generation_params.get('Wildcards', []) + [trimmed] return prompt, True except Exception as e: log.error(f'Wildcards: wildcard={wildcard} file={file} {e}') @@ -206,6 +205,8 @@ def apply_wildcards_to_prompt(prompt, all_wildcards, seed=-1, silent=False, p: S log.debug(f'Apply wildcards: {replaced} path="{shared.opts.wildcards_dir}" type=style time={t1-t0:.2f}') if (len(replaced_file) > 0 or len(not_found) > 0) and not silent: log.debug(f'Apply wildcards: found={replaced_file} missing={not_found} path="{shared.opts.wildcards_dir}" type=file seed={seed} time={t2-t2:.2f}') + if p is not None: + p.extra_generation_params['Wildcards'] = p.extra_generation_params.get('Wildcards', []) + [replaced_file] if old_state is not None: random.setstate(old_state) return prompt diff --git a/modules/timer.py b/modules/timer.py index dcb2b642b..cdd443188 100644 --- a/modules/timer.py +++ b/modules/timer.py @@ -64,6 +64,11 @@ class Timer: res += " ".join([f"{category}={time_taken:.2f}" for category, time_taken in additions]) return res + def max(self): + if not self.records: + return 0 + return max(self.records.values()) + def get_total(self): return sum(self.records.values()) diff --git a/modules/ui_control.py b/modules/ui_control.py index dd07aaa03..f31edb989 100644 --- a/modules/ui_control.py +++ b/modules/ui_control.py @@ -35,11 +35,12 @@ def return_stats(t: float | None = None): ooms = mem_mon_read.pop("oom") retries = mem_mon_read.pop("retries") vram = {k: v // 1048576 for k, v in mem_mon_read.items()} - peak = max(vram.get('active_peak', 0), vram.get('reserved_peak', 0), vram.get('used', 0)) - used = round(100.0 * peak / vram.get('total', 0)) if vram.get('total', 0) > 0 else 0 - if peak > 0: - gpu += f"| 🕮 GPU {peak} MB" - gpu += f" {used}%" if used > 0 else '' + peak_mb = max(vram.get('active_peak', 0), vram.get('reserved_peak', 0), vram.get('used', 0)) + peak_gb = round(100.0 * peak_mb / 1024) / 100.0 + used_perc = round(100.0 * peak_mb / vram.get('total', 0)) if vram.get('total', 0) > 0 else 0 + if peak_mb > 0: + gpu += f"| 🕮 GPU {peak_gb} GB" + gpu += f" {used_perc}%" if used_perc > 0 else '' gpu += f" | Retries {retries} OOM {ooms}" if retries > 0 or ooms > 0 else '' ram = ram_stats() if ram['used'] > 0: diff --git a/modules/ui_definitions.py b/modules/ui_definitions.py index 4aa269a11..dc356cbfb 100644 --- a/modules/ui_definitions.py +++ b/modules/ui_definitions.py @@ -138,8 +138,6 @@ def create_settings(cmd_opts): "offload_sep": OptionInfo("