diff --git a/studio/backend/core/inference/diffusion_cache.py b/studio/backend/core/inference/diffusion_cache.py index b5bc2341a1..ce5848ad02 100644 --- a/studio/backend/core/inference/diffusion_cache.py +++ b/studio/backend/core/inference/diffusion_cache.py @@ -131,17 +131,17 @@ def effective_denoise_steps(steps: int, strength: Optional[float]) -> int: An image-conditioned workflow with ``strength`` < 1 (img2img / upscale / inpaint) runs only a fraction of ``num_inference_steps``: diffusers' ``get_timesteps`` computes - ``init_timestep = min(num_inference_steps * strength, num_inference_steps)`` and denoises - ``num_inference_steps - int(num_inference_steps - init_timestep)`` steps. The auto + ``init_timestep = min(int(num_inference_steps * strength), num_inference_steps)`` and + denoises exactly ``init_timestep`` steps -- the product is FLOORED, not rounded. The auto step-cache policy must key on THIS count -- e.g. a 28-step upscale at strength 0.35 runs - ~10 real steps, exactly the short trajectory FBCache should stay off (each skipped step - is a large quality hit). ``strength`` None (txt2img / reference) or >= 1 -> the full count. + ``int(9.8) = 9`` real steps, exactly the short trajectory FBCache should stay off (each + skipped step is a large quality hit). ``strength`` None (txt2img / reference) or >= 1 -> the + full count. """ s = int(steps) if strength is None or float(strength) >= 1.0: return s - init = min(s * float(strength), s) - return max(1, s - int(max(s - init, 0))) + return max(1, min(int(s * float(strength)), s)) def maybe_toggle_step_cache( diff --git a/studio/backend/core/inference/sd_cpp_backend.py b/studio/backend/core/inference/sd_cpp_backend.py index e40a2c178c..6f0d7cae96 100644 --- a/studio/backend/core/inference/sd_cpp_backend.py +++ b/studio/backend/core/inference/sd_cpp_backend.py @@ -97,7 +97,8 @@ def _default_threads() -> int: path runs, oversubscribing the hyperthreads adds scheduling contention without extra throughput, so pin to physical cores (``cpu_count // 2``) instead. Falls back to 8 when the count is unknown, and clamps to at least 1.""" - return max(1, (os.cpu_count() or 8) // 2) + cpu = os.cpu_count() + return max(1, cpu // 2 if cpu else 8) def _server_binary_runnable(binary: str) -> bool: diff --git a/studio/backend/tests/test_diffusion_cache.py b/studio/backend/tests/test_diffusion_cache.py index aa0e9d354e..0d68792f58 100644 --- a/studio/backend/tests/test_diffusion_cache.py +++ b/studio/backend/tests/test_diffusion_cache.py @@ -192,19 +192,19 @@ def test_effective_steps_txt2img_is_full_count(): def test_effective_steps_low_strength_shrinks_below_the_bar(): - # A 28-step upscale at strength 0.35 denoises ~10 steps (diffusers get_timesteps), - # which is below FBCACHE_MIN_STEPS -> the auto policy must NOT engage FBCache there. + # A 28-step upscale at strength 0.35 denoises int(9.8) = 9 steps (diffusers get_timesteps + # floors the product), which is below FBCACHE_MIN_STEPS -> the auto policy must NOT engage + # FBCache there. eff = effective_denoise_steps(28, 0.35) - assert eff == 10 + assert eff == 9 assert eff < FBCACHE_MIN_STEPS def test_effective_steps_matches_diffusers_get_timesteps(): - # Mirror diffusers exactly: num_inference_steps - int(num_inference_steps - - # min(num_inference_steps * strength, num_inference_steps)). + # Mirror diffusers exactly: it denoises init_timestep = min(int(num_inference_steps * + # strength), num_inference_steps) steps (the product is floored, not rounded). for steps, strength in [(28, 0.35), (28, 0.8), (50, 0.5), (20, 0.99), (30, 0.1)]: - init = min(steps * strength, steps) - expected = max(1, steps - int(max(steps - init, 0))) + expected = max(1, min(int(steps * strength), steps)) assert effective_denoise_steps(steps, strength) == expected