feat(vae): support running VAEs on CPU via cpu_only setting#9293
Conversation
Extends the cpu_only mechanism from invoke-ai#8777 (text encoders) to VAE decode. Adds a cpu_only field to all standalone VAE configs; the loader already forces standalone configs with cpu_only=True onto the CPU. The 7 decode invocations now move latents to the VAE's effective device instead of hard-coding CUDA, and the SD/SDXL path falls back to fp32 on CPU (fp16 conv is unsupported there). Adds a "Run on CPU" toggle to the VAE model settings panel and regenerates the API schema. Decode-only for now; encode and main-model VAE submodels are unchanged. Closes invoke-ai#7276 (VAE part)
There was a problem hiding this comment.
Review
Overview
Extends the #8777 text-encoder cpu_only mechanism to standalone VAE decode. I verified the load-bearing claims against the branch: _get_execution_device (load_default.py:120) already handles any config with a cpu_only attr, so no loader change is genuinely needed; cache invalidation is covered by the explicit cpu_only comparison at model_manager.py:471; the new INFO log does mirror an existing per-lock INFO line (model_cache.py:554); and the new test passes (4/4 locally). The design is sound and consistent with the established precedent.
Issues
1. i18n copy is text-encoder-specific — wrong on the VAE panel (should fix)
VAEModelSettings.tsx reuses modelManager.runOnCpu, which reads "Run text encoder model on CPU only", and the cpuOnly popover says "only the text encoder component will run on CPU… The conditioning outputs are automatically moved to GPU for the denoiser." Both render on the VAE settings panel and are incorrect/confusing there. Suggest a VAE-specific label (e.g. runVaeOnCpu) and popover feature, or generalizing the shared strings.
2. lock() INFO log fires for all models on GPU-less installs, mislabeled (cpu_only) (should fix)
The early-return at model_cache.py:443 triggers whenever compute_device.type == "cpu" — which is true for every model on a CPU-only install (no GPU), not just models configured cpu_only. Previously that branch logged at debug; now every lock of every model on such systems emits "Loaded model ... onto cpu device (cpu_only); skipping VRAM load" at INFO — noisy and factually wrong about the cause. Suggest only using the (cpu_only) wording (and INFO level) when the model's compute device differs from the cache's default execution device, keeping the plain-CPU-install case at debug.
3. The l2i comment's rationale is stale (minor)
"fp16 VAE ops are not supported on CPU" is false on the pinned torch — conv2d and conv3d run fine in fp16 on CPU with torch 2.10 (verified empirically). The fp32 force is still the right call (SD/SDXL VAE has known fp16 overflow → black images, and CPU fp16 conv is much slower than fp32), but the comment should give the real reason. Related asymmetry worth a look: standalone Qwen and Anima VAEs load as fp16 on CUDA-default systems (vae.py uses self._torch_dtype) and their decode paths do fp16 conv3d on CPU with no dtype guard. It works, but fp16 CPU conv3d is very slow — likely a big part of the "Anima is really slow on CPU" observation. Casting to fp32/bf16 on the CPU path there would probably help substantially.
4. Frontend duplication (minor)
useVAEModelSettings is byte-for-byte useEncoderModelSettings with a narrower type, and VAEModelSettings.tsx duplicates EncoderModelSettings.tsx including the whole DefaultCpuOnly controller. Widening the existing hook/component's accepted config union to include VAEModelConfig would eliminate ~140 duplicated lines and keep the two panels from drifting. If they stay separate, the shared EncoderModelSettingsFormData type import from the encoder folder is an awkward cross-dependency.
5. SD3/CogView4 changes are no-ops (informational)
There are no standalone SD3 or CogView4 VAE configs, so those graphs always use the main model's VAE submodel, where cpu_only never applies — get_effective_device returns the GPU device exactly as before. The changes are harmless consistency, but the QA instruction to smoke-test cpu_only on SD3/CogView4 can't actually be exercised since the toggle can't exist for those architectures.
What's good
- Zero-loader-change design: the config field alone activates the existing
_get_execution_device→ModelCache.put(execution_device=...)path, and toggle-time eviction was already wired up via_load_settings_changed. get_effective_deviceis the right helper — it handles partially-loaded models and matches all seven existing text-encoder call sites from #8777.- The new test file is focused and passes; testing the helper via
ModelLoader.__new__is a pragmatic way to avoid heavy fixtures, and theSimpleNamespace-without-attrs case guards thehasattrfallback. - Schema/openapi regen is consistent with the pattern the encoder configs established.
Future Enhancement
After the multi-GPU PR goes in, a good follow-on PR would be to execute the VAE on an idle GPU in a multi-GPU environment, following the same pattern as is used for the text encoder.
Verdict
Approve with minor changes — items 1 and 2 are the ones I'd want fixed before merge (both small); 3–5 are comment/cleanup suggestions. No security concerns, no migration needed, and the untested-architecture risk turns out to be perf-only, not correctness.
lstein
left a comment
There was a problem hiding this comment.
Minor changes requested. Please see earlier review comment.
Item 1 — VAE settings panel showed text-encoder-specific copy: - Add `runVaeOnCpu` label and a `cpuOnlyVae` popover feature with VAE-accurate wording instead of reusing the encoder's `runOnCpu` / `cpuOnly` strings (which describe the text encoder and conditioning outputs, wrong on the VAE panel). - Register `cpuOnlyVae` in the InformationalPopover feature union. Item 2 — mislabeled INFO log on GPU-less installs: - ModelCache.lock only logs the "(cpu_only)" cause at INFO when the cache's default execution device is a GPU (a genuine per-model choice). On a CPU-only install every model has a CPU compute_device, so that case now logs at DEBUG without the misleading wording. Item 3 — stale l2i comment: - Replace the false "fp16 VAE ops are not supported on CPU" rationale with the real reason: fp16 conv runs on CPU with the pinned torch but is much slower, and SD/SDXL VAE has known fp16 overflow (black images). The fp32 force is unchanged. Item 4 — frontend duplication: - Extract the shared CPU-only settings panel into CpuOnlyModelSettings and a single useCpuOnlyModelSettings hook, parameterized by label, popover feature, and toast id. EncoderModelSettings and VAEModelSettings become thin wrappers; the config unions stay separate and each panel keeps its own copy. Removes ~200 lines of duplicated code and the awkward cross-folder type import; deletes the now-redundant useEncoderModelSettings / useVAEModelSettings hooks. SD3 / CogView4 left unchanged (out of scope): - No standalone VAE config exists for these bases, and a main model's default_settings.cpu_only only applies to text-encoder submodels (load_default.py), so cpu_only can never reach an SD3/CogView4 VAE — their l2i changes are harmless consistency, nothing to toggle. - Adding standalone VAE configs for them is not worth it here: there is no real ecosystem of separate SD3/CogView4 VAE files, and their VAEs are 16-channel AutoencoderKL that can't be reliably told apart from each other (or Flux) at import-probe time — which is exactly why standalone VAE configs are limited to the distinguishable bases (SD1/SD2/SDXL, Flux). The runtime is already cpu_only-ready, so if such a config is ever added the toggle works with no further change.
Summary
Extends the
cpu_onlymechanism from #8777 (text encoders) to VAE decode. Adds acpu_onlyfield to all standalone VAE configs; the loader already forces standalone configs withcpu_only=Trueonto the CPU. The 7 decode invocations now move latents to the VAE's effective device instead of hard-coding CUDA, and the SD/SDXL path falls back to fp32 on CPU (fp16 conv is unsupported there). Adds a "Run on CPU" toggle to the VAE model settings panel and regenerates the API schema.Decode-only for now; encode and main-model VAE submodels are unchanged.
Backend
cpu_onlyfield added to all standalone VAE configs (SD1/SD2/SDXL/FLUX checkpoint, SD1/SDXL diffusers, FLUX.2/Qwen-Image/Anima, FLUX.2 diffusers)._get_execution_devicealready returnscpufor any standalone config withcpu_only=True.l2i, FLUX, FLUX.2, SD3, CogView4, Qwen-Image, Z-Image, Anima) now send latents toget_effective_device(vae)rather thanTorchDevice.choose_torch_device()._LOAD_AFFECTING_SETTINGS(cpu_onlyis in it).Frontend
VAEModelSettingspanel +useVAEModelSettingshook (mirrors the encoder panel, reuses the shared form-data type).ModelViewrenders the panel fortype === 'vae'.Related Issues / Discussions
Closes #7276 (VAE part — the CLIP/text-encoder part was delivered in #8777)
QA Instructions
l2i), FLUX, FLUX.2, SD3, CogView4, Qwen-Image, Z-Image, Anima.Tested on SD1,SDXL,Flux1,Flux2klein9b,Anima,Zimage. Open are SD3,CogView4, Qwen-Image,Flux2klein4b(should work). Anima is really slow on CPU, anything else is ok.
Automated:
pytest tests/backend/model_manager/load/test_load_default_cpu_only.py tests/app/routers/test_update_model_record_cache_invalidation.pypnpm lint+pnpm test:no-watch(all green).Merge Plan
Standard merge. No DB schema or redux migration.
Checklist
What's Newcopy (if doing a release after this PR)