From 5123bfd96b06851ead0a85aae9732d65bb07b364 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Tue, 11 Aug 2026 15:42:29 +0200 Subject: [PATCH] cleanup minimax Signed-off-by: Vladimir Mandic --- cli/hf-info.py | 30 ++++++++++++++++++--------- data/reference-base.json | 4 ++-- data/reference-quantized.json | 2 +- installer.py | 2 +- modules/devices.py | 12 +++++++---- modules/logger.py | 1 + modules/sd_hijack_hfhub.py | 3 +++ modules/video_models/video_modular.py | 24 ++++++++++----------- pipelines/model_minimax.py | 9 +++++++- 9 files changed, 56 insertions(+), 31 deletions(-) diff --git a/cli/hf-info.py b/cli/hf-info.py index 0da4aae1f..756a8f314 100755 --- a/cli/hf-info.py +++ b/cli/hf-info.py @@ -199,7 +199,7 @@ def discover_components(model_index: dict[str, Any] | None, files_map: dict[str, components: dict[str, Any] = { "mains": [], "text_encoders": [], - "ae": None, + "ae": [], } if isinstance(model_index, dict): @@ -210,8 +210,8 @@ def discover_components(model_index: dict[str, Any] | None, files_map: dict[str, text_keys = sorted([k for k in keys if re.fullmatch(r"text_encoder(_\d+)?", k or "")]) components["text_encoders"] = text_keys - if "vae" in keys: - components["ae"] = "vae" + vae_keys = sorted([k for k in keys if re.fullmatch(r".*vae?", k or "")]) + components["ae"] = vae_keys top_dirs = {f.split("/", 1)[0] for f in files_map if "/" in f} @@ -221,8 +221,8 @@ def discover_components(model_index: dict[str, Any] | None, files_map: dict[str, if not components["text_encoders"]: components["text_encoders"] = sorted([d for d in top_dirs if re.fullmatch(r"text_encoder|mllm(_\d+)?", d or "")]) - if components["ae"] is None and "vae" in top_dirs: - components["ae"] = "vae" + if not components["ae"]: + components["ae"] = sorted([d for d in top_dirs if re.fullmatch(r".*vae?", d or "")]) return components @@ -635,7 +635,7 @@ def search(repo_id: str) -> int: main_components = components["mains"] text_components = components["text_encoders"] - ae_component = components["ae"] + ae_components = components["ae"] main_files: list[str] = [] for main_component in main_components: @@ -643,7 +643,9 @@ def search(repo_id: str) -> int: te_files: list[str] = [] for te_component in text_components: te_files.extend(component_weight_files(te_component, files_map)) - ae_files = component_weight_files(ae_component, files_map) + ae_files: list[str] = [] + for ae_component in ae_components: + ae_files.extend(component_weight_files(ae_component, files_map)) fs = hf.HfFileSystem(token=token) @@ -674,8 +676,12 @@ def search(repo_id: str) -> int: arch = arch_from_config(cfg, component_type="te") te_arches.append(arch if arch is not None else te_component) - ae_cfg = component_config(ae_component, repo_id, token) - ae_arch = arch_from_config(ae_cfg, component_type="ae") + ae_arches: list[str] = [] + for ae_component in ae_components: + cfg = component_config(ae_component, repo_id, token) + arch = arch_from_config(cfg, component_type="ae") + ae_arches.append(arch if arch is not None else ae_component) + model_class = class_from_model_index(model_index) if model_class is None: first_main_class = next((c for c in main_component_classes if isinstance(c, str) and c.strip()), None) @@ -707,10 +713,14 @@ def search(repo_id: str) -> int: "dit": ", ".join(main_dit_entries) if len(main_dit_entries) > 0 else None, "dit_params": model_params_raw, "dit_size": model_size_raw, + "dit_size_gb": round(model_size_raw / (1024**3), 2) if isinstance(model_size_raw, int) else None, "te": ", ".join(te_arches) if len(te_arches) > 0 else None, "te_params": te_params_raw, "te_size": te_size_raw, - "ae": ae_arch, + "te_size_gb": round(te_size_raw / (1024**3), 2) if isinstance(te_size_raw, int) else None, + "ae": ", ".join(ae_arches) if len(ae_arches) > 0 else None, + "ae_size": ae_size_raw, + "ae_size_gb": round(ae_size_raw / (1024**3), 2) if isinstance(ae_size_raw, int) else None, "downloads": downloads_int, "tags": tags, } diff --git a/data/reference-base.json b/data/reference-base.json index 9ab483474..5ad8fad56 100644 --- a/data/reference-base.json +++ b/data/reference-base.json @@ -379,7 +379,7 @@ "preview": "MiniMaxAI--MiniMax-H3.jpg", "desc": "MiniMax-H3 generates video with synchronized stereo audio in a single denoising pass through a 33B single-stream transformer with a Qwen3-VL conditioner. In image tabs the model runs in experimental still mode, keeping the first frame of a minimal generation.", "extras": "sampler: Default", - "size": 134, + "size": 134.12, "date": "2026 August" }, "MiniMaxAI MiniMax-H3 Ref2VA": { @@ -388,7 +388,7 @@ "preview": "MiniMaxAI--MiniMax-H3.jpg", "desc": "The omni-reference variant of MiniMax-H3, sharing one repository with the base model as a separate checkpoint partition. Video with synchronized stereo audio is conditioned on reference images for identity and appearance, with reference rows held clean while video rows denoise.", "extras": "sampler: Default", - "size": 134, + "size": 134.12, "date": "2026 August" }, "Freepik F-Lite": { diff --git a/data/reference-quantized.json b/data/reference-quantized.json index 57f3f39e4..575e1c952 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": 51, + "size": 47.33, "date": "2026 August" }, "Z-Image-Turbo sdnq-svd-uint4": { diff --git a/installer.py b/installer.py index b37458157..4d1ce2534 100644 --- a/installer.py +++ b/installer.py @@ -584,7 +584,7 @@ def check_diffusers(): t_start = time.time() if args.skip_all: return - target_commit = "9f169d98d0bce392a889c3b6524d0d97734dfc0e" # diffusers commit hash == 0.40.0.dev0 == 08-05-2026 + target_commit = "90c0ffdc045902a3667d473d2fbfc03e8716dba9" # diffusers commit hash == 0.40.0.dev0 == 08-11-2026 # if args.use_rocm or args.use_zluda: # sha = '043ab2520f6a19fce78e6e060a68dbc947edb9f9' # lock diffusers versions for now pkg = package_spec('diffusers') diff --git a/modules/devices.py b/modules/devices.py index f46615165..c6e792145 100644 --- a/modules/devices.py +++ b/modules/devices.py @@ -230,9 +230,12 @@ def torch_gc(force: bool = False, fast: bool = False, reason: str | None = None) collected = gc.collect() if not fast else 0 # python gc try: if hasattr(torch, "accelerator") and torch.accelerator.is_available(): # torch >= 2.6 - torch.accelerator.synchronize() - torch.accelerator.empty_cache() - torch.accelerator.empty_host_cache() + if hasattr(torch.accelerator, "synchronize"): + torch.accelerator.synchronize() + if hasattr(torch.accelerator, "empty_cache"): + torch.accelerator.empty_cache() + if hasattr(torch.accelerator, "empty_host_cache"): + torch.accelerator.empty_host_cache() if torch.cuda.is_available() and hasattr(torch.cuda, "ipc_collect"): torch.cuda.ipc_collect() elif hasattr(torch, "xpu") and hasattr(torch.xpu, "ipc_collect"): @@ -240,7 +243,8 @@ def torch_gc(force: bool = False, fast: bool = False, reason: str | None = None) elif torch.cuda.is_available(): # Fallback for older PyTorch versions torch.cuda.synchronize() torch.cuda.empty_cache() - torch.cuda.ipc_collect() + if hasattr(torch.cuda, "ipc_collect"): + torch.cuda.ipc_collect() elif hasattr(torch, "xpu") and torch.xpu.is_available(): torch.xpu.synchronize() torch.xpu.empty_cache() diff --git a/modules/logger.py b/modules/logger.py index 740c74782..85b2d47b5 100644 --- a/modules/logger.py +++ b/modules/logger.py @@ -282,6 +282,7 @@ def setup_logging(debug=None, trace=None, filename=None): logging.getLogger("ControlNet").handlers = log.handlers logging.getLogger("diffusers").setLevel(logging.ERROR) + logging.getLogger("diffusers.modular_pipelines").setLevel(logging.ERROR) logging.getLogger("transformers").setLevel(logging.ERROR) logging.getLogger("torch").setLevel(logging.ERROR) logging.getLogger("urllib3").setLevel(logging.ERROR) diff --git a/modules/sd_hijack_hfhub.py b/modules/sd_hijack_hfhub.py index be7bc384e..42f963ee3 100644 --- a/modules/sd_hijack_hfhub.py +++ b/modules/sd_hijack_hfhub.py @@ -54,6 +54,9 @@ def xet_get_hijack(*args, **kwargs): if fn and not fn.endswith(".json"): log.debug(f'Download: type=xet mode="{opts.hf_transfer_mode}" fn="{fn}" size={size}') debug(f'Download start: type=xet args={args} kwargs={kwargs}') + # import tqdm # TODO xet_download: hijack progress bar + # bar_format = 'Download {rate_fmt}{postfix} {bar} {percentage:3.0f}% {n_fmt}/{total_fmt} {elapsed} {remaining} ' + '\x1b[38;5;71m' + '{desc}' + '\x1b[0m' + # kwargs['_tqdm_bar'] = tqdm.tqdm(*args, bar_format=bar_format, ncols=80, colour='#327fba', **kwargs) res = orig_xet_get(*args, **kwargs) debug(f'Download end: type=xet res={res}') state.end(jobid) diff --git a/modules/video_models/video_modular.py b/modules/video_models/video_modular.py index 0732defe8..8e057a6b1 100644 --- a/modules/video_models/video_modular.py +++ b/modules/video_models/video_modular.py @@ -37,11 +37,11 @@ def load_modular_pipe(repo_cls, repo: str, workflow: str | None = None, revision cache_dir=cache_dir, **offline_args, ) - # workflow selection stays out of from_pretrained: pruning the blocks tree to one task - # would disable runtime auto-dispatch between them; only the component fetch is restricted + # workflow selection stays out of from_pretrained: pruning the blocks tree to one task would disable runtime auto-dispatch between them; only the component fetch is restricted load_kwargs = {} quant_config = {} quant_args = model_quant.create_config(module='Model') + # TODO load_modular: need to handle component names dynamically if 'quantization_config' in quant_args: quant_config['transformer'] = quant_args['quantization_config'] quant_config['transformer_ref'] = quant_args['quantization_config'] @@ -49,8 +49,7 @@ def load_modular_pipe(repo_cls, repo: str, workflow: str | None = None, revision if 'quantization_config' in te_args: quant_config['text_encoder'] = te_args['quantization_config'] if quant_config: - # per-component dict without a default entry: only the listed components quantize while - # loading, everything else loads unquantized + # per-component dict without a default entry: only the listed components quantize while loading, everything else loads unquantized load_kwargs['quantization_config'] = quant_config log.debug(f'Load modular: quant={next(iter(quant_config.values())).__class__.__name__} modules={list(quant_config)}') pipe.load_components( @@ -61,10 +60,11 @@ def load_modular_pipe(repo_cls, repo: str, workflow: str | None = None, revision **offline_args, ) loaded = [name for name, component in pipe.components.items() if component is not None] + empty = [name for name, component in pipe.components.items() if component is None] pipe.sdnext_video_workflow = workflow # lets a pipe loaded outside the video registry report its own workflow if hasattr(pipe, 'min_duration') and hasattr(pipe, 'fps'): pipe.sdnext_supported_min_frames = int(pipe.min_duration * pipe.fps) # fresh pipes report the true floor; still mode gates per instance - log.debug(f'Load modular: cls={pipe.__class__.__name__} workflow={workflow} components={loaded} time={time.time()-t0:.2f}') + log.info(f'Load modular: cls={pipe.__class__.__name__} workflow={workflow} components={loaded} empty={empty} time={time.time()-t0:.2f}') return pipe except Exception as e: log.error(f'Load modular: repo="{repo}" workflow={workflow} {e}') @@ -87,7 +87,7 @@ def apply_minimax_overrides(p, pipe, still: bool = False, audio: bool = True): set_still(pipe, still) if still: frames = 5 # two latent frames; decode pads to the decoder floor and only the first frame is kept - log.info(f'Video modular: cls={pipe.__class__.__name__} mode=still experimental') + log.info(f'Pipeline: cls={pipe.__class__.__name__} mode=still') else: frames = max(getattr(p, 'frames', 1), getattr(pipe, 'sdnext_supported_min_frames', 120)) while frames % pipe.vae_frames_per_chunk != pipe.vae_latents_per_chunk: # frame counts align to 17n+5 @@ -96,14 +96,14 @@ def apply_minimax_overrides(p, pipe, still: bool = False, audio: bool = True): while frames > max_frames: frames -= pipe.vae_frames_per_chunk if frames != getattr(p, 'frames', None): - log.debug(f'Video modular: cls={pipe.__class__.__name__} frames={getattr(p, "frames", None)} aligned={frames}') + log.debug(f'Pipeline: cls={pipe.__class__.__name__} frames={getattr(p, "frames", None)} aligned={frames}') p.frames = frames p.task_args['num_frames'] = frames p.steps = max(2, p.steps) p.task_args['num_inference_steps'] = p.steps pipe.num_timesteps = p.steps - 1 # sigma grid includes the terminal point; feeds the progress total if p.sampler_name not in ('None', 'Default'): - log.warning(f'Video modular: cls={pipe.__class__.__name__} sampler={p.sampler_name} unsupported: using model default') + log.warning(f'Pipeline: cls={pipe.__class__.__name__} sampler={p.sampler_name} unsupported: using model default') p.sampler_name = 'Default' # the model default is the bespoke scheduler pair, which discrete samplers must not replace pipe.vae.enable_tiling() # model always tiles; the shared vae params path may have disabled it set_audio(pipe, audio) @@ -149,10 +149,10 @@ def set_audio(pipe, enabled: bool): stashed = getattr(pipe, 'sdnext_audio_decode_block', None) if stashed is not None: sub.insert('audio', stashed, len(sub)) - log.debug(f'Video modular: cls={pipe.__class__.__name__} audio=enabled') + log.debug(f'Pipeline: cls={pipe.__class__.__name__} audio=enabled') elif not enabled and 'audio' in sub: pipe.sdnext_audio_decode_block = sub.pop('audio') - log.debug(f'Video modular: cls={pipe.__class__.__name__} audio=disabled') + log.debug(f'Pipeline: cls={pipe.__class__.__name__} audio=disabled') class InterruptLogFilter(logging.Filter): @@ -172,7 +172,7 @@ def install_state_hook(pipe): if getattr(pipe, 'sdnext_phase', None) != phase: pipe.sdnext_phase = phase shared.state.textinfo = phase - log.debug(f'Video modular: cls={pipe.__class__.__name__} phase="{phase}"') + log.debug(f'Pipeline: cls={pipe.__class__.__name__} phase={phase}') def state_hook(module, args): # pylint: disable=unused-argument set_phase('Generate') @@ -189,7 +189,7 @@ def install_state_hook(pipe): raise AssertionError('Interrupted...') def encode_hook(module, args): # pylint: disable=unused-argument - set_phase('Text encode') + set_phase('TextEncode') if shared.state.interrupted or shared.state.skipped: raise AssertionError('Interrupted...') diff --git a/pipelines/model_minimax.py b/pipelines/model_minimax.py index bba966856..4ca367e69 100644 --- a/pipelines/model_minimax.py +++ b/pipelines/model_minimax.py @@ -13,8 +13,9 @@ def load_minimax(checkpoint_info, diffusers_load_config=None): # pylint: disable workflow = (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}') + repo_cls = diffusers.MiniMaxH3ModularPipeline pipe = video_modular.load_modular_pipe( - getattr(diffusers, 'MiniMaxH3ModularPipeline', None), + repo_cls, repo_id, workflow=workflow, offline_args=offline_args, @@ -22,6 +23,12 @@ def load_minimax(checkpoint_info, diffusers_load_config=None): # pylint: disable ) if pipe is None: return None + if pipe.text_encoder is None: + # TODO minimax missing te: we should never be here + import transformers + from pipelines import generic + text_encoder = generic.load_text_encoder(repo_id, cls_name=transformers.Qwen3VLForConditionalGeneration, load_config=diffusers_load_config, allow_shared=False) + pipe.update_components(text_encoder=text_encoder) video_modular.install_state_hook(pipe) video_load.loaded_model = None # image-path load invalidates the video tab's name cache