Key the auto step-cache on the pipe's default strength when the request omits it

The auto FBCache policy keyed on the full step count whenever strength was omitted, but the
loader only passes the strength kwarg when it is set, so an img2img/inpaint pipe then runs its
OWN signature default (< 1, e.g. FluxImg2ImgPipeline's 0.6). FBCache would engage on the full
28 steps while the pipe actually denoises ~16, degrading the image on exactly the short
trajectory the policy exists to keep uncached. Thread the pipe's signature default into the
policy via a new effective_request_strength helper (unit-tested), so the effective denoise count
matches what the pipe runs.
This commit is contained in:
Daniel Han 2026-07-06 14:49:47 +00:00
commit b6b507c48b
3 changed files with 52 additions and 8 deletions

View file

@ -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(

View file

@ -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,
*,

View file

@ -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).