Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,15 @@ function(sd_native_carry_patch patch)
get_target_property(sources stable-diffusion SOURCES)
foreach(path IN LISTS ARGN)
set(upstream_file "${SD_NATIVE_UPSTREAM_DIR}/${path}")
if(NOT "${upstream_file}" IN_LIST sources)
string(MD5 source_key "${upstream_file}")
get_property(current_source GLOBAL PROPERTY "SD_NATIVE_PATCHED_${source_key}")
if(NOT current_source)
set(current_source "${upstream_file}")
endif()
if(NOT "${current_source}" IN_LIST sources)
message(FATAL_ERROR "patches/${patch}: ${path} is not a stable-diffusion source")
endif()
file(READ "${upstream_file}" text)
file(READ "${current_source}" text)
# A Windows checkout can convert upstream's line endings; patches use LF.
string(REPLACE "\r\n" "\n" text "${text}")
file(WRITE "${patched_dir}/${path}" "${text}")
Expand All @@ -57,15 +62,21 @@ function(sd_native_carry_patch patch)

foreach(path IN LISTS ARGN)
set(upstream_file "${SD_NATIVE_UPSTREAM_DIR}/${path}")
string(MD5 source_key "${upstream_file}")
get_property(current_source GLOBAL PROPERTY "SD_NATIVE_PATCHED_${source_key}")
if(NOT current_source)
set(current_source "${upstream_file}")
endif()
set(patched_file "${patched_dir}/${path}")
file(READ "${upstream_file}" original)
file(READ "${current_source}" original)
string(REPLACE "\r\n" "\n" original "${original}")
file(READ "${patched_file}" patched)
if(original STREQUAL patched)
message(FATAL_ERROR "patches/${patch} left ${path} unchanged:\n${output}")
endif()
list(REMOVE_ITEM sources "${upstream_file}")
list(REMOVE_ITEM sources "${current_source}")
list(APPEND sources "${patched_file}")
set_property(GLOBAL PROPERTY "SD_NATIVE_PATCHED_${source_key}" "${patched_file}")
# Quoted includes in the copy must still find the upstream files next to it.
get_filename_component(upstream_file_dir "${upstream_file}" DIRECTORY)
set_source_files_properties("${patched_file}" TARGET_DIRECTORY stable-diffusion
Expand All @@ -78,10 +89,12 @@ endfunction()
sd_native_carry_patch(pickle_line_span.patch src/model_io/pickle_io.cpp)
sd_native_carry_patch(fp8_metal_f16_load.patch src/model_manager.cpp src/core/backend_fit.cpp)
sd_native_carry_patch(lora_immediate_fallback.patch src/pipeline/diffusion_engine.cpp)
sd_native_carry_patch(intrinsic_tiny_decoder_policy.patch src/stable-diffusion.cpp src/pipeline/diffusion_engine.cpp)

# Wrapper exports are compiled into the upstream library target, so they share
# its SD_API export macro and reach upstream without patching it.
target_sources(stable-diffusion PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/src/sd_dart_decoder_policy.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/src/sd_dart_device.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/src/sd_dart_exit.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/src/sd_dart_internal.h"
Expand Down
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -669,3 +669,26 @@ Archive names:
## License

MIT. Release archives include the stable-diffusion.cpp and ggml licenses.

### Intrinsic tiny-decoder policy

`sd_dart_new_sd_ctx_intrinsic_tiny_v1(params)` creates a tracked context that
requires an intrinsic SDXS (512-DS or 09) tiny decoder without an external VAE
or TAE. It rejects other candidates before runner construction, load-time
backend probes and auto-fit. Automatic source refresh rechecks the candidate
under the context execution lock before rebuilding; image and video calls
check the decoder again after refresh, before generation. An atomic source
replacement by a full-VAE model therefore fails instead of silently changing
the decoder. A refusal returns null on load, or false with no images on
generation; `sd_dart_last_error()` reports the reason. Dispose with
`sd_dart_exit_free()`. Existing unrestricted contexts keep their behavior.

The versioned export is the capability probe; an older runtime without it
cannot offer this invariant. This is the owner prerequisite for
[llamadart #1047](https://github.com/leehack/llamadart/issues/1047), not physical
iPhone qualification or an artifact publication. Run the model-backed
regression with `SD_INTRINSIC_TINY_MODEL=<sdxs.gguf>
SD_FULL_VAE_MODEL=<sd-turbo.gguf> python3 -m unittest discover -s tests
-p test_decoder_policy_runtime.py`. It runs unchanged and refreshed SDXS,
refuses the SDXS-to-SD-Turbo replacement, and keeps an unrestricted reload
control on CPU and the available Metal backend.
17 changes: 14 additions & 3 deletions patches/README.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,28 @@
# Carried upstream patches

Fixes the pinned stable-diffusion.cpp lacks, carried until a pin includes them.
Owner-carried changes the pinned stable-diffusion.cpp public C API cannot provide.
`CMakeLists.txt` applies each patch to build-tree copies of the upstream
sources it names and compiles the copies, so the submodule stays pristine.
Configuring fails when a patch no longer applies or changes nothing; then drop
or rebase it. Build info and the release manifest list the patches next to the
upstream commit.

A patch here needs a regression test that fails against the unpatched pin and
an upstream issue or pull request to retire it.
Every patch needs a regression test that fails against the unpatched pin.
An upstream bug fix must link an upstream issue or pull request that defines
its retirement. A versioned owner-only context contract may instead link the
canonical consumer requirement when upstream's public C API cannot enforce
it. Keep those patches to the minimum insertion points, exercise causal
controls against real refresh/execution paths, and retire them when upstream
exposes an equivalent contract. Consumer requirements are not upstream bug
reports.

| Patch | Sources | Fixes | Test |
| --- | --- | --- | --- |
| `pickle_line_span.patch` | `src/model_io/pickle_io.cpp` | A legacy PyTorch checkpoint above 2 GiB is rejected: the pickle scanner narrows the rest of the file to `int` before looking for a newline ([#23](https://github.com/leehack/stable-diffusion-native/issues/23)). | `tests/test_pickle_span.py` |
| `fp8_metal_f16_load.patch` | `src/model_manager.cpp`, `src/core/backend_fit.cpp` | An FP8 (E4M3 or E5M2) checkpoint aborts the process on Metal: ggml-metal claims FP8 `MUL_MAT` but has no FP8 kernel. Weights a Metal backend computes with now load as F16, which holds every FP8 value, so they take twice their file size; other backends keep FP8 ([#29](https://github.com/leehack/stable-diffusion-native/issues/29)). Auto-fit plans them at that size, and FP8 LoRA tensors, which crashed ggml-cpu, load as F16 on every backend ([#31](https://github.com/leehack/stable-diffusion-native/issues/31)). | `tests/test_fp8_runtime.py` |
| `lora_immediate_fallback.patch` | `src/pipeline/diffusion_engine.cpp` | The `immediately` LoRA apply mode, which `auto` selects for unquantized weights, crashes the CPU merging into FP8 weights (ggml-cpu can neither multiply nor write FP8) and aborts Metal merging into F16 weights (ggml-metal adds only equal types), which every model has: convolution weights load as F16. FP8 weights, and every model on a backend that cannot run that add outside host memory, now take the LoRA `at_runtime`, with a warning when `immediately` was requested ([#31](https://github.com/leehack/stable-diffusion-native/issues/31)). | `tests/test_fp8_runtime.py` |

| `intrinsic_tiny_decoder_policy.patch` | `src/stable-diffusion.cpp`, `src/pipeline/diffusion_engine.cpp` | Owner-only `sd_dart_new_sd_ctx_intrinsic_tiny_v1` context contract for [llamadart #1047](https://github.com/leehack/llamadart/issues/1047), not an upstream defect: automatic model refresh is valid upstream behavior. The public C API cannot preserve a decoder invariant, so minimal context creation/free and candidate/execution insertion points reject a full-VAE replacement before runner probes or decode. Real unchanged/refreshed SDXS and unrestricted reload controls exercise the boundary. Retire when upstream exposes an equivalent persistent decoder policy. | `tests/test_decoder_policy_runtime.py` |

Patches touching the same source compose in declaration order. Each next
patch applies to the preceding build-tree copy; the submodule stays pristine.
128 changes: 128 additions & 0 deletions patches/intrinsic_tiny_decoder_policy.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
--- a/src/stable-diffusion.cpp
+++ b/src/stable-diffusion.cpp
@@ -11,6 +11,10 @@
#include "pipeline/diffusion_engine.h"
#include "pipeline/generation.h"
#include "pipeline/request.h"
+
+// Implemented by the owner wrapper; policy state outlives C exit teardown.
+bool sd_dart_intrinsic_tiny_required(const void* engine);
+void sd_dart_set_intrinsic_tiny_required(const void* engine, bool required);

#define NONE_STR "NONE"

@@ -645,7 +649,7 @@
StableDiffusionGGML* sd = nullptr;
};

-sd_ctx_t* new_sd_ctx(const sd_ctx_params_t* sd_ctx_params) {
+static sd_ctx_t* new_sd_ctx_with_decoder_policy(const sd_ctx_params_t* sd_ctx_params, bool intrinsic_tiny) {
sd_ctx_t* sd_ctx = (sd_ctx_t*)malloc(sizeof(sd_ctx_t));
if (sd_ctx == nullptr) {
return nullptr;
@@ -657,7 +661,11 @@
return nullptr;
}

+ if (intrinsic_tiny) {
+ sd_dart_set_intrinsic_tiny_required(sd_ctx->sd, true);
+ }
if (!sd_ctx->sd->init(sd_ctx_params)) {
+ sd_dart_set_intrinsic_tiny_required(sd_ctx->sd, false);
delete sd_ctx->sd;
sd_ctx->sd = nullptr;
free(sd_ctx);
@@ -666,8 +674,31 @@
return sd_ctx;
}

+sd_ctx_t* new_sd_ctx(const sd_ctx_params_t* sd_ctx_params) {
+ return new_sd_ctx_with_decoder_policy(sd_ctx_params, false);
+}
+
+sd_ctx_t* sd_dart_new_sd_ctx_intrinsic_tiny_impl(const sd_ctx_params_t* params) {
+ return new_sd_ctx_with_decoder_policy(params, true);
+}
+
+static bool decoder_policy_valid(const StableDiffusionGGML& sd) {
+ if (!sd_dart_intrinsic_tiny_required(&sd)) {
+ return true;
+ }
+ if ((sd.version == VERSION_SDXS_512_DS || sd.version == VERSION_SDXS_09) &&
+ sd.config_ && sd.uses_tae() && !sd.tae_preview_only() &&
+ strlen(SAFE_STR(sd.config_->params.vae_path)) == 0 &&
+ strlen(SAFE_STR(sd.config_->params.taesd_path)) == 0) {
+ return true;
+ }
+ LOG_ERROR("intrinsic tiny decoder policy v1 rejects the current decoder");
+ return false;
+}
+
void free_sd_ctx(sd_ctx_t* sd_ctx) {
if (sd_ctx->sd != nullptr) {
+ sd_dart_set_intrinsic_tiny_required(sd_ctx->sd, false);
delete sd_ctx->sd;
sd_ctx->sd = nullptr;
}
@@ -759,7 +790,7 @@
if (!result_out || !sd_ctx || !sd_ctx->sd || !params)
return false;
StableDiffusionGGML::ExecutionScope execution(*sd_ctx->sd);
- if (!execution.ready)
+ if (!execution.ready || !decoder_policy_valid(*sd_ctx->sd))
return false;
sd_image_t* images = nullptr;
int count = 0;
@@ -785,7 +816,7 @@
if (!result_out || !sd_ctx || !sd_ctx->sd || !params)
return false;
StableDiffusionGGML::ExecutionScope execution(*sd_ctx->sd);
- if (!execution.ready)
+ if (!execution.ready || !decoder_policy_valid(*sd_ctx->sd))
return false;
auto* result = static_cast<sd_video_generation_result_t*>(calloc(1, sizeof(sd_video_generation_result_t)));
if (!result)
--- a/src/pipeline/diffusion_engine.cpp
+++ b/src/pipeline/diffusion_engine.cpp
@@ -218,10 +218,31 @@
return candidate.refresh_files(false) && apply_model_update(std::move(candidate));
}

+bool sd_dart_intrinsic_tiny_required(const void* engine);
+
+static bool intrinsic_tiny_candidate_allowed(const void* engine, SDVersion version,
+ const sd_ctx_params_t* params) {
+ if (!sd_dart_intrinsic_tiny_required(engine)) {
+ return true;
+ }
+ if ((version == VERSION_SDXS_512_DS || version == VERSION_SDXS_09) && params &&
+ strlen(SAFE_STR(params->vae_path)) == 0 && strlen(SAFE_STR(params->taesd_path)) == 0) {
+ return true;
+ }
+ LOG_ERROR("intrinsic tiny decoder policy v1 rejects the updated model or external decoder");
+ return false;
+}
+
bool StableDiffusionGGML::apply_model_update(ModelLoader candidate,
std::unique_ptr<ModelConfig> next_config,
RunnerGroups groups) {
const SDVersion next_version = candidate.get_sd_version();
+ const auto* decoder_config = next_config ? next_config.get() : config_.get();
+ // Reject a changed family before runners, probes or weight placement.
+ if (!intrinsic_tiny_candidate_allowed(this, next_version,
+ decoder_config ? &decoder_config->params : nullptr)) {
+ return false;
+ }
if (next_version == VERSION_COUNT) {
LOG_ERROR("cannot identify updated diffusion model");
return false;
@@ -975,6 +996,9 @@
LOG_INFO("Version: %s ", sd_version_name(version));
}

+ if (!intrinsic_tiny_candidate_allowed(this, version, &configuration->params)) {
+ return false;
+ }
if (auto_fit_enabled) {
if (!sd::backend_fit::derive_backend_specs(model_loader,
sd_type_to_ggml_type(sd_ctx_params->wtype),
33 changes: 33 additions & 0 deletions src/sd_dart_decoder_policy.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#include "sd_dart_internal.h"

#include <mutex>
#include <unordered_set>

namespace {
struct Policies {
std::mutex mutex;
std::unordered_set<const void*> intrinsic_tiny;
};

Policies& policies() {
// Native calls and exit teardown can still use the registry at C exit.
static auto* value = new Policies();
return *value;
}
} // namespace

bool sd_dart_intrinsic_tiny_required(const void* engine) {
auto& state = policies();
std::lock_guard<std::mutex> lock(state.mutex);
return state.intrinsic_tiny.count(engine) != 0;
}

void sd_dart_set_intrinsic_tiny_required(const void* engine, bool required) {
auto& state = policies();
std::lock_guard<std::mutex> lock(state.mutex);
if (required) {
state.intrinsic_tiny.insert(engine);
} else {
state.intrinsic_tiny.erase(engine);
}
}
13 changes: 11 additions & 2 deletions src/sd_dart_exit.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,7 @@ SdDartStaticsCall::~SdDartStaticsCall() {
}
}

sd_ctx_t* sd_dart_new_sd_ctx(const sd_ctx_params_t* sd_ctx_params) {
static sd_ctx_t* new_context(const sd_ctx_params_t* sd_ctx_params, bool intrinsic_tiny) {
Call call(kObjectCall | kWorkCall);
ErrorWindow errors;
// The pinned upstream asserts when a VRAM budget prevents its load-time
Expand Down Expand Up @@ -451,11 +451,20 @@ sd_ctx_t* sd_dart_new_sd_ctx(const sd_ctx_params_t* sd_ctx_params) {
sd_ctx_params = &unbatched;
}
#endif
sd_ctx_t* context = new_sd_ctx(sd_ctx_params);
sd_ctx_t* context = intrinsic_tiny ? sd_dart_new_sd_ctx_intrinsic_tiny_impl(sd_ctx_params)
: new_sd_ctx(sd_ctx_params);
insert(context, free_context, cancel_context, SD_DART_EXIT_STAGE_CONTEXT);
return context;
}

sd_ctx_t* sd_dart_new_sd_ctx(const sd_ctx_params_t* params) {
return new_context(params, false);
}

sd_ctx_t* sd_dart_new_sd_ctx_intrinsic_tiny_v1(const sd_ctx_params_t* params) {
return new_context(params, true);
}

namespace {

// Moves the images out of upstream's result into an array that
Expand Down
6 changes: 6 additions & 0 deletions src/sd_dart_internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
#define SD_DART_INTERNAL_H

#include <cstdint>
#include "stable-diffusion.h"


bool sd_dart_intrinsic_tiny_required(const void* engine);
void sd_dart_set_intrinsic_tiny_required(const void* engine, bool required);
sd_ctx_t* sd_dart_new_sd_ctx_intrinsic_tiny_impl(const sd_ctx_params_t* params);

// What the wrapper's sources share with each other. Not shipped and not
// exported.
Expand Down
11 changes: 11 additions & 0 deletions src/sd_dart_wrapper.h
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,17 @@ SD_API void sd_dart_exit_teardown(void);
// not modified.
SD_API sd_ctx_t* sd_dart_new_sd_ctx(const sd_ctx_params_t* sd_ctx_params);

// Version 1 of the persistent intrinsic tiny-decoder policy. Like
// sd_dart_new_sd_ctx(), but requires an intrinsic SDXS (512-DS or 09) decoder
// and no external VAE/TAE. Rejected before load-time decoder probes. Every
// automatic refresh validates the candidate under the context execution lock
// before rebuilding runners, and image/video generation validates it again
// after refresh. A later source replacement cannot select a full VAE.
// Returns null on policy refusal; use sd_dart_last_error() for the reason.
// Free a returned context with sd_dart_exit_free(). Old runtimes without this
// export cannot enforce the policy.
SD_API sd_ctx_t* sd_dart_new_sd_ctx_intrinsic_tiny_v1(const sd_ctx_params_t* sd_ctx_params);

// generate_image() as a call in flight. Enables native log recording and
// returns false if Metal reports a failed command buffer, even if upstream
// returns success. Returns the images as an array of `*num_images_out` that
Expand Down
Loading
Loading