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
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,19 @@ writes only the first batch item
([leejet/stable-diffusion.cpp#2149](https://github.com/leejet/stable-diffusion.cpp/issues/2149)),
so a batched CFG step returns a wrong image without an error.

### VRAM-budget load safety

Until [#34](https://github.com/leehack/stable-diffusion-native/issues/34) is
repaired upstream, `sd_dart_new_sd_ctx` returns NULL and records an actionable
`sd_dart_last_error()` when nonempty `max_vram` is combined with
`auto_fit=false` or a nonempty `params_backend` (which disables auto-fit even
when `auto_fit=true`). It also rejects an empty/default backend on an integrated-only
GPU with a budget: select an explicit backend so auto-fit includes that device.
These checks run before upstream context creation. The raw `new_sd_ctx` export
remains unguarded, and consumers must retain their existing restrictions until
a published artifact and affected-device regression are qualified. This is a
safety restriction, not a repair of upstream memory planning.

## Release

Run the `Native Build & Release` workflow with a tag such as `v0.1.0`. Tags are
Expand Down
6 changes: 6 additions & 0 deletions src/sd_dart_device.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ void copy_text(char* destination, size_t capacity, const char* text) {

} // namespace

bool sd_dart_default_gpu_is_integrated() {
register_backends();
return ggml_backend_dev_by_type(GGML_BACKEND_DEVICE_TYPE_GPU) == nullptr &&
ggml_backend_dev_by_type(GGML_BACKEND_DEVICE_TYPE_IGPU) != nullptr;
}

int32_t sd_dart_gpu_device_count(void) {
if (!SD_DART_GPU_BACKEND) {
return SD_DART_GPU_NO_BACKEND;
Expand Down
23 changes: 23 additions & 0 deletions src/sd_dart_exit.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,29 @@ SdDartStaticsCall::~SdDartStaticsCall() {
sd_ctx_t* sd_dart_new_sd_ctx(const sd_ctx_params_t* sd_ctx_params) {
Call call(kObjectCall | kWorkCall);
ErrorWindow errors;
// The pinned upstream asserts when a VRAM budget prevents its load-time
// prediction probe. Auto-fit skips planning on an integrated-only default
// GPU, so that route needs an explicit backend before it is safe too.
if (sd_ctx_params != nullptr && sd_ctx_params->max_vram != nullptr &&
sd_ctx_params->max_vram[0] != '\0') {
const bool explicit_params_backend = sd_ctx_params->params_backend != nullptr &&
sd_ctx_params->params_backend[0] != '\0';
if (!sd_ctx_params->auto_fit || explicit_params_backend) {
sd_dart_log_load_rejected("max_vram requires auto_fit=true and an empty params_backend; the pinned runtime disables auto-fit with an explicit params_backend and may abort without planning.");
return nullptr;
}
if (sd_ctx_params->backend == nullptr || sd_ctx_params->backend[0] == '\0') {
try {
if (sd_dart_default_gpu_is_integrated()) {
sd_dart_log_load_rejected("max_vram on an integrated-only GPU requires an explicit backend so auto-fit can plan it.");
return nullptr;
}
} catch (...) {
sd_dart_log_load_rejected("Cannot inspect the default GPU safely for max_vram; select an explicit backend.");
return nullptr;
}
}
}
#if SD_DART_METAL
// ggml-metal's GROUP_NORM writes only the first batch item
// (leejet/stable-diffusion.cpp#2149), so batched CFG returns a wrong
Expand Down
5 changes: 5 additions & 0 deletions src/sd_dart_internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,10 @@ void sd_dart_log_call_end();
// failure conservatively invalidates every generation overlapping it.
uint64_t sd_dart_log_backend_failures();
void sd_dart_log_generation_failed();
void sd_dart_log_load_rejected(const char* reason);

// Called only inside the tracked context-creation call. May throw on a
// backend registry failure, which the caller must convert to a load error.
bool sd_dart_default_gpu_is_integrated();

#endif // SD_DART_INTERNAL_H
4 changes: 4 additions & 0 deletions src/sd_dart_log.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,10 @@ void sd_dart_log_generation_failed() {
record(SD_LOG_ERROR, "", "Metal backend failed; discard the image result and recreate the context before generating again.");
}

void sd_dart_log_load_rejected(const char* reason) {
record(SD_LOG_ERROR, "", reason);
}

void sd_dart_log_enable(void) {
// Upstream and ggml keep their callbacks in unsynchronized globals, so
// they are written once rather than on every call.
Expand Down
7 changes: 7 additions & 0 deletions src/sd_dart_wrapper.h
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,13 @@ SD_API void sd_dart_exit_set_wait_ms(int32_t wait_ms, int32_t work_wait_ms);
// isolates.
SD_API void sd_dart_exit_teardown(void);

// Returns NULL with sd_dart_last_error() when max_vram is nonempty and
// auto_fit is false or params_backend is nonempty (which disables auto-fit),
// or when auto_fit uses an integrated-only default GPU
// without an explicit backend. The pinned upstream can abort in these cases.
// Safe loads still require sufficient device memory; raw new_sd_ctx() is not
// guarded by this wrapper.
//
// new_sd_ctx() that tracks the context in the CONTEXT stage before it
// returns. Free the context with sd_dart_exit_free(). A load cannot be
// cancelled: teardown waits for it, with the longer bound. Blocks after
Expand Down
79 changes: 77 additions & 2 deletions tests/native/exit_teardown_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#include <cstdlib>
#include <cstring>
#include <mutex>
#include <stdexcept>
#include <string>
#include <thread>
#include <vector>
Expand Down Expand Up @@ -1760,6 +1761,8 @@ int test_exit_in_fork_child() {

} // namespace

std::atomic<enum ggml_backend_dev_type> test_device_type{GGML_BACKEND_DEVICE_TYPE_GPU};

struct ggml_backend_device {
const char* name = "MTL0";
};
Expand Down Expand Up @@ -1795,7 +1798,7 @@ ggml_backend_dev_t ggml_backend_dev_get(size_t) {

ggml_backend_dev_t ggml_backend_dev_by_type(enum ggml_backend_dev_type type) {
RegistryRead read;
return type == GGML_BACKEND_DEVICE_TYPE_GPU ? device_registry().device : nullptr;
return type == test_device_type.load() ? device_registry().device : nullptr;
}

const char* ggml_backend_dev_name(ggml_backend_dev_t device) {
Expand All @@ -1807,7 +1810,7 @@ const char* ggml_backend_dev_description(ggml_backend_dev_t) {
}

enum ggml_backend_dev_type ggml_backend_dev_type(ggml_backend_dev_t) {
return GGML_BACKEND_DEVICE_TYPE_GPU;
return test_device_type.load();
}

void ggml_backend_dev_memory(ggml_backend_dev_t, size_t* free, size_t* total) {
Expand Down Expand Up @@ -1911,6 +1914,77 @@ void free_sd_image_generation_result(sd_image_generation_result_t* result) {

// A Metal build loads with batched CFG off from a copy of the caller's
// parameters; any other build passes them through.
int test_vram_load_guard() {
sd_ctx_params_t params{};
params.max_vram = "1";
sd_dart_log_set_level(SD_LOG_ERROR + 1);
auto before = loads_begun.load();
CHECK(sd_dart_new_sd_ctx(&params) == nullptr);
CHECK(loads_begun.load() == before);
CHECK(sd_dart_exit_tracked_count() == 0);
CHECK(last_error().find("auto_fit=true") != std::string::npos);

params.auto_fit = true;
// Upstream disables planning for any nonempty params_backend, even when
// auto_fit is true and the runtime backend is explicitly selected.
params.backend = "gpu";
for (const char* placement : {"gpu", "cpu", "diffusion=gpu"}) {
params.params_backend = placement;
CHECK(sd_dart_new_sd_ctx(&params) == nullptr);
CHECK(loads_begun.load() == before);
CHECK(sd_dart_exit_tracked_count() == 0);
CHECK(last_error().find("empty params_backend") != std::string::npos);
}
params.params_backend = "";
sd_ctx_t* explicit_context = sd_dart_new_sd_ctx(&params);
CHECK(explicit_context != nullptr);
CHECK(last_error().empty());
sd_dart_exit_free(explicit_context);
before = loads_begun.load();
params.backend = nullptr;
params.params_backend = nullptr;
test_device_type = GGML_BACKEND_DEVICE_TYPE_IGPU;
CHECK(sd_dart_new_sd_ctx(&params) == nullptr);
CHECK(loads_begun.load() == before);
CHECK(last_error().find("explicit backend") != std::string::npos);
params.backend = "";
CHECK(sd_dart_new_sd_ctx(&params) == nullptr);
CHECK(loads_begun.load() == before);

// Explicit auto-fit planning, a discrete default GPU and no budget remain
// admitted, and a rejected call must not poison a later successful load.
params.backend = "gpu";
sd_ctx_t* context = sd_dart_new_sd_ctx(&params);
CHECK(context != nullptr && loads_begun.load() == before + 1);
CHECK(last_error().empty());
sd_dart_exit_free(context);
params.backend = nullptr;
test_device_type = GGML_BACKEND_DEVICE_TYPE_GPU;
context = sd_dart_new_sd_ctx(&params);
CHECK(context != nullptr && loads_begun.load() == before + 2);
sd_dart_exit_free(context);
// A CPU-only registry does not have the unplanned integrated-GPU path.
test_device_type = GGML_BACKEND_DEVICE_TYPE_CPU;
context = sd_dart_new_sd_ctx(&params);
CHECK(context != nullptr);
sd_dart_exit_free(context);
test_device_type = GGML_BACKEND_DEVICE_TYPE_GPU;
hold_query.store(+[] { throw std::runtime_error("registry unavailable"); });
CHECK(sd_dart_new_sd_ctx(&params) == nullptr);
CHECK(last_error().find("Cannot inspect") != std::string::npos);
hold_query.store(nullptr);
params.auto_fit = false;
params.params_backend = "gpu";
for (const char* budget : {static_cast<const char*>(nullptr), ""}) {
params.max_vram = budget;
context = sd_dart_new_sd_ctx(&params);
CHECK(context != nullptr);
sd_dart_exit_free(context);
}
CHECK(sd_dart_exit_tracked_count() == 0);
return 0;
}

int test_batched_cfg() {
sd_ctx_params_t params{};
params.n_threads = 3;
Expand Down Expand Up @@ -1979,6 +2053,7 @@ int main(int argc, char** argv) {
{"generation-without-images", test_generation_without_images},
{"log-at-exit", test_log_at_exit},
{"batched-cfg", test_batched_cfg},
{"vram-load-guard", test_vram_load_guard},
#if defined(__APPLE__)
{"exit-in-flight", test_exit_in_flight},
#endif
Expand Down
Loading