diff --git a/studio/backend/core/inference/diffusion.py b/studio/backend/core/inference/diffusion.py index 8b423299ff..f784e61826 100644 --- a/studio/backend/core/inference/diffusion.py +++ b/studio/backend/core/inference/diffusion.py @@ -79,6 +79,7 @@ from .diffusion_cache import ( TC_FBCACHE, apply_step_cache, effective_denoise_steps, + effective_request_strength, maybe_toggle_step_cache, normalize_transformer_cache, ) @@ -2281,14 +2282,15 @@ class DiffusionBackend: # trajectory the policy keeps uncached. Only fold in `strength` when it # is ACTUALLY applied to the pipe (same gate as the kwarg below), so a # stray strength on a txt2img request never shortens the count. - strength_applied = ( - strength - if ( - strength is not None - and init_pil is not None - and "strength" in call_params - ) - else None + # The pipe denoises `steps * strength`. When the request omits strength the + # kwarg above is NOT passed, so the pipe runs its OWN signature default (< 1 for + # every img2img/inpaint pipeline here, e.g. 0.6) -- still a short trajectory the + # policy must key on, or FBCache engages on a fraction of the advertised steps. + strength_applied = effective_request_strength( + strength, + init_pil is not None, + "strength" in call_params, + call_params["strength"].default if "strength" in call_params else None, ) denoise_steps = effective_denoise_steps(steps, strength_applied) toggled = maybe_toggle_step_cache( diff --git a/studio/backend/core/inference/diffusion_cache.py b/studio/backend/core/inference/diffusion_cache.py index ce5848ad02..758719da95 100644 --- a/studio/backend/core/inference/diffusion_cache.py +++ b/studio/backend/core/inference/diffusion_cache.py @@ -144,6 +144,28 @@ def effective_denoise_steps(steps: int, strength: Optional[float]) -> int: return max(1, min(int(s * float(strength)), s)) +def effective_request_strength( + request_strength: Optional[float], + has_init_image: bool, + pipe_accepts_strength: bool, + pipe_default_strength: Any, +) -> Optional[float]: + """The strength the pipe will ACTUALLY apply, for keying the auto step-cache policy. + + Only image-conditioned pipelines that take ``strength`` apply it (txt2img / a pipe without + the kwarg run the full trajectory -> None). When the request omits ``strength`` the loader + does NOT pass the kwarg, so the pipe runs its OWN signature default (< 1 for every img2img / + inpaint pipeline here, e.g. 0.6); the policy must key on that default, not the full step + count, or FBCache engages on a fraction of the advertised steps. A non-numeric default + (``inspect.Parameter.empty``) falls back to the full count (None). + """ + if not (has_init_image and pipe_accepts_strength): + return None + if request_strength is not None: + return request_strength + return pipe_default_strength if isinstance(pipe_default_strength, (int, float)) else None + + def maybe_toggle_step_cache( pipe: Any, *, diff --git a/studio/backend/tests/test_diffusion_cache.py b/studio/backend/tests/test_diffusion_cache.py index 0d68792f58..06838edd2c 100644 --- a/studio/backend/tests/test_diffusion_cache.py +++ b/studio/backend/tests/test_diffusion_cache.py @@ -180,6 +180,7 @@ from core.inference.diffusion_cache import ( # noqa: E402 FBCACHE_MIN_STEPS, TC_AUTO, effective_denoise_steps, + effective_request_strength, maybe_toggle_step_cache, ) @@ -200,6 +201,25 @@ def test_effective_steps_low_strength_shrinks_below_the_bar(): assert eff < FBCACHE_MIN_STEPS +def test_effective_request_strength_uses_pipe_default_when_omitted(): + import inspect + + # txt2img (no init image) or a pipe without the strength kwarg -> full trajectory (None). + assert effective_request_strength(None, False, True, 0.6) is None + assert effective_request_strength(0.5, True, False, None) is None + # img2img with an explicit strength -> that value. + assert effective_request_strength(0.2, True, True, 0.6) == 0.2 + # img2img with an OMITTED strength -> the pipe's own signature default (< 1), so the auto + # policy keys on the real (short) trajectory, not the full step count. This is the fix: + # int(28 * 0.6) = 16 real steps, not 28. + s = effective_request_strength(None, True, True, 0.6) + assert s == 0.6 + assert effective_denoise_steps(28, s) == 16 + # A non-numeric signature default (inspect.Parameter.empty) falls back to the full count. + assert effective_request_strength(None, True, True, inspect.Parameter.empty) is None + assert effective_request_strength(None, True, True, None) is None + + def test_effective_steps_matches_diffusers_get_timesteps(): # Mirror diffusers exactly: it denoises init_timestep = min(int(num_inference_steps * # strength), num_inference_steps) steps (the product is floored, not rounded).