Commit 0b3b98d for stable-diffusion.cpp

commit 0b3b98d7b83ec433925252e1fbc00118629a3c5f
Author: leejet <leejet714@gmail.com>
Date:   Sun Nov 8 16:24:53 2026 +0800

    fix: avoid CPU fallback based on backend allocation chunk size (#2294)

diff --git a/src/core/ggml_runner.cpp b/src/core/ggml_runner.cpp
index 2fa14b1..770f46b 100644
--- a/src/core/ggml_runner.cpp
+++ b/src/core/ggml_runner.cpp
@@ -212,6 +212,10 @@ ggml_backend_t GGMLRunner::backend_for_weight(const ggml_tensor* tensor) const {
 }

 static bool backend_can_allocate_op(ggml_backend_t backend, const ggml_tensor* node) {
+    // Vulkan reports a preferred allocation chunk size, not a hard tensor limit.
+    if (sd_backend_is(backend, "Vulkan")) {
+        return true;
+    }
     auto buft        = ggml_backend_get_default_buffer_type(backend);
     size_t alignment = ggml_backend_buft_get_alignment(buft);
     size_t max_size  = ggml_backend_buft_get_max_size(buft);
@@ -219,6 +223,7 @@ static bool backend_can_allocate_op(ggml_backend_t backend, const ggml_tensor* n
     if (ggml_backend_buft_get_alloc_size(buft, node) > max_size) {
         return false;
     }
+    // Cross-backend copies must also fit the destination's allocation limit.
     for (auto src : node->src) {
         if (src != nullptr && ggml_backend_buft_get_alloc_size(buft, src) > max_size) {
             return false;
@@ -251,8 +256,6 @@ void GGMLRunner::pin_multi_device_nodes(ggml_backend_sched_t sched, ggml_cgraph*
             node->op == GGML_OP_PERMUTE || node->op == GGML_OP_TRANSPOSE) {
             continue;
         }
-        // Operation support can allow tensors larger than the backend's allocation limit.
-        // Check inputs too, since cross-backend copies must also fit a single allocation.
         if (!backend_can_allocate_op(current, node)) {
             auto cpu = workspace_.cpu_backend();
             if (cpu != nullptr && ggml_backend_supports_op(cpu, node)) {
diff --git a/src/model/diffusion/minimax_h3.hpp b/src/model/diffusion/minimax_h3.hpp
index 97fd230..5f388d8 100644
--- a/src/model/diffusion/minimax_h3.hpp
+++ b/src/model/diffusion/minimax_h3.hpp
@@ -174,14 +174,14 @@ namespace MiniMaxH3 {

             int64_t chunk_size = x->ne[1];
             if (ctx->backend != nullptr) {
-                auto buft        = ggml_backend_get_default_buffer_type(ctx->backend);
-                size_t alignment = ggml_backend_buft_get_alignment(buft);
-                size_t max_size  = ggml_backend_buft_get_max_size(buft);
-                max_size -= max_size % alignment;
-                // Matmul outputs are F32 even for quantized weights. Split before fc1
-                // so its expanded activation never needs an oversized backend buffer.
+                auto buft           = ggml_backend_get_default_buffer_type(ctx->backend);
+                size_t alignment    = ggml_backend_buft_get_alignment(buft);
+                size_t chunk_budget = ggml_backend_buft_get_max_size(buft);
+                chunk_budget -= chunk_budget % alignment;
+                // Use the backend's preferred allocation size as a conservative chunk budget.
+                // Matmul outputs are F32 even for quantized weights.
                 size_t token_bytes = ggml_row_size(GGML_TYPE_F32, std::max(x->ne[0], 2 * ffn_hidden_size)) * x->ne[2] * x->ne[3];
-                chunk_size         = static_cast<int64_t>(std::min<size_t>(chunk_size, std::max<size_t>(1, max_size / token_bytes)));
+                chunk_size         = static_cast<int64_t>(std::min<size_t>(chunk_size, std::max<size_t>(1, chunk_budget / token_bytes)));
             }
             if (chunk_size == x->ne[1]) {
                 return forward_chunk(x);
@@ -269,10 +269,11 @@ namespace MiniMaxH3 {
             if (ctx->backend != nullptr) {
                 auto buft        = ggml_backend_get_default_buffer_type(ctx->backend);
                 size_t alignment = ggml_backend_buft_get_alignment(buft);
-                size_t max_size  = ggml_backend_buft_get_max_size(buft);
-                max_size -= max_size % alignment;
+                // This is a conservative chunk budget, not an operation support limit.
+                size_t chunk_budget = ggml_backend_buft_get_max_size(buft);
+                chunk_budget -= chunk_budget % alignment;
                 size_t token_bytes = ggml_row_size(GGML_TYPE_F32, 3 * heads * head_dim) * inputs[0]->ne[2] * inputs[0]->ne[3];
-                chunk_size         = static_cast<int64_t>(std::min<size_t>(chunk_size, std::max<size_t>(1, max_size / token_bytes)));
+                chunk_size         = static_cast<int64_t>(std::min<size_t>(chunk_size, std::max<size_t>(1, chunk_budget / token_bytes)));
             }
             if (chunk_size == sequence) {
                 ggml_tensor* projected = nullptr;
@@ -284,7 +285,7 @@ namespace MiniMaxH3 {
                 return split_qkv(ctx, projected);
             }

-            // Split Q/K/V before concatenating tokens so the full packed QKV never exceeds the buffer limit.
+            // Split Q/K/V before concatenating tokens to avoid materializing the full packed QKV.
             std::vector<ggml_tensor*> chunks[3];
             for (size_t i = 0; i < inputs.size(); ++i) {
                 auto input = inputs[i];