Compare commits

..

5 Commits

23 changed files with 636 additions and 108 deletions

View File

@ -63,6 +63,12 @@ See [backend selection](./backend.md) for full syntax.
When a graph has cut markers and its missing weights plus incremental compute workspace exceed the available device headroom, it runs its fixed segment list in order. A reusable monolithic compute buffer is not counted as a new allocation. An explicit `--max-vram` budget deducts already-resident managed weights and compute/cache buffers registered by every runner sharing the device, so later graph runs remain segmented when the full graph exceeds the budget. The current segment's weights are pinned during compute, and the next parameter-bearing segment is prefetched when the device supports asynchronous transfer. No opt-in streaming flag is required.
When choosing between monolithic and segmented execution, the runner requires
an additional 128 MiB of headroom in both available device memory and any explicit
managed budget. This planning headroom absorbs small allocation estimate changes;
subsequent capacity checks can consume it while still preserving the 512 MiB device
scratch reserve and respecting the managed budget.
- `--max-vram <GiB>` optionally lowers the live-memory limit. A positive value is a managed per-device budget, `0` uses the device's current free memory without an explicit budget, and a negative value snapshots free memory at startup while reserving that many GiB (`--max-vram -1` reserves about 1 GiB). Driver contexts and unrelated external allocations remain outside the managed budget.
- `--disable-prefetch` disables asynchronous next-segment prefetch while retaining synchronous loading, eviction, and segmented execution.
- `--disable-segmented-compute` forces monolithic graph execution for diagnostics or compatibility, even when the automatic memory check would select segments.

View File

@ -23,7 +23,7 @@ Run the following commands from the build directory. Use image dimensions divisi
### Text to image
```powershell
.\bin\Release\sd-cli.exe --diffusion-model ..\models\diffusion_models\qwen_image_2.1_int8_convrot.safetensors --vae ..\models\vae\qwen_image_2.1_vae_bf16.safetensors --llm ..\models\text_encoders\Qwen3VL-8B-Instruct-Q4_K_M.gguf -p "a lovely cat holding a sign says 'qwen2.1.cpp'" --cfg-scale 6.0 --sampling-method euler -v --offload-to-cpu -o qwen_image_2.1.png
.\bin\Release\sd-cli.exe --diffusion-model ..\models\diffusion_models\qwen_image_2.1_int8_convrot.safetensors --vae ..\models\vae\qwen_image_2.1_vae_bf16.safetensors --llm ..\models\text_encoders\Qwen3VL-8B-Instruct-Q4_K_M.gguf -p "a lovely cat holding a sign says 'qwen2.1.cpp'" --cfg-scale 6.0 --sampling-method euler -v --offload-to-cpu --fa -o qwen_image_2.1.png
```
<img alt="Qwen Image 2.1 example" src="../assets/qwen/qwen_image_2.1.png" />
@ -35,7 +35,7 @@ To use GGUF diffusion weights, set `--diffusion-model` to the path of a file suc
Pass the reference image with `-r` and describe the edit in `-p`. Vision weights are required; the example below loads them separately with `--llm_vision`.
```powershell
.\bin\Release\sd-cli.exe --diffusion-model ..\models\diffusion_models\qwen_image_2.1_int8_convrot.safetensors --vae ..\models\vae\qwen_image_2.1_vae_bf16.safetensors --llm ..\models\text_encoders\Qwen3VL-8B-Instruct-Q4_K_M.gguf --llm_vision ..\models\text_encoders\Qwen3VL-8B-Instruct-mmproj-BF16.gguf -r ..\assets\qwen\qwen_image_2.1.png -p "change 'qwen2.1.cpp' to 'sd.cpp'" --cfg-scale 6.0 --sampling-method euler -v --offload-to-cpu -o qwen_image_2.1_edit.png
.\bin\Release\sd-cli.exe --diffusion-model ..\models\diffusion_models\qwen_image_2.1_int8_convrot.safetensors --vae ..\models\vae\qwen_image_2.1_vae_bf16.safetensors --llm ..\models\text_encoders\Qwen3VL-8B-Instruct-Q4_K_M.gguf --llm_vision ..\models\text_encoders\Qwen3VL-8B-Instruct-mmproj-BF16.gguf -r ..\assets\qwen\qwen_image_2.1.png -p "change 'qwen2.1.cpp' to 'sd.cpp'" --cfg-scale 6.0 --sampling-method euler -v --offload-to-cpu --fa -o qwen_image_2.1_edit.png
```
For multiple reference images, repeat `-r` in the desired order, for example `-r first.png -r second.png`.
@ -44,7 +44,27 @@ For multiple reference images, repeat `-r` in the desired order, for example `-r
By default, the first denoising call for each fixed condition saves the text and reference-image keys and values from every transformer layer. Later calls only compute the target-image tokens. Positive and negative conditions use separate caches, which are released when sampling ends.
The cache uses FP32 on all attention backends. For the default 32-layer model, a prefix of 4096 tokens takes about 4 GiB per condition, in addition to weights and working buffers. The runner accounts for the cache when checking the memory budget. If a cached execution runs out of memory, it releases the prefix caches, disables caching for the rest of that sampling run, and retries the full sequence once. Per-step conditioning extensions currently use the full-sequence path.
Set `qwen_image_2_1_prefix_cache_type` in `--model-args` to `auto` or a type name using the same parser and case-sensitive names as `--type`:
- `auto` (default): use FP16 only when Flash Attention is enabled, Sage Attention is disabled, the attention scale is unchanged, and every attention operation in the cache-writing or cache-reading graph selects Flash Attention after backend support checks. If an operation falls back, rebuild the prefix in FP32 before executing and keep FP32 for the rest of that sampling run.
- `f32`: always store FP32 keys and values.
- `f16`: always store FP16 keys and values, including with ordinary attention or custom attention scaling. This saves cache memory but can introduce additional rounding error.
- Other types, such as `bf16`, `q4_1`, `q5_0`, `q5_1`, `q8_0`, `q4_K`, `q6_K`, `iq4_nl`, and `iq4_xs`: use the requested storage type if the ggml build provides runtime conversion to and from FP32. Quantization is lossy and must be selected explicitly; `auto` never selects a quantized type.
Cache data is packed into contiguous rows of `hidden_size` elements before conversion, so 256-element quantization blocks work with the model's 128-element attention heads without padding. The type's block size must divide `hidden_size`. Unknown types, types lacking runtime conversion (for example `q8_1` and several IQ formats), and incompatible block sizes are ignored with a warning, leaving the previous setting or the default `auto` unchanged.
For example, use `--model-args qwen_image_2_1_prefix_cache_type=q8_0` to enable 8-bit cache storage. Cached keys and values are converted back to the attention input type before concatenating with the current target tokens. This reduces persistent cache memory; attention working buffers still use floating-point values, and conversion adds work on each step. Backends without the required conversion operations use the existing CPU fallback.
For the default 32-layer model, a prefix of 4096 tokens takes approximately the following memory per condition, excluding weights, working buffers, and allocation overhead:
| Cache type | Memory |
| --- | ---: |
| `f32` | 4 GiB |
| `f16` | 2 GiB |
| `q8_0` | 1.0625 GiB |
| `q4_0` | 0.5625 GiB |
The runner accounts for the cache when checking the memory budget. If a cached execution runs out of memory, it releases the prefix caches, disables caching for the rest of that sampling run, and retries the full sequence once. Per-step conditioning extensions currently use the full-sequence path.
Disable this optimization with `--model-args qwen_image_2_1_prefix_cache=false`. It reuses step-independent activations; numerical results can still differ slightly because the matrix sizes change.

View File

@ -518,7 +518,8 @@ ArgOptions SDContextParams::get_options() {
{"",
"--model-args",
"extra model args, key=value list. Supports chroma_use_dit_mask, chroma_use_t5_mask, "
"chroma_t5_mask_pad, qwen_image_zero_cond_t, qwen_image_2_1_prefix_cache",
"chroma_t5_mask_pad, qwen_image_zero_cond_t, qwen_image_2_1_prefix_cache, "
"qwen_image_2_1_prefix_cache_type (auto or a type name from --type)",
(int)',',
&model_args},
{"",

View File

@ -56,6 +56,7 @@ Current endpoints include:
- `GET /sdcpp/v1/jobs/{id}`
- `POST /sdcpp/v1/jobs/{id}/cancel`
- `POST /sdcpp/v1/vid_gen`
- `POST /sdcpp/v1/upscale`
## `sd_cpp_extra_args`
@ -434,7 +435,8 @@ Top-level fields:
| `samplers` | `array<string>` | Available sampling methods |
| `schedulers` | `array<string>` | Available schedulers |
| `loras` | `array<object>` | Available LoRA entries |
| `upscalers` | `array<object>` | Available model-backed highres upscalers |
| `upscalers` | `array<object>` | Available highres upscalers, built-in and model-backed |
| `upscale` | `boolean` | Whether a compatible RGB ESRGAN model is available for `POST /sdcpp/v1/upscale` |
| `limits` | `object` | Shared queue and size limits |
`model`
@ -476,6 +478,8 @@ Shared nested fields:
| Field | Type | Notes |
| --- | --- | --- |
| `upscalers[].name` | `string` | Built-in name or model stem; use this value in `hires.upscaler` |
| `upscalers[].model` | `boolean` | True for a model-backed upscaler, false for a built-in scaling filter |
| `upscalers[].image_upscale` | `boolean` | Whether this model can be selected by `POST /sdcpp/v1/upscale`; false for latent upscalers and built-in filters |
Built-in entries include `None`, `Lanczos`, `Nearest`, `Latent`, `Latent (nearest)`, `Latent (nearest-exact)`, `Latent (antialiased)`, `Latent (bicubic)`, and `Latent (bicubic antialiased)`. Model-backed entries are scanned from the top level of `--hires-upscalers-dir`; subdirectories are not scanned.
@ -489,6 +493,8 @@ Built-in entries include `None`, `Lanczos`, `Nearest`, `Latent`, `Latent (neares
| `limits.max_height` | `integer` |
| `limits.max_batch_count` | `integer` |
| `limits.max_queue_size` | `integer` |
| `limits.max_upscale_width` | `integer` |
| `limits.max_upscale_height` | `integer` |
Shared default fields used by both `img_gen` and `vid_gen`:
@ -641,6 +647,52 @@ Typical status codes:
- `404 Not Found`
- `410 Gone`
#### `POST /sdcpp/v1/upscale`
Runs one RGB ESRGAN upscaler over an image, with no generation involved. Latent upscaler models remain available for hires generation but cannot be used here.
This is the HTTP equivalent of `sd-cli -M upscale`: no diffusion model, text
encoder or sampling is used, so it is fast enough to answer synchronously and
does not create a job.
Request fields:
| Field | Type | Notes |
| --- | --- | --- |
| `image` | `string` | Required. Base64 or data URL image |
| `upscaler` | `string` | A name from `upscalers` with `image_upscale: true`; the first compatible entry when omitted |
| `repeats` | `integer` | Run the upscaler this many times, 1 to 4 (default `1`) |
| `tile_size` | `integer` | Tile size, defaulting to the server's `--upscale-tile-size` |
| `output_format` | `string` | `png`, `jpeg`, or `webp` when built with WebP support (default `png`); unsupported formats return 400 |
| `output_compression` | `integer` | Range is clamped to `0..100` |
Response fields:
| Field | Type | Notes |
| --- | --- | --- |
| `images` | `array<object>` | One image |
| `images[].index` | `integer` | |
| `images[].b64_json` | `string` | Base64-encoded image bytes |
| `upscaler` | `string` | The upscaler actually used |
| `scale` | `integer` | The model's scale factor |
| `repeats` | `integer` | How many times it was run |
| `width` | `integer` | Result width |
| `height` | `integer` | Result height |
| `output_format` | `string` | Final encoded image format |
Typical status codes:
- `200 OK`
- `400 Bad Request` (invalid request, unsupported output format, unreadable image, incompatible upscaler, or output dimensions exceeding the limit)
- `500 Internal Server Error`
Notes:
- Final output dimensions, including all repeats, must not exceed 8192 pixels on either axis (`limits.max_upscale_width` and `limits.max_upscale_height`). Requests exceeding this bound are rejected before upscaling.
- The upscaler models are three-channel; alpha is not preserved.
- The request holds the generation context lock, so an upscale and a
generation never run on the device at the same time.
#### `POST /sdcpp/v1/jobs/{id}/cancel`
Attempts to cancel an accepted job.

View File

@ -3,12 +3,33 @@
#include <algorithm>
#include <cmath>
#include <filesystem>
#include <limits>
#include "async_jobs.h"
#include "common/common.h"
#include "common/media_io.h"
#include "common/resource_owners.hpp"
namespace fs = std::filesystem;
static constexpr uint32_t k_max_upscale_dimension = 8192;
static bool valid_upscale_dimensions(const sd_image_t& image, int factor, int repeats) {
if (image.width == 0 || image.height == 0 || factor < 1 || repeats < 1 || repeats > 4) {
return false;
}
uint32_t width = image.width;
uint32_t height = image.height;
for (int i = 0; i < repeats; ++i) {
if (width > k_max_upscale_dimension / factor || height > k_max_upscale_dimension / factor) {
return false;
}
width *= factor;
height *= factor;
}
return true;
}
static bool parse_cache_mode(const std::string& mode_str, sd_cache_mode_t& mode_out) {
if (mode_str == "disabled") {
mode_out = SD_CACHE_DISABLED;
@ -241,37 +262,59 @@ static json make_capabilities_json(ServerRuntime& runtime) {
available_upscalers.push_back({
{"name", "None"},
{"model", false},
{"image_upscale", false},
});
available_upscalers.push_back({
{"name", "Lanczos"},
{"model", false},
{"image_upscale", false},
});
available_upscalers.push_back({
{"name", "Nearest"},
{"model", false},
{"image_upscale", false},
});
available_upscalers.push_back({
{"name", "Latent"},
{"model", false},
{"image_upscale", false},
});
available_upscalers.push_back({
{"name", "Latent (nearest)"},
{"model", false},
{"image_upscale", false},
});
available_upscalers.push_back({
{"name", "Latent (nearest-exact)"},
{"model", false},
{"image_upscale", false},
});
available_upscalers.push_back({
{"name", "Latent (antialiased)"},
{"model", false},
{"image_upscale", false},
});
available_upscalers.push_back({
{"name", "Latent (bicubic)"},
{"model", false},
{"image_upscale", false},
});
available_upscalers.push_back({
{"name", "Latent (bicubic antialiased)"},
{"model", false},
{"image_upscale", false},
});
bool have_upscaler_models = false;
{
std::lock_guard<std::mutex> lock(*runtime.upscaler_mutex);
for (const auto& entry : *runtime.upscaler_cache) {
available_upscalers.push_back({
{"name", entry.name},
{"model", true},
{"image_upscale", entry.image_upscale_factor > 0},
});
have_upscaler_models = have_upscaler_models || entry.image_upscale_factor > 0;
}
}
@ -341,6 +384,8 @@ static json make_capabilities_json(ServerRuntime& runtime) {
{"max_height", 4096},
{"max_batch_count", 8},
{"max_queue_size", manager.max_pending_jobs},
{"max_upscale_width", k_max_upscale_dimension},
{"max_upscale_height", k_max_upscale_dimension},
};
result["samplers"] = samplers;
result["schedulers"] = schedulers;
@ -350,6 +395,7 @@ static json make_capabilities_json(ServerRuntime& runtime) {
result["features_by_mode"] = features_by_mode;
result["loras"] = available_loras;
result["upscalers"] = available_upscalers;
result["upscale"] = have_upscaler_models;
return result;
}
@ -415,6 +461,171 @@ void register_sdcpp_api_endpoints(httplib::Server& svr, ServerRuntime& rt) {
res.set_content(make_capabilities_json(*runtime).dump(), "application/json");
});
svr.Post("/sdcpp/v1/upscale", [runtime](const httplib::Request& req, httplib::Response& res) {
try {
if (req.body.empty()) {
res.status = 400;
res.set_content(R"({"error":"empty body"})", "application/json");
return;
}
json body = json::parse(req.body);
if (!body.is_object()) {
res.status = 400;
res.set_content(R"({"error":"body must be an object"})", "application/json");
return;
}
for (const char* key : {"repeats", "tile_size", "output_compression"}) {
if (!body.contains(key)) {
continue;
}
const auto& value = body[key];
const bool valid = value.is_number_unsigned()
? value.get<uint64_t>() <= static_cast<uint64_t>(std::numeric_limits<int>::max())
: value.is_number_integer() && value.get<int64_t>() >= std::numeric_limits<int>::min() &&
value.get<int64_t>() <= std::numeric_limits<int>::max();
if (!valid) {
res.status = 400;
res.set_content(json({{"error", std::string(key) + " must be a 32-bit integer"}}).dump(), "application/json");
return;
}
}
ImgGenJobRequest output_options;
std::string error_message;
if (!assign_output_options(output_options,
body.value("output_format", std::string("png")),
body.value("output_compression", 100),
true,
error_message)) {
res.status = 400;
res.set_content(json({{"error", error_message}}).dump(), "application/json");
return;
}
const int tile_size = std::max(32, body.value("tile_size", runtime->default_gen_params->upscale_tile_size));
const int repeats = std::clamp(body.value("repeats", 1), 1, 4);
const std::string wanted = body.value("upscaler", std::string());
const std::string encoded = body.value("image", std::string());
if (encoded.empty()) {
res.status = 400;
res.set_content(R"({"error":"image is required"})", "application/json");
return;
}
SDImageOwner input;
if (!decode_base64_image(encoded, 3, 0, 0, input) || input.get().data == nullptr) {
res.status = 400;
res.set_content(R"({"error":"image could not be read"})", "application/json");
return;
}
refresh_upscaler_cache(*runtime);
int model_scale = 0;
std::string model_path;
std::string used_name;
{
std::lock_guard<std::mutex> lock(*runtime->upscaler_mutex);
for (const auto& entry : *runtime->upscaler_cache) {
if (entry.image_upscale_factor > 0 && (wanted.empty() || entry.name == wanted)) {
model_path = entry.fullpath;
used_name = entry.name;
model_scale = entry.image_upscale_factor;
break;
}
}
}
if (model_path.empty()) {
res.status = 400;
res.set_content(json({{"error", wanted.empty()
? std::string("no RGB ESRGAN upscaler models are available; "
"start the server with --hires-upscalers-dir")
: "no compatible image upscaler called " + wanted}})
.dump(),
"application/json");
return;
}
if (!valid_upscale_dimensions(input.get(), model_scale, repeats)) {
res.status = 400;
res.set_content(R"({"error":"upscaled dimensions must not exceed 8192 x 8192"})", "application/json");
return;
}
// One GPU: an upscale must not run while a generation is using it.
std::lock_guard<std::mutex> ctx_lock(*runtime->sd_ctx_mutex);
UpscalerCtxPtr upscaler_ctx(new_upscaler_ctx(model_path.c_str(),
runtime->ctx_params->diffusion_conv_direct,
runtime->ctx_params->n_threads,
tile_size,
runtime->ctx_params->backend.c_str(),
runtime->ctx_params->params_backend.c_str()));
if (upscaler_ctx == nullptr) {
res.status = 500;
res.set_content(R"({"error":"the upscaler model could not be loaded"})", "application/json");
return;
}
const int factor = get_upscale_factor(upscaler_ctx.get());
// The model file may have changed since its metadata was cached.
if (!valid_upscale_dimensions(input.get(), factor, repeats)) {
res.status = 400;
res.set_content(R"({"error":"upscaled dimensions must not exceed 8192 x 8192"})", "application/json");
return;
}
SDImageOwner current(input.release());
for (int i = 0; i < repeats; ++i) {
sd_image_t* out_images = nullptr;
int out_count = 0;
if (!upscale(upscaler_ctx.get(), current.get(), (uint32_t)factor, &out_images, &out_count) ||
out_count <= 0 || out_images[0].data == nullptr) {
free_sd_images(out_images, out_count);
res.status = 500;
res.set_content(R"({"error":"upscale failed"})", "application/json");
return;
}
sd_image_t produced = out_images[0];
out_images[0] = {0, 0, 0, nullptr};
free_sd_images(out_images, out_count);
current.reset(produced);
}
const std::string& format = output_options.output_format;
const int compression = output_options.output_compression;
const sd_image_t result = current.get();
auto image_bytes = encode_image_to_vector(format == "jpeg" ? EncodedImageFormat::JPEG
: format == "webp" ? EncodedImageFormat::WEBP
: EncodedImageFormat::PNG,
result.data,
result.width,
result.height,
result.channel,
"",
compression);
if (image_bytes.empty()) {
res.status = 500;
res.set_content(R"({"error":"the result could not be encoded"})", "application/json");
return;
}
json out;
out["upscaler"] = used_name;
out["scale"] = factor;
out["repeats"] = repeats;
out["width"] = result.width;
out["height"] = result.height;
out["output_format"] = format;
json images = json::array();
images.push_back({{"index", 0}, {"b64_json", base64_encode(image_bytes)}});
out["images"] = std::move(images);
res.set_content(out.dump(), "application/json");
res.status = 200;
} catch (const json::exception& e) {
res.status = 400;
res.set_content(json({{"error", "invalid request"}, {"message", e.what()}}).dump(), "application/json");
} catch (const std::exception& e) {
res.status = 500;
res.set_content(json({{"error", std::string("server_error: ") + e.what()}}).dump(), "application/json");
}
});
svr.Post("/sdcpp/v1/img_gen", [runtime](const httplib::Request& req, httplib::Response& res) {
try {
if (req.body.empty()) {

View File

@ -295,6 +295,11 @@ std::string get_lora_full_path(ServerRuntime& rt, const std::string& path) {
void refresh_upscaler_cache(ServerRuntime& rt) {
std::vector<UpscalerEntry> new_cache;
std::vector<UpscalerEntry> previous_cache;
{
std::lock_guard<std::mutex> lock(*rt.upscaler_mutex);
previous_cache = *rt.upscaler_cache;
}
fs::path upscaler_dir = rt.ctx_params->hires_upscalers_dir;
if (fs::exists(upscaler_dir) && fs::is_directory(upscaler_dir)) {
@ -308,10 +313,24 @@ void refresh_upscaler_cache(ServerRuntime& rt) {
}
UpscalerEntry upscaler_entry;
upscaler_entry.name = p.stem().u8string();
upscaler_entry.fullpath = fs::absolute(p).lexically_normal().u8string();
upscaler_entry.model_name = "ESRGAN_4x";
upscaler_entry.path = p.filename().u8string();
upscaler_entry.name = p.stem().u8string();
upscaler_entry.fullpath = fs::absolute(p).lexically_normal().u8string();
upscaler_entry.model_name = "ESRGAN_4x";
upscaler_entry.path = p.filename().u8string();
upscaler_entry.file_size = entry.file_size();
upscaler_entry.last_modified = entry.last_write_time();
auto previous = std::find_if(previous_cache.begin(), previous_cache.end(), [&](const UpscalerEntry& cached) {
return cached.fullpath == upscaler_entry.fullpath &&
cached.file_size == upscaler_entry.file_size &&
cached.last_modified == upscaler_entry.last_modified;
});
upscaler_entry.image_upscale_factor = previous != previous_cache.end()
? previous->image_upscale_factor
: get_upscaler_model_scale(upscaler_entry.fullpath.c_str());
if (upscaler_entry.image_upscale_factor > 0) {
upscaler_entry.scale = upscaler_entry.image_upscale_factor;
upscaler_entry.model_name = "ESRGAN_" + std::to_string(upscaler_entry.scale) + "x";
}
new_cache.push_back(std::move(upscaler_entry));
}

View File

@ -2,6 +2,7 @@
#include <algorithm>
#include <cstdint>
#include <filesystem>
#include <mutex>
#include <string>
#include <vector>
@ -43,6 +44,9 @@ struct UpscalerEntry {
std::string fullpath;
std::string model_name;
int scale = 4;
int image_upscale_factor = 0;
uintmax_t file_size = 0;
std::filesystem::file_time_type last_modified;
};
struct ServerRuntime {

View File

@ -558,6 +558,8 @@ SD_API bool upscale(upscaler_ctx_t* upscaler_ctx,
int* num_images_out);
SD_API int get_upscale_factor(upscaler_ctx_t* upscaler_ctx);
// Reads model metadata only; returns 0 if the file is not a recognized RGB ESRGAN model.
SD_API int get_upscaler_model_scale(const char* model_path);
typedef struct adetailer_ctx_t adetailer_ctx_t;

View File

@ -623,7 +623,11 @@ ggml_tensor* ggml_ext_attention_ext(ggml_context* ctx,
bool skip_reshape,
bool flash_attn,
float kv_scale,
bool sage_attn) { // avoid overflow
bool sage_attn,
bool* used_flash_attn) { // avoid overflow
if (used_flash_attn != nullptr) {
*used_flash_attn = false;
}
int64_t L_q;
int64_t L_k;
int64_t C;
@ -755,6 +759,9 @@ ggml_tensor* ggml_ext_attention_ext(ggml_context* ctx,
if (can_use_flash_attn) {
kqv = build_kqv(q, k, v, mask);
if (kqv != nullptr) {
if (used_flash_attn != nullptr) {
*used_flash_attn = true;
}
kqv = ggml_view_4d(ctx,
kqv,
d_head,

View File

@ -217,11 +217,12 @@ ggml_tensor* ggml_ext_attention_ext(ggml_context* ctx,
ggml_tensor* k,
ggml_tensor* v,
int64_t n_head,
ggml_tensor* mask = nullptr,
bool skip_reshape = false,
bool flash_attn = false,
float kv_scale = 1.0f,
bool sage_attn = false);
ggml_tensor* mask = nullptr,
bool skip_reshape = false,
bool flash_attn = false,
float kv_scale = 1.0f,
bool sage_attn = false,
bool* used_flash_attn = nullptr);
ggml_tensor* ggml_ext_layer_norm(ggml_context* ctx,
ggml_tensor* x,

View File

@ -21,11 +21,12 @@ ggml_tensor* ggml_ext_attention_ext(GGMLRunnerContext* ctx,
ggml_tensor* mask,
bool skip_reshape,
bool flash_attn,
float kv_scale) {
float kv_scale,
bool* used_flash_attn) {
if (ctx->attn_scale > 0.f) {
kv_scale = ctx->attn_scale;
}
return ggml_ext_attention_ext(ctx->ggml_ctx, ctx->backend, q, k, v, n_head, mask, skip_reshape, flash_attn, kv_scale, ctx->sage_attn_enabled);
return ggml_ext_attention_ext(ctx->ggml_ctx, ctx->backend, q, k, v, n_head, mask, skip_reshape, flash_attn, kv_scale, ctx->sage_attn_enabled, used_flash_attn);
}
void GGMLRunner::alloc_params_ctx() {
@ -515,9 +516,10 @@ GGMLRunner::~GGMLRunner() {
free_params_ctx();
}
GGMLRunnerContext GGMLRunner::get_context() {
GGMLRunnerContext GGMLRunner::get_context(ggml_cgraph* graph) {
GGMLRunnerContext runner_ctx;
runner_ctx.ggml_ctx = compute_ctx;
runner_ctx.graph = graph;
runner_ctx.backend = runtime_backend;
runner_ctx.flash_attn_enabled = flash_attn_enabled;
runner_ctx.sage_attn_enabled = sage_attn_enabled;
@ -532,8 +534,8 @@ GGMLRunnerContext GGMLRunner::get_context() {
runner_ctx.get_cache_tensor = [this](const std::string& name) {
return this->get_cache_tensor_by_name(name);
};
runner_ctx.cache_tensor = [this](const std::string& name, ggml_tensor* tensor) {
this->cache(name, tensor);
runner_ctx.cache_tensor = [this, graph](const std::string& name, ggml_tensor* tensor) {
this->cache(name, tensor, graph);
};
runner_ctx.set_backend_tensor_data = [this](ggml_tensor* tensor, const void* data) {
this->set_backend_tensor_data(tensor, data);
@ -575,7 +577,7 @@ ggml_tensor* GGMLRunner::to_backend(ggml_tensor* tensor) {
}
}
void GGMLRunner::cache(const std::string name, ggml_tensor* tensor) {
void GGMLRunner::cache(const std::string name, ggml_tensor* tensor, ggml_cgraph* graph) {
if (tensor != nullptr && tensor->view_src != nullptr) {
tensor = ggml_cont(compute_ctx, tensor);
}
@ -583,6 +585,10 @@ void GGMLRunner::cache(const std::string name, ggml_tensor* tensor) {
ggml_set_output(tensor);
}
cache_.stage(name, tensor);
if (graph != nullptr && tensor != nullptr) {
// Schedule the cache output here so its source can be reused before graph end.
ggml_build_forward_expand(graph, tensor);
}
}
std::optional<sd::Tensor<float>> GGMLRunner::compute(get_graph_cb_t get_graph,
@ -831,11 +837,20 @@ std::optional<Tensor<float>> GGMLRunner::execute_graph(ggml_cgraph* graph, int n
last_compute_status_ = GGML_STATUS_ALLOC_FAILED;
return std::nullopt;
}
auto fits_monolithic = [&]() {
// Planning headroom absorbs allocation estimate drift; execution keeps the normal limits.
constexpr size_t planning_headroom = 128ULL * 1024ULL * 1024ULL;
auto requests = memory_requests(full_measurement.buffers, cache_.pending_bytes(graph));
for (auto& request : requests) {
request.pending_allocation_bytes = add_bytes(request.pending_allocation_bytes, planning_headroom);
}
return fits(requests, params);
};
auto manager = residency_manager.lock();
const bool segmented = !is_multi_device() && !sd_backend_is_cpu(runtime_backend) &&
manager != nullptr && manager->segmented_compute_enabled() &&
cached_plan.valid && cached_plan.has_cuts && cached_plan.segments.size() > 1 &&
!fits(memory_requests(full_measurement.buffers, cache_.pending_bytes(graph)), params);
!fits_monolithic();
ggml_graph_cut::Plan monolithic_plan;
if (!segmented) {
monolithic_plan.segments.emplace_back();

View File

@ -67,6 +67,7 @@ struct WeightAdapter {
struct GGMLRunnerContext {
ggml_backend_t backend = nullptr;
ggml_context* ggml_ctx = nullptr;
ggml_cgraph* graph = nullptr;
bool flash_attn_enabled = false;
bool sage_attn_enabled = false;
float linear_scale = 0.f;
@ -102,6 +103,12 @@ struct GGMLRunnerContext {
return get_cache_tensor(name);
}
void expand_graph(ggml_tensor* tensor) const {
if (graph != nullptr && tensor != nullptr) {
ggml_build_forward_expand(graph, tensor);
}
}
void persist_cache_tensor(const std::string& name, ggml_tensor* tensor) const {
if (!cache_tensor || tensor == nullptr) {
return;
@ -122,10 +129,11 @@ ggml_tensor* ggml_ext_attention_ext(GGMLRunnerContext* ctx,
ggml_tensor* k,
ggml_tensor* v,
int64_t n_head,
ggml_tensor* mask = nullptr,
bool skip_reshape = false,
bool flash_attn = false,
float kv_scale = 1.f);
ggml_tensor* mask = nullptr,
bool skip_reshape = false,
bool flash_attn = false,
float kv_scale = 1.f,
bool* used_flash_attn = nullptr);
struct GGMLRunner {
private:
@ -289,7 +297,8 @@ public:
virtual ~GGMLRunner();
virtual GGMLRunnerContext get_context();
// Binding a graph schedules cache outputs at registration instead of graph end.
virtual GGMLRunnerContext get_context(ggml_cgraph* graph = nullptr);
void reset_compute_ctx();
@ -324,7 +333,7 @@ public:
ggml_tensor* to_backend(ggml_tensor* tensor);
void cache(const std::string name, ggml_tensor* tensor);
void cache(const std::string name, ggml_tensor* tensor, ggml_cgraph* graph = nullptr);
ggml_tensor* get_cache_tensor_by_name(const std::string& name) {
return cache_.get(name);

View File

@ -376,7 +376,7 @@ struct ControlNet : public GGMLRunner {
hint = make_input(hint_tensor);
}
auto runner_ctx = get_context();
auto runner_ctx = get_context(gf);
auto outs = control_net.forward(&runner_ctx,
x,
@ -389,8 +389,7 @@ struct ControlNet : public GGMLRunner {
if (guided_hint_input == nullptr && !outs.empty()) {
guided_hint_output_ggml = outs[0];
ggml_set_output(guided_hint_output_ggml);
cache(guided_hint_cache_name(), guided_hint_output_ggml);
ggml_build_forward_expand(gf, guided_hint_output_ggml);
runner_ctx.persist_cache_tensor(guided_hint_cache_name(), guided_hint_output_ggml);
}
control_outputs_ggml.reserve(outs.size() > 0 ? outs.size() - 1 : 0);

View File

@ -134,6 +134,8 @@ namespace Qwen {
std::string name;
std::string cut_group;
int64_t prefix_length = 0;
ggml_type type = GGML_TYPE_F32;
bool* flash_attn_used = nullptr;
};
class QwenImage21ZeroCenterRMSNorm : public RMSNorm {
@ -189,10 +191,15 @@ namespace Qwen {
q = Rope::apply_rope(ctx->ggml_ctx, q, pe);
k = Rope::apply_rope(ctx->ggml_ctx, k, pe);
if (cache.mode == QwenImage21PrefixCache::Mode::STORE) {
// Preserve query-first attention evaluation while writing each layer's
// prefix before its full-sequence K/V can accumulate across layers.
ctx->expand_graph(q);
auto persist = [&](ggml_tensor* tensor, int axis, const char* name) {
auto part = ggml_ext_slice(ctx->ggml_ctx, tensor, axis, 0, cache.prefix_length);
auto copy = ggml_new_tensor(ctx->ggml_ctx, GGML_TYPE_F32, 4, part->ne);
copy = ggml_cpy(ctx->ggml_ctx, part, copy);
// Pack the contiguous data into wider rows so quantization blocks
// can exceed head_dim without padding or changing element order.
part = ggml_reshape_2d(ctx->ggml_ctx, part, x->ne[0], cache.prefix_length);
auto copy = ggml_cast(ctx->ggml_ctx, part, cache.type);
// Keep the copy in this layer's segment so graph cuts do not
// retain or recompute the full-sequence K/V in the final segment.
sd::ggml_graph_cut::mark_graph_cut(copy, cache.cut_group, name);
@ -201,21 +208,37 @@ namespace Qwen {
persist(k, 1, "k");
persist(v, 2, "v");
}
auto attend = [&](ggml_tensor* aq, ggml_tensor* ak, ggml_tensor* av, ggml_tensor* mask) {
bool used_flash_attn = false;
auto out = ggml_ext_attention_ext(ctx, aq, ak, av, heads, mask, true, ctx->flash_attn_enabled, 1.f, &used_flash_attn);
if (cache.flash_attn_used != nullptr) {
*cache.flash_attn_used &= used_flash_attn;
}
return out;
};
ggml_tensor* result = nullptr;
if (cache.mode == QwenImage21PrefixCache::Mode::REUSE) {
auto prefix_k = ctx->load_cache_tensor(cache.name + ".k");
auto prefix_v = ctx->load_cache_tensor(cache.name + ".v");
GGML_ASSERT(prefix_k != nullptr && prefix_v != nullptr);
k = ggml_concat(ctx->ggml_ctx, prefix_k, k, 1);
v = ggml_concat(ctx->ggml_ctx, prefix_v, v, 2);
result = ggml_ext_attention_ext(ctx, q, k, v, heads, nullptr, true, ctx->flash_attn_enabled);
if (prefix_k->type != k->type) {
prefix_k = ggml_cast(ctx->ggml_ctx, prefix_k, k->type);
}
if (prefix_v->type != v->type) {
prefix_v = ggml_cast(ctx->ggml_ctx, prefix_v, v->type);
}
prefix_k = ggml_reshape_4d(ctx->ggml_ctx, prefix_k, dim_head, cache.prefix_length, heads, k->ne[3]);
prefix_v = ggml_reshape_4d(ctx->ggml_ctx, prefix_v, dim_head, heads, cache.prefix_length, v->ne[3]);
k = ggml_concat(ctx->ggml_ctx, prefix_k, k, 1);
v = ggml_concat(ctx->ggml_ctx, prefix_v, v, 2);
result = attend(q, k, v, nullptr);
} else {
for (size_t i = 0; i < segments.size(); ++i) {
const auto& segment = segments[i];
auto sq = ggml_ext_slice(ctx->ggml_ctx, q, 1, segment.start, segment.end);
auto sk = ggml_ext_slice(ctx->ggml_ctx, k, 1, 0, segment.end);
auto sv = ggml_ext_slice(ctx->ggml_ctx, v, 2, 0, segment.end);
auto out = ggml_ext_attention_ext(ctx, sq, sk, sv, heads, masks[i], true, ctx->flash_attn_enabled);
auto out = attend(sq, sk, sv, masks[i]);
result = result == nullptr ? out : ggml_concat(ctx->ggml_ctx, result, out, 1);
}
}
@ -351,8 +374,35 @@ namespace Qwen {
QwenImage21Model model;
std::vector<float> pe_data;
std::vector<sd::Tensor<float>> mask_data;
bool prefix_cache_enabled = true;
bool prefix_cache_disabled = false;
ggml_type prefix_cache_type = GGML_TYPE_COUNT;
bool prefix_cache_enabled = true;
bool prefix_cache_disabled = false;
bool prefix_cache_auto_f32 = false;
static bool supports_prefix_cache_type(ggml_type type) {
if (type == GGML_TYPE_F32) {
return true;
}
const auto* traits = ggml_get_type_traits(type);
if (traits->from_float_ref == nullptr || traits->to_float == nullptr) {
return false;
}
auto cpu = ggml_backend_dev_by_type(GGML_BACKEND_DEVICE_TYPE_CPU);
if (cpu == nullptr) {
return false;
}
auto ctx = std::unique_ptr<ggml_context, decltype(&ggml_free)>(
ggml_init({3 * ggml_tensor_overhead(), nullptr, true}), ggml_free);
if (ctx == nullptr) {
return false;
}
// Some reference quantizers have no runtime copy support. Query the
// device through the registry so dynamically loaded CPU backends work.
auto source = ggml_new_tensor_1d(ctx.get(), GGML_TYPE_F32, ggml_blck_size(type));
auto encoded = ggml_cast(ctx.get(), source, type);
auto decoded = ggml_cast(ctx.get(), encoded, GGML_TYPE_F32);
return ggml_backend_dev_supports_op(cpu, encoded) && ggml_backend_dev_supports_op(cpu, decoded);
}
QwenImage21Runner(ggml_backend_t backend, const String2TensorStorage& weights, const std::string& prefix, std::shared_ptr<RunnerWeightManager> weight_manager = nullptr, const char* model_args = nullptr)
: DiffusionModelRunner(backend, prefix, weight_manager),
@ -361,6 +411,22 @@ namespace Qwen {
for (const auto& [key, value] : parse_key_value_args(model_args, "model arg")) {
if (key == "qwen_image_2_1_prefix_cache" && !parse_strict_bool(value, prefix_cache_enabled)) {
LOG_WARN("ignoring invalid Qwen Image 2.1 model arg '%s=%s'", key.c_str(), value.c_str());
} else if (key == "qwen_image_2_1_prefix_cache_type") {
if (value == "auto") {
prefix_cache_type = GGML_TYPE_COUNT;
continue;
}
const auto type = sd_type_to_ggml_type(str_to_sd_type(value.c_str()));
if (type == GGML_TYPE_COUNT) {
LOG_WARN("ignoring unknown Qwen Image 2.1 cache type '%s'", value.c_str());
} else if (!supports_prefix_cache_type(type)) {
LOG_WARN("ignoring Qwen Image 2.1 cache type '%s': runtime conversion to and from F32 is unavailable", value.c_str());
} else if (config.hidden_size % ggml_blck_size(type) != 0) {
LOG_WARN("ignoring Qwen Image 2.1 cache type '%s': block size %" PRId64 " does not divide hidden size %" PRId64,
value.c_str(), ggml_blck_size(type), config.hidden_size);
} else {
prefix_cache_type = type;
}
}
}
model.init(params_ctx, weights, prefix);
@ -377,11 +443,9 @@ namespace Qwen {
const auto name = cache.name + "." + std::to_string(i);
auto k = get_cache_tensor_by_name(name + ".k");
auto v = get_cache_tensor_by_name(name + ".v");
if (k == nullptr || v == nullptr || k->type != GGML_TYPE_F32 || v->type != GGML_TYPE_F32 ||
k->ne[0] != config.head_dim || k->ne[1] != cache.prefix_length ||
k->ne[2] != config.hidden_size / config.head_dim || k->ne[3] != 1 ||
v->ne[0] != config.head_dim || v->ne[1] != config.hidden_size / config.head_dim ||
v->ne[2] != cache.prefix_length || v->ne[3] != 1) {
if (k == nullptr || v == nullptr || k->type != cache.type || v->type != cache.type ||
k->ne[0] != config.hidden_size || k->ne[1] != cache.prefix_length || k->ne[2] != 1 || k->ne[3] != 1 ||
v->ne[0] != config.hidden_size || v->ne[1] != cache.prefix_length || v->ne[2] != 1 || v->ne[3] != 1) {
return false;
}
}
@ -418,15 +482,28 @@ namespace Qwen {
}
if (!runner_started()) {
prefix_cache_disabled = false;
prefix_cache_auto_f32 = false;
}
QwenImage21PrefixCache cache;
if (prefix_cache_enabled && !prefix_cache_disabled && extra != nullptr && extra->prefix_id != 0 && layout.prefix_length > 0) {
cache.name = "qwen_image_2_1.prefix." + std::to_string(extra->prefix_id) +
".circular." + std::to_string(circular_x_enabled) + std::to_string(circular_y_enabled);
cache.prefix_length = layout.prefix_length;
cache.mode = has_prefix_cache(cache) ? QwenImage21PrefixCache::Mode::REUSE : QwenImage21PrefixCache::Mode::STORE;
if (prefix_cache_type != GGML_TYPE_COUNT) {
cache.type = prefix_cache_type;
} else if (!prefix_cache_auto_f32 && flash_attn_enabled && !sage_attn_enabled &&
(attn_scale <= 0.f || attn_scale == 1.f)) {
cache.type = GGML_TYPE_F16;
}
cache.mode = has_prefix_cache(cache) ? QwenImage21PrefixCache::Mode::REUSE : QwenImage21PrefixCache::Mode::STORE;
}
auto run = [&](const QwenImage21PrefixCache& active_cache) {
bool flash_attn_used = true;
auto run = [&](const QwenImage21PrefixCache& active_cache) {
flash_attn_used = true;
auto checked_cache = active_cache;
if (prefix_cache_type == GGML_TYPE_COUNT && active_cache.type == GGML_TYPE_F16) {
checked_cache.flash_attn_used = &flash_attn_used;
}
const bool cached = active_cache.mode == QwenImage21PrefixCache::Mode::REUSE;
const auto first_position = layout.positions.begin() + (cached ? layout.prefix_length : 0);
Rope::Embedding embedding;
@ -459,7 +536,7 @@ namespace Qwen {
auto build = [&]() {
auto graph = new_graph_custom(QWEN_IMAGE_GRAPH_SIZE * 2);
auto pe = ggml_new_tensor_4d(compute_ctx, GGML_TYPE_F32, 2, 2, config.head_dim / 2,
layout.positions.size() - (cached ? layout.prefix_length : 0));
layout.positions.size() - (cached ? layout.prefix_length : 0));
set_backend_tensor_data(pe, pe_data.data());
std::vector<ggml_tensor*> masks, ref_inputs;
for (const auto& mask : mask_data) {
@ -470,15 +547,28 @@ namespace Qwen {
ref_inputs.push_back(make_input(ref));
}
}
auto ctx = get_context();
auto ctx = get_context(graph);
auto out = model.forward(&ctx, make_input(x), make_input(*inputs.timesteps), cached ? nullptr : make_input(context),
ref_inputs, pe, layout, masks, active_cache);
ref_inputs, pe, layout, masks, checked_cache);
if (!flash_attn_used) {
return static_cast<ggml_cgraph*>(nullptr);
}
ggml_build_forward_expand(graph, out);
return graph;
};
return restore_trailing_singleton_dims(GGMLRunner::compute(build, n_threads, false), x.dim());
};
auto result = run(cache);
if (result.empty() && !flash_attn_used) {
// Casting an F16 cache back to F32 cannot recover its original values.
// Recompute the prefix before executing a graph that falls back from FA.
free_cache_ctx_and_buffer();
prefix_cache_auto_f32 = true;
cache.type = GGML_TYPE_F32;
cache.mode = QwenImage21PrefixCache::Mode::STORE;
LOG_DEBUG("Qwen Image 2.1: Flash Attention unavailable; using F32 prefix caching for this sampling run");
result = run(cache);
}
if (result.empty() && last_compute_status() == GGML_STATUS_ALLOC_FAILED &&
(cache.mode != QwenImage21PrefixCache::Mode::NONE || !cache_.empty())) {
// The failed graph has ended before persistent inputs are released.
@ -493,7 +583,7 @@ namespace Qwen {
prefix_cache_disabled = true;
LOG_WARN("Qwen Image 2.1: incomplete prefix cache; disabling it for this sampling run");
} else {
LOG_DEBUG("Qwen Image 2.1: cached prefix %" PRIu64 " (%" PRId64 " tokens)", extra->prefix_id, layout.prefix_length);
LOG_DEBUG("Qwen Image 2.1: cached prefix %" PRIu64 " (%" PRId64 " tokens, %s)", extra->prefix_id, layout.prefix_length, ggml_type_name(cache.type));
}
}
return result;

View File

@ -442,16 +442,9 @@ namespace SenseNovaU1 {
k = ggml_concat(ctx->ggml_ctx, prefix_k, k, 2);
v = ggml_concat(ctx->ggml_ctx, prefix_v, v, 2);
} else {
// Keep dedicated graph outputs alive until the runner copies them
// into its persistent cache buffer after graph execution.
auto cache_k = ggml_dup_tensor(ctx->ggml_ctx, k);
cache_k = ggml_cpy(ctx->ggml_ctx, k, cache_k);
ggml_set_output(cache_k);
auto cache_v = ggml_dup_tensor(ctx->ggml_ctx, v);
cache_v = ggml_cpy(ctx->ggml_ctx, v, cache_v);
ggml_set_output(cache_v);
ctx->persist_cache_tensor(layer_cache + ".k", cache_k);
ctx->persist_cache_tensor(layer_cache + ".v", cache_v);
ctx->expand_graph(q);
ctx->persist_cache_tensor(layer_cache + ".k", k);
ctx->persist_cache_tensor(layer_cache + ".v", v);
}
q = ggml_cont(ctx->ggml_ctx,
@ -687,7 +680,7 @@ namespace SenseNovaU1 {
ggml_set_name(attention_mask, "snu15.prefix.attention_mask");
set_backend_tensor_data(attention_mask, attention_mask_vec.data());
auto runner_ctx = get_context();
auto runner_ctx = get_context(graph);
auto text_model = model.text_model();
auto hidden = text_model->embed(&runner_ctx, ids);
hidden = text_model->forward(&runner_ctx,

View File

@ -1300,7 +1300,7 @@ struct LTXVideoVAE : public VAE {
feat_map[feat_idx] = get_cache_tensor_by_name(temporal_feat_cache_name(feat_idx));
}
auto runner_ctx = get_context();
auto runner_ctx = get_context(gf);
int feat_count = 0;
ggml_tensor* out = vae.decode_tiled_chunk(&runner_ctx,
z,
@ -1313,8 +1313,7 @@ struct LTXVideoVAE : public VAE {
for (int feat_idx = 0; feat_idx < feat_count && feat_idx < static_cast<int>(feat_map.size()); ++feat_idx) {
ggml_tensor* feat_cache = feat_map[static_cast<size_t>(feat_idx)];
if (feat_cache != nullptr) {
cache(temporal_feat_cache_name(static_cast<size_t>(feat_idx)), feat_cache);
ggml_build_forward_expand(gf, feat_cache);
runner_ctx.persist_cache_tensor(temporal_feat_cache_name(static_cast<size_t>(feat_idx)), feat_cache);
}
}

View File

@ -1443,15 +1443,14 @@ namespace WAN {
ggml_tensor* z = make_input(z_tensor);
auto runner_ctx = get_context();
auto runner_ctx = get_context(gf);
ggml_tensor* out = ae.decode_tiled_chunk(&runner_ctx, z, chunk_idx);
for (size_t feat_idx = 0; feat_idx < ae._feat_map.size(); feat_idx++) {
ggml_tensor* feat_cache = ae._feat_map[feat_idx];
if (feat_cache != nullptr) {
cache("feat_idx:" + std::to_string(feat_idx), feat_cache);
ggml_build_forward_expand(gf, feat_cache);
runner_ctx.persist_cache_tensor("feat_idx:" + std::to_string(feat_idx), feat_cache);
}
}

View File

@ -1349,9 +1349,8 @@ bool is_first_stage_model_name(const std::string& name) {
}
static std::string convert_esrgan_tensor_name(std::string name) {
static std::unordered_map<std::string, std::string> esrgan_name_map;
if (esrgan_name_map.empty()) {
static const auto esrgan_name_map = [] {
std::unordered_map<std::string, std::string> esrgan_name_map;
esrgan_name_map["model.0."] = "conv_first.";
constexpr int max_num_blocks = 64;
@ -1377,7 +1376,8 @@ static std::string convert_esrgan_tensor_name(std::string name) {
esrgan_name_map["model.7."] = "conv_last.";
esrgan_name_map["model.8."] = "conv_hr.";
esrgan_name_map["model.10."] = "conv_last.";
}
return esrgan_name_map;
}();
replace_with_prefix_map(name, esrgan_name_map);
return name;

View File

@ -2441,6 +2441,7 @@ sd::Tensor<float> StableDiffusionGGML::sample(const std::shared_ptr<DiffusionMod
timesteps_tensor,
cond,
&controls);
bool uncond_controls_ready = false;
static const std::vector<sd::Tensor<float>> empty_ref_latents;
bool uncond_without_ref_latents = !img_uncond.empty() &&
@ -2530,6 +2531,17 @@ sd::Tensor<float> StableDiffusionGGML::sample(const std::shared_ptr<DiffusionMod
return std::move(cached_output);
}
// A re-enabled condition can miss the cache even when the positive pass was reused.
if (!uncond_controls_ready && !uncond.empty() &&
(&condition == &uncond || &condition == &img_uncond)) {
compute_sample_controls(control_image,
noised_input,
timesteps_tensor,
uncond,
&controls);
uncond_controls_ready = true;
}
for (const auto& extension : generation_extensions) {
extension->before_diffusion(diffusion_params, step);
}
@ -2569,41 +2581,69 @@ sd::Tensor<float> StableDiffusionGGML::sample(const std::shared_ptr<DiffusionMod
}
}
float effective_guidance_scale = guidance_schedule.empty()
? cfg_scale
: guidance_schedule[guidance_schedule.size() - 1 - step];
float image_guidance_scale = img_cfg_scale;
constexpr float kEpsilon = 1e-5f;
bool skip_uncond = false;
if (!uncond.empty() && !needs_uncond_denoised && !use_apg_guidance) {
if (!img_uncond.empty()) {
skip_uncond = std::abs(image_guidance_scale - effective_guidance_scale) < kEpsilon;
} else {
skip_uncond = std::abs(effective_guidance_scale - 1.0f) < kEpsilon;
}
}
bool skip_img_uncond = false;
if (!img_uncond.empty() && !needs_uncond_denoised && !use_apg_guidance) {
if (!uncond.empty()) {
skip_img_uncond = std::abs(image_guidance_scale - 1.0f) < kEpsilon;
} else {
skip_img_uncond = std::abs(effective_guidance_scale - 1.0f) < kEpsilon;
}
}
cond_out = run_condition(*positive_condition, c_concat_override);
if (cond_out.empty()) {
return {};
}
if (!uncond.empty()) {
if (!step_cache.is_step_skipped()) {
compute_sample_controls(control_image,
noised_input,
timesteps_tensor,
uncond,
&controls);
}
const std::vector<int>* uncond_skip_layers = nullptr;
if (is_skiplayer_step && slg_uncond) {
LOG_VERBOSE("Skipping layers at uncond step %d\n", step);
uncond_skip_layers = &skip_layer_guidance.layers();
}
uncond_out = run_condition(uncond,
uncond.c_concat.empty() ? nullptr : &uncond.c_concat,
uncond_skip_layers,
nullptr,
true);
if (uncond_out.empty()) {
return {};
if (!skip_uncond) {
const std::vector<int>* uncond_skip_layers = nullptr;
if (is_skiplayer_step && slg_uncond) {
LOG_VERBOSE("Skipping layers at uncond step %d\n", step);
uncond_skip_layers = &skip_layer_guidance.layers();
}
uncond_out = run_condition(uncond,
uncond.c_concat.empty() ? nullptr : &uncond.c_concat,
uncond_skip_layers,
nullptr,
true);
if (uncond_out.empty()) {
return {};
}
} else {
step_cache.invalidate_condition(&uncond);
}
}
if (!img_uncond.empty()) {
img_uncond_out = run_condition(img_uncond,
img_uncond.c_concat.empty() ? nullptr : &img_uncond.c_concat,
nullptr,
uncond_without_ref_latents ? &empty_ref_latents : nullptr,
true);
if (img_uncond_out.empty()) {
return {};
if (!skip_img_uncond) {
img_uncond_out = run_condition(img_uncond,
img_uncond.c_concat.empty() ? nullptr : &img_uncond.c_concat,
nullptr,
uncond_without_ref_latents ? &empty_ref_latents : nullptr,
true);
if (img_uncond_out.empty()) {
return {};
}
} else {
step_cache.invalidate_condition(&img_uncond);
}
}
sd::guidance::GuidanceInput guidance_input;
@ -2613,7 +2653,7 @@ sd::Tensor<float> StableDiffusionGGML::sample(const std::shared_ptr<DiffusionMod
guidance_input.pred_uncond = uncond_out.empty() ? nullptr : &uncond_out;
guidance_input.pred_img_uncond = img_uncond_out.empty() ? nullptr : &img_uncond_out;
sd::guidance::GuiderOutput guided = guidance_schedule.empty() ? primary_guidance.forward(guidance_input, {}) : primary_guidance.forward(guidance_input, {}, guidance_schedule[guidance_schedule.size() - 1 - step]);
sd::guidance::GuiderOutput guided = primary_guidance.forward(guidance_input, {}, effective_guidance_scale);
if (guided.pred.empty()) {
return {};
}

View File

@ -728,12 +728,20 @@ inline float flux_time_shift(float mu, float sigma, float t) {
// https://github.com/black-forest-labs/flux/blob/main/src/flux/sampling.py#L289
struct FluxScheduler : SigmaScheduler {
int image_seq_len = 0;
float base_shift = 0.5f;
float max_shift = 1.15f;
int image_seq_len = 0;
int base_image_seq_len = 256;
int max_image_seq_len = 4096;
float base_shift = 0.5f;
float max_shift = 1.15f;
float shift_terminal = 0.0f;
explicit FluxScheduler(int image_seq_len, const char* extra_sample_args = nullptr)
FluxScheduler(int image_seq_len, SDVersion version, const char* extra_sample_args = nullptr)
: image_seq_len(image_seq_len) {
if (version == VERSION_QWEN_IMAGE_2_1) {
max_image_seq_len = 8192;
max_shift = 0.9f;
shift_terminal = 0.02f;
}
parse_extra_sample_args(extra_sample_args);
}
@ -752,10 +760,8 @@ struct FluxScheduler : SigmaScheduler {
}
float compute_mu() const {
constexpr float base_shift_anchor = 256.0f;
constexpr float max_shift_anchor = 4096.0f;
float m = (max_shift - base_shift) / (max_shift_anchor - base_shift_anchor);
float b = base_shift - m * base_shift_anchor;
float m = (max_shift - base_shift) / static_cast<float>(max_image_seq_len - base_image_seq_len);
float b = base_shift - m * static_cast<float>(base_image_seq_len);
return static_cast<float>(image_seq_len) * m + b;
}
@ -764,7 +770,7 @@ struct FluxScheduler : SigmaScheduler {
sigmas.reserve(n + 1);
float mu = compute_mu();
LOG_VERBOSE("Flux scheduler: image_seq_len=%d, steps=%u, mu=%.3f", image_seq_len, n, mu);
LOG_VERBOSE("Flux scheduler: image_seq_len=%d, steps=%u, mu=%.3f, shift_terminal=%.3f", image_seq_len, n, mu, shift_terminal);
if (n == 0) {
sigmas.push_back(1.0f);
@ -780,6 +786,16 @@ struct FluxScheduler : SigmaScheduler {
}
}
if (shift_terminal > 0.0f && n > 1) {
// The terminal shift applies to the last model evaluation, not the final zero sigma.
float scale_factor = (1.0f - sigmas[n - 1]) / (1.0f - shift_terminal);
if (std::isfinite(scale_factor) && scale_factor > 0.0f) {
for (uint32_t i = 0; i < n; ++i) {
sigmas[i] = 1.0f - (1.0f - sigmas[i]) / scale_factor;
}
}
}
sigmas[n] = 0.0f;
return sigmas;
}
@ -1178,7 +1194,7 @@ struct Denoiser {
}
case FLUX_SCHEDULER: {
LOG_INFO("get_sigmas with Flux scheduler");
scheduler = std::make_shared<FluxScheduler>(image_seq_len, extra_sample_args);
scheduler = std::make_shared<FluxScheduler>(image_seq_len, version, extra_sample_args);
break;
}
default:

View File

@ -275,6 +275,26 @@ namespace sd_sample {
}
}
void SampleStepCacheDispatcher::invalidate_condition(const void* condition) {
if (condition == nullptr) {
return;
}
switch (runtime.mode) {
case SampleCacheMode::EASYCACHE:
runtime.easycache.cache_diffs.erase(condition);
break;
case SampleCacheMode::UCACHE:
runtime.ucache.cache_diffs.erase(condition);
break;
case SampleCacheMode::CACHEDIT:
runtime.cachedit.cache_diffs.erase(condition);
break;
case SampleCacheMode::NONE:
break;
}
}
bool SampleStepCacheDispatcher::is_step_skipped() const {
switch (runtime.mode) {
case SampleCacheMode::EASYCACHE:

View File

@ -46,6 +46,7 @@ namespace sd_sample {
bool before_condition(const void* condition, const sd::Tensor<float>& input, sd::Tensor<float>* output);
void after_condition(const void* condition, const sd::Tensor<float>& input, const sd::Tensor<float>& output);
void invalidate_condition(const void* condition);
bool is_step_skipped() const;
};

View File

@ -262,6 +262,30 @@ int get_upscale_factor(upscaler_ctx_t* upscaler_ctx) {
return upscaler_ctx->upscaler->esrgan_upscaler->config.scale;
}
int get_upscaler_model_scale(const char* model_path) {
if (model_path == nullptr || model_path[0] == '\0') {
return 0;
}
try {
ModelLoader loader;
if (!loader.init_from_file_and_convert_name(model_path, "", VERSION_ESRGAN)) {
return 0;
}
const auto& tensors = loader.get_tensor_storage_map();
auto first = tensors.find("conv_first.weight");
auto last = tensors.find("conv_last.weight");
if (first == tensors.end() || last == tensors.end() ||
tensors.count("body.0.rdb1.conv1.weight") == 0 ||
first->second.n_dims != 4 || last->second.n_dims != 4 ||
first->second.ne[2] != 3 || last->second.ne[3] != 3) {
return 0;
}
return ESRGANConfig::detect_from_weights(tensors).scale;
} catch (const std::exception&) {
return 0;
}
}
void free_upscaler_ctx(upscaler_ctx_t* upscaler_ctx) {
if (upscaler_ctx->upscaler != nullptr) {
delete upscaler_ctx->upscaler;