refactor modular

Signed-off-by: Vladimir Mandic <mandic00@live.com>
This commit is contained in:
Vladimir Mandic
2026-08-12 14:24:09 +02:00
parent 6ec06f7019
commit 3f69bee9dd
22 changed files with 268 additions and 203 deletions
+4 -4
View File
@@ -248,14 +248,14 @@ timer.startup.record("cv2")
class _tqdm_cls:
def __call__(self, *args, **kwargs):
bar_format = 'Progress {rate_fmt}{postfix} {bar} {percentage:3.0f}% {n_fmt}/{total_fmt} {elapsed} {remaining} ' + '\x1b[38;5;71m' + '{desc}' + '\x1b[0m'
return tqdm_lib.tqdm(*args, bar_format=bar_format, ncols=80, colour='#327fba', **kwargs)
bar_format = 'Progress {rate_fmt}{postfix} {bar:15} {percentage:3.0f}% {n_fmt}/{total_fmt} {elapsed} {remaining} ' + '\x1b[38;5;71m' + '{desc}' + '\x1b[0m'
return tqdm_lib.tqdm(*args, bar_format=bar_format, ncols=120, colour='#327fba', **kwargs)
class _tqdm_old(tqdm_lib.tqdm):
def __init__(self, *args, **kwargs):
kwargs.pop("name", None)
kwargs['bar_format'] = 'Progress {rate_fmt}{postfix} {bar} {percentage:3.0f}% {n_fmt}/{total_fmt} {elapsed} {remaining} ' + '\x1b[38;5;71m' + '{desc}' + '\x1b[0m'
kwargs['ncols'] = 80
kwargs['bar_format'] = 'Progress {rate_fmt}{postfix} {bar:15} {percentage:3.0f}% {n_fmt}/{total_fmt} {elapsed} {remaining} ' + '\x1b[38;5;71m' + '{desc}' + '\x1b[0m'
kwargs['ncols'] = 120
super().__init__(*args, **kwargs)
try:
+1 -1
View File
@@ -291,7 +291,7 @@ def setup_logging(debug=None, trace=None, filename=None):
if os.environ.get('SD_TRANSFORMERS_DEBUG', None) is not None:
logging.getLogger("transformers").setLevel(logging.DEBUG)
else:
logging.getLogger("transformers").setLevel(logging.WARNING)
logging.getLogger("transformers").setLevel(logging.ERROR)
if os.environ.get('SD_TORCH_DEBUG', None) is not None:
logging.getLogger("torch").setLevel(logging.DEBUG)
else:
@@ -5,7 +5,88 @@ from modules import shared, errors, devices
from modules.logger import log
MIN_LATENT_FRAMES = 7 # decoder floor: fewer latent frames leave the chunked decode with nothing to emit
class InterruptLogFilter(logging.Filter):
"""Drops the per-block error dumps the modular runner logs when an interrupt raises through it."""
def filter(self, record):
return 'Interrupted...' not in record.msg
def apply_progress_bar_config(block):
kwargs = {
"ncols": 120,
"colour": "#327fba",
"bar_format": "Progress {rate_fmt}{postfix} {bar:15} {percentage:3.0f}% {n_fmt}/{total_fmt} {elapsed} {remaining} {desc}",
}
if hasattr(block, "set_progress_bar_config"):
block.set_progress_bar_config(**kwargs)
for child in getattr(block, "sub_blocks", {}).values():
apply_progress_bar_config(child)
def install_state_hook(pipe):
runner_log = logging.getLogger('diffusers.modular_pipelines.modular_pipeline')
if not any(isinstance(f, InterruptLogFilter) for f in runner_log.filters):
runner_log.addFilter(InterruptLogFilter())
def set_phase(phase: str, module: torch.nn.Module | None = None):
# every stage runs inside one pipeline call, so the forward hooks are the only place the current stage is visible; state.begin clears the label per job
if getattr(pipe, 'sdnext_phase', None) != phase:
pipe.sdnext_phase = phase
jobid = getattr(pipe, 'sdnext_phaseid', None)
shared.state.end(jobid)
pipe.sdnext_phaseid = shared.state.begin(phase)
log.debug(f'Pipeline: phase={phase} cls={pipe.__class__.__name__} module={module.__class__.__name__ if module is not None else None}')
def _pre_transformer_hook(module, args): # pylint: disable=unused-argument
set_phase('Generate', module)
if shared.state.sampling_steps == 0 and getattr(pipe, 'num_timesteps', 0) > 0:
shared.state.sampling_steps = pipe.num_timesteps
if shared.state.paused:
log.debug('Sampling paused')
while shared.state.paused:
if shared.state.interrupted or shared.state.skipped:
raise AssertionError('Interrupted...')
time.sleep(0.1)
shared.state.step()
if shared.state.interrupted or shared.state.skipped:
raise AssertionError('Interrupted...')
def _pre_text_encode_hook(module, args): # pylint: disable=unused-argument
set_phase('TextEncode', module)
if shared.state.interrupted or shared.state.skipped:
raise AssertionError('Interrupted...')
def _pre_vae_decode_hook(module, args): # pylint: disable=unused-argument
set_phase('Decode', module)
if shared.state.interrupted or shared.state.skipped: # fires per tile, so tiled decodes abort promptly
raise AssertionError('Interrupted...')
def _pre_vae_encode_hook(module, args): # pylint: disable=unused-argument
set_phase('Encode', module)
if shared.state.interrupted or shared.state.skipped: # fires per tile, so tiled encodes abort promptly
raise AssertionError('Interrupted...')
for name in ('transformer', 'transformer_ref'):
module = getattr(pipe, name, None)
if module is not None:
target = getattr(module, 'model', module) # conditioning calls the inner model directly
if isinstance(target, torch.nn.Module) and getattr(target, 'sdnext_state_hook', None) is None:
target.sdnext_state_hook = target.register_forward_pre_hook(_pre_transformer_hook)
for name in ('text_encoder', 'text_encoder_2'):
module = getattr(pipe, name, None)
if module is not None:
target = getattr(module, 'model', module) # conditioning calls the inner model directly
if isinstance(target, torch.nn.Module) and getattr(target, 'sdnext_state_hook', None) is None:
target.sdnext_state_hook = target.register_forward_pre_hook(_pre_text_encode_hook)
for name in ('vae', 'audio_vae'):
decoder = getattr(getattr(pipe, name, None), 'decoder', None) # decode entry points bypass forward, the inner decoder does not
if isinstance(decoder, torch.nn.Module) and getattr(decoder, 'sdnext_state_hook', None) is None:
decoder.sdnext_state_hook = decoder.register_forward_pre_hook(_pre_vae_decode_hook)
encoder = getattr(getattr(pipe, name, None), 'encoder', None) # decode entry points bypass forward, the inner encoder does not
if isinstance(encoder, torch.nn.Module) and getattr(encoder, 'sdnext_state_hook', None) is None:
encoder.sdnext_state_hook = encoder.register_forward_pre_hook(_pre_vae_encode_hook)
def is_modular(obj) -> bool:
@@ -110,157 +191,16 @@ def load_modular_pipe(repo_cls, repo: str, workflow: str | None = None, revision
missing = missing_components(pipe, workflow)
pipe.sdnext_missing_components = missing # a caller that can recover a component clears its own entry
pipe.sdnext_video_workflow = workflow # the workflow this pipe was loaded for, which is what the reference-workflow guard reads; the executed task is chosen per request
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.info(f'Load modular: cls={pipe.__class__.__name__} workflow={workflow} components={loaded} empty={empty} time={time.time()-t0:.2f}')
if missing:
# load_components builds each component in its own try/except and reports a failure as a warning on the
# diffusers logger, so the reason is in the log above this line rather than in the exception path
log.error(f'Load modular: cls={pipe.__class__.__name__} workflow={workflow} missing={missing} components the workflow requires did not load')
install_state_hook(pipe)
apply_progress_bar_config(pipe._blocks) # pylint: disable=protected-access
return pipe
except Exception as e:
log.error(f'Load modular: repo="{repo}" workflow={workflow} {e}')
errors.display(e, 'video')
return None
def load_modular(selected, offline_args: dict):
return load_modular_pipe(selected.repo_cls, selected.repo, workflow=selected.workflow, revision=selected.repo_revision, offline_args=offline_args, base=selected.base)
def apply_minimax_overrides(p, pipe, still: bool = False, audio: bool = True):
"""Per-generation constraints shared by the video tab and the image path: canvas and frame
alignment, the bespoke scheduler guard, tiling, and the audio/still toggles."""
if still:
audio = False # a sub-second soundtrack is pure waste on a kept single frame
multiple = pipe.canvas_multiple
p.task_args['width'] = multiple * (p.width // multiple)
p.task_args['height'] = multiple * (p.height // multiple)
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'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
frames += 1
max_frames = int(pipe.max_duration * pipe.fps)
while frames > max_frames:
frames -= pipe.vae_frames_per_chunk
if frames != getattr(p, 'frames', None):
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'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)
p.task_args['output'] = ['videos', 'audio', 'sampling_rate'] if audio else ['videos']
p.task_args['output_type'] = 'pil' # the image path otherwise requests latent output, which the decode block rejects
p.video_still = still
def set_still(pipe, enabled: bool = True):
"""Toggle sub-floor generation for single-frame output. The duration floor is lifted only
while the instance flag is set, so other pipes of the class and later normal runs keep the
supported floor; decoded latents below the decoder floor are padded by duplicating the
trailing latent. The causal VAE keeps padding out of frame 0."""
cls = type(pipe)
if getattr(cls, 'sdnext_min_duration_orig', None) is None:
orig = cls.min_duration
cls.sdnext_min_duration_orig = orig
cls.min_duration = property(lambda self: 0.0 if getattr(self, 'sdnext_still_mode', False) else orig.fget(self))
pipe.sdnext_still_mode = enabled
if not enabled:
return
vae = getattr(pipe, 'vae', None)
if vae is not None and getattr(vae, 'sdnext_orig_decode', None) is None:
vae.sdnext_orig_decode = vae.decode
def padded_decode(z, *args, **kwargs):
if z.ndim == 5 and z.shape[2] < MIN_LATENT_FRAMES:
pad = z[:, :, -1:].repeat(1, 1, MIN_LATENT_FRAMES - z.shape[2], 1, 1)
z = torch.cat([z, pad], dim=2)
return vae.sdnext_orig_decode(z, *args, **kwargs)
vae.decode = padded_decode
def set_audio(pipe, enabled: bool):
"""Pop or restore the audio decode block. The joint denoise still carries the audio rows
(a few percent of the sequence), but without the block the audio VAE never runs.
Operates on the backing block tree: the public blocks property deep-copies per access."""
blocks = getattr(pipe, '_blocks', None) # pylint: disable=protected-access
decode = blocks.sub_blocks.get('decode', None) if blocks is not None and hasattr(blocks, 'sub_blocks') else None
sub = getattr(decode, 'sub_blocks', None)
if sub is None:
return
if enabled and 'audio' not in sub:
stashed = getattr(pipe, 'sdnext_audio_decode_block', None)
if stashed is not None:
sub.insert('audio', stashed, len(sub))
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'Pipeline: cls={pipe.__class__.__name__} audio=disabled')
class InterruptLogFilter(logging.Filter):
"""Drops the per-block error dumps the modular runner logs when an interrupt raises through it."""
def filter(self, record):
return 'Interrupted...' not in record.getMessage()
def install_state_hook(pipe):
runner_log = logging.getLogger('diffusers.modular_pipelines.modular_pipeline')
if not any(isinstance(f, InterruptLogFilter) for f in runner_log.filters):
runner_log.addFilter(InterruptLogFilter())
def set_phase(phase: str):
# every stage runs inside one pipeline call, so the forward hooks are the only
# place the current stage is visible; state.begin clears the label per job
if getattr(pipe, 'sdnext_phase', None) != phase:
pipe.sdnext_phase = phase
shared.state.textinfo = phase
log.debug(f'Pipeline: cls={pipe.__class__.__name__} phase={phase}')
def state_hook(module, args): # pylint: disable=unused-argument
set_phase('Generate')
if shared.state.sampling_steps == 0 and getattr(pipe, 'num_timesteps', 0) > 0:
shared.state.sampling_steps = pipe.num_timesteps
if shared.state.paused:
log.debug('Sampling paused')
while shared.state.paused:
if shared.state.interrupted or shared.state.skipped:
raise AssertionError('Interrupted...')
time.sleep(0.1)
shared.state.step()
if shared.state.interrupted or shared.state.skipped:
raise AssertionError('Interrupted...')
def encode_hook(module, args): # pylint: disable=unused-argument
set_phase('TextEncode')
if shared.state.interrupted or shared.state.skipped:
raise AssertionError('Interrupted...')
def decode_hook(module, args): # pylint: disable=unused-argument
set_phase('Decode')
if shared.state.interrupted or shared.state.skipped: # fires per tile, so tiled decodes abort promptly
raise AssertionError('Interrupted...')
for name in ('transformer', 'transformer_ref'):
module = getattr(pipe, name, None)
if module is None or getattr(module, 'sdnext_state_hook', None) is not None:
continue
module.sdnext_state_hook = module.register_forward_pre_hook(state_hook)
text_encoder = getattr(pipe, 'text_encoder', None)
if text_encoder is not None:
target = getattr(text_encoder, 'model', text_encoder) # conditioning calls the inner model directly
if isinstance(target, torch.nn.Module) and getattr(target, 'sdnext_state_hook', None) is None:
target.sdnext_state_hook = target.register_forward_pre_hook(encode_hook)
for name in ('vae', 'audio_vae'):
decoder = getattr(getattr(pipe, name, None), 'decoder', None) # decode entry points bypass forward, the inner decoder does not
if isinstance(decoder, torch.nn.Module) and getattr(decoder, 'sdnext_state_hook', None) is None:
decoder.sdnext_state_hook = decoder.register_forward_pre_hook(decode_hook)
+1 -1
View File
@@ -33,7 +33,7 @@ class UpscalerDiffusion(Upscaler):
else:
model = diffusers.DiffusionPipeline.from_pretrained(scaler.data_path, cache_dir=shared.opts.diffusers_dir, torch_dtype=devices.dtype)
if hasattr(model, "set_progress_bar_config"):
model.set_progress_bar_config(bar_format='Progress {rate_fmt}{postfix} {bar} {percentage:3.0f}% {n_fmt}/{total_fmt} {elapsed} {remaining} ' + '\x1b[38;5;71m' + 'Upscale', ncols=80, colour='#327fba')
model.set_progress_bar_config(bar_format='Progress {rate_fmt}{postfix} {bar:15} {percentage:3.0f}% {n_fmt}/{total_fmt} {elapsed} {remaining} ' + '\x1b[38;5;71m' + 'Upscale', ncols=120, colour='#327fba')
set_diffuser_options(model, vae=None, op='upscaler')
self.models[path] = model
return self.models[path]
+2 -1
View File
@@ -428,7 +428,8 @@ def print_stats():
from modules.sd_models_compile import update_compile_times
update_compile_times()
dynamo_dct = timer.dynamo.dct(min_time=2.0, no_total=True)
dynamo_dct = timer.dynamo.dct(min_time=0.5, no_total=True)
timer.dynamo.reset()
if dynamo_dct:
log.debug(f'Processed: dynamo={dynamo_dct}')
+17 -4
View File
@@ -183,8 +183,21 @@ def get_defaults(model, kwargs):
remove = ['return_dict', 'output_type', 'num_images_per_prompt', 'callback', 'callback_on_step_end_tensor_inputs']
default_cfg = 0
try:
signature = inspect.signature(type(model).__call__, follow_wrapped=True)
defaults = {k: v.default for k, v in signature.parameters.items() if v.default is not inspect.Parameter.empty and v.default is not None} # get all defaults
defaults = {}
if hasattr(model, 'blocks') and hasattr(model.blocks, 'inputs'):
for input_param in model.blocks.inputs:
if input_param.name is None:
continue
if input_param.default is None:
continue
if input_param.name in kwargs or input_param.name in remove:
continue
defaults[input_param.name] = input_param.default
if not defaults:
signature = inspect.signature(type(model).__call__, follow_wrapped=True)
defaults = {k: v.default for k, v in signature.parameters.items() if v.default is not inspect.Parameter.empty and v.default is not None} # get all defaults
defaults = {k: v for k, v in defaults.items() if k not in kwargs} # only log defaults that are not already set by kwargs
defaults = {k: v for k, v in defaults.items() if k not in remove} # remove common args that are not useful to log
log.debug(f'Pipeline: cls={model.__class__.__name__} defaults={defaults}')
@@ -222,9 +235,9 @@ def set_pipeline_args(p, model, prompts:list, negative_prompts:list, prompts_2:l
model.register_to_config(boundary_ratio=boundary_target)
if hasattr(model, "set_progress_bar_config"):
if disable_pbar:
model.set_progress_bar_config(bar_format='Progress {rate_fmt}{postfix} {bar} {percentage:3.0f}% {n_fmt}/{total_fmt} {elapsed} {remaining} ' + '\x1b[38;5;71m' + desc, ncols=80, colour='#327fba', disable=disable_pbar)
model.set_progress_bar_config(bar_format='Progress {rate_fmt}{postfix} {bar:15} {percentage:3.0f}% {n_fmt}/{total_fmt} {elapsed} {remaining} ' + '\x1b[38;5;71m' + desc, ncols=120, colour='#327fba', disable=disable_pbar)
else:
model.set_progress_bar_config(bar_format='Progress {rate_fmt}{postfix} {bar} {percentage:3.0f}% {n_fmt}/{total_fmt} {elapsed} {remaining} ' + '\x1b[38;5;71m' + desc, ncols=80, colour='#327fba')
model.set_progress_bar_config(bar_format='Progress {rate_fmt}{postfix} {bar:15} {percentage:3.0f}% {n_fmt}/{total_fmt} {elapsed} {remaining} ' + '\x1b[38;5;71m' + desc, ncols=120, colour='#327fba')
possible = get_params(model)
+6 -2
View File
@@ -76,6 +76,7 @@ def process_pre(p: processing.StableDiffusionProcessing):
log.info('Processing modifiers: apply')
try:
# apply-with-unapply
# sd_hijack_compile.install()
sd_models_compile.check_deepcache(enable=True)
ipadapter.apply(shared.sd_model, p)
token_merge.apply_token_merging(shared.sd_model)
@@ -235,6 +236,9 @@ def process_base(p: processing.StableDiffusionProcessing):
if hasattr(shared.sd_model, 'postprocess') and callable(shared.sd_model.postprocess):
output = shared.sd_model.postprocess(p, output)
if hasattr(shared.sd_model, 'sdnext_phaseid'):
shared.state.end(shared.sd_model.sdnext_phaseid)
shared.sd_model.sdnext_phaseid = None
shared.state.end(jobid)
shared.state.nextjob()
return output
@@ -530,8 +534,8 @@ def update_pipeline(sd_model, p: processing.StableDiffusionProcessing):
updated_model = sd_model
if 'MiniMaxH3' in sd_model.__class__.__name__ and not isinstance(p, processing.StableDiffusionProcessingVideo):
# image tabs run the model in still mode; the video tab applies its own overrides
from modules.video_models import video_modular
video_modular.apply_minimax_overrides(p, sd_model, still=True, audio=False)
from modules.video_models import video_minimax
video_minimax.apply_overrides(p, sd_model, still=True, audio=False)
if getattr(p, 'detailer_enabled', False):
log.warning(f'Processing: cls={sd_model.__class__.__name__} detailer not supported')
p.detailer_enabled = False
+31
View File
@@ -0,0 +1,31 @@
import time
import logging
from modules.timer import dynamo
fn = None
ts = None
class CompilationLogInterceptor(logging.Handler):
def emit(self, record):
try:
global fn, ts # pylint: disable=global-statement
if 'torchdynamo start tracing' in record.msg:
fn = record.msg.split('torchdynamo start tracing')[-1].strip() # extract first string after 'torchdynamo start tracing' and start timer
fn = fn.split(' ')[0] # extract first word after 'torchdynamo start tracing'
ts = time.time()
if 'run_gc_after_compile' in record.msg:
if fn is not None:
dynamo.ts(fn, ts) # log the time taken for compilation
fn = None
except Exception:
pass
def install():
dynamo_logger = logging.getLogger("torch._dynamo")
dynamo_logger.setLevel(logging.INFO)
if not any(isinstance(h, CompilationLogInterceptor) for h in dynamo_logger.handlers):
dynamo_interceptor = CompilationLogInterceptor()
dynamo_logger.addHandler(dynamo_interceptor)
+5 -3
View File
@@ -1,5 +1,6 @@
import os
import time
import tqdm
from modules.logger import log
@@ -54,9 +55,10 @@ 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)
bar_format = 'Download {percentage:3.0f}% {bar:15} {n:.1f}/{total:.1f}{postfix} {elapsed} {remaining} ' + '\x1b[38;5;71m' + '{desc}' + '\x1b[0m'
kwargs['_tqdm_bar'] = tqdm.tqdm(*args, bar_format=bar_format, colour='#327fba', unit='MiB', unit_scale=1/(1024*1024), desc=fn, total=size, ncols=120)
res = orig_xet_get(*args, **kwargs)
debug(f'Download end: type=xet res={res}')
state.end(jobid)
+1 -1
View File
@@ -893,7 +893,7 @@ def set_defaults(sd_model, checkpoint_info: CheckpointInfo):
sd_model.logvar = sd_model.logvar.to(devices.device) if hasattr(sd_model, 'logvar') else None # fix for training
shared.opts.data["sd_checkpoint_hash"] = checkpoint_info.sha256
if hasattr(sd_model, "set_progress_bar_config"):
sd_model.set_progress_bar_config(bar_format='Progress {rate_fmt}{postfix} {bar} {percentage:3.0f}% {n_fmt}/{total_fmt} {elapsed} {remaining}', ncols=80, colour='#327fba')
sd_model.set_progress_bar_config(bar_format='Progress {rate_fmt}{postfix} {bar:15} {percentage:3.0f}% {n_fmt}/{total_fmt} {elapsed} {remaining}', ncols=120, colour='#327fba')
def load_diffuser(checkpoint_info: CheckpointInfo | None = None, op='model', revision=None): # pylint: disable=unused-argument
-11
View File
@@ -391,17 +391,6 @@ def update_compile_times():
try:
times = [float(t.strip()) for t in parts[1:] if t.strip()]
if times:
# parsed.append((fn, sum(times), len(times), max(times)))
dynamo.add(fn, round(sum(times), 2))
except ValueError:
continue
"""
parsed.sort(key=lambda x: x[1], reverse=True)
results = {}
min_time = 0.1
for fn, total, count, max_val in parsed:
if total > min_time:
dynamo.ts(fn, total)
results[fn] = { "total": round(total, 2), "count": count, "avg": round(total / count, 2), "max": round(max_val, 2) }
return results
"""
+1 -1
View File
@@ -164,7 +164,7 @@ def apply_group_offload_component(module, module_name: str, main: bool) -> bool:
module = accelerate.hooks.remove_hook_from_module(module, recurse=True)
remove_group_offload_component(module)
module.requires_grad_(False)
log.debug(f'Offload: type=group op=apply module={module_name} pin={cfg["use_stream"] and not cfg["low_cpu_mem_usage"]}') # before the apply: pinning large components takes a while and would otherwise run silently
log.debug(f'Offload: type=group op=apply type={shared.opts.group_offload_type} module={module_name} pin={cfg["use_stream"] and not cfg["low_cpu_mem_usage"]}') # before the apply: pinning large components takes a while and would otherwise run silently
apply_group_offloading(module, onload_device=devices.device, offload_device=devices.cpu, **cfg)
module.sdnext_group_offload_sig = sig
return True
+3 -3
View File
@@ -21,12 +21,12 @@ def get_default_modes(cmd_opts, mem_stat):
cmd_opts.medvram = True # VAE Tiling and other stuff
default_offload_mode = "balanced"
default_diffusers_offload_min_gpu_memory = 0
default_diffusers_offload_always = ', '.join(['T5EncoderModel', 'UMT5EncoderModel'])
default_diffusers_offload_always = ''
log.info(f"Device detect: memory={gpu_memory:.1f} default=balanced optimization=medvram")
elif gpu_memory >= 24:
elif gpu_memory >= 22:
default_offload_mode = "balanced"
default_diffusers_offload_max_gpu_memory = 0.8
default_diffusers_offload_always = ', '.join(['T5EncoderModel', 'UMT5EncoderModel'])
default_diffusers_offload_always = ''
default_diffusers_offload_never = ', '.join(['CLIPTextModel', 'CLIPTextModelWithProjection', 'AutoencoderKL'])
log.info(f"Device detect: memory={gpu_memory:.1f} default=balanced optimization=highvram")
else:
+8 -7
View File
@@ -4,9 +4,9 @@ import copy
import time
import transformers
import diffusers
from modules import shared, errors, sd_models, sd_checkpoint, model_quant, devices, sd_hijack_te, sd_hijack_vae
from modules import shared, errors, sd_models, sd_checkpoint, model_quant, devices, sd_hijack_te, sd_hijack_vae, modular_load
from modules.logger import log
from modules.video_models import models_def, video_utils, video_overrides, video_cache, video_modular
from modules.video_models import models_def, video_utils, video_overrides, video_cache
def _loader(component):
@@ -151,8 +151,9 @@ def load_model(selected: models_def.Model):
# model
try:
if selected.workflow is not None or video_modular.is_modular(selected.repo_cls):
shared.sd_model = video_modular.load_modular(selected, offline_args)
if selected.workflow is not None or modular_load.is_modular(selected.repo_cls):
from modules.modular_load import load_modular_pipe
return load_modular_pipe(selected.repo_cls, selected.repo, workflow=selected.workflow, revision=selected.repo_revision, offline_args=offline_args, base=selected.base)
elif selected.repo_cls is None:
shared.sd_model = load_custom(selected.repo)
else:
@@ -208,12 +209,12 @@ def load_model(selected: models_def.Model):
shared.sd_model.vae.enable_tiling()
tiling = True
if hasattr(shared.sd_model, "set_progress_bar_config"):
shared.sd_model.set_progress_bar_config(bar_format='Progress {rate_fmt}{postfix} {bar} {percentage:3.0f}% {n_fmt}/{total_fmt} {elapsed} {remaining} ' + '\x1b[38;5;71m', ncols=80, colour='#327fba')
shared.sd_model.set_progress_bar_config(bar_format='Progress {rate_fmt}{postfix} {bar:15} {percentage:3.0f}% {n_fmt}/{total_fmt} {elapsed} {remaining} ' + '\x1b[38;5;71m', ncols=120, colour='#327fba')
shared.sd_model = model_quant.do_post_load_quant(shared.sd_model, allow=False)
sd_models.set_diffuser_offload(shared.sd_model)
if video_modular.is_modular(shared.sd_model):
video_modular.install_state_hook(shared.sd_model)
if modular_load.is_modular(shared.sd_model):
modular_load.install_state_hook(shared.sd_model)
loaded_model = selected.name
msg = f'Load video: cls={shared.sd_model.__class__.__name__} model="{selected.name}" time={t1-t0:.2f}'
+84
View File
@@ -0,0 +1,84 @@
import torch
from modules.logger import log
MIN_LATENT_FRAMES = 7 # decoder floor: fewer latent frames leave the chunked decode with nothing to emit
def apply_overrides(p, pipe, still: bool = False, audio: bool = True):
"""Per-generation constraints shared by the video tab and the image path: canvas and frame
alignment, the bespoke scheduler guard, tiling, and the audio/still toggles."""
if still:
audio = False # a sub-second soundtrack is pure waste on a kept single frame
multiple = pipe.canvas_multiple
p.task_args['width'] = multiple * (p.width // multiple)
p.task_args['height'] = multiple * (p.height // multiple)
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'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
frames += 1
max_frames = int(pipe.max_duration * pipe.fps)
while frames > max_frames:
frames -= pipe.vae_frames_per_chunk
if frames != getattr(p, 'frames', None):
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'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)
p.task_args['output'] = ['videos', 'audio', 'sampling_rate'] if audio else ['videos']
p.task_args['output_type'] = 'pil' # the image path otherwise requests latent output, which the decode block rejects
p.video_still = still
def set_still(pipe, enabled: bool = True):
"""Toggle sub-floor generation for single-frame output. The duration floor is lifted only
while the instance flag is set, so other pipes of the class and later normal runs keep the
supported floor; decoded latents below the decoder floor are padded by duplicating the
trailing latent. The causal VAE keeps padding out of frame 0."""
cls = type(pipe)
if getattr(cls, 'sdnext_min_duration_orig', None) is None:
orig = cls.min_duration
cls.sdnext_min_duration_orig = orig
cls.min_duration = property(lambda self: 0.0 if getattr(self, 'sdnext_still_mode', False) else orig.fget(self))
pipe.sdnext_still_mode = enabled
if not enabled:
return
vae = getattr(pipe, 'vae', None)
if vae is not None and getattr(vae, 'sdnext_orig_decode', None) is None:
vae.sdnext_orig_decode = vae.decode
def padded_decode(z, *args, **kwargs):
if z.ndim == 5 and z.shape[2] < MIN_LATENT_FRAMES:
pad = z[:, :, -1:].repeat(1, 1, MIN_LATENT_FRAMES - z.shape[2], 1, 1)
z = torch.cat([z, pad], dim=2)
return vae.sdnext_orig_decode(z, *args, **kwargs)
vae.decode = padded_decode
def set_audio(pipe, enabled: bool):
"""Pop or restore the audio decode block. The joint denoise still carries the audio rows
(a few percent of the sequence), but without the block the audio VAE never runs.
Operates on the backing block tree: the public blocks property deep-copies per access."""
blocks = getattr(pipe, '_blocks', None) # pylint: disable=protected-access
decode = blocks.sub_blocks.get('decode', None) if blocks is not None and hasattr(blocks, 'sub_blocks') else None
sub = getattr(decode, 'sub_blocks', None)
if sub is None:
return
if enabled and 'audio' not in sub:
stashed = getattr(pipe, 'sdnext_audio_decode_block', None)
if stashed is not None:
sub.insert('audio', stashed, len(sub))
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'Pipeline: cls={pipe.__class__.__name__} audio=disabled')
+2 -2
View File
@@ -4,7 +4,6 @@ import diffusers
from modules import shared, processing, devices
from modules.logger import log
from modules.video_models.models_def import Model
from modules.video_models import video_modular
debug = log.trace if os.environ.get('SD_VIDEO_DEBUG', None) is not None else lambda *args, **kwargs: None
@@ -107,4 +106,5 @@ def set_overrides(p: processing.StableDiffusionProcessingVideo, selected: Model)
shared.sd_model.transformer.set_attention_backend("flex")
# MiniMax H3
if 'MiniMaxH3' in cls:
video_modular.apply_minimax_overrides(p, shared.sd_model, still=getattr(p, 'video_still', False), audio=getattr(p, 'video_audio', True))
from modules.video_models import video_minimax
video_minimax.apply_overrides(p, shared.sd_model, still=getattr(p, 'video_still', False), audio=getattr(p, 'video_audio', True))
+3 -3
View File
@@ -2,9 +2,9 @@ import os
import copy
import time
from dataclasses import dataclass
from modules import shared, errors, sd_models, processing, devices, images, ui_common, scripts_manager
from modules import shared, errors, sd_models, processing, devices, images, ui_common, scripts_manager, modular_load
from modules.logger import log
from modules.video_models import models_def, video_utils, video_load, video_vae, video_overrides, video_save, video_modular
from modules.video_models import models_def, video_utils, video_load, video_vae, video_overrides, video_save
from modules.paths import resolve_output_path
@@ -49,7 +49,7 @@ def resolve_model(engine: str | None, model: str | None) -> tuple[models_def.Mod
raise VideoError(f'no video model loaded: cls={cls} select engine and model or load a video-capable checkpoint first', 400)
pipe = shared.sd_model
workflow = getattr(pipe, 'sdnext_video_workflow', None)
if workflow is None and video_modular.is_modular(pipe):
if workflow is None and modular_load.is_modular(pipe):
workflow = models_def.workflow_for_class(cls) or 'auto' # modular pipes dispatch on inputs, so any workflow marker selects the modular branch
ckpt = getattr(pipe, 'sd_checkpoint_info', None)
selected = models_def.Model(
+5 -3
View File
@@ -11,7 +11,7 @@ from pipelines.generic_shared import shared_te_map
debug = os.environ.get('SD_LOAD_DEBUG', None) is not None
def get_shared(cls, repo_id, subfolder=None, variant=None):
def get_shared(cls, repo_id, subfolder=None, variant=None, shared_id: str | None = None):
args = {}
if variant is not None:
args['variant'] = variant
@@ -22,7 +22,8 @@ def get_shared(cls, repo_id, subfolder=None, variant=None):
if isinstance(identifiers, str):
identifiers = [identifiers]
identifiers = [identifier.lower() for identifier in identifiers if identifier is not None]
if item['cls'] == cls and (not identifiers or any(identifier in repo_id.lower() for identifier in identifiers)):
shared_id = shared_id or repo_id.lower()
if item['cls'] == cls and (not identifiers or any(identifier in shared_id for identifier in identifiers)):
if item.get('config_class', None) is not None and item.get('config_path', None) is not None:
with open(item['config_path'], encoding='utf8') as f:
args['config'] = item['config_class'](**json.load(f))
@@ -91,6 +92,7 @@ def load_text_encoder(
modules_to_not_convert=None,
modules_dtype_dict=None,
use_safetensors=True,
shared_id: str | None = None,
**kwargs):
if shared.state.interrupted:
@@ -146,7 +148,7 @@ def load_text_encoder(
# 3. load shared from repo
if allow_shared and (text_encoder is None):
log.debug(f'Load model: text_encoder="{repo_id}" cls={cls_name.__name__} quant="{quant_type}" loader={get_loader("transformers")}')
target_repo, extra_args = get_shared(cls_name, repo_id, subfolder=subfolder, variant=variant)
target_repo, extra_args = get_shared(cls_name, repo_id, subfolder=subfolder, variant=variant, shared_id=shared_id)
text_encoder = cls_name.from_pretrained(
target_repo,
cache_dir=shared.opts.hfcache_dir,
+6 -8
View File
@@ -4,7 +4,8 @@ from modules.logger import log
def load_minimax(checkpoint_info, diffusers_load_config=None): # pylint: disable=unused-argument
from modules.video_models import video_modular, video_load
from modules.video_models import video_load
from modules.modular_load import load_modular_pipe
repo_id = sd_models.path_to_repo(checkpoint_info)
sd_models.hf_auth_check(checkpoint_info)
if repo_id is None or repo_id.lower() == 'none':
@@ -14,7 +15,7 @@ def load_minimax(checkpoint_info, diffusers_load_config=None): # pylint: disable
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(
pipe = load_modular_pipe(
repo_cls,
repo_id,
workflow=workflow,
@@ -24,13 +25,10 @@ def load_minimax(checkpoint_info, diffusers_load_config=None): # pylint: disable
)
if pipe is None:
return None
missing = video_modular.missing_components(pipe, workflow)
if missing:
# a component that failed to build is unusable, and loading it by another route only defers the failure into generation as corrupt output
log.error(f'Load model: type=MiniMaxH3 repo="{repo_id}" workflow={workflow} missing={missing}')
return None
video_modular.install_state_hook(pipe)
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
video_load.loaded_model = None # image-path load invalidates the video tab's name cache
if hasattr(pipe, 'vae') and hasattr(pipe.vae, 'enable_tiling'):
pipe.vae.enable_tiling()
+1 -1
View File
@@ -266,7 +266,7 @@ def download_pretrained_from_url(
return download_target
with urllib.request.urlopen(url) as source, open(download_target, "wb") as output:
with tqdm(total=int(source.headers.get("Content-Length")), ncols=80, unit='iB', unit_scale=True) as loop:
with tqdm(total=int(source.headers.get("Content-Length")), ncols=120, unit='iB', unit_scale=True) as loop:
while True:
buffer = source.read(8192)
if not buffer:
+1 -1
View File
@@ -283,7 +283,7 @@ class StableDiffusionXLPuLIDPipeline:
debug(f'PulID embedding: cond={id_embedding.shape} uncond={uncond_id_embedding.shape}')
return uncond_id_embedding, id_embedding
def set_progress_bar_config(self, bar_format: str | None = None, ncols: int = 80, colour: str | None = None):
def set_progress_bar_config(self, bar_format: str | None = None, ncols: int = 120, colour: str | None = None):
import functools
from tqdm.auto import trange as trange_orig
import pulid_sampling