diff --git a/CHANGELOG.md b/CHANGELOG.md index e74ba6e4d..08b5a96b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,8 @@ ### Highlights for 2026-09-11 *What's New*? Well, code-wise, this is a big one... -First, a-lot-of-optimizations: +First, a-lot-of-optimizations: +- updated core packages - improved **LoRA** performance and quality, especially with quantized models - newly structured **attention** mechanisms - modular pipelines with new **guidance** methods @@ -32,6 +33,7 @@ Plus inevitable bug-fixes... LLaDA-Image is a 6.5B transformer with massive 16.3B fully-custom MoE text-encoder and optional 1.3B SigVQ conditioning model with support for text-to-image, vq-conditioned text-to-image and image-editing workflows *note* model is extremely quantization sensitive so minimum allowed quant type is `uint8` + - [MiniMax-H3](https://huggingface.co/MiniMaxAI/MiniMax-H3) update with pre-quantized `sdnq-uint8` *pruned* variants - **LoRA** - see [LoRA docs](https://vladmandic.github.io/sdnext-docs/LoRA) for all of the improvements and usage instructions *note*: lora now has its own settings section in *settings -> lora* @@ -94,6 +96,7 @@ Plus inevitable bug-fixes... - option to skip triton autotune and use default config for all triton kernels in *settings -> compute settings* *note*: this may improve initial generate time, but may also reduce performance on some models + - rocm: update `rocm` script and add detailed `miopen` logging, thanks @resonantsky - new optional transformer hooks in *settings -> compute add-ons* *PAG: Perturbed attention guidance, PAB: Pyramid attention broadcast, FBC: First Block Cache, FC: Faster Cache, LS: Layer Skip, MC: Mag Cache, TS: TaylorSeer* @@ -119,6 +122,7 @@ Plus inevitable bug-fixes... - lucida: handle requirements - lumina-dimoo: attention-kwargs, thanks @Anai-Guo - minimax: crop image to video aspect ratio + - modular: handle module with remote-code - network: improve type/version lookup - offline: honor offline mode for more models, thanks @ryanmeador - openvino: optimize recompile checks and lora loading diff --git a/modules/gr_hijack.py b/modules/gr_hijack.py index 8b6ac3e08..9b3d3cbee 100644 --- a/modules/gr_hijack.py +++ b/modules/gr_hijack.py @@ -208,13 +208,13 @@ def patch_gradio(): return {"is_generating": False, "data": [], "error": "empty response"} return response except GeneratorExit as e: - log.error(f"Gradio queue: events={len(events)} batch={batch} error: {e}") + log.error(f"Gradio queue: events={len(events)} batch={batch} reason=GeneratorExit {e}") return {"is_generating": False, "data": [None, None, None, None, "cancelled", ""], "error": None} except Exception as e: - log.error(f"Gradio queue: events={len(events)} batch={batch} error: {e}") + log.error(f"Gradio queue: events={len(events)} batch={batch} reason=Exception {e}") raise except BaseException as e: - log.error(f"Gradio queue: events={len(events)} batch={batch} error: {e}") + log.error(f"Gradio queue: events={len(events)} batch={batch} reason=BaseException {e}") raise def wrap_blocks_preprocess_data(self, fn_index: int, inputs: list, state: dict): diff --git a/modules/minimax/minimax_ui.py b/modules/minimax/minimax_ui.py index 56dbf0e65..a101f521a 100644 --- a/modules/minimax/minimax_ui.py +++ b/modules/minimax/minimax_ui.py @@ -24,6 +24,7 @@ def create_ui(prompt, _negative, styles, overrides, script_inputs, mp4_fps, mp4_ with gr.Accordion(open=True, label='Parameters', elem_id='minimax_param_accordion') as _param_accordion: with gr.Row(): width, height = ui_sections.create_resolution_inputs('minimax', default_width=1024, default_height=576, step=32) + btn_detect_image_size = ToolButton(value=ui_symbols.detect, elem_id="minimax_resize_detect_size") 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=362, step=17, value=124, elem_id='minimax_frames') @@ -57,19 +58,29 @@ def create_ui(prompt, _negative, styles, overrides, script_inputs, mp4_fps, mp4_ model_info = next((m for m in models['MiniMax'] if m.name == model_name), None) if model_info is None or model_info.name is None or model_info.name == '' or model_info.name == 'None': return gr.update(value='none'), gr.update(visible=False), gr.update(visible=False) - log.debug(f'Selected: name="{model_info.name}" repo="{model_info.repo}" cls={model_info.repo_cls}') if model_info.workflow == 'fl2va': workflow = 'fl2va' if init_image is not None else 't2va' else: workflow = model_info.workflow + log.debug(f'Video: workflow={workflow} name="{model_info.name}" repo="{model_info.repo}" cls={model_info.repo_cls} image={init_image} selected') return gr.update(value=f'Workflow: {workflow}'), gr.update(visible=workflow != 'ref2va'), gr.update(visible=workflow == 'ref2va') def on_load(model_name: str): model_info = next((m for m in models['MiniMax'] if m.name == model_name), None) minimax_video.load_model(model_info.name if model_info is not None else None) + def on_image_size(init_image): + if init_image is not None: + try: + width, height = init_image.size + return gr.update(value=width), gr.update(value=height) + except Exception: + pass + return gr.update(), gr.update() + model.change(fn=on_change, inputs=[model, init_image], outputs=[workflow, input_accordion, reference_accordion], show_progress='hidden') init_image.change(fn=on_change, inputs=[model, init_image], outputs=[workflow, input_accordion, reference_accordion], show_progress='hidden') + btn_detect_image_size.click(fn=on_image_size, inputs=[init_image], outputs=[width, height]) btn_load.click(fn=on_load, inputs=[model], outputs=[]) task_id = gr.Textbox(visible=False, value='') diff --git a/modules/modular_load.py b/modules/modular_load.py index 06245934e..3579b6f72 100644 --- a/modules/modular_load.py +++ b/modules/modular_load.py @@ -59,18 +59,21 @@ def preload_components(pipe, workflow: str | None, load_config: dict | None = No if spec is None or getattr(spec, 'default_creation_method', None) != 'from_pretrained': continue repo = getattr(spec, 'pretrained_model_name_or_path', None) - cls = getattr(spec, 'type_hint', None) + cls = getattr(spec, 'type_hint', None) or {} if not repo or cls is None: continue origin = getattr(cls, '__module__', '') or '' - cls_name = getattr(cls, '__name__', '') or '' # TODO preload: components with remote code resolve to cls none + cls_name = getattr(cls, '__name__', '') or '' subfolder = getattr(spec, 'subfolder', None) or name component = None if origin.startswith('diffusers') and ('Transformer' in cls_name or 'UNet' in cls_name): component = generic.load_transformer(repo, cls_name=cls, load_config=load_config, subfolder=subfolder, trust_remote_code=True) - elif origin.startswith('transformers') and 'text_encoder' in name: + elif origin.startswith('transformers') and ('text_encoder' in name): # shared substitution is on: the map matches class plus a substring of the repo name, so its entries have to run narrow before broad component = generic.load_text_encoder(repo, cls_name=cls, load_config=load_config, subfolder=subfolder) + if 'transformer' in name: + # fallback for component with remote-code as it does not have resolvable cls + component = generic.load_transformer(repo, cls_name=None, load_config=load_config, subfolder=subfolder, trust_remote_code=True) if component is not None: loaded[name] = component return loaded diff --git a/modules/sd_offload_utils.py b/modules/sd_offload_utils.py index eb43dc781..3ff2f4173 100644 --- a/modules/sd_offload_utils.py +++ b/modules/sd_offload_utils.py @@ -174,7 +174,11 @@ def report_model_stats(module_name, module): size, _params = get_module_size(module) quant = getattr(module, "quantization_method", None) params = sum(p.numel() for p in module.parameters(recurse=True)) + try: + dtype = next(module.parameters(), torch.tensor([])).dtype + except Exception: + dtype = None logical = get_logical_param_count(module) - log.debug(f'Module: name={module_name} cls={module.__class__.__name__} size={size:.3f} params={params} logical={logical} quant={quant}') + log.debug(f'Module: name={module_name} cls={module.__class__.__name__} size={size:.3f} params={params} logical={logical} quant={quant} dtype={dtype}') except Exception as e: log.error(f'Module stats: name={module_name} {e}') diff --git a/modules/shared.py b/modules/shared.py index 246105c8d..9df752130 100644 --- a/modules/shared.py +++ b/modules/shared.py @@ -211,7 +211,7 @@ def restart_server(restart=True): demo.server.should_exit = True demo.server.force_exit = True demo.close(verbose=False) - demo.server.close() + # demo.server.close() demo.fns = [] time.sleep(1) sys.tracebacklimit = 100 diff --git a/modules/video_models/models_def.py b/modules/video_models/models_def.py index 370f1af82..801adced0 100644 --- a/modules/video_models/models_def.py +++ b/modules/video_models/models_def.py @@ -723,6 +723,30 @@ try: image_hijack=False, vae_hijack=False, vae_remote=False), + Model(name='MiniMax H3 Pruned SDNQ uint8', + url='https://huggingface.co/MiniMaxAI/MiniMax-H3', + repo='OzzyGT/MiniMax_H3_sdnq_8bit_pruned', + repo_cls='MiniMaxH3ModularPipeline', + workflow='fl2va', + base=True, + te_cls=None, + dit_cls=None, + te_hijack=False, + image_hijack=False, + vae_hijack=False, + vae_remote=False), + Model(name='MiniMax H3 Pruned SDNQ uint8 Ref2VA', + url='https://huggingface.co/MiniMaxAI/MiniMax-H3', + repo='OzzyGT/MiniMax_H3_sdnq_8bit_pruned', + repo_cls='MiniMaxH3ModularPipeline', + workflow='ref2va', + base=True, + te_cls=None, + dit_cls=None, + te_hijack=False, + image_hijack=False, + vae_hijack=False, + vae_remote=False), Model(name='MiniMax H3', url='https://huggingface.co/MiniMaxAI/MiniMax-H3', repo='MiniMaxAI/MiniMax-H3', diff --git a/pipelines/generic_transformer.py b/pipelines/generic_transformer.py index 7b1900eaf..801431e15 100644 --- a/pipelines/generic_transformer.py +++ b/pipelines/generic_transformer.py @@ -48,6 +48,9 @@ def load_transformer( modules_to_not_convert = [] if modules_dtype_dict is None: modules_dtype_dict = {} + if cls_name is None: + from diffusers import AutoModel + cls_name = AutoModel offline_args = {'local_files_only': True} if shared.opts.offline_mode else {} jobid = shared.state.begin('Load DiT') try: @@ -75,11 +78,14 @@ def load_transformer( if trust_remote_code: load_args['trust_remote_code'] = True load_kwargs = {**load_args, **quant_args, **offline_args, **kwargs} - return cls_name.from_pretrained( + module = cls_name.from_pretrained( repo_id, cache_dir=shared.opts.hfcache_dir, **load_kwargs, ) + if cls_name.__name__ == 'AutoModel': + log.debug(f'Load model: transformer="{repo_id}" cls={module.__class__.__name__}') + return module local_file = None override_name = None @@ -158,13 +164,11 @@ def load_transformer( **load_kwargs, ) - # 4. default loading from diffusers repo (also the fallback when an - # incompatible override is dropped above) + # 4. default loading from local file (also the fallback when an incompatible override is dropped above) # 5. default loading from diffusers repo (also the fallback when an incompatible override is dropped above) else: transformer = load_from_repo() - # mark the dropdown selection as loaded so the slot's onchange callback - # does not force a redundant full reload for an already-consumed override + # mark the dropdown selection as loaded so the slot's onchange callback, does not force a redundant full reload for an already-consumed override if transformer is not None and override_name is not None and getattr(shared.opts, override_opt, None) == override_name: setattr(sd_unet, tracker_attr, override_name) @@ -192,8 +196,7 @@ def load_transformer( log.debug(f'Load model: transformer="{repo_id}" quant="{quant_type}" size={module_size:.3f} params={param_num:.3f} memory={module_memory}') try: - # quantized models legitimately report the storage dtype (e.g. fp8 comfy_quant - # adopted via SDNQ); the compute dtype lives in the dequantizers, not the params + # quantized models legitimately report the storage dtype (e.g. fp8 comfy_quant adopted via SDNQ); the compute dtype lives in the dequantizers, not the params if getattr(transformer, 'quantization_config', None) is None: actual_dtype = transformer.dtype if isinstance(actual_dtype, torch.dtype) and isinstance(dtype, torch.dtype) and actual_dtype != dtype: diff --git a/pipelines/minimax/minimax_latents.py b/pipelines/minimax/minimax_latents.py index 576befc37..fe146fa90 100644 --- a/pipelines/minimax/minimax_latents.py +++ b/pipelines/minimax/minimax_latents.py @@ -1,7 +1,14 @@ import diffusers +from modules.logger import log + + +warned = False def unpack_latents(latents, components: diffusers.modular_pipelines.ModularPipeline, state: diffusers.modular_pipelines.BlockState): + global warned # pylint: disable=global-statement + if warned: + return latents from diffusers.modular_pipelines.minimax_h3.modular_pipeline import align_num_frames, video_latent_num_frames from modules import processing_callbacks frames = getattr(processing_callbacks.p, 'frames', 1) @@ -9,29 +16,35 @@ def unpack_latents(latents, components: diffusers.modular_pipelines.ModularPipel height = getattr(processing_callbacks.p, 'height', 1024) if frames <= 0 or width <= 0 or height <= 0: return latents - num_frames = align_num_frames(frames, components.vae_frames_per_chunk, components.vae_latents_per_chunk) - num_latent_frames = video_latent_num_frames(num_frames, components.vae_frames_per_chunk, components.vae_latents_per_chunk) - latent_height = height // components.vae_spatial_compression_ratio - latent_width = width // components.vae_spatial_compression_ratio - patch_t, patch_h, patch_w = components.patch_size - channels = components.vae_latent_channels - rows = state.latents[state.num_condition_video_rows :] - rows = rows.reshape( - -1, - num_latent_frames // patch_t, - latent_height // patch_h, - latent_width // patch_w, - channels, - patch_t, - patch_h, - patch_w, - ) - rows = rows.permute(0, 4, 1, 5, 2, 6, 3, 7) - latents = rows.reshape( - -1, - channels, - num_latent_frames, - latent_height, - latent_width, - ).contiguous() + try: + num_frames = align_num_frames(frames, components.vae_frames_per_chunk, components.vae_latents_per_chunk) + num_latent_frames = video_latent_num_frames(num_frames, components.vae_frames_per_chunk, components.vae_latents_per_chunk) + latent_height = height // components.vae_spatial_compression_ratio + latent_width = width // components.vae_spatial_compression_ratio + patch_t, patch_h, patch_w = components.patch_size + channels = components.vae_latent_channels + rows = state.latents[state.num_condition_video_rows :] + rows = rows.reshape( + -1, + num_latent_frames // patch_t, + latent_height // patch_h, + latent_width // patch_w, + channels, + patch_t, + patch_h, + patch_w, + ) + rows = rows.permute(0, 4, 1, 5, 2, 6, 3, 7) + latents = rows.reshape( + -1, + channels, + num_latent_frames, + latent_height, + latent_width, + ).contiguous() + except Exception as e: + # fails with sliced attention due to shape mismatch as state.latents only contains a subset of the full video latents + if not warned: + warned = True + log.warning(f'Video unpack latents: {e}') return latents diff --git a/scripts/dlss_ext.py b/scripts/dlss_ext.py index fc0a96053..404c81b88 100644 --- a/scripts/dlss_ext.py +++ b/scripts/dlss_ext.py @@ -105,7 +105,6 @@ def verify(pkg_path): return { 'error': 'package path not found' } if not c.controller.get_python(pkg_path): return { 'error': 'python not found in package path' } - shared.opts.dlss_pkg_path = pkg_path response = c.controller.call(pkg_path, 'verify', { 'gpu_uuid': 'auto', 'options': { 'level': 'deep' } }) if response.get('status') != 'ok': error = response.get('error') or {} @@ -121,6 +120,8 @@ def verify(pkg_path): else: checks['failed'] += 1 log.error(f'DLSS : {check}') + shared.opts.dlss_pkg_path = pkg_path + shared.opts.save() log.debug(f'DLSS: gpu={report.get("gpu", "unknown")} checks={checks}') return report @@ -281,12 +282,18 @@ def dlss(p: processing.StableDiffusionProcessing | None, pp: processing.Processe ss_scale_factor = float(getattr(p, 'ss_scale_factor', ss_scale_factor)) fg_source_fps = str(getattr(p, 'fg_source_fps', fg_source_fps)) fg_target_fps = str(getattr(p, 'fg_target_fps', fg_target_fps)) + if (p is not None) and ('video' in p.ops): # should not add video frames + nr_append = False + ss_append = False images = [] originals = [] current_images = inputs t = timer.Timer() + jobid = shared.state.begin('DLSS') + t_start = time.time() + if ss_enabled: t0 = time.time() if p: @@ -332,6 +339,9 @@ def dlss(p: processing.StableDiffusionProcessing | None, pp: processing.Processe current_images = output t.ts('framegen', t0) + shared.state.end(jobid) + timer.process.ts('dlss', t_start) + log.debug(f'DLSS: frames={len(images)} {t.summary(min_time=0)}') if update == 'images': pp.images = images