Skip to content

fix: prevent clip_preprocess center crop from exceeding the resized image - #1995

Merged
leejet merged 2 commits into
leejet:masterfrom
akhenakh:fix/clip-preprocess-resize-rounding
Sep 19, 2026
Merged

leejet merged 2 commits into
leejet:masterfrom
akhenakh:fix/clip-preprocess-resize-rounding

Conversation

@akhenakh

Copy link
Copy Markdown
Contributor

Summary

clip_preprocess computed the aspect-preserving resize size by truncating scale * dim to int64_t. For some aspect ratios the single-precision result lands a hair below an integer (e.g. 736.0f / 730.0f * 730.0f == 735.999…), so the resized side is one pixel smaller than the crop target. The center crop then reads one element past the end of the resized tensor and throws an uncaught exception:

sd::Tensor error: Tensor index out of range: shape=[650, 735, 3, 1]
terminate called after throwing an instance of 'std::invalid_argument'
  what():  Tensor index out of range: shape=[650, 735, 3, 1]
SIGABRT: abort

Reproduction (Qwen3VL / Krea2 reference images): a second reference of 645×730 is snapped to w_bar=640, h_bar=736; scale = max(640/645, 736/730) = 1.008219, so resized_height = (int64_t)(scale * 730) = (int64_t)735.999… = 735, while the crop loops up to target_height = 736. Any caller of clip_preprocess can hit this (Qwen3VL/Krea2 vision encoder, CLIP vision, PhotoMaker) — it only depends on the image aspect ratio.

The fix rounds the resized dimensions up and clamps them to at least the crop target, so the crop window is always inside the resized image:

int64_t resized_width  = static_cast<int64_t>(std::ceil(scale * static_cast<float>(image.shape()[0])));
int64_t resized_height = static_cast<int64_t>(std::ceil(scale * static_cast<float>(image.shape()[1])));
resized_width  = std::max<int64_t>(resized_width, target_width);
resized_height = std::max<int64_t>(resized_height, target_height);

Related Issue / Discussion

Additional Information

Verification (exact failing setup):

  • Model: Krea-2-Turbo-Q8_0.gguf + Qwen3VL text encoder, --backend vulkan1,te=cpu
  • Args: -W 960 -H 720 --steps 10 --cfg-scale 1 --img-cfg-scale 1 --flow-shift 3
  • Refs: -r ref0.png (960×720, i.e. 720×960 H×W) -r ref1.png (645×730) with --ref-image-args preset=krea2_ostris_edit
  • Prompt: an edit instruction with two Picture N: references.

Before the fix:

[INFO] image.cpp:271  - EDIT mode
[INFO] conditioner resize ... ref image 1 from 730x645 to 736x640
sd::Tensor error: Tensor index out of range: shape=[650, 735, 3, 1]
[abort]

After the fix (same command, same refs):

[INFO] image.cpp:516  - get_learned_condition completed, taking 26.60s
[INFO] image.cpp:886  - sampling completed, taking 100.81s
[INFO] main.cpp:504  - save result image 0 to '...png' (success)
exit 0
  • Platform: Arch Linux, RX 7900 XTX (RADV), Mesa 26.2.2, Vulkan 1.4.354.
  • Formatting: clang-format clean (.clang-format, Chromium base).

Checklist

…mage

clip_preprocess truncated the aspect-preserving resize dimensions to
int64_t, so a float result a hair below an integer (e.g. 736.0f/730.0f *
730.0f = 735.999...) left the resized side one pixel shorter than the
crop target. The center crop then indexed past the end of the resized
tensor and threw "Tensor index out of range" (SIGABRT).

Round the resized dimensions up with std::ceil and clamp them to at
least the crop target so the crop window is always covered.

@leejet leejet left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

I think the crash fix is valid, but using ceil here changes the resize result for inputs that were previously handled correctly.

For example, in the reported 645x730 -> 640x736 case, only the resized height needs to be clamped from 735 to 736. Changing to ceil also changes the width from 650 to 651, which unnecessarily alters the CLIP preprocessing result.

Could we keep the existing truncation behavior and only enforce the crop invariant instead?

int64_t resized_width =
    static_cast<int64_t>(scale * static_cast<float>(image.shape()[0]));
int64_t resized_height =
    static_cast<int64_t>(scale * static_cast<float>(image.shape()[1]));

resized_width = std::max<int64_t>(resized_width, target_width);
resized_height = std::max<int64_t>(resized_height, target_height);

This should fix the out-of-bounds issue while minimizing behavior changes for existing inputs.

Rounding the resized dimensions up changed the CLIP preprocessing result
for inputs that were already fine. Keep the original truncation and rely
solely on clamping to the crop target to keep the center crop in bounds.
@akhenakh

Copy link
Copy Markdown
Contributor Author

thanks for catching the width regression. Updated to the clamp-only version

@leejet
leejet merged commit d32b4e8 into leejet:master Sep 19, 2026
9 checks passed
@akhenakh
akhenakh deleted the fix/clip-preprocess-resize-rounding branch September 19, 2026 16:56
danielhanchen added a commit to unslothai/stable-diffusion.cpp that referenced this pull request Sep 21, 2026
* fix: preserve "token_refiner" token for MiniMax H3 LoRAs (leejet#1864)

* fix: fail with a message when MiniMax-H3 is run in img_gen mode (leejet#1863)

* feat: support INT8 ConvRot safetensors (leejet#1857)

* fix: replace free_compute_buffer with runner_done in vae (leejet#1872)

* sync: update ggml (leejet#1873)

* fix(ci): trigger builds for ggml updates

* feat: add taeh3 support (leejet#1874)

* fix: prevent gallocr hash overflow in tiny graph-cut segments (leejet#1880)

* fix: re-clamp streaming VRAM budget to currently free memory (leejet#1878)

* fix: mark graph cuts with both a prefix and a suffix (leejet#1883)

* fix: make max_order of lms sampler configurable (leejet#1885)

* fix: guard against missing sampler/scheduler names (leejet#1887)

* chore: format code

* fix: use sd_get_preview_interval() (leejet#1907)

* feat: configurable image / video compression (leejet#1909)

* feat: support standard Qwen3-VL weights for MiniMax-H3 (leejet#1910)

* feat: load scaled FP8 weights without upfront conversion (leejet#1913)

* fix: match exact weights in LLM config detection (leejet#1923)

* feat: add LTX-2.5 support (leejet#1893)

Co-authored-by: leejet <leejet714@gmail.com>

* fix: correct MiniMax H3 reference audio encoding (leejet#1886)

* fix: correct MiniMax H3 audio Euler steps (leejet#1908)

* feat: use backend-native FP8 matmul when supported (leejet#1916)

* sync: update ggml

* feat: additional `--preview-interval` values (leejet#1915)

Co-authored-by: leejet <leejet714@gmail.com>

* feat: support numbering for preview images (leejet#1895)

* fix: use carrier sampling for MiniMax H3 audio (leejet#1924)

* feat: generalize temporal tiling across video VAEs (leejet#1926)

* feat: prefetch streamed layers during compute (leejet#1905)

Co-authored-by: leejet <leejet714@gmail.com>

* refactor: unify runner lifecycles and weight residency (leejet#1940)

* feat: add verbose logging and log-level selection (leejet#1941)

* feat: enable single-GPU auto-fit with tiered parameter placement (leejet#1942)

* fix: reuse graph cut plans across CFG passes (leejet#1943)

* refactor: split ggml extensions and move implementations to cpp files (leejet#1945)

* fix: preserve K-quantized embedding weights (leejet#1936)

* fix: correct SDXL embeddings loading (leejet#1939)

* refactor: unify model source and weight lifecycle management (leejet#1956)

* docs: reflect GGML_MAX_NAME value change in rpc docs (and in ggml_extend assert) (leejet#1950)

* refactor: split generation pipeline out of stable-diffusion.cpp (leejet#1957)

* fix: enable VAE decode tiling fallback without auto-fit (leejet#1932)

* fix: preserve BF16 embedding weights for get_rows (leejet#1959)

* fix: handle invalid option numbers (leejet#1961)

* feat: expose the loaded model version name through the public API (leejet#1962)

* feat: add SenseNova U1.5 support (leejet#1935)

* fix: reuse graph plans when scale parameters change (leejet#1963)

* feat: add linear and attention scale overrides (leejet#1964)

* feat: preserve explicit backend assignments during auto-fit (leejet#1967)

* fix: guard GPU memory capacity and propagate encoding failures (leejet#1958)

* fix: bound plain-text runs in parse_prompt_attention regex (leejet#1919)

* feat: add Wan2.2 S2V (audio+img-to-video) support (leejet#1925)

Co-authored-by: leejet <leejet714@gmail.com>

* fix: validate vision projector output dim against LLM hidden size (leejet#1918)

* fix: resolve MSVC narrowing conversion warnings (leejet#1969)

* feat: Add generation parameters into video metadata (leejet#1901)

Co-authored-by: leejet <leejet714@gmail.com>

* feat: support external Hugging Face tokenizer JSON files (leejet#1973)

* refactor: require external Gemma 2 and GPT-OSS tokenizers (leejet#1974)

* feat: support Brownian tree noise in all noise injection samplers (leejet#1899)

* fix: use tokenizer-specific pre-tokenization rules (leejet#1975)

* fix: remove vision_model. from ununsed tensors (leejet#1983)

* perf: eliminate temporary allocations in Philox rounds (leejet#1982)

* fix: honor flash attention flag in LLM text encoder attention (leejet#1987)

* refactor: remove obsolete unused tensor filtering (leejet#1984)

* perf: pad small attention heads to 64 for MMA Flash Attention (leejet#1992)

* perf: update ggml for faster direct convolutions (leejet#1993)

* perf: accelerate VAE direct 3D convolutions (leejet#1996)

* fix: propagate CUDA driver dependency to shared library consumers

* fix: prevent clip_preprocess center crop from exceeding the resized image (leejet#1995)

* perf: reduce CPU overhead in graph execution and sampling (leejet#1997)

* perf: parallelize host tensor elementwise and broadcast ops (leejet#1998)

* feat: support building with upstream ggml (leejet#1999)

* feat: add Qwen Image 2.1 support (leejet#1994)

* feat: restore legacy fp8 handling when building with upstream ggml (leejet#2001)

* fix: avoid passing ggml logs as format strings (leejet#2002)

* feat: add LLaDA-Image support (leejet#1968)

Co-authored-by: leejet <leejet714@gmail.com>

* feat: add native CUDA SageAttention support (leejet#2005)

* fix: avoid narrowing conversion in SigVQ patch embedding and format code

* docs: update CONTRIBUTING.md

---------

Co-authored-by: stduhpf <stephduh@live.fr>
Co-authored-by: leejet <leejet714@gmail.com>
Co-authored-by: LostRuins Concedo <39025047+LostRuins@users.noreply.github.com>
Co-authored-by: fszontagh <51741446+fszontagh@users.noreply.github.com>
Co-authored-by: Wagner Bruna <wbruna@users.noreply.github.com>
Co-authored-by: vmobilis <75476228+vmobilis@users.noreply.github.com>
Co-authored-by: Piotr Wilkin (ilintar) <ilintar@gmail.com>
Co-authored-by: jk212h20 <101200018+jk212h20@users.noreply.github.com>
Co-authored-by: assouan <750048+assouan@users.noreply.github.com>
Co-authored-by: nan <zjn32202153@gmail.com>
Co-authored-by: Hmission <62598659+Hmission@users.noreply.github.com>
Co-authored-by: LED-M <105789115+xledx@users.noreply.github.com>
Co-authored-by: Maphist0 <28743569+Maphist0@users.noreply.github.com>
Co-authored-by: George <35490284+noctrex@users.noreply.github.com>
Co-authored-by: Санька Четвёртый <CAHbKA-IV@mail.ru>
Co-authored-by: Lin Xuhao <linxuhao84@gmail.com>
Co-authored-by: Fabrice Aneche <akhenakh@users.noreply.github.com>
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