Skip to content

Fix image-generation load, training, and inference bugs#6872

Open
oobabooga wants to merge 11 commits into
unslothai:image-generationfrom
oobabooga:fix/imggen-review-bugs
Open

Fix image-generation load, training, and inference bugs#6872
oobabooga wants to merge 11 commits into
unslothai:image-generationfrom
oobabooga:fix/imggen-review-bugs

Conversation

@oobabooga

@oobabooga oobabooga commented Jul 4, 2026

Copy link
Copy Markdown
Member

This fixes a batch of bugs in the local image-generation feature, found by running it on NVIDIA GPUs (RTX 6000 Ada, RTX 3090) and an Apple Silicon Mac (8GB M1), and from a code review pass. Branched off image-generation and kept in sync with it. There are two commits. The first covers the highest-impact bugs on the load-and-generate path. The second covers everything else. Each bullet names how to reproduce the problem on the image-generation branch.

All the image-generation test suites pass (1067 tests). The only failures are 14 unrelated, pre-existing ones in a flash-attention test that fails the same way on the base branch. The frontend compiles clean. A few of these were fixed upstream while this was in progress, so they are not duplicated here.

Commit 1: loading and generating images

  • Requesting fp8/int8 quantization silently did nothing. Load a GGUF model such as unsloth/Z-Image-Turbo-GGUF (Q8_0) with transformer_quant=fp8 on a CUDA card with plenty of free VRAM. It loads unquantized: the status reports transformer_quant: null and the output matches a plain GGUF load, with no error. The load was overestimating the memory the quantized path needed (it double-counted the base model's transformer), decided it would not fit, and fell back. Confirmed live on a 49GB Ada: the same request now reports fp8 and produces a correct image.
  • ControlNet crashed out of the box. Load a FLUX.1 or Qwen-Image model, pick one of the two bundled ControlNets (the FLUX or Qwen "union" model), leave the control type on its default of "passthrough", and generate. It fails with a diffusers error about a missing control mode, after already downloading the multi-GB ControlNet. Confirmed by test and by tracing the failure.
  • Generating at a new image size crashed when the FirstBlockCache speedup was on. Load a FLUX or Qwen model with transformer_cache=fbcache, generate one image (say 1024x1024), then generate another at a different size (768x768). The second crashes on leftover state from the first. Confirmed live: two images at different sizes with the cache on now both succeed.
  • Models would not load on smaller Macs. On Apple Silicon with 8-16GB, load a diffusers model on MPS whose size sits near the memory ceiling (for example unsloth/Z-Image-Turbo-GGUF at Q2_K on an 8GB M1) with default settings. It fails at load with "MPS backend out of memory" even though it fits in system RAM. Confirmed live on an 8GB M1: it now loads. While testing I also caught that my first attempt at the fix had no effect, and corrected it.
  • The native (stable-diffusion.cpp) engine kept re-downloading itself and ran slower than it should. Install the native engine through its prebuilt installer on a fresh machine, then start the backend. It never finds the server it just installed (the binary lands in a versioned subfolder it did not search), so it re-installs on every launch and uses the slower one-shot mode instead of the persistent server. Confirmed live against a real install: it now finds the installed server.
  • A stopped training run could not be deployed. Start a LoRA training run (FLUX, Qwen, or Z-Image), let a few steps run, and click "Stop training" (the normal "stop once the loss looks good" flow). The adapter is saved and published, but the Train tab shows no Deploy button and never adds it to the model list. Confirmed through the UI logic and the frontend build.

Commit 2: everything else

Training:

  • A typo in a training option kicked out your loaded model before failing. With a chat or image model loaded, start a diffusion training run with an invalid learning-rate schedule (for example lr_scheduler=constnat). It passed the initial checks, unloaded your model to free the GPU, and only then failed inside the training process. It is now rejected immediately. Confirmed live: an invalid value is refused with a 4xx before anything is unloaded.
  • Choosing fp16 for a model that requires bf16 reported success, then failed. Start a run with model_family=z-image (or qwen-image) and mixed_precision=fp16. It returned "running", unloaded your model, and failed a moment later on the next status poll. It is now refused up front. Confirmed live: refused immediately with "requires bf16".
  • A valid local model folder could be wrongly refused for training if its path contained the text "gguf" (for example a diffusers model under /models/gguf-experiments/). It was mistaken for a GGUF file, which cannot be trained. Confirmed by test.
  • Re-training a LoRA under the same name silently overwrote the previous one. Train two runs with the same adapter name. The second replaced the first in the LoRA picker. It now keeps both (adds a numbered suffix). Confirmed by test.

Generation:

  • An fp8 model file could run out of VRAM on a card that looked like it had room. Load the bundled fp8 single-file model (unsloth/Qwen-Image-2512-FP8). It expands to roughly twice its on-disk size once loaded, but the planner budgeted the on-disk size, so it could accept a load that then runs out of memory. The estimate now accounts for the expansion. Confirmed by test.
  • Switching between different ControlNets in one session leaked VRAM. In one loaded session, generate several times in a row with different ControlNets (for example a canny model, then a depth model). Each stayed loaded on top of the last until VRAM ran out. Only one is now kept at a time. Confirmed by reviewing the load/unload path.
  • Image-editing models silently ignored a mask you supplied. On an edit model such as Qwen-Image-Edit, send an image plus a mask. The mask was dropped and the whole image was edited. It is now rejected with a clear message. Confirmed by test.
  • A CPU-only install without PyTorch failed with a confusing internal error instead of a clear message about the missing dependency. Confirmed by test.
  • When quantization cannot be applied because of the chosen memory mode, it now says so. Load a GGUF model with transformer_quant=fp8 and memory_mode=balanced (or low_vram). The quantization is dropped, because those modes stream the model instead of keeping it fully in VRAM. Before, there was no indication; now it logs why. Confirmed live: the explanation appears in the log.
  • Tidied an inaccurate code comment (no behavior change).

Errors and edge cases:

  • A typo in a Hugging Face LoRA id returned a generic server error. Generate with a LoRA whose id is a nonexistent Hub repo. It returned HTTP 500 (and the message could include the request URL) instead of a clear "not found". It is now a 4xx not-found with the URL stripped out. A genuine Hub outage still reports as a server error. Confirmed live: a bad id now returns a clean not-found.
  • On the native engine, a disabled ControlNet was rejected. With the native engine active, send a ControlNet with strength 0 (which everywhere else means "off"). The native path returned an error instead of ignoring it. It is now ignored consistently. Confirmed by test.

Frontend polish:

  • Load a GGUF model (the selector shows its quantization, for example "GGUF · Q4_K_M"), then pick a non-GGUF model whose load fails. The selector dropped the label and never restored it. It now reverts on failure.
  • Restore the settings of a saved image that had guidance 0 and a negative prompt. The negative prompt loaded but stayed hidden, then silently applied once guidance was raised. It is now cleared on restore.
  • Restore a saved image's settings while a ControlNet is selected. The old ControlNet selection was left in place. It is now cleared.

Scripts:

  • On Windows, run the uninstaller while the native engine (sd-cli / sd-server) is still running. The delete could be refused. It now stops those processes first. (A related auto-delete was left out on purpose, since it could remove a folder the user manages themselves.)
  • Two developer benchmark scripts (sparse_accum_probe.py, nvfp4_t211_probe.py) crashed on any machine without the original dev box's hard-coded output path. Fixed to a relative path.

@oobabooga oobabooga requested a review from danielhanchen as a code owner July 4, 2026 14:10
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Repo admins can enable using credits for code reviews in their settings.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

Gemini encountered an error creating the review. You can try again by commenting /gemini review.

oobabooga and others added 3 commits July 4, 2026 13:25
…hai#6872

huggingface_hub 1.x makes HfHubHTTPError's response argument required and
reads only .headers and .request from it, while 0.x defaults it to None.
Pass a small stub response so the test constructs the error on both,
verified against 0.36.2 and the 1.22.0 wheel.
@danielhanchen

Copy link
Copy Markdown
Member

Thanks for the batch, these are great catches. I verified the FBCache reset claim against diffusers 0.39 (CacheMixin only exposes _reset_stateful_cache, so the old call really was a no-op) and the companion size double count, both fixes look right.

Pushed ee61c7b to your branch to clear the last CI failure: the new hub error test constructed RepositoryNotFoundError without a response, which huggingface_hub 1.x (what CI resolves) makes required. It now passes a small stub that satisfies both 0.x and 1.x, checked against 0.36.2 and the 1.22.0 wheel. All 258 tests in the touched suites plus the import hoist lint pass locally on your head.

One heads up: we will port the _reset_stateful_cache fix to the video backend on the video PR stack as a follow up, since it reuses the same pattern.

rhsCZ pushed a commit to rhsCZ/unsloth that referenced this pull request Jul 5, 2026
The video backend never cleared FBCache residuals between clips, so with the
step cache engaged a second generation at a different resolution would hit
stale state from the first. Mirrors the image backend fix from unslothai#6872: call
the transformer level _reset_stateful_cache (reset_stateful_hooks only exists
on the HookRegistry in diffusers 0.39), covering transformer_2 for the Wan
dual expert, only when a cache is engaged.
# Conflicts:
#	studio/backend/core/inference/diffusion.py
#	studio/backend/core/training/diffusion_train_common.py
# Conflicts:
#	studio/backend/core/inference/diffusion.py
#	studio/backend/routes/training.py
#	studio/backend/tests/test_diffusion_dataset_api.py
@danielhanchen

Copy link
Copy Markdown
Member

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 14dc3b86a7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

for p in written:
try:
dest.unlink(missing_ok = True)
p.unlink(missing_ok = True)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve existing dataset files on rollback

When an upload targets an existing dataset and reuses a filename, this rollback now blindly unlinks every destination path if a later file in the same batch exceeds the size limit or hits an I/O error. Because this endpoint explicitly allows repeat uploads into the same dataset to accumulate, a failed batch like existing.png followed by an over-limit file can delete the user's pre-existing existing.png; write to temporary files and only replace originals after the whole batch is committed, or track/restore pre-existing paths.

Useful? React with 👍 / 👎.

# Multiple revisions may be cached; the active one is the fullest, so take the max.
return max(
(
DiffusionBackend._local_dir_weight_bytes(rev, exclude_transformer = True)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Budget the dense transformer before enabling quant

When transformer_quant is requested for a GGUF load without a pre-quantized source, the fast path later materializes the base repo's dense bf16 transformer/ before quantizing it, but this estimate now always excludes those shards from the resident budget. On cards that fit the selected GGUF plus companions but not the dense transformer, plan.offload_policy can stay none, so the loader evicts the current model and then OOMs/falls back during finalization instead of skipping the dense path up front; include the dense transformer size for that specific preflight.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants