Completed: Improve best-effort preload warmup for local image backends¶
Metadata¶
- Created: 2026-05-20
- Status: Completed
- Completed: 2026-05-20
- Priority: P1
Main goals¶
- Make
preload()execute a representative first-inference warmup for in-process local backends where that work can persist. - Keep the public semantics simple:
preload()remains a best-effort eager warmup, not a new explicit "true warm" contract.
Secondary goals¶
- Avoid paying the extra warmup cost more than once for the same loaded model or pipeline state.
- Add deterministic tests that prove
preload()shifts real generation work earlier for supported engines.
Context / problem¶
Current local warmup is uneven across engines:
src/abstractvision/backends/mflux.py:preload()only reaches_ensure_model_impl(), while the real first-request path still runs later in_generate_impl().src/abstractvision/backends/huggingface_diffusers.py:preload()only loads the commont2ipipeline and does not execute a representative pipeline call.src/abstractvision/backends/stable_diffusion_cpp.py:preload()only constructs the python-binding model eagerly; CLI mode shells out a fresh subprocess per request, so no persistent package-local warmup exists there.
This means AbstractVision is already decent at "load resident weights", but it is not yet as good as it could be at "make the next real request as warm as possible" for the in-process local engines.
Expected first-matching-request upside from code inspection:
mflux: likely the largest win, roughly20-50%on the first matching request when compile/init work is moved intopreload().diffusers: smaller but still useful, roughly5-15%typical for long-lived workers.stable-diffusion.cpppython mode: modest, roughly5-15%.
These are engineering estimates, not measured guarantees. CI/unit tests should confirm that the real generation path is exercised during preload(), but they will not prove exact wall-clock gains on real weights.
Constraints¶
- Keep the public backend contract stable; do not add a new user-facing warmup API for this task.
- Keep
preload()best-effort and backend-owned. - Do not pretend CLI mode or remote HTTP backends can be persistently warmed when the process model does not support it.
- Keep tests lightweight and deterministic with fakes/mocks rather than real model downloads.
- Preserve existing generation behavior and current routing/residency semantics.
Research, options, and references¶
- Option A: keep load-only
preload()behavior. - Trade-off: zero implementation risk, but it leaves the known MFLUX gap and smaller Diffusers/sdcpp gaps untouched.
- Option B: introduce a separate public warmup API or explicit "warmness" metadata first.
- Trade-off: more semantic clarity, but the user goal here is practical warmup improvement, not a larger public API change.
- Option C: improve existing
preload()so supported in-process backends perform one representative discarded generation and remember they are already warmed for the current loaded state. - Trade-off: slightly higher preload cost, but it keeps the public surface simple and moves more cold-start work out of the first user request.
Decision¶
Chosen approach: implement Option C.
Why:
preload()already exists specifically to do eager preparation work.- The strongest gap is behavioral, not semantic: the code is not yet pushing enough real first-inference work into preload.
- A backend-local, idempotent warmup step is the smallest change that can improve the actual user-visible latency profile.
Dependencies¶
- Completed backlog:
docs/backlog/completed/007_local_hf_backend_strategy_diffusers.mddocs/backlog/completed/013_stable_diffusion_cpp_gguf_backend.mddocs/backlog/completed/018_capability_residency_hooks.md- Key code paths:
src/abstractvision/backends/base_backend.pysrc/abstractvision/backends/mflux.pysrc/abstractvision/backends/huggingface_diffusers.pysrc/abstractvision/backends/stable_diffusion_cpp.pytests/test_mflux_backend.pytests/test_huggingface_diffusers_backend.pytests/test_stable_diffusion_cpp_backend.py
Implementation plan¶
- Add backend-local warmup bookkeeping so repeated
preload()calls do not rerun the same warmup unnecessarily. - For
mflux, after model construction, run one minimal valid representative generation on the runtime thread and discard the output. - For
diffusers, keep the currentt2ifocus but run one normalized representative pipeline call and discard the output. - For
stable-diffusion.cpppython mode, start by testing representative warmup, but keep the backend free to stop at eager model construction if measurement shows full hidden generation is not worth the cost; leave CLI mode unchanged. - Add tests that prove
preload()executes the real generate path once and that the first subsequent user request reuses the already-warmed state.
Success criteria¶
preload()onmfluxexecutes a representative generate path, not just model construction.preload()ondiffusersexecutes a representativet2ipipeline call, not just pipeline loading.preload()onstable-diffusion.cpppython mode performs the most cost-effective eager preparation that survives into the next request; CLI mode remains load-only/no-op.- Repeated
preload()calls for the same loaded state do not rerun warmup. - Existing generation/edit behavior remains intact and the relevant backend tests pass.
Test plan¶
PYTHONPATH=src python -m unittest tests.test_playground_server -qPYTHONPATH=src python -m unittest tests.test_mflux_backend -qPYTHONPATH=src python -m unittest tests.test_huggingface_diffusers_backend -qPYTHONPATH=src python -m unittest tests.test_stable_diffusion_cpp_backend -qPYTHONPATH=src python -m unittest tests.test_playground_server tests.test_mflux_backend tests.test_huggingface_diffusers_backend tests.test_stable_diffusion_cpp_backend -q
Completion Report¶
Date: 2026-05-20
Summary¶
mfluxpreload()now executes one real deterministic text-to-image warmup on the existing runtime thread and remembers warm state by_model_key, so repeated preload calls do not rerun the same work.huggingface_diffuserspreload()still targets the commont2ipipeline, but it now runs a real warmup inference once per loaded pipeline object and serializes preload/generate/edit behind a backend-localRLock.stable_diffusion_cpppython mode was initially implemented with a real warmup generate duringpreload(), but follow-up measurement showed that eager model construction captures the useful gain more cleanly. The final behavior is load-onlypreload()for python mode; CLI mode remains unchanged because it has no persistent in-process state to warm.PlaygroundState.load_model()now preloads the replacement backend before swapping active state, so a warmup failure no longer drops the previously active model.
Files And Symbols Touched¶
src/abstractvision/backends/mflux.pyMFluxVisionBackend.preload()MFluxVisionBackend._preload_impl()MFluxVisionBackend._warmup_request()MFluxVisionBackend._generate_impl()src/abstractvision/backends/huggingface_diffusers.pyHuggingFaceDiffusersVisionBackend.preload()HuggingFaceDiffusersVisionBackend.unload()HuggingFaceDiffusersVisionBackend.generate_image_with_progress()HuggingFaceDiffusersVisionBackend.edit_image_with_progress()HuggingFaceDiffusersVisionBackend._set_pipeline()HuggingFaceDiffusersVisionBackend._warmup_generation_request()src/abstractvision/backends/stable_diffusion_cpp.pyStableDiffusionCppVisionBackend.preload()StableDiffusionCppVisionBackend._generate_image_python()StableDiffusionCppVisionBackend.generate_image_with_progress()src/abstractvision/playground_server.pyPlaygroundState.load_model()tests/test_mflux_backend.pytests/test_huggingface_diffusers_backend.pytests/test_stable_diffusion_cpp_backend.pytests/test_playground_server.py
Validation¶
git diff --checkPYTHONPATH=src python -m unittest tests.test_playground_server -qPYTHONPATH=src python -m unittest tests.test_mflux_backend -qPYTHONPATH=src python -m unittest tests.test_huggingface_diffusers_backend -qPYTHONPATH=src python -m unittest tests.test_stable_diffusion_cpp_backend -qPYTHONPATH=src python -m unittest tests.test_playground_server tests.test_mflux_backend tests.test_huggingface_diffusers_backend tests.test_stable_diffusion_cpp_backend -q- Result:
Ran 61 tests in 5.524sandOK - After installing the missing
mfluxruntime for empirical measurement: PYTHONPATH=src python -m unittest tests.test_playground_server tests.test_mflux_backend tests.test_huggingface_diffusers_backend tests.test_stable_diffusion_cpp_backend -q- Result:
Ran 61 tests in 6.462sandOK
Measured Benchmark Examples¶
Measured on this machine:
macOS arm64withMPS- local cached model weights
- fresh subprocess per sample to preserve honest cold-start behavior
- two cold runs and two preload runs per benchmark
Benchmarked configurations:
mflux:flux2-klein-4bwithdefault_width=512,default_height=512, requestseed=1diffusers:black-forest-labs/FLUX.2-klein-4Bondevice='mps', requeststeps=1,seed=1
Measured medians:
mflux- cold first request:
6.793s - preload call:
6.341s - first request after preload:
3.964s - first-request latency improvement after preload:
41.6% diffusers- cold first request:
16.836s - preload call:
16.618s - first request after preload:
6.787s - first-request latency improvement after preload:
59.7% stable-diffusion.cpppython mode- model stack:
leejet/FLUX.2-klein-base-4B-GGUF(Q8_0) + officialblack-forest-labs/FLUX.2-klein-base-4BVAE +unsloth/Qwen3-4B-GGUF(Q4_K_M) - backend/runtime:
stable-diffusion-cpp-python 0.4.5, CPU backend on this machine - benchmark shape:
256x256,steps=1, Euler, fresh subprocesses - final shipped
preload()median:2.432s - cold first request median:
27.321s - first request after
preload()median:24.753s - first-request latency improvement after
preload():9.4% - deeper comparison showed that this gain comes from eager model construction; an experimental hidden full-warmup variant was worse operationally at about
49.511spreload and25.723sfirst request after preload
Interpretation:
preload()does not reduce totalpreload + first requestwall time; it shifts expensive one-time work out of the first user-visible request.mfluxnow lands in the expected “large win” class and materially validates the implementation direction.diffusersimproved more than originally estimated on this machine and model, which suggests this backend had more first-inference one-time work than the earlier code-only estimate implied.stable-diffusion.cpppython mode now has measured validation too, but the gain is modest and comes almost entirely from eager model construction. A hidden full warmup generation madepreload()much more expensive without improving the next request enough to justify it.
Outcome Against Estimates¶
mflux: the implementation now warms the real in-process generate path using backend-default dimensions and model-default steps/guidance, which is the strongest practical move available in the current architecture.diffusers: the implementation now warms one realt2ipipeline call using backend/model defaults, which should reduce first-request latency for long-lived workers without expanding the public API.stable-diffusion.cpppython mode: follow-up measurement showed the useful benefit is eager model construction, not hidden representative generation. The final implementation keeps load-onlypreload()for python mode and still shows a measured9.4%first-request reduction on a real FLUX.2 component stack; CLI mode remains correctly untouched.
Residual Risks And Limits¶
- The unit tests prove the current backend-specific preload behavior and that repeated preload calls remain safe. Real benchmark examples now exist for
mflux,diffusers, andstable-diffusion.cpppython mode on this machine. huggingface_diffuserswarmup intentionally targets only the commont2ipath. It does not separately warmi2i,inpaint, LoRA-specific, or Rapid-AIO-specific variants during preload.- Warmup remains best-effort. It improves the first matching request, but it is not a promise that every later request shape or backend-specific variant is fully hot.
- The
stable-diffusion.cppnumber above is for python-binding CPU mode. CLI mode still has no meaningful persistent warm state in the current architecture, so the benchmark does not change the earlier conclusion that CLI warmup remains effectively unsupported. - The
stable-diffusion.cppbenchmark also used an official Black Forest Labs FLUX.2 VAE side artifact rather than a Comfy-hosted VAE, which confirms that this FLUX.2 component stack does not require a Comfy-specific VAE source. - Installing
mfluxfor measurement upgraded the local Python environment'snumpyandpillowversions to satisfymfluxruntime requirements. AbstractVision's tested slice still passed afterward, but the shared interpreter now reports dependency conflicts withdigital-article-backend.
Post-Completion Insights¶
- Once
preload()does real inference work, caller rollback behavior matters. The playground fix was necessary and suggests other future preload callers should be reviewed with the same assumption. - Public "true warmness" semantics are still not required for this task. The current implementation is valuable even while
preload()remains an intentionally best-effort contract.