diff --git a/extensions-builtin/sdnext-modernui b/extensions-builtin/sdnext-modernui index e8fa7a839..6871752df 160000 --- a/extensions-builtin/sdnext-modernui +++ b/extensions-builtin/sdnext-modernui @@ -1 +1 @@ -Subproject commit e8fa7a839dcce6bf54d8fb06da0469a86f7745bb +Subproject commit 6871752dfdc373460e5feb13975ec7450df2eec4 diff --git a/modules/loader.py b/modules/loader.py index 4184840e7..275afadae 100644 --- a/modules/loader.py +++ b/modules/loader.py @@ -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: diff --git a/modules/logger.py b/modules/logger.py index 71214a40a..a5c817404 100644 --- a/modules/logger.py +++ b/modules/logger.py @@ -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: diff --git a/modules/video_models/video_modular.py b/modules/modular_load.py similarity index 57% rename from modules/video_models/video_modular.py rename to modules/modular_load.py index e272cb3e1..76bc28636 100644 --- a/modules/video_models/video_modular.py +++ b/modules/modular_load.py @@ -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) diff --git a/modules/postprocess/sdupscaler_model.py b/modules/postprocess/sdupscaler_model.py index a13cdf30d..01a6efc73 100644 --- a/modules/postprocess/sdupscaler_model.py +++ b/modules/postprocess/sdupscaler_model.py @@ -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] diff --git a/modules/processing.py b/modules/processing.py index f897dae82..2134a8ac5 100644 --- a/modules/processing.py +++ b/modules/processing.py @@ -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}') diff --git a/modules/processing_args.py b/modules/processing_args.py index 0755f169a..d6f427131 100644 --- a/modules/processing_args.py +++ b/modules/processing_args.py @@ -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) diff --git a/modules/processing_diffusers.py b/modules/processing_diffusers.py index 46b7d884b..d7361fc87 100644 --- a/modules/processing_diffusers.py +++ b/modules/processing_diffusers.py @@ -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 diff --git a/modules/sd_hijack_compile.py b/modules/sd_hijack_compile.py new file mode 100644 index 000000000..1b306c9ec --- /dev/null +++ b/modules/sd_hijack_compile.py @@ -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) diff --git a/modules/sd_hijack_hfhub.py b/modules/sd_hijack_hfhub.py index 42f963ee3..87c62586d 100644 --- a/modules/sd_hijack_hfhub.py +++ b/modules/sd_hijack_hfhub.py @@ -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) diff --git a/modules/sd_models.py b/modules/sd_models.py index daa4771ae..a393652fd 100644 --- a/modules/sd_models.py +++ b/modules/sd_models.py @@ -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 diff --git a/modules/sd_models_compile.py b/modules/sd_models_compile.py index d49cc8c48..a6c4acf54 100644 --- a/modules/sd_models_compile.py +++ b/modules/sd_models_compile.py @@ -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 - """ diff --git a/modules/sd_offload.py b/modules/sd_offload.py index 543d5826a..14defe646 100644 --- a/modules/sd_offload.py +++ b/modules/sd_offload.py @@ -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 diff --git a/modules/shared_defaults.py b/modules/shared_defaults.py index 512f5913b..c18a59e24 100644 --- a/modules/shared_defaults.py +++ b/modules/shared_defaults.py @@ -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: diff --git a/modules/video_models/video_load.py b/modules/video_models/video_load.py index ad3fc2237..7b68e3231 100644 --- a/modules/video_models/video_load.py +++ b/modules/video_models/video_load.py @@ -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}' diff --git a/modules/video_models/video_minimax.py b/modules/video_models/video_minimax.py new file mode 100644 index 000000000..31e6fa34a --- /dev/null +++ b/modules/video_models/video_minimax.py @@ -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') diff --git a/modules/video_models/video_overrides.py b/modules/video_models/video_overrides.py index 2c9b8c79e..a294afd4d 100644 --- a/modules/video_models/video_overrides.py +++ b/modules/video_models/video_overrides.py @@ -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)) diff --git a/modules/video_models/video_run.py b/modules/video_models/video_run.py index 92296b4b5..6444607eb 100644 --- a/modules/video_models/video_run.py +++ b/modules/video_models/video_run.py @@ -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( diff --git a/pipelines/generic_text_encoder.py b/pipelines/generic_text_encoder.py index a67ac308d..c1b6a57a2 100644 --- a/pipelines/generic_text_encoder.py +++ b/pipelines/generic_text_encoder.py @@ -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, diff --git a/pipelines/model_minimax.py b/pipelines/model_minimax.py index 0d13cd065..4c7ba4f94 100644 --- a/pipelines/model_minimax.py +++ b/pipelines/model_minimax.py @@ -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() diff --git a/scripts/pulid/eva_clip/pretrained.py b/scripts/pulid/eva_clip/pretrained.py index bf957db74..f7a1afe4c 100644 --- a/scripts/pulid/eva_clip/pretrained.py +++ b/scripts/pulid/eva_clip/pretrained.py @@ -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: diff --git a/scripts/pulid/pulid_sdxl.py b/scripts/pulid/pulid_sdxl.py index 74eca27b8..a36283be3 100644 --- a/scripts/pulid/pulid_sdxl.py +++ b/scripts/pulid/pulid_sdxl.py @@ -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