Merge branch 'dev' into feat/ltx-2.5

This commit is contained in:
Vladimir Mandic
2026-08-14 10:51:44 +02:00
committed by GitHub
18 changed files with 69 additions and 34 deletions
+5 -2
View File
@@ -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
+6 -6
View File
@@ -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": {
Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

+4 -1
View File
@@ -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(),
+1
View File
@@ -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
+10 -5
View File
@@ -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:
+8 -1
View File
@@ -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]
+1 -2
View File
@@ -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'
+10 -2
View File
@@ -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"')
+3 -3
View File
@@ -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
+1 -1
View File
@@ -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,
+3 -2
View File
@@ -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
+5
View File
@@ -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())
+6 -5
View File
@@ -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:
+3 -2
View File
@@ -138,8 +138,6 @@ def create_settings(cmd_opts):
"offload_sep": OptionInfo("<h2>Model Offloading</h2>", "", gr.HTML),
"diffusers_offload_mode": OptionInfo(startup_offload_mode, "Model offload mode", gr.Radio, {"choices": ['none', 'balanced', 'group', 'model', 'sequential']}),
"diffusers_offload_nonblocking": OptionInfo(False, "Non-blocking move operations"),
"caption_offload": OptionInfo(True, "Offload caption models"),
"caption_to_gpu": OptionInfo(True, "Load caption models direct to GPU"),
"offload_balanced_sep": OptionInfo("<h2>Balanced Offload</h2>", "", gr.HTML),
"diffusers_offload_pre": OptionInfo(True, "Offload during pre-forward", gr.Checkbox, {"visible": False}),
"diffusers_offload_streams": OptionInfo(False, "Offload using streams"),
@@ -155,6 +153,9 @@ def create_settings(cmd_opts):
'group_offload_record': OptionInfo(False, "Record torch streams", gr.Checkbox),
'group_offload_pin': OptionInfo(True, "Pin offload memory", gr.Checkbox),
'group_offload_blocks': OptionInfo(1, "Offload blocks", gr.Number),
"caption_offload_sep": OptionInfo("<h2>Caption Model Offloading</h2>", "", gr.HTML),
"caption_offload": OptionInfo(True, "Offload caption models"),
"caption_to_gpu": OptionInfo(True, "Load caption models direct to GPU"),
}))
# --- Model Quantization ---
+1
View File
@@ -14,6 +14,7 @@ def load_minimax(checkpoint_info, diffusers_load_config = None, workflow: str |
workflow = (workflow or getattr(checkpoint_info, 'subfolder', None) or 'fl2va').lower() # one repo holds both checkpoint partitions; reference entries select ref2va via the subfolder tag
log.debug(f'Load model: type=MiniMaxH3 repo="{repo_id}" workflow={workflow} offload={shared.opts.diffusers_offload_mode} dtype={devices.dtype}')
sd_models.warn_group_offload(min_vram=20)
repo_cls = diffusers.MiniMaxH3ModularPipeline
pipe = load_modular_pipe(
repo_cls,
+1 -1
Submodule wiki updated: 4114a2e8c2...5bca12bb6c