Merge branch 'vladmandic:dev' into dev

This commit is contained in:
resonantsky
2026-09-11 13:59:36 +02:00
committed by GitHub
10 changed files with 115 additions and 43 deletions
+5 -1
View File
@@ -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
+3 -3
View File
@@ -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):
+12 -1
View File
@@ -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='')
+6 -3
View File
@@ -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
+5 -1
View File
@@ -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}')
+1 -1
View File
@@ -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
+24
View File
@@ -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',
+10 -7
View File
@@ -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:
+38 -25
View File
@@ -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
+11 -1
View File
@@ -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