Repository navigation
fix: apply LoRAs at runtime where a merge crashes, and size FP8 on Metal - #32
Merged
Merged
Conversation
The immediately LoRA apply mode, which auto selects for unquantized weights, merges each diff into the weight in place. Merging into FP8 weights segfaults ggml-cpu (the F32 cast is a MUL_MAT with no FP8 vec_dot, and there is no F32 to FP8 conversion), and merging an F32 diff into F16 weights aborts Metal (ggml-metal ADD needs equal types; the scheduler cannot move an op on a pre-allocated weight). lora_immediate_fallback.patch selects at_runtime for FP8 weights, and for F16 weights on a backend whose supports_op rejects that add, with a warning when immediately was asked. An FP8 LoRA tensor crashed ggml-cpu in every mode for the same missing kernel; fp8_metal_f16_load.patch now loads FP8 LoRA tensors as F16 on every backend. It also makes auto-fit count FP8 weights at F16 size when Metal is present, since Metal holds them as F16. tests/test_fp8_runtime.py now covers E5M2, applies a synthetic LoRA in each mode to F32, F16 and FP8 weights on every device, and checks the auto-fit plan on Metal. The retirement check also catches FP8 helpers and _fp8 kernel names. Refs #31
8 tasks
…ght type Upstream creates Conv2d, grouped Conv2d and Conv3d weights as F16 whatever the file holds, so an F32 or BF16 checkpoint with a LoRA on a convolution still aborted Metal under immediately or auto. The fallback no longer depends on stored F16 weights: it probes the TE, diffusion and VAE backends of every model without FP8 weights. A backend whose default buffers are host memory passes, since the scheduler runs the add on the CPU there. The probe prints the runtime log, so the tests check the fallback warning and that the LoRA applied. They add a convolution LoRA, BF16 and E5M2 weights, the text encoder and TAESD on Metal behind a CPU diffusion model, and FP8 weights a type rule loads as F16. Refs #31
GitHub's macOS runners list MTL0 but cannot generate on it; the other Metal tests skip on the F32 control there. Refs #31
leehack
marked this pull request as ready for review
October 27, 2026 17:26
This was referenced Oct 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #31.
LoRAs no longer crash the process on FP8 weights, FP8 adapters or Metal; auto-fit plans FP8 checkpoints at the size Metal holds them; the FP8 test covers E5M2. Every crash below is pre-existing (same on
mainand the pre-#30 pin).Root causes
immediately(andauto, which picks it for unquantized weights), CPU: SIGSEGVLoraModel::build_lora_graph→ggml_ext_cast_f32→ggml_compute_forward_mul_mat_one_chunkcalls address 0mul_mat(weight, ones); ggml-cpu has no FP8vec_dot. The write-back would then abort too: no F32→FP8from_float.lora_down, any mode, CPU: SIGSEGVget_out_diff→ggml_ext_linear(x, lora_down)→ same nullvec_dotLinearcasts FP8 weights first.immediately/auto, Metal: SIGABRTggml-backend.cpp:1089"pre-allocated tensor … (ADD)"ggml_add_inplace(weight F16, diff F32)inbuild_lora_graphADDneedssrc0->type == src1->type; the weight lives in an MTL0 buffer, so the scheduler cannot move the op. Every model has F16 weights: upstream creates Conv2d, grouped Conv2d and Conv3d weights as F16 whatever the file holds (ggml_block.hpp:519/604/761), so F32 and BF16 checkpoints abort too with a LoRA on a convolution; FP8 bases, which #30 loads as F16 on Metal, abort with any LoRA.All three confirmed under lldb on a RelWithDebInfo build (operand types printed at the faulting node) and reproduced on SD-Turbo F16/FP8 with the audit's synthetic LoRAs.
Fix
patches/lora_immediate_fallback.patch(diffusion_engine.cpp,configure_weight_loading): FP8 weights, and any model whose TE, diffusion or VAE runtime backend rejects an in-place F16 += F32 add (asupports_opprobe) outside host memory, take the LoRAat_runtime.autoselects it silently;immediatelywarns, as upstream already does for INT8 tensorwise weights. In practice Metal always applies LoRAs at runtime; CPU, BLAS (host buffers, where the scheduler runs the add on the CPU) and Vulkan merges are unchanged. Since dc48762 the probe no longer depends on stored F16 weights (audit P2).patches/fp8_metal_f16_load.patch: FP8 LoRA tensors load as F16 on every backend (theregister_param_tensorsretype now also applies toModelComponent::LoRA), andbackend_fit.cppestimate_componentscounts FP8 weights at F16 size when a Metal device exists (it overestimates a component that the plan keeps off Metal).tests/test_fp8_runtime.py: E5M2 and BF16 copies; linear, FP8-down and convolution (x_embedder.proj) LoRAs in each apply mode on F32/F16/BF16/E4M3/E5M2 weights per device, checking the image againstat_runtime, the applied-tensor log and the fallback warning; TE and TAESD on MTL0 behind a CPU diffusion model; FP8 weights ablocks=f16type rule loads as F16 merge without fallback (CPU); auto-fit plan on Metal (a model with an ignored 8M-element DiT tensor, since the plan prints whole MiB); retirement check now also matches FP8 helpers (fp8, any case) and_fp8kernel names.Not chosen: fixing the merge graph in
lora.hpp(a header; the carry mechanism compiles patched copies of.cppsources only, and diverging inline copies would violate ODR), adding FP8 kernels or an F32→FP8 conversion to ggml (upstream ggml).Evidence (Apple M4 Max, macOS arm64, release builds)
Tiny PixArt model, 3 weight types × 2 LoRAs × 3 modes + plain, on CPU and MTL0 (42 runs, at 5077a47):
main1694916immediately/auto; FP8lora_downin every mode), 8 SIGABRT on MTL0 (F16/FP8 baseimmediately/auto)at_runtimeimage, and different from the plain imageSD-Turbo (
sd_turbo-f16.gguf,sd_turbo-fp8-all.safetensors), audit LoRAs: all 12 previously crashing or reference cases exit 0;immediatelylogs the fallback; F16 CPUimmediatelyoutput is byte-identical to the #30 head.Test measurements at dc48762: weight types vs F32 control mean ≤ 0.031 (CPU), 0 (Metal); LoRA modes vs
at_runtime≤ 0.112 (CPU), 0 (Metal), tolerance 0.5; LoRA effect 1.6 (linear), 0.66 (conv). Application is checked from the runtime log rather than an effect threshold. Auto-fit DiT plan with the pad: F16 32 MiB, FP8 16 MiB onmain, 32 MiB here.Audit P2 at 5077a47 (
llamadart-evidence/sd-native-pr-32/audit/): F32 and BF16 weights with a convolution LoRA abort MTL0 underimmediately/auto; at dc48762 they fall back and pass.Mutations at dc48762 (each rebuilt and run with
SD_REQUIRE_RUNTIME=1 SD_REQUIRE_METAL_GENERATION=1)6c4d699 changes only the test: the module test first generates the F32 control on Metal, so it skips on GitHub's macOS runners (MTL0 listed, cannot generate; dc48762's run SIGBUSed there). The probe-only-DIFFUSION mutant is re-checked against it: still killed (te, vae); the whole module passes locally on Metal.
immediately/autoon MTL0 (8 subtests)expected_typeautoignores blocker /immediatelyignores blockerSurvives by design: removing the host-buffer shortcut only makes BLAS fall back to
at_runtime(same image); the test asserts warnings on CPU and Metal only.Mutations at 5077a47
mainpatches (failing-first)immediately/auto(CPU, BLAS, MTL0)immediately/autoon MTL0autoignores blockerautosubtests (CPU, MTL0)immediatelyignores blockerimmediatelysubtestslora_downon CPU/BLAStest_fp8_generates_on_each_gpu(E5M2, MTL0)test_auto_fit_plans_fp8_as_f16_on_metalRetirement regexes checked on samples: match
GGML_TYPE_F8_E4M3,ggml_is_type_fp8(…),kernel_mul_mm_f8_e4m3_f32,kernel_mul_mv_fp8_f32; notkernel_mul_mv_ext_q8_0_f32_r1_8or the pinned sources.Also at dc48762:
validate_artifacts.py macos-arm64ok;unittest discover -s testswithSD_REQUIRE_RUNTIME,SD_REQUIRE_METAL_GENERATION: 73 tests, 7 skipped, all pass; patched upstream sources are clang-format clean.Reachability from llamadart
main(0a33025e9). It never setssd_img_gen_params_t.loras, the classifier rejects LoRA files ("does not load yet"), and the C API parses no prompt LoRA syntax. llamadart keeps the defaultlora_apply_mode(auto), so image LoRA support (llamadart#1223) without this fix would abort on Apple GPUs with ordinary F16 checkpoints and crash the CPU with FP8 checkpoints or FP8 adapters.auto_fiton and relies on it for models that do not fit; an FP8 checkpoint on an Apple GPU was planned at half its resident size.Open items
Behavior: on Metal
immediatelynow always applies at runtime. A LoRA on TAESD weights, which only the immediately mode merged, is then not applied (upstream applies no LoRA to TAESD at runtime); only the generic fallback warning is logged.Converting weights to FP8 with a tensor type rule aborts at load (
ggml.c:8312,ggml_quantize_chunk) with or without a LoRA: pre-existing upstream, not addressed.Upstream reports for the three root causes (stable-diffusion.cpp
lora.hppmerge, leejet/ggml CPU FP8MUL_MAT/CPYsupport claims) are not filed; patches/README asks for an upstream link to retire the patches.Vulkan: CI lavapipe only (source: Vulkan adds F16+F32, rejects FP8
MUL_MATsoLinearcasts). CUDA not run.ASan exit teardown on Metal and iOS devices not run on this head.