Recover from a ggml unsupported-op abort by restarting on the CPU backend

ggml checks every node against the device's supports_op and calls GGML_ABORT
when one is not implemented, because a single-backend graph has nowhere else to
put it: there is no per-op CPU fallback. The whole sd-server dies with SIGABRT
mid-generation and the user gets "the native image renderer stopped
unexpectedly" with no way forward.

Seen on macos-14 arm64 with FLUX.2-klein-4B Q2_K through the cross-platform CI:
the text encoder is already pinned to CPU, and the abort moved into the denoise
loop instead.

    ggml_metal_op_encode_impl: error: unsupported op 'MUL_MAT' -> ggml_abort
    StableDiffusionGGML::sample -> sample_k_diffusion

A retry on the same backend would abort identically, so the load is restarted
once with --backend cpu (the only flag that changes which backend executes the
graph; --offload-to-cpu moves parameters, not compute) and the generation is
re-submitted. The same checkpoint then renders slower rather than not at all.
Strictly bounded: the signature must carry both the unsupported-op line and
ggml_abort, the device must not already be CPU, and it happens once per load,
so an OOM kill or a genuine crash still surfaces as itself.
This commit is contained in:
Daniel Han 2026-07-27 09:08:42 +00:00
commit 9a9999f8cd
5 changed files with 244 additions and 9 deletions

View file

@ -18,6 +18,7 @@ from core.inference.diffusion_memory import (
OFFLOAD_SEQUENTIAL,
)
from core.inference.sd_cpp_args import (
CPU_BACKEND_FLAGS,
SdCppGenParams,
SdCppModelFiles,
SdCppUpscaleParams,
@ -25,6 +26,7 @@ from core.inference.sd_cpp_args import (
build_sd_cpp_command,
build_sd_cpp_server_command,
build_sd_cpp_upscale_command,
is_ggml_unsupported_op_abort,
metal_text_encoder_flags,
native_speed_flags,
offload_flags,
@ -526,3 +528,39 @@ def test_img_gen_request_flux_uses_distilled_guidance():
def test_img_gen_request_requires_prompt():
with pytest.raises(ValueError):
build_img_gen_request(prompt = " ", steps = 4)
def test_ggml_unsupported_op_abort_is_recognised_only_with_both_markers():
# The CPU-backend rescue must fire for the deterministic "this backend cannot run this graph"
# abort and for nothing else: an OOM kill or a plain crash has to surface as itself.
abort = (
"sd-server connection lost during img_gen poll (process exited, code -6)\n"
"Last output:\n"
"[ERROR] ggml_extend.hpp:70 - ggml_metal_op_encode_impl: error: unsupported op 'MUL_MAT'\n"
"1 sd-server 0x00000001044f8df4 ggml_abort + 156"
)
assert is_ggml_unsupported_op_abort(abort) is True
# The RMS_NORM shape of the same abort (text encoder) counts too.
assert is_ggml_unsupported_op_abort(
"error: unsupported op 'RMS_NORM'\nggml_abort + 156"
) is True
# Neither marker alone is enough, and an unrelated death is never a match.
assert is_ggml_unsupported_op_abort("unsupported op 'MUL_MAT'") is False
assert is_ggml_unsupported_op_abort("ggml_abort + 156") is False
assert is_ggml_unsupported_op_abort("process exited, code -9") is False
assert is_ggml_unsupported_op_abort("") is False
def test_server_command_appends_extra_args_last():
# --backend cpu is passed as extra_args by the abort rescue, and sd.cpp is last-wins, so it has
# to land after every flag the normal build emits.
cmd = build_sd_cpp_server_command(
"/x/sd-server",
SdCppModelFiles(diffusion_model = "/m/z.gguf", vae = "/m/vae.sft"),
host = "127.0.0.1",
port = 1234,
native_speed = "default",
extra_args = list(CPU_BACKEND_FLAGS),
)
assert cmd[-2:] == ["--backend", "cpu"]
assert cmd[0] == "/x/sd-server"