Studio: refuse a single-file diffusion load that would exhaust host RAM instead of crashing by danielhanchen · Pull Request #14510 · unslothai/unsloth · GitHub
Skip to content

Studio: refuse a single-file diffusion load that would exhaust host RAM instead of crashing - #14510

Open
danielhanchen wants to merge 11 commits into
mainfrom
studio-krea2-custom-models
Open

danielhanchen wants to merge 11 commits into
mainfrom
studio-krea2-custom-models

Conversation

@danielhanchen

@danielhanchen danielhanchen commented Oct 21, 2026 •

Copy link
Copy Markdown
Member

Summary

Loading a custom Krea 2 safetensors from the custom models folder (the ComfyUI diffusion_models file, usually fp8 or fp8_scaled, 13 GB) could take the whole app down (#14474). Studio already supports these files: the family is detected from the header, the DiT comes from the file and the text encoder / VAE from krea/Krea-2-Turbo. What kills the process is host RAM:

  • A plain fp8 file goes through load_original_layout_transformer, which widens every fp8 tensor to the compute dtype in host RAM (value.to(want)). That is about 23 to 26 GB of new memory for the 13B Krea 2 DiT.
  • An fp8_scaled ComfyUI file goes through load_comfy_quant_transformer. On a GPU with no fp8 runtime (anything below sm_89, such as the reporter's RTX 3060 Ti), every layer is dequantized to bf16 the same way.
  • Nothing checks host RAM on this branch (the small-host route only runs for full pipelines). The load runs in a thread of the server process, so running out of memory is not an exception it can catch: Linux's OOM killer ends the server, and on Windows the commit limit runs out inside native code.

This PR prices the new host memory from the safetensors header before any widening. Tensors already at the compute dtype stay memory-mapped and cost nothing. When the widened weights plus the small-host reserve do not fit in free RAM, the load is refused with a readable error. This applies to both loaders and to every family that uses them:

Not enough system RAM to load k4_fp8.safetensors: widening its weights to float32 needs about 11.9 GB of free host memory (8.9 GB of weights + 3.0 GB reserve) and 2.4 GB is free. Close other applications, use a GGUF or a smaller quant of this model, or set UNSLOTH_DIFFUSION_HOST_RAM_CHECK=0 to try anyway.

UNSLOTH_DIFFUSION_HOST_RAM_CHECK=0 (the existing small-host switch) skips the check, and loads under 1 GiB of widening are never checked.

Evidence

  • Five tiny Krea 2 single files (plain bf16, fp8, fp8_scaled with scale_weight / scaled_fp8, model.diffusion_model. prefix, diffusers layout) all load through DiffusionBackend.load_pipeline from a custom folder on main. The load path itself is fine.
  • I used a realistic file: real Krea 2 shapes, 4 of the 28 blocks, fp8 linears, 2.9 GB. Widening it to bf16 added 3318 MiB of private RAM, against 3312 MiB predicted from the header.
  • I loaded that same file through the real backend on CPU inside a 3 GB memory cgroup:
    • main: the process is SIGKILLed (exit 137) with no Python error.
    • This branch: the readable refusal above.
    • With 20 GB: it loads normally.
  • Tests, main vs this branch:
    • New tests/test_diffusion_single_file_host_ram.py. Four tests fail on main and pass here: the fp8 file is refused before any weight is read, the fp8_scaled file with no fp8 runtime is refused, a bf16 file is never refused, and the widened byte count matches. Two pass on both: the same files load when there is room, and the kill switch / unreadable RAM never refuse.
    • Related files: test_diffusion_comfy_quant.py, test_diffusion_krea2.py, test_diffusion_single_file_krea2_hunyuanimage.py, test_diffusion_single_file_registry.py, test_diffusion_small_host.py, test_hub_safetensors_single_file.py, test_video_comfy_quant.py. No change between base and head.

Review follow-ups

The header estimate alone kept missing per-layer choices the loader makes later (layers a runtime filter rejects, fp16 deferred dequantization, the packed NVFP4 width, FNUZ float8, nibble-swapped NVFP4 copies, key-map row reorders, tiled scale buffers). So the header estimate is now only the first gate, refusing before any read. During the load, every helper that allocates host memory (_dequant, dequant_block, swap_nibbles, untile_scales, tile_scales, the dtype casts in both loaders, the segment join, the key-map torch.cat) checks its allocation against the host's live available memory (cgroup-capped, about 0.5 ms per reading) minus the reserve, and the load is refused before the allocation that would not fit. Because it reads live memory rather than keeping a running total, freed buffers count as free again and a copy made where nothing checks still shows up in the next reading. Files under 1 GiB are not checked, and UNSLOTH_DIFFUSION_HOST_RAM_CHECK=0 installs no check at all.

Real-size check: a 13.2 GiB Krea 2 fp8_scaled ComfyUI file (rebuilt from the bf16 transformer) loading at bf16 on CPU, with a simulated 64 GiB host whose free memory falls as the process's anonymous memory grows (measured peak 23.26 GiB, reserve 6.4 GiB):

Free at start Result
peak + reserve + 2% loads (398 checks, no refusal)
peak + reserve - 5% refused before any read
peak + reserve - 5%, up-front check bypassed refused by the live check at 22.0 of 23.2 GiB, reserve intact

Each review item has a test that fails before its fix and passes after, including a 48-layer NVFP4 fallback load that fits and was refused by the earlier running-total version.

Not covered

  • Not run on Windows. The check reads free physical RAM, not the available commit (RAM plus page file). A machine with a large page file that used to finish such a load slowly is now refused, and the message names the switch that allows it.
  • Only the DiT is priced. The text encoder (4.8 to 8.9 GB) and the VAE load afterwards, so a load that just passes can still run short.
  • ComfyUI loads these files without widening: the fp8 weights stay fp8 in memory and are upcast per layer at compute time. Doing the same in Studio (layerwise casting on the denoiser on GPUs without an fp8 runtime) is the real fix for low-RAM machines. It touches offload, compile and LoRA, so it is left as a follow-up.
  • Krea 2 offers only Create (txt2img) in Studio by design today: diffusers ships no Krea 2 img2img / inpaint pipeline. ComfyUI does img2img / inpaint for Krea 2 through its generic latent denoise, so that is a follow-up, not a detection bug. The Hub models that offer more modes are FLUX.1 Krea dev, a different (FLUX.1) family.
  • A kept NVFP4 load (FlashInfer runtime on Blackwell) was covered by unit tests only, not measured on a real file.

An fp8 or fp8_scaled ComfyUI Krea 2 file (13 GB) on a GPU with no fp8 runtime
is widened to bf16 in host RAM (about 26 GB) before the denoiser is placed.
With too little free memory the process was killed (Linux OOM killer, Windows
commit limit), taking the whole app down instead of failing the load.

Price the new host memory from the safetensors header (tensors already at the
compute dtype stay memory-mapped and cost nothing) and refuse with a readable
message before any widening, in both the original-layout loader and the
ComfyUI-quantized loader. UNSLOTH_DIFFUSION_HOST_RAM_CHECK=0 skips the check.
@danielhanchen danielhanchen self-assigned this Oct 21, 2026
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-21T16:09:44.869065Z 508a2ff Manual request
🔒 Security Review ⚠️ Failed 2026-10-21T12:34:11.953323Z
We couldn’t complete this review. Comment @codex security review to try again.
0bd9aed PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

The account paying for this security review has reached its Codex usage limits. The payer can check the Codex usage dashboard. For personal accounts, using credits requires enabling “Use credits for security reviews” in Code review settings. If you do not manage the paying account, contact this repository's admins.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

pre-commit-ci Bot and others added 4 commits October 21, 2026 12:55
With an fp16 pipeline and _keep_in_fp32_modules the ComfyUI loader only
postpones the dtype choice; with no fp8 runtime (never available off bf16)
every quantized layer is still expanded. Price those layers as widened, and
block formats under a key map, which are always dequantized.
An nvfp4 header describes packed uint8 with K / 2 columns, while the
dequantization allocates the dense N x K weight, so the check counted half
of it. nvfp4 is the only packed ComfyUI format the loader reads; int8, fp8
and mxfp8 store one code per element.
chatgpt-codex-connector[bot]

This comment was marked as resolved.

The header estimate could not see per-layer choices the loader makes later,
so it kept drifting from what was really written: layers a runtime filter
rejects are still dequantized, a kept nvfp4 weight gets a nibble-swapped copy
plus untiled scales, and FNUZ float8 tensors were skipped entirely.

A HostRamBudget now holds free RAM minus the small-host reserve for the
whole load. The header estimate still refuses up front (it now prices FNUZ
float8 too), and every allocating helper (dequant, dequant_block, the
nibble swap, scale untiling, dtype casts, segment joins) charges the budget
before it allocates, so the load is refused before the allocation that would
not fit instead of running the host out of memory.
With UNSLOTH_DIFFUSION_HOST_RAM_CHECK=0 (or host RAM unreadable) the budget
could never refuse, but it was still installed and charged by every helper.
Leave it inactive so the kill switch costs nothing.

[skip ci]
chatgpt-codex-connector[bot]

This comment was marked as resolved.

The in-load check summed every allocation it was told about and never
released any, so it over-counted copies freed again before the next layer
(the nibble-swapped codes and untiled scales of an nvfp4 fallback load, a
false refusal of a load that fits) and still missed copies made where
nothing charged (the row reorder torch.cat of a key-mapped layer such as
MiniMax-H3's fc1, the tiled scale buffer of a runtime Linear).

Keep the header estimate as the first gate, then before each allocation
refuse only if it plus the small-host reserve exceeds the host's live
available memory (host_ram_mib, cgroup capped). Freed buffers come back in
the next reading and unchecked copies show up in it. The key map join and
tile_scales now check their own allocation too. The kill switch still
installs no check, and the error text is unchanged.

[skip ci]
chatgpt-codex-connector[bot]

This comment was marked as resolved.

Arm the in-load check by the widened estimate as well as the file size: a
900 MiB nvfp4 file widens to 3.6 GiB of bf16, so a small file is exempt only
when its estimate is small too.

Dequantize fallback nvfp4 / mxfp8 layers in bounded row chunks straight into
the output and check the output plus one chunk of int32 / float32
intermediates before starting, instead of the output alone. Every op is per
row, so the result is bit-identical to one pass.

Check every full-size host copy the loaders make before it is made: row
reorders of kept codes and their scales rebuilt by a stock converter, the
joined codes and scales of a runtime block Linear, the HunyuanImage row
join, and contiguous copies of non-contiguous kept codes.
chatgpt-codex-connector[bot]

This comment was marked as resolved.

…8 dequant peak

A HostRamShortfallError raised during the narrow-tag conversion was caught by
the retry handler and the conversion ran again at full float64 width, where a
converter's joins allocate more before the next check. Re-raise it. That is
the only broad except in the single-file loaders that wraps a host RAM check.

int8 / fp8 dequantization holds float32 codes and their float32 product
together, and ConvRot's batched matmul a float32 copy and result on top, so
check 8 bytes per element (12 with ConvRot) before starting instead of the
output size alone.

[skip ci]
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Oct 21, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Oct 21, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Oct 21, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Oct 21, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Oct 21, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Oct 21, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Oct 21, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Oct 21, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Oct 21, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Oct 21, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Oct 21, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Oct 21, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Oct 21, 2026
@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

1 similar comment
@danielhanchen

Copy link
Copy Markdown
Member Author

This branch has not been deployed

No deployments
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.

1 participant