What happens
On Windows ROCm an under-estimated diffusion memory plan does not fail. The generation keeps allocating, WDDM backs the overflow out of system RAM, and the machine becomes unusable (available RAM under 1.2 GB, pagefile past 50 GB). On Linux the same shortfall raises torch.OutOfMemoryError and the job dies cleanly.
The trigger is that the image memory plan is resolution-blind.
Why
estimate_image_runtime_mib (studio/backend/core/inference/diffusion_memory.py:273-292) scales headroom by pixel area against DEFAULT_IMAGE_WIDTH/HEIGHT, both 1024. Its only caller is _plan_memory (studio/backend/core/inference/diffusion.py:3006), and that caller passes no dimensions (diffusion.py:3130):
runtime_headroom = estimate_image_runtime_mib(width = None, height = None, family = variant_hint)
pixel_scale is therefore always exactly 1.0. Every image at every resolution is budgeted as one 1 MP frame. studio/backend/core/inference/video.py:1186 passes real values, so image is the odd one out.
The arithmetic
Z-Image-Turbo GGUF Q4_K_S at 1088x1920, 9 steps:
| quantity |
value |
| requested area |
2,088,960 px, 1.99x of 1024^2 |
pixel_scale actually used |
1.0 |
| planned headroom |
8192 x 1.0 x 0.85 = 6,963 MiB |
| headroom actually needed |
~13,872 MiB |
| shortfall |
~6.7 GiB |
The planner picked its most conservative tier and still overran, because it was conservative about a workload half the real size:
diffusion.loaded: repo=unsloth/Z-Image-Turbo-GGUF base=Tongyi-MAI/Z-Image-Turbo
device=cuda offload=model tiling=True reasons=companions exceed budget;
whole-module offload of every component
Why Windows is worse than Linux
Measured from Windows performance counters on the backend process during the overrun:
| counter |
value |
GPU Process Memory\Local Usage (dedicated VRAM) |
13,215 MB |
GPU Process Memory\Non Local Usage (system RAM as GPU memory) |
14,025 MB |
| total held by the process |
~27 GB on a 15.92 GiB card |
| system Available RAM |
1,228 MB |
| Committed |
80.5 GB of an 89.1 GB limit |
| pagefile |
grown to 52.5 GB |
Under WDDM the driver satisfies the overflow from shared system memory instead of failing. No OOM, no error, no failed job, just a desktop that stops responding.
Nothing in the inference path bounds this: torch.cuda.set_per_process_memory_fraction appears exactly once in the backend (studio/backend/core/training/worker.py:3734), inside run_training_process. No inference process reaches it.
Environment
- GPU: Radeon RX 9060 XT (gfx1200), discrete, 15.92 GiB
- OS: Windows 11
- torch 2.11.0+rocm7.13.0
- Model:
unsloth/Z-Image-Turbo-GGUF, Q4_K_S, base Tongyi-MAI/Z-Image-Turbo
Reproduce
Load Z-Image-Turbo GGUF Q4_K_S on a ~16 GB Windows ROCm card, generate at 1088x1920. Watch GPU Process Memory\Non Local Usage for the backend process climb into the tens of GB while available RAM collapses, with no exception raised.
At 1024x1024 the same model completes, because that is the size the plan silently assumed.
Fix
Re-check at generate time, where the real resolution is known. The load-time width=None is defensible on its own (no request exists yet); the defect is that nothing revisited it. Compare the activation term alone against the current free budget: weights can be offloaded, activations cannot, so a runtime estimate over budget is an overrun at every tier rather than a tuning guess.
Longer term the ROCm OOM guard should extend to inference processes. Cross-importing from the training worker is already an established pattern (studio/backend/core/inference/llama_cpp.py:4864 does from core.training.worker import _rocm_classify_unified_memory).
Related
Open question
It is not yet established whether the ~14 GB of non-local memory is caching-allocator bytes that WDDM placed in system RAM, or pinned host buffers created by enable_model_cpu_offload. This matters for the fix: set_per_process_memory_fraction bounds the former and not the latter.
Evidence leaning toward some of it sitting outside the allocator: the separate video OOM on this host reported "Of the allocated memory 9.09 GiB is allocated by PyTorch" while the process held substantially more GPU-addressable memory than that.
Settling it requires sampling torch.cuda.memory_reserved() against the live GPU Process Memory\Non Local Usage counter during a large generation.
What happens
On Windows ROCm an under-estimated diffusion memory plan does not fail. The generation keeps allocating, WDDM backs the overflow out of system RAM, and the machine becomes unusable (available RAM under 1.2 GB, pagefile past 50 GB). On Linux the same shortfall raises
torch.OutOfMemoryErrorand the job dies cleanly.The trigger is that the image memory plan is resolution-blind.
Why
estimate_image_runtime_mib(studio/backend/core/inference/diffusion_memory.py:273-292) scales headroom by pixel area againstDEFAULT_IMAGE_WIDTH/HEIGHT, both 1024. Its only caller is_plan_memory(studio/backend/core/inference/diffusion.py:3006), and that caller passes no dimensions (diffusion.py:3130):pixel_scaleis therefore always exactly 1.0. Every image at every resolution is budgeted as one 1 MP frame.studio/backend/core/inference/video.py:1186passes real values, so image is the odd one out.The arithmetic
Z-Image-Turbo GGUF Q4_K_S at 1088x1920, 9 steps:
pixel_scaleactually usedThe planner picked its most conservative tier and still overran, because it was conservative about a workload half the real size:
Why Windows is worse than Linux
Measured from Windows performance counters on the backend process during the overrun:
GPU Process Memory\Local Usage(dedicated VRAM)GPU Process Memory\Non Local Usage(system RAM as GPU memory)Under WDDM the driver satisfies the overflow from shared system memory instead of failing. No OOM, no error, no failed job, just a desktop that stops responding.
Nothing in the inference path bounds this:
torch.cuda.set_per_process_memory_fractionappears exactly once in the backend (studio/backend/core/training/worker.py:3734), insiderun_training_process. No inference process reaches it.Environment
unsloth/Z-Image-Turbo-GGUF, Q4_K_S, baseTongyi-MAI/Z-Image-TurboReproduce
Load Z-Image-Turbo GGUF Q4_K_S on a ~16 GB Windows ROCm card, generate at 1088x1920. Watch
GPU Process Memory\Non Local Usagefor the backend process climb into the tens of GB while available RAM collapses, with no exception raised.At 1024x1024 the same model completes, because that is the size the plan silently assumed.
Fix
Re-check at generate time, where the real resolution is known. The load-time
width=Noneis defensible on its own (no request exists yet); the defect is that nothing revisited it. Compare the activation term alone against the current free budget: weights can be offloaded, activations cannot, so a runtime estimate over budget is an overrun at every tier rather than a tuning guess.Longer term the ROCm OOM guard should extend to inference processes. Cross-importing from the training worker is already an established pattern (
studio/backend/core/inference/llama_cpp.py:4864doesfrom core.training.worker import _rocm_classify_unified_memory).Related
_group_fits()goes false and you get needless whole-module offload and a 48-minute image. Above 1 MP the same flat value is too small, which is this report. Note that a generate-time check does not close [BUG] AMD: diffusion in studio falls back to whole-module offload and a 20 step image takes 48 minutes #8081: that one needs the load-time plan itself changed, and these two want the estimate moved in opposite directions, so they should be fixed together rather than independently.torch.distributed has no attribute 'Work') #7992 (closed) was fixed by fix(AMD): let diffusion load on Windows ROCm, and say what a gated repo needs #7981._native_attentionmaterialized a 66.54 GiB attention matrix. That one raised cleanly and was handled. Mentioned only to keep it out of scope.Open question
It is not yet established whether the ~14 GB of non-local memory is caching-allocator bytes that WDDM placed in system RAM, or pinned host buffers created by
enable_model_cpu_offload. This matters for the fix:set_per_process_memory_fractionbounds the former and not the latter.Evidence leaning toward some of it sitting outside the allocator: the separate video OOM on this host reported "Of the allocated memory 9.09 GiB is allocated by PyTorch" while the process held substantially more GPU-addressable memory than that.
Settling it requires sampling
torch.cuda.memory_reserved()against the liveGPU Process Memory\Non Local Usagecounter during a large generation.