mirror of
https://github.com/leejet/stable-diffusion.cpp.git
synced 2026-09-24 06:57:54 -05:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a4ebba818 | ||
|
|
70c1dbc01e | ||
|
|
36746936c0 | ||
|
|
2dc7f5408a | ||
|
|
e6281b6318 | ||
|
|
241518b35d | ||
|
|
c92d73c408 | ||
|
|
28b454bda1 | ||
|
|
2bb72947cb | ||
|
|
ac45422a05 |
@@ -0,0 +1,61 @@
|
||||
name: Close PRs from organization forks
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, reopened]
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
close-organization-fork-pr:
|
||||
if: >-
|
||||
github.event.pull_request.head.repo.owner.type == 'Organization' &&
|
||||
github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Explain the contribution policy and close the PR
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const { data: pr } = await github.rest.pulls.get({
|
||||
...context.repo,
|
||||
pull_number: context.issue.number,
|
||||
});
|
||||
const headRepo = pr.head.repo;
|
||||
if (pr.state !== 'open' || !headRepo ||
|
||||
headRepo.id === pr.base.repo.id || headRepo.owner.type !== 'Organization') {
|
||||
return;
|
||||
}
|
||||
|
||||
const marker = '<!-- organization-fork-policy -->';
|
||||
const comments = await github.paginate(github.rest.issues.listComments, {
|
||||
...context.repo,
|
||||
issue_number: pr.number,
|
||||
per_page: 100,
|
||||
});
|
||||
const alreadyExplained = comments.some(comment =>
|
||||
comment.user?.login === 'github-actions[bot]' && comment.body?.includes(marker));
|
||||
if (!alreadyExplained) {
|
||||
await github.rest.issues.createComment({
|
||||
...context.repo,
|
||||
issue_number: pr.number,
|
||||
body: [
|
||||
marker,
|
||||
'This repository requires contributions from forks to use a personal fork with **Allow edits from maintainers** enabled.',
|
||||
'GitHub does not support this option for organization-owned forks, so this PR is being closed automatically.',
|
||||
'Please open a new PR from a fork in your personal GitHub account and enable **Allow edits from maintainers** so maintainers can help update the branch.',
|
||||
'See [the GitHub documentation](https://docs.github.com/en/pull-requests/how-tos/work-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork).',
|
||||
].join('\n\n'),
|
||||
});
|
||||
}
|
||||
await github.rest.pulls.update({
|
||||
...context.repo,
|
||||
pull_number: pr.number,
|
||||
state: 'closed',
|
||||
});
|
||||
@@ -12,6 +12,10 @@ If you want to update a third-party dependency, please open an issue first inste
|
||||
|
||||
## Pull Requests
|
||||
|
||||
When contributing from a fork, use a fork under your personal GitHub account and enable **Allow edits from maintainers**. This lets maintainers make follow-up fixes directly on the PR branch.
|
||||
|
||||
PRs from organization-owned forks are automatically closed when opened or reopened because GitHub does not support this maintainer-edit option for those forks. Submit the changes from a personal fork instead. See [GitHub's documentation](https://docs.github.com/en/pull-requests/how-tos/work-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork).
|
||||
|
||||
Keep each PR focused on one clear change. Large or overly complex PRs are harder to review and may not be merged.
|
||||
|
||||
Do not include test code or test scripts in commits or PRs. Keep them local and report verification results in the PR description.
|
||||
|
||||
@@ -2,6 +2,16 @@
|
||||
|
||||
Caching methods accelerate diffusion inference by reusing intermediate computations when changes between steps are small.
|
||||
|
||||
### Conditioning Cache
|
||||
|
||||
Conditioning results are cached per model context using an LRU cache. The default
|
||||
capacity is **0 (disabled) for `sd-cli`** and **4 entries for `sd-server` and the C
|
||||
API**. Set `--conditioning-cache-size N` to change the limit; `0` disables caching.
|
||||
For example, `sd-cli -m model.safetensors -p "a cat" --conditioning-cache-size 4`
|
||||
enables the cache in the CLI. The C API option is
|
||||
`sd_ctx_params_t::conditioning_cache_size`, initialized by `sd_ctx_params_init()`.
|
||||
This cache is independent of the diffusion-step `--cache-mode` options below.
|
||||
|
||||
### Cache Modes
|
||||
|
||||
| Mode | Target | Description |
|
||||
|
||||
@@ -26,6 +26,9 @@ Stable-diffusion.spp also supports basic Unet-based editing models like instruct
|
||||
|
||||
## Configuring Reference Modes (`--ref-image-args`)
|
||||
|
||||
For a one-time input transform before reference presets and model processing,
|
||||
including cropping, padding, and resizing algorithms, see [Image preprocessing](./image_preprocessing.md).
|
||||
|
||||
Different DiT-based editing models require different configurations to process reference images correctly (e.g., whether to use a Vision Language Model (VLM) encoder or pass VAE-encoded images directly to the DiT).
|
||||
|
||||
To simplify this, we provide **Presets**. By default, the system automatically selects the best preset based on the model architecture. However, you can override this using the `--ref-image-args` argument.
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
You can use ESRGAN—such as the model [RealESRGAN_x4plus_anime_6B.pth](https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.2.4/RealESRGAN_x4plus_anime_6B.pth)—to upscale the generated images and improve their overall resolution and clarity.
|
||||
|
||||
RGBA images, including Qwen Image 2.1 output, keep their alpha channel during model upscaling and hires fix. ESRGAN processes the RGB channels; the alpha channel is resized with bilinear interpolation and recombined with the upscaled image.
|
||||
|
||||
- Specify the model path using the `--upscale-model PATH` parameter. example:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
# Image preprocessing
|
||||
|
||||
Use `--image-preprocess` to transform each image input once, before generation:
|
||||
|
||||
```sh
|
||||
sd-cli ... \
|
||||
--image-preprocess "target=init,mode=crop-resize,filter=lanczos,antialias=true" \
|
||||
--image-preprocess "target=mask,filter=nearest-exact" \
|
||||
--image-preprocess "target=ref,index=0,mode=fit-pad,width=768,height=768,filter=bicubic"
|
||||
```
|
||||
|
||||
CLI and server image loaders decode at the original resolution. The generation
|
||||
entry point merges input defaults with user rules and prepares one transformed
|
||||
image per input. The original pipeline then consumes those images, including
|
||||
its mandatory canvas adaptation, reference resizing, and encoder preprocessing.
|
||||
|
||||
```text
|
||||
native-resolution image
|
||||
-> input defaults + user overrides
|
||||
-> one input transform
|
||||
-> original generation pipeline and model-specific processing
|
||||
```
|
||||
|
||||
These rules do not override internal VAE, CLIP/VLM, ControlNet, or pixel-patch preprocessing.
|
||||
`--ref-image-args` retains its existing meaning and runs after this input transform.
|
||||
|
||||
## Inputs and defaults
|
||||
|
||||
| `target` | Input | Default geometry | Indexed? |
|
||||
| --- | --- | --- | --- |
|
||||
| `init` | img2img image or video first frame | Center crop to the generation aspect ratio, then resize | No |
|
||||
| `end` | Video last frame | Center crop, then resize | No |
|
||||
| `mask` | Inpainting mask | Inherit init geometry; otherwise center crop, then resize | No |
|
||||
| `control` | Control image | Center crop, then resize | No |
|
||||
| `ref` | Reference images | Preserve source dimensions | Yes |
|
||||
| `ip-adapter` | IP-Adapter image | Preserve source dimensions | No |
|
||||
| `id` | PhotoMaker identity images | Preserve source dimensions | Yes |
|
||||
| `control-frame` | Control video frames | Center crop, then resize | Yes |
|
||||
|
||||
Canvas defaults use the aligned generation dimensions. Reference, IP-Adapter,
|
||||
and identity inputs use their original dimensions unless overridden. Default
|
||||
resampling is nearest for images and nearest-exact for masks.
|
||||
|
||||
These defaults are shared by CLI, server, and C API. Moving geometry out of
|
||||
the loaders replaces the previous CLI/server BOX/sRGB resizing, so default
|
||||
pixels are not guaranteed to match earlier builds.
|
||||
|
||||
Reference video and audio preprocessing are outside these image rules.
|
||||
Preprocessing options apply to `img_gen` and `vid_gen`, not standalone upscale
|
||||
or ADetailer mode. ADetailer clears the user's rules for its internal crops.
|
||||
|
||||
## Rules
|
||||
|
||||
Rules are comma-separated `key=value` lists. Repeat the CLI option or separate
|
||||
rules with semicolons. Every rule requires a `target` and at least one option.
|
||||
Rule syntax and input compatibility are checked when image/video generation
|
||||
starts. Unknown keys, invalid values, duplicate keys in a rule, missing images,
|
||||
and out-of-range indices cause generation to fail with an error log.
|
||||
|
||||
Omit `index` to configure every image of that type; otherwise use a zero-based
|
||||
index. CLI directory inputs follow filename order. Indexed rules override
|
||||
type-wide rules field by field, regardless of order. At equal specificity,
|
||||
the last value for a field wins. `auto` selects the input preset.
|
||||
|
||||
| `mode` | Input transform |
|
||||
| --- | --- |
|
||||
| `auto` | Use the input's default geometry |
|
||||
| `none` | Keep source dimensions without resizing, cropping, or padding |
|
||||
| `stretch` | Resize to the target dimensions |
|
||||
| `crop` | Crop a target-sized rectangle without resizing; fail if the source is too small |
|
||||
| `crop-resize` | Crop to the target aspect ratio, then resize |
|
||||
| `fit-pad` | Fit the entire image inside the target dimensions, preserving aspect ratio, then pad |
|
||||
|
||||
`width` and `height` must be specified together as positive integers. They
|
||||
override the input transform's dimensions, not the generation or encoder size.
|
||||
For a native-size preset, specifying dimensions without a mode selects stretch.
|
||||
`mode=none` with explicit dimensions different from the source is contradictory
|
||||
and is rejected.
|
||||
|
||||
`anchor=center|top|bottom|left|right` selects crop/padding placement.
|
||||
`pad_color=#RRGGBB` or `#RRGGBBAA` selects padding, defaulting to opaque black.
|
||||
A grayscale mask uses the first color component.
|
||||
|
||||
`filter=auto|nearest|nearest-exact|bilinear|bicubic|lanczos` selects resampling.
|
||||
`antialias=auto|true|false` enables antialiasing automatically for filtered
|
||||
downscaling; explicit true requires bilinear, bicubic, or Lanczos.
|
||||
Filtered RGBA resizing uses premultiplied alpha.
|
||||
|
||||
`canny=true|false` enables edge detection for any supported image target,
|
||||
defaulting to `false`. It runs once after geometry, before the original
|
||||
generation pipeline, including with `mode=none`. Grayscale, grayscale-alpha,
|
||||
RGB, and RGBA inputs are supported; alpha is preserved.
|
||||
|
||||
Each input has its own Canny setting. Indexed rules can enable or disable it
|
||||
for individual references, identity images, or video control frames.
|
||||
|
||||
```sh
|
||||
--image-preprocess "target=init,mode=fit-pad,canny=true"
|
||||
--image-preprocess "target=ref,index=0,mode=none,canny=true"
|
||||
--image-preprocess "target=control-frame,index=2,canny=true"
|
||||
```
|
||||
|
||||
Init and mask sources must have the same dimensions. The mask inherits the
|
||||
init crop, resize, and padding coordinates, while retaining its own filter,
|
||||
padding value, and Canny setting. Conflicting mask geometry is rejected. An
|
||||
omitted mask remains absent until the original pipeline creates its default mask.
|
||||
|
||||
## Downstream behavior
|
||||
|
||||
`mode=none` only skips the input geometry transform. For example:
|
||||
|
||||
```sh
|
||||
--image-preprocess "target=init,mode=none" \
|
||||
--image-preprocess "target=ref,mode=none"
|
||||
```
|
||||
|
||||
The init image is still adapted to the generation canvas by the original
|
||||
pipeline. Reference images still follow `--ref-image-args` and model-specific
|
||||
resizing. CLIP retains its fixed input dimensions and normalization. HiDream-O1
|
||||
retains its original pixel-reference and visual preprocessing.
|
||||
|
||||
Existing sharing between consumers is preserved: for example, Wan img2video
|
||||
uses the same adapted first frame for VAE conditioning and CLIP. High-resolution
|
||||
passes reuse the prepared images and apply their original size adaptation;
|
||||
they do not apply the user's crop a second time.
|
||||
|
||||
To disable reference resizing before VAE encoding, use
|
||||
`--ref-image-args "resize_before_vae=false"` or the server field
|
||||
`"ref_image_args": "resize_before_vae=false"`. This is separate from
|
||||
`target=ref,mode=none`, which only skips input geometry. Model constraints
|
||||
still apply.
|
||||
|
||||
## Server requests
|
||||
|
||||
Native image/video requests and SDAPI accept `image_preprocess` as a string or
|
||||
an array of rule strings:
|
||||
|
||||
```json
|
||||
{
|
||||
"image_preprocess": [
|
||||
"target=init,mode=fit-pad,filter=bicubic",
|
||||
"target=mask,filter=nearest-exact",
|
||||
"target=ref,index=0,mode=none"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
OpenAI-compatible requests accept it through
|
||||
`<sd_cpp_extra_args>{...}</sd_cpp_extra_args>` in the prompt.
|
||||
Request rules replace server-default rules. Generation metadata records the
|
||||
user rules; image encodings and channel conventions are unchanged.
|
||||
|
||||
## C API
|
||||
|
||||
Set `image_preprocess` on the existing image/video generation parameters.
|
||||
The `generate_image()` and `generate_video()` signatures are unchanged:
|
||||
|
||||
```c
|
||||
sd_img_gen_params_t params;
|
||||
sd_img_gen_params_init(¶ms);
|
||||
/* Set prompt, original-resolution input images, and generation options. */
|
||||
params.image_preprocess.rules = "target=init,mode=crop-resize,filter=lanczos;"
|
||||
"target=mask,filter=nearest-exact";
|
||||
bool ok = generate_image(ctx, ¶ms, &images, &count);
|
||||
```
|
||||
|
||||
Both generation parameter initializers set `image_preprocess.rules` to `NULL`,
|
||||
selecting input presets. Rule strings are borrowed for the synchronous call.
|
||||
The library owns temporary transformed pixels; caller images and arrays are
|
||||
not modified. Add `canny=true` to the desired target's rule in
|
||||
`image_preprocess.rules` to enable Canny.
|
||||
|
||||
The parameter structs have grown; applications and bindings must be rebuilt.
|
||||
@@ -40,6 +40,14 @@ Pass the reference image with `-r` and describe the edit in `-p`. Vision weights
|
||||
|
||||
For multiple reference images, repeat `-r` in the desired order, for example `-r first.png -r second.png`.
|
||||
|
||||
### Prefix cache
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
### Alpha channel
|
||||
|
||||
This model supports alpha channel output. As the model determines whether to output a regular image or with transparency through the prompt, according to [official recommendation](https://github.com/QwenLM/Qwen-Image-2.1#transparent-image-generation-rgba), use the following prompt format for better results:
|
||||
|
||||
@@ -14,6 +14,12 @@ equivalent to `--log-level verbose`. If repeated, the last logging option wins.
|
||||
For direct image repair or automatic post-generation YOLOv8 detection followed by cropped inpainting, see
|
||||
[ADetailer](../../docs/adetailer.md).
|
||||
|
||||
Use repeatable `--image-preprocess` rules to select resizing, cropping, padding,
|
||||
and resampling separately for each image input. Add `canny=true` to any input
|
||||
rule for edge detection. See
|
||||
[Image preprocessing](../../docs/image_preprocessing.md) for input selectors,
|
||||
input defaults, downstream model processing, mask alignment, and examples.
|
||||
|
||||
Metadata mode inspects PNG/JPEG container metadata without loading any model:
|
||||
|
||||
```bash
|
||||
|
||||
+12
-51
@@ -41,7 +41,6 @@ struct SDCliParams {
|
||||
std::string metadata_format = "text";
|
||||
|
||||
sd_log_level_t log_level = SD_LOG_INFO;
|
||||
bool canny_preprocess = false;
|
||||
bool convert_name = false;
|
||||
|
||||
preview_t preview_method = PREVIEW_NONE;
|
||||
@@ -107,10 +106,6 @@ struct SDCliParams {
|
||||
};
|
||||
|
||||
options.bool_options = {
|
||||
{"",
|
||||
"--canny",
|
||||
"apply canny preprocessor (edge detection)",
|
||||
true, &canny_preprocess},
|
||||
{"",
|
||||
"--convert-name",
|
||||
"convert tensor name (for convert mode)",
|
||||
@@ -268,7 +263,6 @@ struct SDCliParams {
|
||||
<< " metadata_format: \"" << metadata_format << "\",\n"
|
||||
<< " log_level: " << log_level_name(log_level) << ",\n"
|
||||
<< " color: " << (color ? "true" : "false") << ",\n"
|
||||
<< " canny_preprocess: " << (canny_preprocess ? "true" : "false") << ",\n"
|
||||
<< " convert_name: " << (convert_name ? "true" : "false") << ",\n"
|
||||
<< " preview_method: " << previews_str[preview_method] << ",\n"
|
||||
<< " preview_interval: " << preview_interval << ",\n"
|
||||
@@ -328,9 +322,7 @@ void sd_log_cb(enum sd_log_level_t level, const char* log, void* data) {
|
||||
|
||||
bool load_images_from_dir(const std::string dir,
|
||||
std::vector<SDImageOwner>& images,
|
||||
int expected_width = 0,
|
||||
int expected_height = 0,
|
||||
int max_image_num = 0) {
|
||||
int max_image_num = 0) {
|
||||
if (!fs::exists(dir) || !fs::is_directory(dir)) {
|
||||
LOG_ERROR("'%s' is not a valid directory\n", dir.c_str());
|
||||
return false;
|
||||
@@ -358,7 +350,7 @@ bool load_images_from_dir(const std::string dir,
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
int loaded_channel = 0;
|
||||
uint8_t* image_buffer = load_image_from_file(path.c_str(), width, height, loaded_channel, expected_width, expected_height);
|
||||
uint8_t* image_buffer = load_image_from_file(path.c_str(), width, height, loaded_channel, 0, 0);
|
||||
if (image_buffer == nullptr) {
|
||||
LOG_ERROR("load image from '%s' failed", path.c_str());
|
||||
return false;
|
||||
@@ -652,10 +644,11 @@ int main(int argc, const char* argv[]) {
|
||||
|
||||
SDCliParams cli_params;
|
||||
SDContextParams ctx_params;
|
||||
ctx_params.conditioning_cache_size = 0;
|
||||
SDGenerationParams gen_params;
|
||||
|
||||
parse_args(argc, argv, cli_params, ctx_params, gen_params);
|
||||
sd_set_log_callback(sd_log_cb, (void*)&cli_params);
|
||||
parse_args(argc, argv, cli_params, ctx_params, gen_params);
|
||||
|
||||
if (cli_params.mode == METADATA) {
|
||||
MetadataReadOptions options;
|
||||
@@ -751,16 +744,8 @@ int main(int argc, const char* argv[]) {
|
||||
|
||||
auto load_image_and_update_size = [&](const std::string& path,
|
||||
SDImageOwner& image,
|
||||
bool resize_image = true,
|
||||
int expected_channel = 3) -> bool {
|
||||
int expected_width = 0;
|
||||
int expected_height = 0;
|
||||
if (resize_image && gen_params.width_and_height_are_set()) {
|
||||
expected_width = gen_params.width;
|
||||
expected_height = gen_params.height;
|
||||
}
|
||||
|
||||
if (!load_sd_image_from_file(image.put(), path.c_str(), expected_width, expected_height, expected_channel)) {
|
||||
if (!load_sd_image_from_file(image.put(), path.c_str(), 0, 0, expected_channel)) {
|
||||
LOG_ERROR("load image from '%s' failed", path.c_str());
|
||||
return false;
|
||||
}
|
||||
@@ -783,7 +768,7 @@ int main(int argc, const char* argv[]) {
|
||||
|
||||
if (gen_params.init_image_path.size() > 0) {
|
||||
const bool native_init = cli_params.mode == IMG_GEN || cli_params.mode == ADETAILER;
|
||||
if (!load_image_and_update_size(gen_params.init_image_path, gen_params.init_image, true, native_init ? 0 : 3)) {
|
||||
if (!load_image_and_update_size(gen_params.init_image_path, gen_params.init_image, native_init ? 0 : 3)) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -798,7 +783,7 @@ int main(int argc, const char* argv[]) {
|
||||
gen_params.ref_images.clear();
|
||||
for (auto& path : gen_params.ref_image_paths) {
|
||||
SDImageOwner ref_image({0, 0, 0, nullptr});
|
||||
if (!load_image_and_update_size(path, ref_image, false, 0)) {
|
||||
if (!load_image_and_update_size(path, ref_image, 0)) {
|
||||
return 1;
|
||||
}
|
||||
gen_params.ref_images.push_back(std::move(ref_image));
|
||||
@@ -839,41 +824,22 @@ int main(int argc, const char* argv[]) {
|
||||
if (gen_params.mask_image_path.size() > 0) {
|
||||
if (!load_sd_image_from_file(gen_params.mask_image.put(),
|
||||
gen_params.mask_image_path.c_str(),
|
||||
gen_params.get_resolved_width(),
|
||||
gen_params.get_resolved_height(),
|
||||
0,
|
||||
0,
|
||||
1)) {
|
||||
LOG_ERROR("load image from '%s' failed", gen_params.mask_image_path.c_str());
|
||||
return 1;
|
||||
}
|
||||
} else {
|
||||
sd_image_t generated_mask = {0, 0, 1, nullptr};
|
||||
generated_mask.data = (uint8_t*)malloc(gen_params.get_resolved_width() * gen_params.get_resolved_height());
|
||||
if (generated_mask.data == nullptr) {
|
||||
LOG_ERROR("malloc mask image failed");
|
||||
return 1;
|
||||
}
|
||||
generated_mask.width = gen_params.get_resolved_width();
|
||||
generated_mask.height = gen_params.get_resolved_height();
|
||||
memset(generated_mask.data, 255, gen_params.get_resolved_width() * gen_params.get_resolved_height());
|
||||
gen_params.mask_image.reset(generated_mask);
|
||||
}
|
||||
|
||||
if (gen_params.control_image_path.size() > 0) {
|
||||
if (!load_sd_image_from_file(gen_params.control_image.put(),
|
||||
gen_params.control_image_path.c_str(),
|
||||
gen_params.get_resolved_width(),
|
||||
gen_params.get_resolved_height())) {
|
||||
0,
|
||||
0)) {
|
||||
LOG_ERROR("load image from '%s' failed", gen_params.control_image_path.c_str());
|
||||
return 1;
|
||||
}
|
||||
if (cli_params.canny_preprocess) { // apply preprocessor
|
||||
preprocess_canny(gen_params.control_image.get(),
|
||||
0.08f,
|
||||
0.08f,
|
||||
0.8f,
|
||||
1.0f,
|
||||
false);
|
||||
}
|
||||
}
|
||||
|
||||
if (gen_params.ip_adapter_image_path.size() > 0) {
|
||||
@@ -890,8 +856,6 @@ int main(int argc, const char* argv[]) {
|
||||
gen_params.control_frames.clear();
|
||||
if (!load_images_from_dir(gen_params.control_video_path,
|
||||
gen_params.control_frames,
|
||||
gen_params.get_resolved_width(),
|
||||
gen_params.get_resolved_height(),
|
||||
gen_params.video_frames)) {
|
||||
return 1;
|
||||
}
|
||||
@@ -900,10 +864,7 @@ int main(int argc, const char* argv[]) {
|
||||
if (!gen_params.pm_id_images_dir.empty()) {
|
||||
gen_params.pm_id_images.clear();
|
||||
if (!load_images_from_dir(gen_params.pm_id_images_dir,
|
||||
gen_params.pm_id_images,
|
||||
0,
|
||||
0,
|
||||
0)) {
|
||||
gen_params.pm_id_images)) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
+73
-57
@@ -518,7 +518,7 @@ 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",
|
||||
"chroma_t5_mask_pad, qwen_image_zero_cond_t, qwen_image_2_1_prefix_cache",
|
||||
(int)',',
|
||||
&model_args},
|
||||
{"",
|
||||
@@ -571,6 +571,10 @@ ArgOptions SDContextParams::get_options() {
|
||||
"number of threads to use during computation (default: -1). "
|
||||
"If threads <= 0, then threads will be set to the number of CPU physical cores",
|
||||
&n_threads},
|
||||
{"",
|
||||
"--conditioning-cache-size",
|
||||
"maximum number of conditioning results cached per model context (default: " + std::to_string(conditioning_cache_size) + ", 0 disables caching)",
|
||||
&conditioning_cache_size},
|
||||
};
|
||||
|
||||
options.bool_options = {
|
||||
@@ -822,6 +826,10 @@ bool SDContextParams::resolve(SDMode mode) {
|
||||
}
|
||||
|
||||
bool SDContextParams::validate(SDMode mode) {
|
||||
if (conditioning_cache_size < 0) {
|
||||
LOG_ERROR("error: conditioning-cache-size must be non-negative");
|
||||
return false;
|
||||
}
|
||||
if (mode == CONVERT) {
|
||||
const bool has_convert_input = model_path.length() != 0 ||
|
||||
clip_l_path.length() != 0 ||
|
||||
@@ -898,6 +906,7 @@ std::string SDContextParams::to_string() const {
|
||||
std::ostringstream oss;
|
||||
oss << "SDContextParams {\n"
|
||||
<< " n_threads: " << n_threads << ",\n"
|
||||
<< " conditioning_cache_size: " << conditioning_cache_size << ",\n"
|
||||
<< " model_path: \"" << model_path << "\",\n"
|
||||
<< " clip_l_path: \"" << clip_l_path << "\",\n"
|
||||
<< " clip_g_path: \"" << clip_g_path << "\",\n"
|
||||
@@ -992,6 +1001,7 @@ sd_ctx_params_t SDContextParams::to_sd_ctx_params_t(bool taesd_preview) {
|
||||
sd_ctx_params.pulid_weights_path = pulid_weights_path.c_str();
|
||||
sd_ctx_params.tensor_type_rules = tensor_type_rules.c_str();
|
||||
sd_ctx_params.n_threads = n_threads;
|
||||
sd_ctx_params.conditioning_cache_size = conditioning_cache_size;
|
||||
sd_ctx_params.wtype = wtype;
|
||||
sd_ctx_params.rng_type = rng_type;
|
||||
sd_ctx_params.sampler_rng_type = sampler_rng_type;
|
||||
@@ -1128,6 +1138,9 @@ ArgOptions SDGenerationParams::get_options() {
|
||||
"Key-value list to set up the way the reference images are processed (empty = auto-detect from model weigths)",
|
||||
(int)',',
|
||||
&ref_image_args},
|
||||
{"", "--image-preprocess",
|
||||
"Image preprocessing rule: target=init|end|mask|control|ref|ip-adapter|id|control-frame,index=N,mode=auto|none|stretch|crop|crop-resize|fit-pad,filter=auto|nearest|nearest-exact|bilinear|bicubic|lanczos,antialias=auto|true|false,width=W,height=H,anchor=center|top|bottom|left|right,pad_color=#RRGGBB[AA],canny=true|false. Repeat for multiple rules.",
|
||||
(int)';', &image_preprocess},
|
||||
};
|
||||
|
||||
options.int_options = {
|
||||
@@ -1308,11 +1321,6 @@ ArgOptions SDGenerationParams::get_options() {
|
||||
"automatically increase the indices of references images based on the order they are listed (starting with 1).",
|
||||
true,
|
||||
&increase_ref_index},
|
||||
{"",
|
||||
"--disable-auto-resize-ref-image",
|
||||
"disable auto resize of ref images",
|
||||
false,
|
||||
&auto_resize_ref_image},
|
||||
{"",
|
||||
"--circular",
|
||||
"enable circular padding on both axes for tileable output",
|
||||
@@ -1870,8 +1878,6 @@ bool decode_base64_image(const std::string& encoded_input,
|
||||
static bool parse_image_json_field(const json& parent,
|
||||
const char* key,
|
||||
int channels,
|
||||
int expected_width,
|
||||
int expected_height,
|
||||
SDImageOwner& out_image) {
|
||||
if (!parent.contains(key)) {
|
||||
return true;
|
||||
@@ -1883,14 +1889,12 @@ static bool parse_image_json_field(const json& parent,
|
||||
if (!parent.at(key).is_string()) {
|
||||
return false;
|
||||
}
|
||||
return decode_base64_image(parent.at(key).get<std::string>(), channels, expected_width, expected_height, out_image);
|
||||
return decode_base64_image(parent.at(key).get<std::string>(), channels, 0, 0, out_image);
|
||||
}
|
||||
|
||||
static bool parse_image_array_json_field(const json& parent,
|
||||
const char* key,
|
||||
int channels,
|
||||
int expected_width,
|
||||
int expected_height,
|
||||
std::vector<SDImageOwner>& out_images) {
|
||||
if (!parent.contains(key)) {
|
||||
return true;
|
||||
@@ -1909,7 +1913,7 @@ static bool parse_image_array_json_field(const json& parent,
|
||||
return false;
|
||||
}
|
||||
SDImageOwner image;
|
||||
if (!decode_base64_image(item.get<std::string>(), channels, expected_width, expected_height, image)) {
|
||||
if (!decode_base64_image(item.get<std::string>(), channels, 0, 0, image)) {
|
||||
return false;
|
||||
}
|
||||
out_images.push_back(std::move(image));
|
||||
@@ -2008,6 +2012,29 @@ static bool resolve_model_file_from_dir(const std::string& model_name,
|
||||
return false;
|
||||
}
|
||||
|
||||
bool SDGenerationParams::parse_image_preprocess_json(const std::string& json_str) {
|
||||
const auto value = json::parse(json_str, nullptr, false);
|
||||
std::string rules;
|
||||
if (value.is_string()) {
|
||||
rules = value.get<std::string>();
|
||||
} else if (value.is_array()) {
|
||||
for (const auto& item : value) {
|
||||
if (!item.is_string()) {
|
||||
LOG_ERROR("image_preprocess must contain rule strings");
|
||||
return false;
|
||||
}
|
||||
if (!rules.empty())
|
||||
rules += ";";
|
||||
rules += item.get<std::string>();
|
||||
}
|
||||
} else {
|
||||
LOG_ERROR("image_preprocess must be a string or array of strings");
|
||||
return false;
|
||||
}
|
||||
image_preprocess = std::move(rules);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SDGenerationParams::from_json_str(
|
||||
const std::string& json_str,
|
||||
const std::function<std::string(const std::string&)>& lora_path_resolver) {
|
||||
@@ -2019,6 +2046,9 @@ bool SDGenerationParams::from_json_str(
|
||||
return false;
|
||||
}
|
||||
|
||||
if (j.contains("image_preprocess") && !parse_image_preprocess_json(j["image_preprocess"].dump()))
|
||||
return false;
|
||||
|
||||
auto load_if_exists = [&](const char* key, auto& out) {
|
||||
if (j.contains(key)) {
|
||||
using T = std::decay_t<decltype(out)>;
|
||||
@@ -2056,6 +2086,7 @@ bool SDGenerationParams::from_json_str(
|
||||
load_if_exists("cache_mode", cache_mode);
|
||||
load_if_exists("cache_option", cache_option);
|
||||
load_if_exists("scm_mask", scm_mask);
|
||||
load_if_exists("ref_image_args", ref_image_args);
|
||||
|
||||
load_if_exists("clip_skip", clip_skip);
|
||||
load_if_exists("width", width);
|
||||
@@ -2073,7 +2104,6 @@ bool SDGenerationParams::from_json_str(
|
||||
load_if_exists("moe_boundary", moe_boundary);
|
||||
load_if_exists("vace_strength", vace_strength);
|
||||
|
||||
load_if_exists("auto_resize_ref_image", auto_resize_ref_image);
|
||||
load_if_exists("increase_ref_index", increase_ref_index);
|
||||
load_if_exists("embed_image_metadata", embed_image_metadata);
|
||||
|
||||
@@ -2217,37 +2247,23 @@ bool SDGenerationParams::from_json_str(
|
||||
LOG_ERROR("invalid lora");
|
||||
return false;
|
||||
}
|
||||
if (!parse_image_json_field(j, "init_image", 0, width, height, init_image)) {
|
||||
LOG_ERROR("invalid init_image");
|
||||
auto load_image = [&](const char* key, int channels, SDImageOwner& image) {
|
||||
if (!parse_image_json_field(j, key, channels, image)) {
|
||||
LOG_ERROR("invalid %s", key);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
if (!load_image("init_image", 0, init_image) ||
|
||||
!load_image("end_image", 3, end_image) ||
|
||||
!load_image("mask_image", 1, mask_image) ||
|
||||
!load_image("control_image", 3, control_image) ||
|
||||
!load_image("ip_adapter_image", 3, ip_adapter_image)) {
|
||||
return false;
|
||||
}
|
||||
if (!parse_image_json_field(j, "end_image", 3, width, height, end_image)) {
|
||||
LOG_ERROR("invalid end_image");
|
||||
return false;
|
||||
}
|
||||
if (!parse_image_array_json_field(j,
|
||||
"ref_images",
|
||||
0,
|
||||
auto_resize_ref_image ? width : 0,
|
||||
auto_resize_ref_image ? height : 0,
|
||||
ref_images)) {
|
||||
LOG_ERROR("invalid ref_images");
|
||||
return false;
|
||||
}
|
||||
if (!parse_image_array_json_field(j, "control_frames", 3, width, height, control_frames)) {
|
||||
LOG_ERROR("invalid control_frames");
|
||||
return false;
|
||||
}
|
||||
if (!parse_image_json_field(j, "mask_image", 1, width, height, mask_image)) {
|
||||
LOG_ERROR("invalid mask_image");
|
||||
return false;
|
||||
}
|
||||
if (!parse_image_json_field(j, "control_image", 3, width, height, control_image)) {
|
||||
LOG_ERROR("invalid control_image");
|
||||
return false;
|
||||
}
|
||||
if (!parse_image_json_field(j, "ip_adapter_image", 3, width, height, ip_adapter_image)) {
|
||||
LOG_ERROR("invalid ip_adapter_image");
|
||||
if (!parse_image_array_json_field(j, "ref_images", 0, ref_images) ||
|
||||
!parse_image_array_json_field(j, "control_frames", 3, control_frames)) {
|
||||
LOG_ERROR("invalid input image array");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -2491,6 +2507,10 @@ bool SDGenerationParams::resolve(const std::string& lora_model_dir, const std::s
|
||||
}
|
||||
|
||||
bool SDGenerationParams::validate(SDMode mode) {
|
||||
if (!image_preprocess.empty() && mode != IMG_GEN && mode != VID_GEN) {
|
||||
LOG_ERROR("--image-preprocess requires img_gen or vid_gen mode");
|
||||
return false;
|
||||
}
|
||||
if (batch_count <= 0) {
|
||||
LOG_ERROR("error: batch_count must be greater than 0");
|
||||
return false;
|
||||
@@ -2666,14 +2686,6 @@ sd_img_gen_params_t SDGenerationParams::to_sd_img_gen_params_t() {
|
||||
pulid_id_weight,
|
||||
};
|
||||
|
||||
if (!auto_resize_ref_image) {
|
||||
if (!ref_image_args.empty()) {
|
||||
ref_image_args += ",";
|
||||
}
|
||||
ref_image_args += "resize_before_vae=0";
|
||||
LOG_WARN("Notice: --disable-auto-resize-ref-image is deprecated. Use --ref-image-args \"resize_before_vae=off\" instead.");
|
||||
}
|
||||
|
||||
if (increase_ref_index) {
|
||||
if (!ref_image_args.empty()) {
|
||||
ref_image_args += ",";
|
||||
@@ -2721,6 +2733,7 @@ sd_img_gen_params_t SDGenerationParams::to_sd_img_gen_params_t() {
|
||||
params.hires.custom_sigmas_count = static_cast<int>(hires_custom_sigmas.size());
|
||||
params.circular_x = circular || circular_x;
|
||||
params.circular_y = circular || circular_y;
|
||||
params.image_preprocess = {image_preprocess.c_str()};
|
||||
return params;
|
||||
}
|
||||
|
||||
@@ -2823,6 +2836,7 @@ sd_vid_gen_params_t SDGenerationParams::to_sd_vid_gen_params_t() {
|
||||
params.hires.custom_sigmas_count = static_cast<int>(hires_custom_sigmas.size());
|
||||
params.circular_x = circular || circular_x;
|
||||
params.circular_y = circular || circular_y;
|
||||
params.image_preprocess = {image_preprocess.c_str()};
|
||||
return params;
|
||||
}
|
||||
|
||||
@@ -2879,7 +2893,8 @@ std::string SDGenerationParams::to_string() const {
|
||||
<< " ref_video_audio_paths: " << vec_str_to_string(ref_video_audio_paths) << ",\n"
|
||||
<< " ref_audio_paths: " << vec_str_to_string(ref_audio_paths) << ",\n"
|
||||
<< " control_video_path: \"" << control_video_path << "\",\n"
|
||||
<< " auto_resize_ref_image: " << (auto_resize_ref_image ? "true" : "false") << ",\n"
|
||||
<< " image_preprocess: " << image_preprocess << ",\n"
|
||||
<< " ref_image_args: " << ref_image_args << ",\n"
|
||||
<< " increase_ref_index: " << (increase_ref_index ? "true" : "false") << ",\n"
|
||||
<< " pm_id_images_dir: \"" << pm_id_images_dir << "\",\n"
|
||||
<< " pm_id_embed_path: \"" << pm_id_embed_path << "\",\n"
|
||||
@@ -3030,12 +3045,13 @@ std::string build_sdcpp_image_metadata_json(const SDContextParams& ctx_params,
|
||||
set_json_basename_if_not_empty(models, "control_net", ctx_params.control_net_path);
|
||||
root["models"] = std::move(models);
|
||||
|
||||
root["clip_skip"] = gen_params.clip_skip;
|
||||
root["strength"] = gen_params.strength;
|
||||
root["control_strength"] = gen_params.control_strength;
|
||||
root["ip_adapter_strength"] = gen_params.ip_adapter_strength;
|
||||
root["auto_resize_ref_image"] = gen_params.auto_resize_ref_image;
|
||||
root["increase_ref_index"] = gen_params.increase_ref_index;
|
||||
root["clip_skip"] = gen_params.clip_skip;
|
||||
root["strength"] = gen_params.strength;
|
||||
root["control_strength"] = gen_params.control_strength;
|
||||
root["ip_adapter_strength"] = gen_params.ip_adapter_strength;
|
||||
root["ref_image_args"] = gen_params.ref_image_args;
|
||||
root["image_preprocess"] = gen_params.image_preprocess;
|
||||
root["increase_ref_index"] = gen_params.increase_ref_index;
|
||||
if (mode == VID_GEN) {
|
||||
root["video"] = {
|
||||
{"frame_count", gen_params.video_frames},
|
||||
|
||||
+15
-13
@@ -116,7 +116,8 @@ bool decode_base64_image(const std::string& encoded_input,
|
||||
SDImageOwner& out_image);
|
||||
|
||||
struct SDContextParams {
|
||||
int n_threads = -1;
|
||||
int n_threads = -1;
|
||||
int conditioning_cache_size = 4;
|
||||
std::string model_path;
|
||||
std::string clip_l_path;
|
||||
std::string clip_g_path;
|
||||
@@ -200,18 +201,17 @@ struct SDGenerationParams {
|
||||
std::string ad_prompt;
|
||||
std::string ad_negative_prompt;
|
||||
std::string extra_ad_args;
|
||||
int clip_skip = -1; // <= 0 represents unspecified
|
||||
int width = -1;
|
||||
int height = -1;
|
||||
int batch_count = 1;
|
||||
int qwen_image_layers = 3;
|
||||
int64_t seed = 42;
|
||||
float strength = 0.75f;
|
||||
float control_strength = 0.9f;
|
||||
float ip_adapter_strength = 1.0f;
|
||||
bool auto_resize_ref_image = true;
|
||||
bool increase_ref_index = false;
|
||||
bool embed_image_metadata = true;
|
||||
int clip_skip = -1; // <= 0 represents unspecified
|
||||
int width = -1;
|
||||
int height = -1;
|
||||
int batch_count = 1;
|
||||
int qwen_image_layers = 3;
|
||||
int64_t seed = 42;
|
||||
float strength = 0.75f;
|
||||
float control_strength = 0.9f;
|
||||
float ip_adapter_strength = 1.0f;
|
||||
bool increase_ref_index = false;
|
||||
bool embed_image_metadata = true;
|
||||
|
||||
std::string init_image_path;
|
||||
std::string end_image_path;
|
||||
@@ -247,6 +247,7 @@ struct SDGenerationParams {
|
||||
std::string extra_tiling_args;
|
||||
|
||||
std::string ref_image_args;
|
||||
std::string image_preprocess;
|
||||
|
||||
std::string pm_id_images_dir;
|
||||
std::string pm_id_embed_path;
|
||||
@@ -310,6 +311,7 @@ struct SDGenerationParams {
|
||||
ArgOptions get_options();
|
||||
bool from_json_str(const std::string& json_str,
|
||||
const std::function<std::string(const std::string&)>& lora_path_resolver = {});
|
||||
bool parse_image_preprocess_json(const std::string& json_str);
|
||||
bool initialize_cache_params();
|
||||
void extract_and_remove_lora(const std::string& lora_model_dir);
|
||||
bool width_and_height_are_set() const;
|
||||
|
||||
+28
-3
@@ -148,6 +148,19 @@ Native extension fields:
|
||||
|
||||
- any `sdcpp API` fields embedded through `sd_cpp_extra_args` inside `prompt`
|
||||
|
||||
Uploaded images are decoded at their original dimensions. The first decoded
|
||||
image establishes the generation dimensions if `size` is omitted. Input
|
||||
geometry follows `image_preprocess`: references preserve their dimensions by
|
||||
default, while init and mask use the generation canvas preset.
|
||||
|
||||
Reference encoding then follows model presets and `ref_image_args`. To skip
|
||||
input geometry for references and disable resizing before VAE encoding, include
|
||||
this in `prompt`:
|
||||
|
||||
```text
|
||||
edit this image <sd_cpp_extra_args>{"image_preprocess":"target=ref,mode=none","ref_image_args":"resize_before_vae=false"}</sd_cpp_extra_args>
|
||||
```
|
||||
|
||||
Response fields:
|
||||
|
||||
| Field | Type | Notes |
|
||||
@@ -526,7 +539,7 @@ LTX and Wan preserve causal state between temporal tiles. Hunyuan Video and TAEH
|
||||
| Field | Type |
|
||||
| --- | --- |
|
||||
| `batch_count` | `integer` |
|
||||
| `auto_resize_ref_image` | `boolean` |
|
||||
| `ref_image_args` | `string` |
|
||||
| `increase_ref_index` | `boolean` |
|
||||
| `control_strength` | `number` |
|
||||
| `ip_adapter_strength` | `number` |
|
||||
@@ -653,7 +666,7 @@ Example:
|
||||
"strength": 0.75,
|
||||
"seed": -1,
|
||||
"batch_count": 1,
|
||||
"auto_resize_ref_image": true,
|
||||
"ref_image_args": "",
|
||||
"increase_ref_index": false,
|
||||
"control_strength": 0.9,
|
||||
"ip_adapter_strength": 1.0,
|
||||
@@ -728,6 +741,17 @@ Example:
|
||||
|
||||
### Image Encoding Rules
|
||||
|
||||
Native image/video requests and SDAPI accept `image_preprocess` as a rule string
|
||||
or array of rule strings. OpenAI-compatible requests can supply it in
|
||||
`sd_cpp_extra_args`. See [Image preprocessing](../../docs/image_preprocessing.md)
|
||||
for one-time input geometry, native-resolution decoding, mask alignment, and
|
||||
`canny=true` for edge detection on any supported image input.
|
||||
|
||||
Image generation also accepts `ref_image_args` as a string (for example,
|
||||
`"resize_before_vae=false"`) in native and SDAPI requests, or through
|
||||
`sd_cpp_extra_args` in OpenAI-compatible requests. It controls downstream
|
||||
reference encoding and is independent of input geometry rules.
|
||||
|
||||
Any image field accepts:
|
||||
|
||||
- a raw base64 string, or
|
||||
@@ -763,7 +787,8 @@ Top-level scalar fields:
|
||||
| `strength` | `number` |
|
||||
| `seed` | `integer` |
|
||||
| `batch_count` | `integer` |
|
||||
| `auto_resize_ref_image` | `boolean` |
|
||||
| `ref_image_args` | `string` |
|
||||
| `image_preprocess` | `string \| array<string>` |
|
||||
| `increase_ref_index` | `boolean` |
|
||||
| `control_strength` | `number` |
|
||||
| `ip_adapter_strength` | `number` |
|
||||
|
||||
@@ -76,9 +76,9 @@ int main(int argc, const char** argv) {
|
||||
SDSvrParams svr_params;
|
||||
SDContextParams ctx_params;
|
||||
SDGenerationParams default_gen_params;
|
||||
parse_args(argc, argv, svr_params, ctx_params, default_gen_params);
|
||||
|
||||
sd_set_log_callback(sd_log_cb, (void*)&svr_params);
|
||||
parse_args(argc, argv, svr_params, ctx_params, default_gen_params);
|
||||
|
||||
LOG_VERBOSE("version: %s", version_string().c_str());
|
||||
LOG_VERBOSE("%s", sd_get_system_info());
|
||||
|
||||
@@ -157,6 +157,7 @@ static bool build_openai_edit_request(const httplib::Request& req,
|
||||
request.gen_params.height = height;
|
||||
request.gen_params.batch_count = n;
|
||||
|
||||
std::string sd_cpp_extra_args_str = extract_and_remove_sd_cpp_extra_args(request.gen_params.prompt);
|
||||
for (auto& bytes : images_bytes) {
|
||||
int img_w = 0;
|
||||
int img_h = 0;
|
||||
@@ -165,7 +166,8 @@ static bool build_openai_edit_request(const httplib::Request& req,
|
||||
reinterpret_cast<const char*>(bytes.data()),
|
||||
static_cast<int>(bytes.size()),
|
||||
img_w, img_h, resolved_channel,
|
||||
0, 0, 0);
|
||||
0, 0,
|
||||
0);
|
||||
if (raw_pixels == nullptr) {
|
||||
continue;
|
||||
}
|
||||
@@ -175,23 +177,10 @@ static bool build_openai_edit_request(const httplib::Request& req,
|
||||
request.gen_params.set_width_and_height_if_unset(image_owner.get().width, image_owner.get().height);
|
||||
|
||||
if (is_first_ref_image) {
|
||||
int init_w = 0;
|
||||
int init_h = 0;
|
||||
if (request.gen_params.width_and_height_are_set()) {
|
||||
init_w = request.gen_params.width;
|
||||
init_h = request.gen_params.height;
|
||||
}
|
||||
|
||||
int init_img_w = 0;
|
||||
int init_img_h = 0;
|
||||
int init_resolved_channel = 0;
|
||||
uint8_t* init_pixels = load_image_from_memory(
|
||||
reinterpret_cast<const char*>(bytes.data()),
|
||||
static_cast<int>(bytes.size()),
|
||||
init_img_w, init_img_h, init_resolved_channel,
|
||||
init_w, init_h, 0);
|
||||
if (init_pixels != nullptr) {
|
||||
request.gen_params.init_image.reset({(uint32_t)init_img_w, (uint32_t)init_img_h, (uint32_t)init_resolved_channel, init_pixels});
|
||||
request.gen_params.init_image = image_owner;
|
||||
if (request.gen_params.init_image.get().data == nullptr) {
|
||||
error_message = "could not allocate init image";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,12 +188,6 @@ static bool build_openai_edit_request(const httplib::Request& req,
|
||||
}
|
||||
|
||||
if (!mask_bytes.empty()) {
|
||||
int expected_width = 0;
|
||||
int expected_height = 0;
|
||||
if (request.gen_params.width_and_height_are_set()) {
|
||||
expected_width = request.gen_params.width;
|
||||
expected_height = request.gen_params.height;
|
||||
}
|
||||
int mask_w = 0;
|
||||
int mask_h = 0;
|
||||
int mask_channel = 0;
|
||||
@@ -213,7 +196,7 @@ static bool build_openai_edit_request(const httplib::Request& req,
|
||||
reinterpret_cast<const char*>(mask_bytes.data()),
|
||||
static_cast<int>(mask_bytes.size()),
|
||||
mask_w, mask_h, mask_channel,
|
||||
expected_width, expected_height, 1);
|
||||
0, 0, 1);
|
||||
request.gen_params.mask_image.reset({(uint32_t)mask_w, (uint32_t)mask_h, 1, mask_raw});
|
||||
const sd_image_t& mask_image = request.gen_params.mask_image.get();
|
||||
request.gen_params.set_width_and_height_if_unset(mask_image.width, mask_image.height);
|
||||
@@ -226,7 +209,6 @@ static bool build_openai_edit_request(const httplib::Request& req,
|
||||
});
|
||||
}
|
||||
|
||||
std::string sd_cpp_extra_args_str = extract_and_remove_sd_cpp_extra_args(request.gen_params.prompt);
|
||||
if (!sd_cpp_extra_args_str.empty() && !request.gen_params.from_json_str(sd_cpp_extra_args_str)) {
|
||||
error_message = "invalid sd_cpp_extra_args";
|
||||
return false;
|
||||
|
||||
@@ -80,17 +80,6 @@ static enum sample_method_t get_sdapi_sample_method(std::string name) {
|
||||
return it != hardcoded.end() ? it->second : SAMPLE_METHOD_COUNT;
|
||||
}
|
||||
|
||||
static void assign_solid_mask(SDImageOwner& mask_owner, int width, int height) {
|
||||
const size_t pixel_count = static_cast<size_t>(width) * static_cast<size_t>(height);
|
||||
uint8_t* raw_mask = static_cast<uint8_t*>(malloc(pixel_count));
|
||||
if (raw_mask == nullptr) {
|
||||
mask_owner.reset({0, 0, 1, nullptr});
|
||||
return;
|
||||
}
|
||||
std::memset(raw_mask, 255, pixel_count);
|
||||
mask_owner.reset({(uint32_t)width, (uint32_t)height, 1, raw_mask});
|
||||
}
|
||||
|
||||
static bool build_sdapi_img_gen_request(const json& j,
|
||||
ServerRuntime& runtime,
|
||||
bool img2img,
|
||||
@@ -193,15 +182,25 @@ static bool build_sdapi_img_gen_request(const json& j,
|
||||
}
|
||||
}
|
||||
|
||||
if (img2img) {
|
||||
const int expected_width = request.gen_params.width_and_height_are_set() ? request.gen_params.width : 0;
|
||||
const int expected_height = request.gen_params.width_and_height_are_set() ? request.gen_params.height : 0;
|
||||
if (j.contains("ref_image_args")) {
|
||||
if (!j["ref_image_args"].is_string()) {
|
||||
error_message = "ref_image_args must be a string";
|
||||
return false;
|
||||
}
|
||||
request.gen_params.ref_image_args = j["ref_image_args"].get<std::string>();
|
||||
}
|
||||
|
||||
if (j.contains("image_preprocess") && !request.gen_params.parse_image_preprocess_json(j["image_preprocess"].dump())) {
|
||||
error_message = "invalid image_preprocess";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (img2img) {
|
||||
if (j.contains("init_images") && j["init_images"].is_array() && !j["init_images"].empty()) {
|
||||
if (decode_base64_image(j["init_images"][0].get<std::string>(),
|
||||
0,
|
||||
expected_width,
|
||||
expected_height,
|
||||
0,
|
||||
0,
|
||||
request.gen_params.init_image)) {
|
||||
const sd_image_t& image = request.gen_params.init_image.get();
|
||||
request.gen_params.set_width_and_height_if_unset(image.width, image.height);
|
||||
@@ -211,8 +210,8 @@ static bool build_sdapi_img_gen_request(const json& j,
|
||||
if (j.contains("mask") && j["mask"].is_string()) {
|
||||
if (decode_base64_image(j["mask"].get<std::string>(),
|
||||
1,
|
||||
expected_width,
|
||||
expected_height,
|
||||
0,
|
||||
0,
|
||||
request.gen_params.mask_image)) {
|
||||
const sd_image_t& image = request.gen_params.mask_image.get();
|
||||
request.gen_params.set_width_and_height_if_unset(image.width, image.height);
|
||||
@@ -225,9 +224,7 @@ static bool build_sdapi_img_gen_request(const json& j,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const int resolved_width = request.gen_params.get_resolved_width();
|
||||
const int resolved_height = request.gen_params.get_resolved_height();
|
||||
assign_solid_mask(request.gen_params.mask_image, resolved_width, resolved_height);
|
||||
request.gen_params.mask_image.reset({0, 0, 1, nullptr});
|
||||
}
|
||||
|
||||
float denoising_strength = j.value("denoising_strength", -1.f);
|
||||
@@ -244,12 +241,7 @@ static bool build_sdapi_img_gen_request(const json& j,
|
||||
SDImageOwner image_owner;
|
||||
if (decode_base64_image(extra_image.get<std::string>(),
|
||||
0,
|
||||
request.gen_params.auto_resize_ref_image && request.gen_params.width_and_height_are_set()
|
||||
? request.gen_params.width
|
||||
: 0,
|
||||
request.gen_params.auto_resize_ref_image && request.gen_params.width_and_height_are_set()
|
||||
? request.gen_params.height
|
||||
: 0,
|
||||
0, 0,
|
||||
image_owner)) {
|
||||
const sd_image_t& image = image_owner.get();
|
||||
request.gen_params.set_width_and_height_if_unset(image.width, image.height);
|
||||
|
||||
@@ -127,7 +127,8 @@ static json make_img_gen_defaults_json(const SDGenerationParams& defaults, const
|
||||
{"seed", defaults.seed},
|
||||
{"batch_count", defaults.batch_count},
|
||||
{"qwen_image_layers", defaults.qwen_image_layers},
|
||||
{"auto_resize_ref_image", defaults.auto_resize_ref_image},
|
||||
{"ref_image_args", defaults.ref_image_args},
|
||||
{"image_preprocess", defaults.image_preprocess},
|
||||
{"increase_ref_index", defaults.increase_ref_index},
|
||||
{"control_strength", defaults.control_strength},
|
||||
{"ip_adapter_strength", defaults.ip_adapter_strength},
|
||||
@@ -153,6 +154,7 @@ static json make_vid_gen_defaults_json(const SDGenerationParams& defaults, const
|
||||
{"strength", defaults.strength},
|
||||
{"seed", defaults.seed},
|
||||
{"video_frames", defaults.video_frames},
|
||||
{"image_preprocess", defaults.image_preprocess},
|
||||
{"fps", defaults.fps},
|
||||
{"moe_boundary", defaults.moe_boundary},
|
||||
{"vace_strength", defaults.vace_strength},
|
||||
|
||||
@@ -247,6 +247,7 @@ typedef struct {
|
||||
float attn_scale; // Override flash-attention K/V scaling; 0 keeps the model default
|
||||
const char* tokenizer; // tokenizer.json path or main=FILE,clip-l=FILE,clip-g=FILE assignments; required for PiD and Lens
|
||||
bool sage_attn;
|
||||
int conditioning_cache_size; // Maximum cached conditioning entries per context; 0 disables caching (default: 4)
|
||||
} sd_ctx_params_t;
|
||||
|
||||
typedef struct {
|
||||
@@ -263,6 +264,11 @@ typedef struct {
|
||||
uint8_t* data;
|
||||
} sd_image_t;
|
||||
|
||||
typedef struct {
|
||||
// Semicolon-separated target=...,key=value rules. NULL preserves defaults.
|
||||
const char* rules;
|
||||
} sd_image_preprocess_params_t;
|
||||
|
||||
typedef struct {
|
||||
sd_image_t* frames;
|
||||
int frame_count;
|
||||
@@ -410,6 +416,7 @@ typedef struct {
|
||||
int qwen_image_layers;
|
||||
bool circular_x;
|
||||
bool circular_y;
|
||||
sd_image_preprocess_params_t image_preprocess;
|
||||
} sd_img_gen_params_t;
|
||||
|
||||
typedef struct {
|
||||
@@ -443,6 +450,7 @@ typedef struct {
|
||||
sd_hires_params_t hires;
|
||||
bool circular_x;
|
||||
bool circular_y;
|
||||
sd_image_preprocess_params_t image_preprocess;
|
||||
} sd_vid_gen_params_t;
|
||||
|
||||
typedef struct sd_ctx_t sd_ctx_t;
|
||||
|
||||
@@ -3166,6 +3166,7 @@ struct LLMEmbedder : public Conditioner {
|
||||
int64_t tag_count = static_cast<int64_t>(tags.size());
|
||||
result.c_token_types = sd::Tensor<int32_t>({tag_count}, std::move(tags));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
#ifndef __SD_CONDITIONING_CONDITIONING_CACHE_H__
|
||||
#define __SD_CONDITIONING_CONDITIONING_CACHE_H__
|
||||
|
||||
#include <algorithm>
|
||||
#include <list>
|
||||
#include <tuple>
|
||||
|
||||
#include "conditioning/conditioner.hpp"
|
||||
|
||||
class ConditioningCache {
|
||||
struct Entry {
|
||||
ConditionerParams params;
|
||||
std::vector<sd::Tensor<float>> ref_images;
|
||||
std::vector<MiniMaxH3PresentationItem> references;
|
||||
SDCondition condition;
|
||||
|
||||
Entry(const ConditionerParams& input, const SDCondition& output)
|
||||
: params(input), condition(output) {
|
||||
// Request-owned reference pointers must not outlive the request.
|
||||
if (input.ref_images != nullptr) {
|
||||
ref_images = *input.ref_images;
|
||||
params.ref_images = &ref_images;
|
||||
}
|
||||
if (input.minimax_h3_references != nullptr) {
|
||||
references = *input.minimax_h3_references;
|
||||
params.minimax_h3_references = &references;
|
||||
}
|
||||
}
|
||||
|
||||
Entry(const Entry&) = delete;
|
||||
Entry& operator=(const Entry&) = delete;
|
||||
};
|
||||
|
||||
size_t capacity_ = 4;
|
||||
std::list<Entry> entries_;
|
||||
|
||||
static bool same_images(const std::vector<sd::Tensor<float>>& a,
|
||||
const std::vector<sd::Tensor<float>>& b) {
|
||||
return std::equal(a.begin(), a.end(), b.begin(), b.end(),
|
||||
[](const sd::Tensor<float>& x, const sd::Tensor<float>& y) {
|
||||
return x.shape() == y.shape() && x.values() == y.values();
|
||||
});
|
||||
}
|
||||
|
||||
static bool same_params(const ConditionerParams& a, const ConditionerParams& b) {
|
||||
const auto fields = [](const ConditionerParams& p) {
|
||||
const auto& r = p.ref_image_params;
|
||||
return std::tie(p.text, p.clip_skip, p.width, p.height, p.zero_out_masked,
|
||||
r.pass_to_vlm, r.pass_to_dit, r.ref_index_mode,
|
||||
r.force_ref_timestep_zero, r.resize_before_vae, r.vae_input_max_pixels,
|
||||
r.vlm_resize_mode, r.vlm_min_size, r.vlm_max_size, r.resize_vae_to_target);
|
||||
};
|
||||
if (fields(a) != fields(b) ||
|
||||
(a.ref_images == nullptr) != (b.ref_images == nullptr) ||
|
||||
(a.minimax_h3_references == nullptr) != (b.minimax_h3_references == nullptr)) {
|
||||
return false;
|
||||
}
|
||||
if (a.ref_images != nullptr && !same_images(*a.ref_images, *b.ref_images)) {
|
||||
return false;
|
||||
}
|
||||
if (a.minimax_h3_references != nullptr &&
|
||||
!std::equal(a.minimax_h3_references->begin(), a.minimax_h3_references->end(),
|
||||
b.minimax_h3_references->begin(), b.minimax_h3_references->end(),
|
||||
[](const MiniMaxH3PresentationItem& x, const MiniMaxH3PresentationItem& y) {
|
||||
return x.kind == y.kind && x.timestamps == y.timestamps && same_images(x.frames, y.frames);
|
||||
})) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public:
|
||||
void set_capacity(size_t capacity) {
|
||||
capacity_ = capacity;
|
||||
while (entries_.size() > capacity_) {
|
||||
entries_.pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
void clear() {
|
||||
entries_.clear();
|
||||
}
|
||||
|
||||
SDCondition get(Conditioner& conditioner, int n_threads, const ConditionerParams& params) {
|
||||
if (capacity_ == 0) {
|
||||
return conditioner.get_learned_condition(n_threads, params);
|
||||
}
|
||||
for (auto it = entries_.begin(); it != entries_.end(); ++it) {
|
||||
if (same_params(it->params, params)) {
|
||||
entries_.splice(entries_.begin(), entries_, it);
|
||||
LOG_INFO("conditioning cache hit");
|
||||
return entries_.front().condition;
|
||||
}
|
||||
}
|
||||
auto condition = conditioner.get_learned_condition(n_threads, params);
|
||||
if (!condition.empty()) {
|
||||
if (entries_.size() == capacity_) {
|
||||
entries_.pop_back();
|
||||
}
|
||||
entries_.emplace_front(params, condition);
|
||||
LOG_VERBOSE("conditioning cache stored (%zu/%zu)", entries_.size(), capacity_);
|
||||
}
|
||||
return condition;
|
||||
}
|
||||
};
|
||||
|
||||
#endif // __SD_CONDITIONING_CONDITIONING_CACHE_H__
|
||||
@@ -644,6 +644,10 @@ std::optional<sd::Tensor<float>> GGMLRunner::compute(get_graph_cb_t get_graph,
|
||||
std::optional<sd::Tensor<float>> output;
|
||||
try {
|
||||
output = execute_graph(graph, n_threads, no_return, read_outputs);
|
||||
} catch (const std::bad_alloc&) {
|
||||
last_compute_status_ = GGML_STATUS_ALLOC_FAILED;
|
||||
LOG_ERROR("%s graph allocation failed", get_desc().c_str());
|
||||
return std::nullopt;
|
||||
} catch (const std::exception& error) {
|
||||
last_compute_status_ = GGML_STATUS_FAILED;
|
||||
LOG_ERROR("%s graph execution failed on %s: %s", get_desc().c_str(),
|
||||
@@ -964,10 +968,16 @@ std::optional<Tensor<float>> GGMLRunner::execute_graph(ggml_cgraph* graph, int n
|
||||
}
|
||||
LOG_DEBUG("%s executing segment %zu/%zu: %s", get_desc().c_str(),
|
||||
index + 1, plan.segments.size(), segment.group_name.c_str());
|
||||
if (!execute_segment(segment_graph, n_threads) ||
|
||||
!cache_.capture(segment_graph) ||
|
||||
!cut_cache_.capture(graph, segment, get_desc().c_str())) {
|
||||
return fail_segment("execution or output caching");
|
||||
if (!execute_segment(segment_graph, n_threads)) {
|
||||
return fail_segment("execution");
|
||||
}
|
||||
auto cache_status = cache_.capture(segment_graph);
|
||||
if (cache_status == GGML_STATUS_SUCCESS) {
|
||||
cache_status = cut_cache_.capture(graph, segment, get_desc().c_str());
|
||||
}
|
||||
if (cache_status != GGML_STATUS_SUCCESS) {
|
||||
last_compute_status_ = cache_status;
|
||||
return fail_segment("output caching");
|
||||
}
|
||||
sync_runtime_residency();
|
||||
if (last) {
|
||||
|
||||
+18
-12
@@ -26,10 +26,13 @@ namespace sd {
|
||||
|
||||
std::unique_ptr<CachedTensor> CachedTensor::copy(ggml_backend_t backend,
|
||||
const std::string& name,
|
||||
ggml_tensor* source) {
|
||||
ggml_tensor* source,
|
||||
ggml_status& status) {
|
||||
status = GGML_STATUS_FAILED;
|
||||
if (ggml_graph_cut::tensor_buffer(source) == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
status = GGML_STATUS_ALLOC_FAILED;
|
||||
auto entry = std::make_unique<CachedTensor>();
|
||||
entry->context = ggml_init({2 * ggml_tensor_overhead(), nullptr, true});
|
||||
if (entry->context == nullptr) {
|
||||
@@ -50,6 +53,7 @@ namespace sd {
|
||||
} else {
|
||||
ggml_backend_tensor_copy(source, entry->tensor);
|
||||
}
|
||||
status = GGML_STATUS_SUCCESS;
|
||||
return entry;
|
||||
}
|
||||
|
||||
@@ -106,9 +110,9 @@ namespace sd {
|
||||
return pending > SIZE_MAX - committed ? SIZE_MAX : committed + pending;
|
||||
}
|
||||
|
||||
bool RunnerCache::capture(ggml_cgraph* graph) {
|
||||
ggml_status RunnerCache::capture(ggml_cgraph* graph) {
|
||||
if (outputs_.empty()) {
|
||||
return true;
|
||||
return GGML_STATUS_SUCCESS;
|
||||
}
|
||||
const auto tensors = cache_graph_tensors(graph);
|
||||
for (const auto& output : outputs_) {
|
||||
@@ -116,14 +120,15 @@ namespace sd {
|
||||
continue;
|
||||
}
|
||||
GGML_ASSERT(ggml_is_contiguous(output.second));
|
||||
auto entry = CachedTensor::copy(backend_, output.first, output.second);
|
||||
ggml_status status;
|
||||
auto entry = CachedTensor::copy(backend_, output.first, output.second, status);
|
||||
if (entry == nullptr) {
|
||||
return false;
|
||||
return status;
|
||||
}
|
||||
pending_[output.first] = std::move(entry);
|
||||
}
|
||||
ggml_backend_synchronize(backend_);
|
||||
return true;
|
||||
return GGML_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
void RunnerCache::graph_end(bool success) {
|
||||
@@ -180,9 +185,9 @@ namespace sd {
|
||||
}
|
||||
}
|
||||
|
||||
bool GraphCutTensorCache::capture(ggml_cgraph* graph,
|
||||
const ggml_graph_cut::Segment& segment,
|
||||
const char* log_desc) {
|
||||
ggml_status GraphCutTensorCache::capture(ggml_cgraph* graph,
|
||||
const ggml_graph_cut::Segment& segment,
|
||||
const char* log_desc) {
|
||||
size_t copied_bytes = 0;
|
||||
size_t copied_count = 0;
|
||||
for (int index : segment.output_node_indices) {
|
||||
@@ -191,10 +196,11 @@ namespace sd {
|
||||
!segment.future_cut_names.count(output->name)) {
|
||||
continue;
|
||||
}
|
||||
auto entry = CachedTensor::copy(backend_, output->name, ggml_graph_cut::cache_source_tensor(output));
|
||||
ggml_status status;
|
||||
auto entry = CachedTensor::copy(backend_, output->name, ggml_graph_cut::cache_source_tensor(output), status);
|
||||
if (entry == nullptr) {
|
||||
LOG_ERROR("%s failed to capture graph cut tensor: %s", log_desc, output->name);
|
||||
return false;
|
||||
return status;
|
||||
}
|
||||
const size_t size = ggml_backend_buffer_get_size(entry->buffer);
|
||||
copied_bytes = size > SIZE_MAX - copied_bytes ? SIZE_MAX : copied_bytes + size;
|
||||
@@ -206,6 +212,6 @@ namespace sd {
|
||||
LOG_DEBUG("%s graph cut cache added %6.2f MB (%zu tensors)",
|
||||
log_desc, copied_bytes / (1024.f * 1024.f), copied_count);
|
||||
}
|
||||
return true;
|
||||
return GGML_STATUS_SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,8 @@ namespace sd {
|
||||
~CachedTensor();
|
||||
static std::unique_ptr<CachedTensor> copy(ggml_backend_t backend,
|
||||
const std::string& name,
|
||||
ggml_tensor* source);
|
||||
ggml_tensor* source,
|
||||
ggml_status& status);
|
||||
};
|
||||
using CachedTensors = std::map<std::string, std::unique_ptr<CachedTensor>>;
|
||||
|
||||
@@ -41,7 +42,8 @@ namespace sd {
|
||||
const std::map<std::string, ggml_tensor*>& outputs() const { return outputs_; }
|
||||
size_t pending_bytes(ggml_cgraph* graph) const;
|
||||
size_t resident_bytes(ggml_backend_dev_t device) const;
|
||||
bool capture(ggml_cgraph* graph);
|
||||
bool empty() const { return committed_.empty(); }
|
||||
ggml_status capture(ggml_cgraph* graph);
|
||||
void graph_end(bool success);
|
||||
void clear();
|
||||
};
|
||||
@@ -57,7 +59,7 @@ namespace sd {
|
||||
size_t resident_bytes(ggml_backend_dev_t device) const;
|
||||
size_t estimate_output_bytes(ggml_cgraph* graph,
|
||||
const ggml_graph_cut::Segment& segment) const;
|
||||
bool capture(ggml_cgraph* graph, const ggml_graph_cut::Segment& segment, const char* log_desc);
|
||||
ggml_status capture(ggml_cgraph* graph, const ggml_graph_cut::Segment& segment, const char* log_desc);
|
||||
void prune(const std::unordered_set<std::string>& keep_names);
|
||||
void clear() { tensors_.clear(); }
|
||||
};
|
||||
|
||||
@@ -970,6 +970,7 @@ bool adetail_image(adetailer_ctx_t* context,
|
||||
generation.pm_params = {};
|
||||
generation.pulid_params = {};
|
||||
generation.hires.enabled = false;
|
||||
generation.image_preprocess = {};
|
||||
if (params.steps > 0) {
|
||||
generation.sample_params.sample_steps = params.steps;
|
||||
generation.sample_params.custom_sigmas = nullptr;
|
||||
|
||||
@@ -264,6 +264,9 @@ namespace MiniMaxH3 {
|
||||
for (int64_t i = 0; i < num_layers; ++i) {
|
||||
auto block = std::dynamic_pointer_cast<TokenRefinerBlock>(blocks["blocks." + std::to_string(i)]);
|
||||
x = block->forward(ctx, x);
|
||||
sd::ggml_graph_cut::mark_graph_cut(x,
|
||||
"minimax_h3.token_refiner.blocks." + std::to_string(i),
|
||||
"hidden_states");
|
||||
}
|
||||
return std::dynamic_pointer_cast<RMSNorm>(blocks["final_norm"])->forward(ctx, x);
|
||||
}
|
||||
@@ -527,7 +530,11 @@ namespace MiniMaxH3 {
|
||||
GGML_ASSERT(context->ne[0] == config.text_dim);
|
||||
auto condition_proj = std::dynamic_pointer_cast<Linear>(blocks["condition_proj"]);
|
||||
auto token_refiner = std::dynamic_pointer_cast<TokenRefiner>(blocks["token_refiner"]);
|
||||
return token_refiner->forward(ctx, condition_proj->forward(ctx, context));
|
||||
auto projected = condition_proj->forward(ctx, context);
|
||||
sd::ggml_graph_cut::mark_graph_cut(projected,
|
||||
"minimax_h3.condition_proj",
|
||||
"hidden_states");
|
||||
return token_refiner->forward(ctx, projected);
|
||||
}
|
||||
|
||||
ggml_tensor* time_embedding(GGMLRunnerContext* ctx,
|
||||
|
||||
@@ -71,6 +71,8 @@ struct AnimaDiffusionExtra {
|
||||
|
||||
struct QwenImage21DiffusionExtra {
|
||||
const sd::Tensor<int32_t>* image_slots = nullptr;
|
||||
// Nonzero IDs identify immutable prefix inputs within one sampling run.
|
||||
uint64_t prefix_id = 0;
|
||||
};
|
||||
|
||||
struct WanDiffusionExtra {
|
||||
|
||||
@@ -121,6 +121,18 @@ namespace Qwen {
|
||||
}
|
||||
};
|
||||
|
||||
struct QwenImage21PrefixCache {
|
||||
enum class Mode {
|
||||
NONE,
|
||||
STORE,
|
||||
REUSE
|
||||
};
|
||||
Mode mode = Mode::NONE;
|
||||
std::string name;
|
||||
std::string cut_group;
|
||||
int64_t prefix_length = 0;
|
||||
};
|
||||
|
||||
class QwenImage21ZeroCenterRMSNorm : public RMSNorm {
|
||||
public:
|
||||
using RMSNorm::RMSNorm;
|
||||
@@ -160,27 +172,49 @@ namespace Qwen {
|
||||
}
|
||||
}
|
||||
|
||||
ggml_tensor* forward(GGMLRunnerContext* ctx, ggml_tensor* x, ggml_tensor* pe, const std::vector<QwenImage21Segment>& segments, const std::vector<ggml_tensor*>& masks) {
|
||||
ggml_tensor* forward(GGMLRunnerContext* ctx, ggml_tensor* x, ggml_tensor* pe, const std::vector<QwenImage21Segment>& segments, const std::vector<ggml_tensor*>& masks, const QwenImage21PrefixCache& cache) {
|
||||
int64_t heads = x->ne[0] / dim_head;
|
||||
auto project = [&](const char* name) {
|
||||
auto h = std::dynamic_pointer_cast<Linear>(blocks[name])->forward(ctx, x);
|
||||
return ggml_reshape_4d(ctx->ggml_ctx, h, dim_head, heads, x->ne[1], x->ne[2]);
|
||||
};
|
||||
auto q = project("to_q");
|
||||
auto k = project("to_k");
|
||||
auto v = project("to_v");
|
||||
q = std::dynamic_pointer_cast<RMSNorm>(blocks["norm_q"])->forward(ctx, q);
|
||||
k = std::dynamic_pointer_cast<RMSNorm>(blocks["norm_k"])->forward(ctx, k);
|
||||
q = Rope::apply_rope(ctx->ggml_ctx, q, pe);
|
||||
k = Rope::apply_rope(ctx->ggml_ctx, k, pe);
|
||||
auto q = project("to_q");
|
||||
auto k = project("to_k");
|
||||
auto v = project("to_v");
|
||||
q = std::dynamic_pointer_cast<RMSNorm>(blocks["norm_q"])->forward(ctx, q);
|
||||
k = std::dynamic_pointer_cast<RMSNorm>(blocks["norm_k"])->forward(ctx, k);
|
||||
q = Rope::apply_rope(ctx->ggml_ctx, q, pe);
|
||||
k = Rope::apply_rope(ctx->ggml_ctx, k, pe);
|
||||
if (cache.mode == QwenImage21PrefixCache::Mode::STORE) {
|
||||
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);
|
||||
// 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);
|
||||
ctx->persist_cache_tensor(cache.name + "." + name, copy);
|
||||
};
|
||||
persist(k, 1, "k");
|
||||
persist(v, 2, "v");
|
||||
}
|
||||
ggml_tensor* result = nullptr;
|
||||
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);
|
||||
result = result == nullptr ? out : ggml_concat(ctx->ggml_ctx, result, out, 1);
|
||||
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);
|
||||
} 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);
|
||||
result = result == nullptr ? out : ggml_concat(ctx->ggml_ctx, result, out, 1);
|
||||
}
|
||||
}
|
||||
auto to_out = std::dynamic_pointer_cast<Linear>(blocks["to_out.0"]);
|
||||
if (sd_backend_is(ctx->backend, "Vulkan") || sd_backend_is(ctx->backend, "ROCm")) {
|
||||
@@ -219,13 +253,14 @@ namespace Qwen {
|
||||
return ggml_concat(ctx, prefix, target, 1);
|
||||
}
|
||||
|
||||
ggml_tensor* forward(GGMLRunnerContext* ctx, ggml_tensor* x, const std::vector<ggml_tensor*>& modulation, ggml_tensor* pe, const QwenImage21Layout& layout, const std::vector<ggml_tensor*>& masks) {
|
||||
auto h = std::dynamic_pointer_cast<LayerNorm>(blocks["img_norm1"])->forward(ctx, x);
|
||||
h = modulate(ctx->ggml_ctx, h, modulation[0], layout.prefix_length);
|
||||
h = std::dynamic_pointer_cast<QwenImage21Attention>(blocks["attn"])->forward(ctx, h, pe, layout.segments, masks);
|
||||
x = ggml_add(ctx->ggml_ctx, x, modulate(ctx->ggml_ctx, h, modulation[1], layout.prefix_length, true));
|
||||
h = std::dynamic_pointer_cast<LayerNorm>(blocks["img_norm2"])->forward(ctx, x);
|
||||
h = modulate(ctx->ggml_ctx, h, modulation[2], layout.prefix_length);
|
||||
ggml_tensor* forward(GGMLRunnerContext* ctx, ggml_tensor* x, const std::vector<ggml_tensor*>& modulation, ggml_tensor* pe, const QwenImage21Layout& layout, const std::vector<ggml_tensor*>& masks, const QwenImage21PrefixCache& cache) {
|
||||
const int64_t prefix_length = cache.mode == QwenImage21PrefixCache::Mode::REUSE ? 0 : layout.prefix_length;
|
||||
auto h = std::dynamic_pointer_cast<LayerNorm>(blocks["img_norm1"])->forward(ctx, x);
|
||||
h = modulate(ctx->ggml_ctx, h, modulation[0], prefix_length);
|
||||
h = std::dynamic_pointer_cast<QwenImage21Attention>(blocks["attn"])->forward(ctx, h, pe, layout.segments, masks, cache);
|
||||
x = ggml_add(ctx->ggml_ctx, x, modulate(ctx->ggml_ctx, h, modulation[1], prefix_length, true));
|
||||
h = std::dynamic_pointer_cast<LayerNorm>(blocks["img_norm2"])->forward(ctx, x);
|
||||
h = modulate(ctx->ggml_ctx, h, modulation[2], prefix_length);
|
||||
ggml_tensor* gate;
|
||||
auto fused = blocks.find("img_mlp.gate_up");
|
||||
if (fused != blocks.end()) {
|
||||
@@ -239,7 +274,7 @@ namespace Qwen {
|
||||
}
|
||||
h = ggml_mul(ctx->ggml_ctx, h, ggml_silu(ctx->ggml_ctx, gate));
|
||||
h = std::dynamic_pointer_cast<Linear>(blocks["img_mlp.out"])->forward(ctx, h);
|
||||
return ggml_add(ctx->ggml_ctx, x, modulate(ctx->ggml_ctx, h, modulation[3], layout.prefix_length, true));
|
||||
return ggml_add(ctx->ggml_ctx, x, modulate(ctx->ggml_ctx, h, modulation[3], prefix_length, true));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -261,7 +296,7 @@ namespace Qwen {
|
||||
}
|
||||
}
|
||||
|
||||
ggml_tensor* forward(GGMLRunnerContext* ctx, ggml_tensor* x, ggml_tensor* timestep, ggml_tensor* context, const std::vector<ggml_tensor*>& refs, ggml_tensor* pe, const QwenImage21Layout& layout, const std::vector<ggml_tensor*>& masks) {
|
||||
ggml_tensor* forward(GGMLRunnerContext* ctx, ggml_tensor* x, ggml_tensor* timestep, ggml_tensor* context, const std::vector<ggml_tensor*>& refs, ggml_tensor* pe, const QwenImage21Layout& layout, const std::vector<ggml_tensor*>& masks, const QwenImage21PrefixCache& cache) {
|
||||
auto time = ggml_concat(ctx->ggml_ctx, timestep, ggml_ext_zeros_like(ctx->ggml_ctx, timestep), 0);
|
||||
// Runtime flow timesteps already use the [0, 1000] scale.
|
||||
time = ggml_ext_timestep_embedding(ctx->ggml_ctx, time, 256, 10000, 1.f);
|
||||
@@ -269,27 +304,37 @@ namespace Qwen {
|
||||
time = ggml_silu(ctx->ggml_ctx, time);
|
||||
auto modulation = std::dynamic_pointer_cast<Linear>(blocks["modulation.1"])->forward(ctx, time);
|
||||
auto mod = ggml_ext_chunk(ctx->ggml_ctx, modulation, 4, 0);
|
||||
auto text = std::dynamic_pointer_cast<QwenImage21TextProjection>(blocks["txt_in"])->forward(ctx, context);
|
||||
auto img_in = std::dynamic_pointer_cast<Linear>(blocks["img_in"]);
|
||||
ggml_tensor* joint = nullptr;
|
||||
for (const auto& segment : layout.segments) {
|
||||
ggml_tensor* h;
|
||||
if (segment.image_index < 0) {
|
||||
h = ggml_ext_slice(ctx->ggml_ctx, text, 1, segment.context_start,
|
||||
segment.context_start + segment.end - segment.start);
|
||||
} else {
|
||||
auto image = segment.image_index == static_cast<int>(refs.size()) ? x : refs[segment.image_index];
|
||||
h = img_in->forward(ctx, DiT::patchify(ctx->ggml_ctx, image, 1, 1));
|
||||
if (cache.mode == QwenImage21PrefixCache::Mode::REUSE) {
|
||||
joint = img_in->forward(ctx, DiT::patchify(ctx->ggml_ctx, x, 1, 1));
|
||||
} else {
|
||||
auto text = std::dynamic_pointer_cast<QwenImage21TextProjection>(blocks["txt_in"])->forward(ctx, context);
|
||||
for (const auto& segment : layout.segments) {
|
||||
ggml_tensor* h;
|
||||
if (segment.image_index < 0) {
|
||||
h = ggml_ext_slice(ctx->ggml_ctx, text, 1, segment.context_start,
|
||||
segment.context_start + segment.end - segment.start);
|
||||
} else {
|
||||
auto image = segment.image_index == static_cast<int>(refs.size()) ? x : refs[segment.image_index];
|
||||
h = img_in->forward(ctx, DiT::patchify(ctx->ggml_ctx, image, 1, 1));
|
||||
}
|
||||
joint = joint == nullptr ? h : ggml_concat(ctx->ggml_ctx, joint, h, 1);
|
||||
}
|
||||
joint = joint == nullptr ? h : ggml_concat(ctx->ggml_ctx, joint, h, 1);
|
||||
}
|
||||
sd::ggml_graph_cut::mark_graph_cut(joint, "qwen_image_2_1.prelude", "joint");
|
||||
for (int i = 0; i < config.num_layers; ++i) {
|
||||
auto block = std::dynamic_pointer_cast<QwenImage21TransformerBlock>(blocks["transformer_blocks." + std::to_string(i)]);
|
||||
joint = block->forward(ctx, joint, mod, pe, layout, masks);
|
||||
sd::ggml_graph_cut::mark_graph_cut(joint, "qwen_image_2_1.transformer_blocks." + std::to_string(i), "joint");
|
||||
const std::string layer = "transformer_blocks." + std::to_string(i);
|
||||
auto layer_cache = cache;
|
||||
layer_cache.name = cache.name + "." + std::to_string(i);
|
||||
layer_cache.cut_group = "qwen_image_2_1." + layer;
|
||||
auto block = std::dynamic_pointer_cast<QwenImage21TransformerBlock>(blocks[layer]);
|
||||
joint = block->forward(ctx, joint, mod, pe, layout, masks, layer_cache);
|
||||
sd::ggml_graph_cut::mark_graph_cut(joint, layer_cache.cut_group, "joint");
|
||||
}
|
||||
if (cache.mode != QwenImage21PrefixCache::Mode::REUSE) {
|
||||
joint = ggml_ext_slice(ctx->ggml_ctx, joint, 1, layout.prefix_length, joint->ne[1]);
|
||||
}
|
||||
joint = ggml_ext_slice(ctx->ggml_ctx, joint, 1, layout.prefix_length, joint->ne[1]);
|
||||
auto scale = std::dynamic_pointer_cast<Linear>(blocks["norm_out.linear"])->forward(ctx, ggml_ext_chunk(ctx->ggml_ctx, time, 2, 1)[0]);
|
||||
joint = std::dynamic_pointer_cast<LayerNorm>(blocks["norm_out.norm"])->forward(ctx, joint);
|
||||
joint = ggml_mul(ctx->ggml_ctx, joint, ggml_scale_bias(ctx->ggml_ctx, scale, 1.f, 1.f));
|
||||
@@ -303,11 +348,18 @@ 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;
|
||||
|
||||
QwenImage21Runner(ggml_backend_t backend, const String2TensorStorage& weights, const std::string& prefix, std::shared_ptr<RunnerWeightManager> weight_manager = nullptr)
|
||||
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),
|
||||
config(QwenImage21Config::detect_from_weights(weights, prefix)),
|
||||
model(config) {
|
||||
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());
|
||||
}
|
||||
}
|
||||
model.init(params_ctx, weights, prefix);
|
||||
}
|
||||
|
||||
@@ -317,6 +369,22 @@ namespace Qwen {
|
||||
model.get_param_tensors(tensors, prefix);
|
||||
}
|
||||
|
||||
bool has_prefix_cache(const QwenImage21PrefixCache& cache) {
|
||||
for (int i = 0; i < config.num_layers; ++i) {
|
||||
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) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
sd::Tensor<float> compute(int n_threads, const DiffusionParams& inputs) override {
|
||||
const auto& x = tensor_or_empty(inputs.x);
|
||||
const auto& context = tensor_or_empty(inputs.context);
|
||||
@@ -345,38 +413,75 @@ namespace Qwen {
|
||||
LOG_ERROR("%s", error.what());
|
||||
return {};
|
||||
}
|
||||
pe_data = Rope::embed_nd(layout.positions, 1, 10000.f, config.axes_dim);
|
||||
mask_data.clear();
|
||||
for (const auto& segment : layout.segments) {
|
||||
sd::Tensor<float> mask;
|
||||
if (segment.image_index < 0) {
|
||||
mask = sd::Tensor<float>::zeros({segment.end, segment.end - segment.start});
|
||||
for (int64_t q = segment.start; q < segment.end; ++q) {
|
||||
for (int64_t k = q + 1; k < segment.end; ++k) {
|
||||
mask[k + segment.end * (q - segment.start)] = -INFINITY;
|
||||
if (!runner_started()) {
|
||||
prefix_cache_disabled = 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);
|
||||
cache.prefix_length = layout.prefix_length;
|
||||
cache.mode = has_prefix_cache(cache) ? QwenImage21PrefixCache::Mode::REUSE : QwenImage21PrefixCache::Mode::STORE;
|
||||
}
|
||||
auto run = [&](const QwenImage21PrefixCache& active_cache) {
|
||||
const bool cached = active_cache.mode == QwenImage21PrefixCache::Mode::REUSE;
|
||||
const auto first_position = layout.positions.begin() + (cached ? layout.prefix_length : 0);
|
||||
pe_data = Rope::embed_nd(std::vector<std::vector<float>>(first_position, layout.positions.end()), 1, 10000.f, config.axes_dim);
|
||||
mask_data.clear();
|
||||
if (!cached) {
|
||||
for (const auto& segment : layout.segments) {
|
||||
sd::Tensor<float> mask;
|
||||
if (segment.image_index < 0) {
|
||||
mask = sd::Tensor<float>::zeros({segment.end, segment.end - segment.start});
|
||||
for (int64_t q = segment.start; q < segment.end; ++q) {
|
||||
for (int64_t k = q + 1; k < segment.end; ++k) {
|
||||
mask[k + segment.end * (q - segment.start)] = -INFINITY;
|
||||
}
|
||||
}
|
||||
}
|
||||
mask_data.push_back(std::move(mask));
|
||||
}
|
||||
}
|
||||
mask_data.push_back(std::move(mask));
|
||||
}
|
||||
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());
|
||||
set_backend_tensor_data(pe, pe_data.data());
|
||||
std::vector<ggml_tensor*> masks, ref_inputs;
|
||||
for (const auto& mask : mask_data) {
|
||||
masks.push_back(mask.empty() ? nullptr : make_input(mask));
|
||||
}
|
||||
for (const auto& ref : refs) {
|
||||
ref_inputs.push_back(make_input(ref));
|
||||
}
|
||||
auto ctx = get_context();
|
||||
auto out = model.forward(&ctx, make_input(x), make_input(*inputs.timesteps), make_input(context),
|
||||
ref_inputs, pe, layout, masks);
|
||||
ggml_build_forward_expand(graph, out);
|
||||
return graph;
|
||||
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));
|
||||
set_backend_tensor_data(pe, pe_data.data());
|
||||
std::vector<ggml_tensor*> masks, ref_inputs;
|
||||
for (const auto& mask : mask_data) {
|
||||
masks.push_back(mask.empty() ? nullptr : make_input(mask));
|
||||
}
|
||||
if (!cached) {
|
||||
for (const auto& ref : refs) {
|
||||
ref_inputs.push_back(make_input(ref));
|
||||
}
|
||||
}
|
||||
auto ctx = get_context();
|
||||
auto out = model.forward(&ctx, make_input(x), make_input(*inputs.timesteps), cached ? nullptr : make_input(context),
|
||||
ref_inputs, pe, layout, masks, active_cache);
|
||||
ggml_build_forward_expand(graph, out);
|
||||
return graph;
|
||||
};
|
||||
return restore_trailing_singleton_dims(GGMLRunner::compute(build, n_threads, false), x.dim());
|
||||
};
|
||||
return restore_trailing_singleton_dims(GGMLRunner::compute(build, n_threads, false), x.dim());
|
||||
auto 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.
|
||||
free_cache_ctx_and_buffer();
|
||||
prefix_cache_disabled = true;
|
||||
LOG_WARN("Qwen Image 2.1: insufficient memory for prefix caching; retrying without it for this sampling run");
|
||||
return run(QwenImage21PrefixCache{});
|
||||
}
|
||||
if (!result.empty() && cache.mode == QwenImage21PrefixCache::Mode::STORE) {
|
||||
if (!has_prefix_cache(cache)) {
|
||||
free_cache_ctx_and_buffer();
|
||||
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);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -82,6 +82,20 @@ namespace WAN {
|
||||
}
|
||||
|
||||
x = ggml_ext_pad_ext(ctx->ggml_ctx, ctx->backend, x, lp0, rp0, lp1, rp1, lp2, rp2, 0, 0, ctx->circular_x_enabled, ctx->circular_y_enabled);
|
||||
if (w->ne[2] == 1 && x->ne[2] == 1 && x->ne[3] == in_channels) {
|
||||
// One frame through a one-frame-deep kernel is a 2D conv; backends without
|
||||
// im2col_3d (Metal) otherwise fall back to a much slower direct conv_3d.
|
||||
if (!ggml_is_contiguous(x)) {
|
||||
x = ggml_cont(ctx->ggml_ctx, x);
|
||||
}
|
||||
ggml_tensor* x2 = ggml_reshape_4d(ctx->ggml_ctx, x, x->ne[0], x->ne[1], in_channels, 1);
|
||||
ggml_tensor* w2 = ggml_reshape_4d(ctx->ggml_ctx, w, w->ne[0], w->ne[1], in_channels, out_channels);
|
||||
x2 = ggml_ext_conv_2d(ctx->ggml_ctx, x2, w2, b,
|
||||
std::get<2>(stride), std::get<1>(stride), 0, 0,
|
||||
std::get<2>(dilation), std::get<1>(dilation),
|
||||
ctx->conv2d_direct_enabled);
|
||||
return ggml_reshape_4d(ctx->ggml_ctx, x2, x2->ne[0], x2->ne[1], 1, out_channels);
|
||||
}
|
||||
return ggml_ext_conv_3d(ctx->ggml_ctx, ctx->backend, x, w, b, in_channels,
|
||||
std::get<2>(stride), std::get<1>(stride), std::get<0>(stride),
|
||||
0, 0, 0,
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <list>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include <tuple>
|
||||
#include <type_traits>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
@@ -29,6 +30,7 @@
|
||||
#include "stable-diffusion.h"
|
||||
|
||||
#include "conditioning/conditioner.hpp"
|
||||
#include "conditioning/conditioning_cache.h"
|
||||
#include "core/backend_fit.h"
|
||||
#include "extensions/generation_extension.h"
|
||||
#include "model/adapter/ip_adapter.hpp"
|
||||
@@ -135,6 +137,7 @@ static_assert(std::atomic<sd_cancel_mode_t>::is_always_lock_free,
|
||||
|
||||
StableDiffusionGGML::StableDiffusionGGML()
|
||||
: rng(std::make_shared<PhiloxRNG>()),
|
||||
conditioning_cache_(std::make_unique<ConditioningCache>()),
|
||||
denoiser(std::make_shared<CompVisDenoiser>()) {}
|
||||
|
||||
StableDiffusionGGML::~StableDiffusionGGML() = default;
|
||||
@@ -204,6 +207,8 @@ void StableDiffusionGGML::end_runners() {
|
||||
}
|
||||
|
||||
bool StableDiffusionGGML::reset_runners(const RunnerGroups& groups) {
|
||||
conditioning_cache_->clear();
|
||||
conditioning_loras_.clear();
|
||||
end_runners();
|
||||
clear_lora_adapters();
|
||||
runtime_lora_models.clear();
|
||||
@@ -915,6 +920,11 @@ bool StableDiffusionGGML::init(const sd_ctx_params_t* sd_ctx_params) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (sd_ctx_params->conditioning_cache_size < 0) {
|
||||
LOG_ERROR("conditioning_cache_size must be non-negative");
|
||||
return false;
|
||||
}
|
||||
conditioning_cache_->set_capacity(static_cast<size_t>(sd_ctx_params->conditioning_cache_size));
|
||||
auto configuration = std::make_unique<ModelConfig>(*sd_ctx_params);
|
||||
n_threads = sd_ctx_params->n_threads;
|
||||
tensor_executor = std::make_unique<sd::ParallelExecutor>(n_threads > 0 ? n_threads : sd_get_num_physical_cores());
|
||||
@@ -1765,8 +1775,19 @@ bool StableDiffusionGGML::apply_loras(const sd_lora_t* loras, uint32_t lora_coun
|
||||
int64_t t0 = ggml_time_ms();
|
||||
end_runners();
|
||||
clear_lora_adapters();
|
||||
if (!model_manager->prepare_lora_sources(all_loras))
|
||||
if (!model_manager->prepare_lora_sources(all_loras)) {
|
||||
conditioning_cache_->clear();
|
||||
return false;
|
||||
}
|
||||
if (!std::equal(all_loras.begin(), all_loras.end(),
|
||||
conditioning_loras_.begin(), conditioning_loras_.end(),
|
||||
[](const ModelManager::LoraSpec& a, const ModelManager::LoraSpec& b) {
|
||||
return a.file_id == b.file_id && a.file_revision == b.file_revision &&
|
||||
a.multiplier == b.multiplier && a.is_high_noise == b.is_high_noise &&
|
||||
a.tensor_name_prefix_filter == b.tensor_name_prefix_filter;
|
||||
})) {
|
||||
conditioning_cache_->clear();
|
||||
}
|
||||
runtime_lora_models.erase(std::remove_if(runtime_lora_models.begin(), runtime_lora_models.end(), [&](const RuntimeLora& entry) {
|
||||
return std::none_of(all_loras.begin(), all_loras.end(), [&](const ModelManager::LoraSpec& spec) {
|
||||
return entry.matches(spec);
|
||||
@@ -1776,6 +1797,7 @@ bool StableDiffusionGGML::apply_loras(const sd_lora_t* loras, uint32_t lora_coun
|
||||
const bool success = apply_lora_immediately ? apply_loras_immediately(all_loras)
|
||||
: apply_loras_at_runtime(all_loras);
|
||||
if (!success) {
|
||||
conditioning_cache_->clear();
|
||||
clear_lora_adapters();
|
||||
runtime_lora_models.clear();
|
||||
return false;
|
||||
@@ -1785,9 +1807,14 @@ bool StableDiffusionGGML::apply_loras(const sd_lora_t* loras, uint32_t lora_coun
|
||||
if (!all_loras.empty()) {
|
||||
LOG_INFO("apply_loras completed, taking %.2fs", (t1 - t0) * 1.0f / 1000);
|
||||
}
|
||||
conditioning_loras_ = std::move(all_loras);
|
||||
return true;
|
||||
}
|
||||
|
||||
SDCondition StableDiffusionGGML::get_learned_condition(const ConditionerParams& params) {
|
||||
return conditioning_cache_->get(*cond_stage_model, n_threads, params);
|
||||
}
|
||||
|
||||
void StableDiffusionGGML::reset_generation_extensions() {
|
||||
for (auto& extension : generation_extensions) {
|
||||
extension->reset_runtime_condition();
|
||||
@@ -1972,6 +1999,8 @@ void StableDiffusionGGML::preview_image(int step,
|
||||
int patch_sz = 1;
|
||||
const float(*latent_rgb_proj)[3] = nullptr;
|
||||
float* latent_rgb_bias = nullptr;
|
||||
const float* latent_alpha_proj = nullptr;
|
||||
float latent_alpha_bias = 1.f;
|
||||
|
||||
if (channels == 128) {
|
||||
if (sd_version_uses_flux2_vae(version)) {
|
||||
@@ -1985,6 +2014,16 @@ void StableDiffusionGGML::preview_image(int step,
|
||||
LOG_WARN("No latent to RGB projection known for this model");
|
||||
return;
|
||||
}
|
||||
} else if (channels == 64) {
|
||||
if (version == VERSION_QWEN_IMAGE_2_1) {
|
||||
latent_rgb_proj = qwen21_latent_rgb_proj;
|
||||
latent_rgb_bias = qwen21_latent_rgb_bias;
|
||||
latent_alpha_proj = qwen21_latent_alpha_proj;
|
||||
latent_alpha_bias = qwen21_latent_alpha_bias;
|
||||
} else {
|
||||
LOG_WARN("No latent to RGB projection known for this model");
|
||||
return;
|
||||
}
|
||||
} else if (channels == 48) {
|
||||
if (sd_version_is_wan(version)) {
|
||||
latent_rgb_proj = wan_22_latent_rgb_proj;
|
||||
@@ -2035,13 +2074,14 @@ void StableDiffusionGGML::preview_image(int step,
|
||||
uint32_t img_width = static_cast<uint32_t>(_latents.shape()[0]) * patch_sz;
|
||||
uint32_t img_height = static_cast<uint32_t>(_latents.shape()[1]) * patch_sz;
|
||||
|
||||
uint8_t* data = (uint8_t*)malloc(frames * img_width * img_height * 3 * sizeof(uint8_t));
|
||||
uint32_t img_channels = latent_alpha_proj != nullptr ? 4 : 3;
|
||||
uint8_t* data = (uint8_t*)malloc(frames * img_width * img_height * img_channels * sizeof(uint8_t));
|
||||
GGML_ASSERT(data != nullptr);
|
||||
preview_latent_video(data, _latents, latent_rgb_proj, latent_rgb_bias, patch_sz);
|
||||
preview_latent_video(data, _latents, latent_rgb_proj, latent_rgb_bias, patch_sz, latent_alpha_proj, latent_alpha_bias);
|
||||
sd_image_t* images = (sd_image_t*)malloc(frames * sizeof(sd_image_t));
|
||||
GGML_ASSERT(images != nullptr);
|
||||
for (uint32_t i = 0; i < frames; i++) {
|
||||
images[i] = {img_width, img_height, 3, data + i * img_width * img_height * 3};
|
||||
images[i] = {img_width, img_height, img_channels, data + i * img_width * img_height * img_channels};
|
||||
}
|
||||
step_callback(step, frames, images, is_noisy, step_callback_data);
|
||||
free(data);
|
||||
@@ -2216,6 +2256,15 @@ sd::Tensor<float> StableDiffusionGGML::sample(const std::shared_ptr<DiffusionMod
|
||||
};
|
||||
RunnerEndOnExit sample_diffusion_runner_end{work_diffusion_model.get()};
|
||||
|
||||
// These inputs are immutable for this sampling run. Extensions may replace or
|
||||
// modify them per step, so those paths need an explicit stability contract first.
|
||||
const bool cache_qwen_prefix = version == VERSION_QWEN_IMAGE_2_1 &&
|
||||
std::none_of(generation_extensions.begin(), generation_extensions.end(),
|
||||
[](const auto& extension) { return extension->is_enabled(); });
|
||||
using QwenPrefixInputs = std::tuple<const sd::Tensor<float>*, const sd::Tensor<int32_t>*,
|
||||
const std::vector<sd::Tensor<float>>*>;
|
||||
std::vector<QwenPrefixInputs> qwen_prefix_inputs;
|
||||
|
||||
RunnerEndOnExit sample_control_runner_end{!control_image.empty() && control_net != nullptr ? control_net.get() : nullptr};
|
||||
|
||||
const bool apply_denoise_mask = !denoise_mask.empty() &&
|
||||
@@ -2485,6 +2534,18 @@ sd::Tensor<float> StableDiffusionGGML::sample(const std::shared_ptr<DiffusionMod
|
||||
extension->before_diffusion(diffusion_params, step);
|
||||
}
|
||||
|
||||
if (cache_qwen_prefix) {
|
||||
auto* extra = std::get_if<QwenImage21DiffusionExtra>(&diffusion_params.extra);
|
||||
if (extra != nullptr) {
|
||||
auto key = std::make_tuple(diffusion_params.context, extra->image_slots,
|
||||
diffusion_params.ref_image_params.pass_to_dit ? diffusion_params.ref_latents : nullptr);
|
||||
auto entry = std::find(qwen_prefix_inputs.begin(), qwen_prefix_inputs.end(), key);
|
||||
extra->prefix_id = static_cast<uint64_t>(entry - qwen_prefix_inputs.begin()) + 1;
|
||||
if (entry == qwen_prefix_inputs.end()) {
|
||||
qwen_prefix_inputs.push_back(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
auto output_opt = work_diffusion_model->compute(n_threads, diffusion_params);
|
||||
if (output_opt.empty()) {
|
||||
LOG_ERROR("diffusion model compute failed");
|
||||
|
||||
@@ -26,6 +26,7 @@ class RNG;
|
||||
struct Denoiser;
|
||||
struct LoraModel;
|
||||
struct ConditionerParams;
|
||||
class ConditioningCache;
|
||||
struct SDCondition;
|
||||
struct RefImageParams;
|
||||
namespace Wav2Vec2 {
|
||||
@@ -178,6 +179,8 @@ public:
|
||||
std::recursive_mutex execution_mutex;
|
||||
std::unique_ptr<ModelConfig> config_;
|
||||
RunnerState runner_state_;
|
||||
std::unique_ptr<ConditioningCache> conditioning_cache_;
|
||||
std::vector<ModelManager::LoraSpec> conditioning_loras_;
|
||||
bool executing_ = false;
|
||||
|
||||
std::shared_ptr<Denoiser> denoiser;
|
||||
@@ -361,6 +364,8 @@ public:
|
||||
|
||||
bool apply_loras(const sd_lora_t* loras, uint32_t lora_count);
|
||||
|
||||
SDCondition get_learned_condition(const ConditionerParams& params);
|
||||
|
||||
void reset_generation_extensions();
|
||||
|
||||
void prepare_generation_extensions(const sd_pm_params_t& pm_params,
|
||||
|
||||
+10
-6
@@ -10,6 +10,7 @@
|
||||
#include "model/vae/vae.hpp"
|
||||
#include "request.h"
|
||||
#include "runtime/denoiser.hpp"
|
||||
#include "runtime/image_preprocess.h"
|
||||
#include "upscaler.h"
|
||||
|
||||
namespace sd::pipeline {
|
||||
@@ -440,8 +441,7 @@ namespace sd::pipeline {
|
||||
sd->compute_ip_adapter_tokens(sd_img_gen_params->ip_adapter_image, sd_img_gen_params->ip_adapter_strength);
|
||||
int64_t prepare_start_ms = ggml_time_ms();
|
||||
condition_params.zero_out_masked = false;
|
||||
auto cond = sd->cond_stage_model->get_learned_condition(sd->n_threads,
|
||||
condition_params);
|
||||
auto cond = sd->get_learned_condition(condition_params);
|
||||
if (cond.empty()) {
|
||||
LOG_ERROR("failed to encode prompt");
|
||||
return std::nullopt;
|
||||
@@ -479,8 +479,7 @@ namespace sd::pipeline {
|
||||
// LLaDA-Image CFG keeps the source latent but drops its SigVQ features.
|
||||
condition_params.ref_images = nullptr;
|
||||
}
|
||||
uncond = sd->cond_stage_model->get_learned_condition(sd->n_threads,
|
||||
condition_params);
|
||||
uncond = sd->get_learned_condition(condition_params);
|
||||
if (uncond.empty()) {
|
||||
LOG_ERROR("failed to encode negative prompt");
|
||||
return std::nullopt;
|
||||
@@ -508,8 +507,7 @@ namespace sd::pipeline {
|
||||
if (use_ref_latent_img_cfg) {
|
||||
condition_params.ref_images = &empty_ref_images;
|
||||
}
|
||||
img_uncond = sd->cond_stage_model->get_learned_condition(sd->n_threads,
|
||||
condition_params);
|
||||
img_uncond = sd->get_learned_condition(condition_params);
|
||||
if (img_uncond.empty()) {
|
||||
LOG_ERROR("failed to encode image guidance prompt");
|
||||
return std::nullopt;
|
||||
@@ -800,6 +798,12 @@ namespace sd::pipeline {
|
||||
int64_t t0 = ggml_time_ms();
|
||||
sd->vae_tiling_params = sd_img_gen_params->vae_tiling_params;
|
||||
GenerationRequest request(sd, sd_img_gen_params);
|
||||
sd::ImagePreprocessor preprocessing(sd_img_gen_params->image_preprocess.rules);
|
||||
sd_img_gen_params_t processed_params = *sd_img_gen_params;
|
||||
if (!preprocessing.prepare_inputs(processed_params, request.width, request.height))
|
||||
return false;
|
||||
sd_img_gen_params = &processed_params;
|
||||
request.pm_params = processed_params.pm_params;
|
||||
LOG_INFO("generate_image %dx%d", request.width, request.height);
|
||||
|
||||
sd->rng->manual_seed(request.seed);
|
||||
|
||||
@@ -291,7 +291,8 @@ namespace sd::model_builders {
|
||||
result.diffusion = std::make_shared<Qwen::QwenImage21Runner>(ctx.backends.runtime_backend(SDBackendModule::DIFFUSION),
|
||||
tensor_storage_map,
|
||||
"model.diffusion_model",
|
||||
weight_manager);
|
||||
weight_manager,
|
||||
sd_ctx_params->model_args);
|
||||
} else {
|
||||
result.diffusion = std::make_shared<Qwen::QwenImageRunner>(ctx.backends.runtime_backend(SDBackendModule::DIFFUSION),
|
||||
tensor_storage_map,
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include "model/vae/vae.hpp"
|
||||
#include "request.h"
|
||||
#include "runtime/denoiser.hpp"
|
||||
#include "runtime/image_preprocess.h"
|
||||
|
||||
namespace sd::pipeline {
|
||||
|
||||
@@ -1161,8 +1162,7 @@ namespace sd::pipeline {
|
||||
}
|
||||
|
||||
int64_t prepare_start_ms = ggml_time_ms();
|
||||
embeds.cond = sd->cond_stage_model->get_learned_condition(sd->n_threads,
|
||||
condition_params);
|
||||
embeds.cond = sd->get_learned_condition(condition_params);
|
||||
if (embeds.cond.empty()) {
|
||||
LOG_ERROR("failed to encode video prompt");
|
||||
return std::nullopt;
|
||||
@@ -1187,8 +1187,7 @@ namespace sd::pipeline {
|
||||
}
|
||||
if (request.use_uncond) {
|
||||
condition_params.text = request.negative_prompt;
|
||||
embeds.uncond = sd->cond_stage_model->get_learned_condition(sd->n_threads,
|
||||
condition_params);
|
||||
embeds.uncond = sd->get_learned_condition(condition_params);
|
||||
if (embeds.uncond.empty()) {
|
||||
LOG_ERROR("failed to encode negative video prompt");
|
||||
return std::nullopt;
|
||||
@@ -1526,6 +1525,7 @@ namespace sd::pipeline {
|
||||
img_gen_params.qwen_image_layers = 0;
|
||||
img_gen_params.circular_x = sd_vid_gen_params->circular_x;
|
||||
img_gen_params.circular_y = sd_vid_gen_params->circular_y;
|
||||
img_gen_params.image_preprocess = sd_vid_gen_params->image_preprocess;
|
||||
|
||||
sd->animatediff_num_frames = n_frames;
|
||||
bool ok = generate_image(sd, &img_gen_params, frames_out, num_frames_out);
|
||||
@@ -1556,6 +1556,11 @@ namespace sd::pipeline {
|
||||
sd->vae_tiling_params = sd_vid_gen_params->vae_tiling_params;
|
||||
sd->apply_circular_axes(sd_vid_gen_params->circular_x, sd_vid_gen_params->circular_y);
|
||||
GenerationRequest request(sd, sd_vid_gen_params);
|
||||
sd::ImagePreprocessor preprocessing(sd_vid_gen_params->image_preprocess.rules);
|
||||
sd_vid_gen_params_t processed_params = *sd_vid_gen_params;
|
||||
if (!preprocessing.prepare_inputs(processed_params, request.width, request.height))
|
||||
return false;
|
||||
sd_vid_gen_params = &processed_params;
|
||||
if (fps_out != nullptr) {
|
||||
*fps_out = request.fps;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,447 @@
|
||||
#include "image_preprocess.h"
|
||||
#include "core/util.h"
|
||||
|
||||
#include <climits>
|
||||
#include <set>
|
||||
|
||||
namespace sd {
|
||||
|
||||
static constexpr std::pair<const char*, ImageTarget> image_targets[] = {
|
||||
{"init", ImageTarget::Init},
|
||||
{"end", ImageTarget::End},
|
||||
{"mask", ImageTarget::Mask},
|
||||
{"control", ImageTarget::Control},
|
||||
{"ref", ImageTarget::Ref},
|
||||
{"ip-adapter", ImageTarget::IPAdapter},
|
||||
{"id", ImageTarget::ID},
|
||||
{"control-frame", ImageTarget::ControlFrame},
|
||||
};
|
||||
|
||||
static constexpr std::pair<const char*, ImageResizeMode> image_resize_modes[] = {
|
||||
{"auto", ImageResizeMode::Auto},
|
||||
{"none", ImageResizeMode::None},
|
||||
{"stretch", ImageResizeMode::Stretch},
|
||||
{"crop", ImageResizeMode::Crop},
|
||||
{"crop-resize", ImageResizeMode::CropResize},
|
||||
{"fit-pad", ImageResizeMode::FitPad},
|
||||
};
|
||||
|
||||
template <typename T, size_t N>
|
||||
static bool parse_enum(const std::string& text, const std::pair<const char*, T> (&names)[N], T& value) {
|
||||
for (const auto& entry : names) {
|
||||
if (text == entry.first) {
|
||||
value = entry.second;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
template <typename T, size_t N>
|
||||
static const char* enum_name(T value, const std::pair<const char*, T> (&names)[N]) {
|
||||
for (const auto& entry : names) {
|
||||
if (value == entry.second)
|
||||
return entry.first;
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
static bool one_of(T value, std::initializer_list<T> choices) {
|
||||
return std::find(choices.begin(), choices.end(), value) != choices.end();
|
||||
}
|
||||
|
||||
static bool one_of(const std::string& value, std::initializer_list<const char*> choices) {
|
||||
for (const char* choice : choices) {
|
||||
if (value == choice)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static ImageResizeMode resolve_mode(const std::map<std::string, std::string>& options, ImageResizeMode default_mode) {
|
||||
auto it = options.find("mode");
|
||||
ImageResizeMode mode = ImageResizeMode::Auto;
|
||||
if (it != options.end())
|
||||
parse_enum(it->second, image_resize_modes, mode);
|
||||
if (mode != ImageResizeMode::Auto)
|
||||
return mode;
|
||||
return options.count("width") && default_mode == ImageResizeMode::None ? ImageResizeMode::Stretch : default_mode;
|
||||
}
|
||||
|
||||
bool ImagePreprocessor::fail(const std::string& message) const {
|
||||
LOG_ERROR("image preprocessing: %s", message.c_str());
|
||||
valid_ = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
ImagePreprocessor::ImagePreprocessor(const char* text) {
|
||||
if (text == nullptr || trim(text).empty())
|
||||
return;
|
||||
for (const auto& part : split_string(text, ';')) {
|
||||
ImagePreprocessRule rule;
|
||||
std::set<std::string> keys;
|
||||
if (trim(part).empty()) {
|
||||
fail("empty rule");
|
||||
return;
|
||||
}
|
||||
for (const auto& entry : split_string(part, ',')) {
|
||||
size_t equal = entry.find('=');
|
||||
if (equal == std::string::npos) {
|
||||
fail("expected key=value: " + entry);
|
||||
return;
|
||||
}
|
||||
std::string key = trim(entry.substr(0, equal));
|
||||
std::string value = trim(entry.substr(equal + 1));
|
||||
bool ok = !value.empty() && keys.insert(key).second;
|
||||
int number = 0;
|
||||
if (key == "target") {
|
||||
ok &= parse_enum(value, image_targets, rule.target);
|
||||
} else if (key == "index") {
|
||||
ok &= parse_strict_int(value, rule.index) && rule.index >= 0;
|
||||
} else {
|
||||
if (key == "mode") {
|
||||
ImageResizeMode mode;
|
||||
ok &= parse_enum(value, image_resize_modes, mode);
|
||||
} else if (key == "filter") {
|
||||
ok &= one_of(value, {"auto", "nearest", "nearest-exact", "bilinear", "bicubic", "lanczos"});
|
||||
} else if (key == "antialias") {
|
||||
ok &= one_of(value, {"auto", "true", "false"});
|
||||
} else if (key == "canny") {
|
||||
ok &= one_of(value, {"true", "false"});
|
||||
} else if (key == "anchor") {
|
||||
ok &= one_of(value, {"center", "top", "bottom", "left", "right"});
|
||||
} else if (key == "width" || key == "height") {
|
||||
ok &= parse_strict_int(value, number) && number > 0;
|
||||
} else if (key == "pad_color") {
|
||||
ok &= value.size() == 7 || value.size() == 9;
|
||||
ok &= !value.empty() && value[0] == '#';
|
||||
for (size_t i = 1; i < value.size(); ++i)
|
||||
ok &= std::isxdigit(static_cast<unsigned char>(value[i])) != 0;
|
||||
} else {
|
||||
ok = false;
|
||||
}
|
||||
rule.options[key] = value;
|
||||
}
|
||||
if (!ok) {
|
||||
fail("invalid or duplicate option: " + entry);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!keys.count("target") || rule.options.empty() ||
|
||||
rule.options.count("width") != rule.options.count("height") ||
|
||||
(rule.index >= 0 && !one_of(rule.target, {ImageTarget::Ref, ImageTarget::ID, ImageTarget::ControlFrame}))) {
|
||||
fail("invalid target, index, or incomplete dimensions: " + part);
|
||||
return;
|
||||
}
|
||||
rules_.push_back(std::move(rule));
|
||||
}
|
||||
for (const auto& rule : rules_) {
|
||||
const auto options = resolve_options(rule.target, std::max(0, rule.index));
|
||||
if (options.count("antialias") && options.at("antialias") == "true" && options.count("filter") &&
|
||||
one_of(options.at("filter"), {"nearest", "nearest-exact"})) {
|
||||
fail("antialias requires bilinear, bicubic, or lanczos");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::map<std::string, std::string> ImagePreprocessor::resolve_options(ImageTarget target, int index) const {
|
||||
std::map<std::string, std::string> options;
|
||||
for (int specificity = 0; specificity < 2; ++specificity) {
|
||||
for (const auto& rule : rules_) {
|
||||
if (rule.target == target &&
|
||||
rule.index == (specificity == 0 ? -1 : index)) {
|
||||
for (const auto& entry : rule.options)
|
||||
options[entry.first] = entry.second;
|
||||
}
|
||||
}
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
bool ImagePreprocessor::validate_inputs(const sd_img_gen_params_t& params) const {
|
||||
const std::map<ImageTarget, int> counts = {
|
||||
{ImageTarget::Init, params.init_image.data != nullptr},
|
||||
{ImageTarget::Mask, params.mask_image.data != nullptr},
|
||||
{ImageTarget::Control, params.control_image.data != nullptr},
|
||||
{ImageTarget::IPAdapter, params.ip_adapter_image.data != nullptr},
|
||||
{ImageTarget::Ref, params.ref_images != nullptr ? params.ref_images_count : 0},
|
||||
{ImageTarget::ID, params.pm_params.id_images != nullptr ? params.pm_params.id_images_count : 0},
|
||||
};
|
||||
for (const auto& rule : rules_) {
|
||||
auto it = counts.find(rule.target);
|
||||
int count = it == counts.end() ? 0 : it->second;
|
||||
if (count <= 0 || rule.index >= count) {
|
||||
return fail(std::string("rule targets an unavailable image: ") + enum_name(rule.target, image_targets));
|
||||
}
|
||||
}
|
||||
return valid_;
|
||||
}
|
||||
|
||||
bool ImagePreprocessor::validate_inputs(const sd_vid_gen_params_t& params) const {
|
||||
const std::map<ImageTarget, int> counts = {
|
||||
{ImageTarget::Init, params.init_image.data != nullptr},
|
||||
{ImageTarget::End, params.end_image.data != nullptr},
|
||||
{ImageTarget::Ref, params.ref_images != nullptr ? params.ref_images_count : 0},
|
||||
{ImageTarget::ControlFrame, params.control_frames != nullptr ? params.control_frames_size : 0},
|
||||
};
|
||||
for (const auto& rule : rules_) {
|
||||
auto it = counts.find(rule.target);
|
||||
int count = it == counts.end() ? 0 : it->second;
|
||||
if (count <= 0 || rule.index >= count)
|
||||
return fail(std::string("rule targets an unavailable video input: ") + enum_name(rule.target, image_targets));
|
||||
}
|
||||
return valid_;
|
||||
}
|
||||
|
||||
static int anchor_offset(int remaining, const std::string& anchor, bool horizontal) {
|
||||
if (anchor == (horizontal ? "left" : "top"))
|
||||
return 0;
|
||||
if (anchor == (horizontal ? "right" : "bottom"))
|
||||
return remaining;
|
||||
return remaining / 2;
|
||||
}
|
||||
|
||||
Tensor<float> ImagePreprocessor::apply_transform(const Tensor<float>& image, const std::map<std::string, std::string>& options, ImageTransform p, const std::string& label, ops::InterpolateMode default_filter) const {
|
||||
auto value = [&](const char* key, const char* fallback) {
|
||||
auto it = options.find(key);
|
||||
return it == options.end() ? std::string(fallback) : it->second;
|
||||
};
|
||||
std::string filter = value("filter", "auto");
|
||||
ops::InterpolateMode mode = default_filter;
|
||||
if (filter == "nearest")
|
||||
mode = ops::InterpolateMode::Nearest;
|
||||
if (filter == "nearest-exact")
|
||||
mode = ops::InterpolateMode::NearestExact;
|
||||
if (filter == "bilinear")
|
||||
mode = ops::InterpolateMode::Bilinear;
|
||||
if (filter == "bicubic")
|
||||
mode = ops::InterpolateMode::Bicubic;
|
||||
if (filter == "lanczos")
|
||||
mode = ops::InterpolateMode::Lanczos;
|
||||
bool filtered = ops::is_2d_filter_interpolate_mode(mode);
|
||||
bool antialias = value("antialias", "auto") == "true" ||
|
||||
(value("antialias", "auto") == "auto" && filtered &&
|
||||
(p.resize_width < p.crop_width || p.resize_height < p.crop_height));
|
||||
if (antialias && !filtered) {
|
||||
fail(label + ": antialias requires bilinear, bicubic, or lanczos");
|
||||
return {};
|
||||
}
|
||||
auto cropped = ops::slice(ops::slice(image, 0, p.x, p.x + p.crop_width), 1, p.y, p.y + p.crop_height);
|
||||
int channels = static_cast<int>(image.shape()[2]);
|
||||
bool resize = p.resize_width != p.crop_width || p.resize_height != p.crop_height;
|
||||
if (resize && channels == 4 && filtered) {
|
||||
for (int64_t i = 0, pixels = cropped.shape()[0] * cropped.shape()[1]; i < pixels; ++i) {
|
||||
for (int c = 0; c < 3; ++c)
|
||||
cropped[i + c * pixels] *= cropped[i + 3 * pixels];
|
||||
}
|
||||
}
|
||||
auto resized = ops::interpolate(cropped, {p.resize_width, p.resize_height, channels, 1}, mode, false, antialias);
|
||||
if (resize && channels == 4 && filtered) {
|
||||
for (int64_t i = 0, pixels = resized.shape()[0] * resized.shape()[1]; i < pixels; ++i) {
|
||||
float alpha = std::clamp(resized[i + 3 * pixels], 0.f, 1.f);
|
||||
for (int c = 0; c < 3; ++c)
|
||||
resized[i + c * pixels] = alpha > 1e-6f ? resized[i + c * pixels] / alpha : 0.f;
|
||||
}
|
||||
}
|
||||
resized = ops::clamp(resized, 0.f, 1.f);
|
||||
Tensor<float> output({p.width, p.height, channels, 1});
|
||||
std::string color = value("pad_color", "#000000ff");
|
||||
if (color.size() == 7)
|
||||
color += "ff";
|
||||
uint8_t rgba[4];
|
||||
for (int c = 0; c < 4; ++c)
|
||||
rgba[c] = static_cast<uint8_t>(std::strtoul(color.substr(1 + c * 2, 2).c_str(), nullptr, 16));
|
||||
for (int c = 0; c < channels; ++c) {
|
||||
float fill = rgba[channels == 1 ? 0 : c] / 255.f;
|
||||
for (int y = 0; y < p.height; ++y) {
|
||||
for (int x = 0; x < p.width; ++x) {
|
||||
output.index(x, y, c, 0) = x >= p.pad_x && x < p.pad_x + p.resize_width && y >= p.pad_y && y < p.pad_y + p.resize_height
|
||||
? resized.index(x - p.pad_x, y - p.pad_y, c, 0)
|
||||
: fill;
|
||||
}
|
||||
}
|
||||
}
|
||||
LOG_INFO("preprocess %s: %dx%d crop=(%d,%d,%d,%d) resize=%dx%d pad=(%d,%d) output=%dx%d filter=%s(%d) antialias=%s",
|
||||
label.c_str(), p.source_width, p.source_height, p.x, p.y, p.crop_width, p.crop_height,
|
||||
p.resize_width, p.resize_height, p.pad_x, p.pad_y, p.width, p.height, filter.c_str(), static_cast<int>(mode), BOOL_STR(antialias));
|
||||
return output;
|
||||
}
|
||||
|
||||
Tensor<float> ImagePreprocessor::apply_geometry(const Tensor<float>& image, ImageTarget target, int index, int width, int height, ImageResizeMode default_mode, ops::InterpolateMode default_filter, ImageTransform* plan_out) const {
|
||||
if (!valid_ || image.empty())
|
||||
return {};
|
||||
const std::string label = std::string(enum_name(target, image_targets)) + "[" + std::to_string(index) + "]";
|
||||
auto options = resolve_options(target, index);
|
||||
if (image.dim() != 4 || image.shape()[3] != 1 || image.shape()[2] < 1 || image.shape()[2] > 4) {
|
||||
fail(label + ": expected one image with 1 to 4 channels");
|
||||
return {};
|
||||
}
|
||||
ImageTransform p;
|
||||
p.source_width = p.crop_width = static_cast<int>(image.shape()[0]);
|
||||
p.source_height = p.crop_height = static_cast<int>(image.shape()[1]);
|
||||
int target_width = width > 0 ? width : p.source_width;
|
||||
int target_height = height > 0 ? height : p.source_height;
|
||||
if (options.count("width")) {
|
||||
parse_strict_int(options.at("width"), target_width);
|
||||
parse_strict_int(options.at("height"), target_height);
|
||||
}
|
||||
ImageResizeMode mode = resolve_mode(options, default_mode);
|
||||
std::string anchor = options.count("anchor") ? options.at("anchor") : "center";
|
||||
p.width = p.resize_width = target_width;
|
||||
p.height = p.resize_height = target_height;
|
||||
if (mode == ImageResizeMode::None) {
|
||||
if (options.count("width") && (target_width != p.source_width || target_height != p.source_height)) {
|
||||
fail(label + ": mode=none conflicts with requested dimensions");
|
||||
return {};
|
||||
}
|
||||
p.width = p.resize_width = p.source_width;
|
||||
p.height = p.resize_height = p.source_height;
|
||||
} else if (mode == ImageResizeMode::Crop || mode == ImageResizeMode::CropResize) {
|
||||
if (mode == ImageResizeMode::Crop) {
|
||||
p.crop_width = target_width;
|
||||
p.crop_height = target_height;
|
||||
} else if (int64_t(p.source_width) * target_height > int64_t(p.source_height) * target_width) {
|
||||
p.crop_width = std::max(1, static_cast<int>(int64_t(p.source_height) * target_width / target_height));
|
||||
} else {
|
||||
p.crop_height = std::max(1, static_cast<int>(int64_t(p.source_width) * target_height / target_width));
|
||||
}
|
||||
if (p.crop_width > p.source_width || p.crop_height > p.source_height) {
|
||||
fail(label + ": crop exceeds source dimensions");
|
||||
return {};
|
||||
}
|
||||
p.x = anchor_offset(p.source_width - p.crop_width, anchor, true);
|
||||
p.y = anchor_offset(p.source_height - p.crop_height, anchor, false);
|
||||
} else if (mode == ImageResizeMode::FitPad) {
|
||||
double scale = std::min(double(target_width) / p.source_width, double(target_height) / p.source_height);
|
||||
p.resize_width = std::max(1, std::min(target_width, static_cast<int>(std::round(p.source_width * scale))));
|
||||
p.resize_height = std::max(1, std::min(target_height, static_cast<int>(std::round(p.source_height * scale))));
|
||||
p.pad_x = anchor_offset(target_width - p.resize_width, anchor, true);
|
||||
p.pad_y = anchor_offset(target_height - p.resize_height, anchor, false);
|
||||
}
|
||||
if (p.width <= 0 || p.height <= 0) {
|
||||
fail(label + ": invalid output dimensions");
|
||||
return {};
|
||||
}
|
||||
uint64_t max_pixels = std::min<uint64_t>(INT64_MAX, SIZE_MAX / sizeof(float)) / static_cast<uint64_t>(image.shape()[2]);
|
||||
if (uint64_t(p.width) * p.height > max_pixels || uint64_t(p.resize_width) * p.resize_height > max_pixels) {
|
||||
fail(label + ": image allocation size overflows");
|
||||
return {};
|
||||
}
|
||||
if (plan_out != nullptr)
|
||||
*plan_out = p;
|
||||
return apply_transform(image, options, p, label, default_filter);
|
||||
}
|
||||
|
||||
Tensor<float> ImagePreprocessor::preprocess_input(sd_image_t image, ImageTarget target, int index, int width, int height) {
|
||||
if (image.data == nullptr || image.width == 0 || image.height == 0 || image.width > INT_MAX || image.height > INT_MAX || image.channel < 1 || image.channel > 4) {
|
||||
fail(std::string(enum_name(target, image_targets)) + ": invalid input image");
|
||||
return {};
|
||||
}
|
||||
auto tensor = sd_image_to_tensor(image);
|
||||
if (target == ImageTarget::Mask && has_init_transform_) {
|
||||
auto options = resolve_options(target, index);
|
||||
if (image.width != init_transform_.source_width || image.height != init_transform_.source_height) {
|
||||
fail("mask and init source dimensions must match");
|
||||
return {};
|
||||
}
|
||||
bool geometry_override = options.count("width") || options.count("anchor") ||
|
||||
(options.count("mode") && options.at("mode") != "auto");
|
||||
if (geometry_override) {
|
||||
ImageTransform p;
|
||||
auto init_options = resolve_options(ImageTarget::Init, 0);
|
||||
ImageResizeMode default_mode = resolve_mode(init_options, ImageResizeMode::CropResize);
|
||||
auto result = apply_geometry(tensor, target, index, init_transform_.width, init_transform_.height, default_mode, ops::InterpolateMode::NearestExact, &p);
|
||||
if (result.empty())
|
||||
return {};
|
||||
const auto& q = init_transform_;
|
||||
if (p.x != q.x || p.y != q.y || p.crop_width != q.crop_width || p.crop_height != q.crop_height ||
|
||||
p.resize_width != q.resize_width || p.resize_height != q.resize_height || p.pad_x != q.pad_x || p.pad_y != q.pad_y || p.width != q.width || p.height != q.height) {
|
||||
fail("mask geometry conflicts with init; configure geometry on init and filter on mask");
|
||||
return {};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return apply_transform(tensor, options, init_transform_, "mask[0]", ops::InterpolateMode::NearestExact);
|
||||
}
|
||||
auto result = apply_geometry(tensor, target, index, width, height, width > 0 ? ImageResizeMode::CropResize : ImageResizeMode::None,
|
||||
target == ImageTarget::Mask ? ops::InterpolateMode::NearestExact : ops::InterpolateMode::Nearest,
|
||||
target == ImageTarget::Init ? &init_transform_ : nullptr);
|
||||
if (target == ImageTarget::Init)
|
||||
has_init_transform_ = !result.empty();
|
||||
return result;
|
||||
}
|
||||
|
||||
ImagePreprocessor::~ImagePreprocessor() {
|
||||
for (const auto& image : owned_images_)
|
||||
std::free(image.data);
|
||||
}
|
||||
|
||||
bool ImagePreprocessor::prepare_image(sd_image_t& image, ImageTarget target, int index, int width, int height) {
|
||||
if (image.data == nullptr)
|
||||
return true;
|
||||
auto options = resolve_options(target, index);
|
||||
bool canny = options.count("canny") && options.at("canny") == "true";
|
||||
auto tensor = preprocess_input(image, target, index, width, height);
|
||||
if (tensor.empty())
|
||||
return false;
|
||||
auto output = tensor_to_sd_image(tensor);
|
||||
if (output.data == nullptr)
|
||||
return fail("could not allocate input preprocessing buffer");
|
||||
owned_images_.push_back(output);
|
||||
if (canny && !preprocess_canny(output, 0.08f, 0.08f, 0.8f, 1.f, false))
|
||||
return fail("Canny preprocessing failed");
|
||||
image = output;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ImagePreprocessor::prepare_array(sd_image_t*& images, int count, ImageTarget target, std::vector<sd_image_t>& storage, int width, int height) {
|
||||
if (count < 0 || (count > 0 && images == nullptr))
|
||||
return fail(std::string("invalid image array: ") + enum_name(target, image_targets));
|
||||
if (count == 0)
|
||||
return true;
|
||||
storage.assign(images, images + count);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
if (storage[i].data == nullptr)
|
||||
return fail(std::string("empty image in array: ") + enum_name(target, image_targets));
|
||||
if (!prepare_image(storage[i], target, i, width, height))
|
||||
return false;
|
||||
}
|
||||
images = storage.data();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ImagePreprocessor::prepare_inputs(sd_img_gen_params_t& params, int width, int height) {
|
||||
if (prepared_)
|
||||
return fail("inputs have already been prepared");
|
||||
prepared_ = true;
|
||||
if (!valid_ || !validate_inputs(params))
|
||||
return false;
|
||||
if (!prepare_image(params.init_image, ImageTarget::Init, 0, width, height) ||
|
||||
!prepare_image(params.mask_image, ImageTarget::Mask, 0, width, height) ||
|
||||
!prepare_image(params.control_image, ImageTarget::Control, 0, width, height) ||
|
||||
!prepare_image(params.ip_adapter_image, ImageTarget::IPAdapter, 0, -1, -1) ||
|
||||
!prepare_array(params.ref_images, params.ref_images_count, ImageTarget::Ref, ref_images_) ||
|
||||
!prepare_array(params.pm_params.id_images, params.pm_params.id_images_count, ImageTarget::ID, id_images_))
|
||||
return false;
|
||||
params.image_preprocess = {};
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ImagePreprocessor::prepare_inputs(sd_vid_gen_params_t& params, int width, int height) {
|
||||
if (prepared_)
|
||||
return fail("inputs have already been prepared");
|
||||
prepared_ = true;
|
||||
if (!valid_ || !validate_inputs(params))
|
||||
return false;
|
||||
if (!prepare_image(params.init_image, ImageTarget::Init, 0, width, height) ||
|
||||
!prepare_image(params.end_image, ImageTarget::End, 0, width, height) ||
|
||||
!prepare_array(params.ref_images, params.ref_images_count, ImageTarget::Ref, ref_images_) ||
|
||||
!prepare_array(params.control_frames, params.control_frames_size, ImageTarget::ControlFrame, control_frames_, width, height))
|
||||
return false;
|
||||
params.image_preprocess = {};
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace sd
|
||||
@@ -0,0 +1,88 @@
|
||||
#ifndef __SD_RUNTIME_IMAGE_PREPROCESS_H__
|
||||
#define __SD_RUNTIME_IMAGE_PREPROCESS_H__
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "core/tensor.hpp"
|
||||
#include "stable-diffusion.h"
|
||||
|
||||
namespace sd {
|
||||
|
||||
enum class ImageTarget {
|
||||
Init,
|
||||
End,
|
||||
Mask,
|
||||
Control,
|
||||
Ref,
|
||||
IPAdapter,
|
||||
ID,
|
||||
ControlFrame,
|
||||
};
|
||||
|
||||
enum class ImageResizeMode {
|
||||
Auto,
|
||||
None,
|
||||
Stretch,
|
||||
Crop,
|
||||
CropResize,
|
||||
FitPad,
|
||||
};
|
||||
|
||||
struct ImageTransform {
|
||||
int source_width = 0;
|
||||
int source_height = 0;
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
int crop_width = 0;
|
||||
int crop_height = 0;
|
||||
int resize_width = 0;
|
||||
int resize_height = 0;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
int pad_x = 0;
|
||||
int pad_y = 0;
|
||||
};
|
||||
|
||||
struct ImagePreprocessRule {
|
||||
ImageTarget target = ImageTarget::Init;
|
||||
int index = -1;
|
||||
std::map<std::string, std::string> options;
|
||||
};
|
||||
|
||||
class ImagePreprocessor {
|
||||
std::vector<ImagePreprocessRule> rules_;
|
||||
mutable bool valid_ = true;
|
||||
ImageTransform init_transform_;
|
||||
bool has_init_transform_ = false;
|
||||
bool prepared_ = false;
|
||||
std::vector<sd_image_t> owned_images_;
|
||||
std::vector<sd_image_t> ref_images_;
|
||||
std::vector<sd_image_t> id_images_;
|
||||
std::vector<sd_image_t> control_frames_;
|
||||
|
||||
bool fail(const std::string& message) const;
|
||||
std::map<std::string, std::string> resolve_options(ImageTarget target, int index) const;
|
||||
Tensor<float> apply_transform(const Tensor<float>& image, const std::map<std::string, std::string>& options, ImageTransform plan, const std::string& label, ops::InterpolateMode default_filter) const;
|
||||
|
||||
bool prepare_image(sd_image_t& image, ImageTarget target, int index, int width, int height);
|
||||
bool prepare_array(sd_image_t*& images, int count, ImageTarget target, std::vector<sd_image_t>& storage, int width = -1, int height = -1);
|
||||
|
||||
public:
|
||||
explicit ImagePreprocessor(const char* rules = nullptr);
|
||||
~ImagePreprocessor();
|
||||
ImagePreprocessor(const ImagePreprocessor&) = delete;
|
||||
ImagePreprocessor& operator=(const ImagePreprocessor&) = delete;
|
||||
bool prepare_inputs(sd_img_gen_params_t& params, int width, int height);
|
||||
bool prepare_inputs(sd_vid_gen_params_t& params, int width, int height);
|
||||
bool is_valid() const { return valid_; }
|
||||
bool validate_inputs(const sd_img_gen_params_t& params) const;
|
||||
bool validate_inputs(const sd_vid_gen_params_t& params) const;
|
||||
Tensor<float> apply_geometry(const Tensor<float>& image, ImageTarget target, int index, int width, int height, ImageResizeMode default_mode = ImageResizeMode::Stretch, ops::InterpolateMode default_filter = ops::InterpolateMode::Nearest, ImageTransform* plan_out = nullptr) const;
|
||||
Tensor<float> preprocess_input(sd_image_t image, ImageTarget target, int index = 0, int width = -1, int height = -1);
|
||||
};
|
||||
|
||||
} // namespace sd
|
||||
|
||||
#endif // __SD_RUNTIME_IMAGE_PREPROCESS_H__
|
||||
+110
-14
@@ -4,6 +4,86 @@
|
||||
#include "core/tensor.hpp"
|
||||
#include "ggml.h"
|
||||
|
||||
// RGB is projected to [-1, 1]; alpha is projected directly to [0, 1].
|
||||
const float qwen21_latent_rgb_proj[64][3] = {
|
||||
{0.00860495522f, 0.01219501462f, -0.00321337196f},
|
||||
{0.01889233090f, 0.01246581216f, 0.01074959482f},
|
||||
{0.1255941446f, 0.1176879344f, -0.0332212352f},
|
||||
{0.0418238528f, 0.1043427754f, 0.0121666316f},
|
||||
{0.02025338f, 0.01453670296f, -0.000224336044f},
|
||||
{-0.01896720702f, -0.0206099030f, -0.0322728584f},
|
||||
{0.00438984796f, -0.01374969766f, 0.02849196f},
|
||||
{-0.0374495856f, -0.0286777126f, -0.0693192810f},
|
||||
{0.01511914734f, 0.0242979386f, 0.0553878870f},
|
||||
{-0.1138629518f, -0.020391466f, 0.001550520522f},
|
||||
{-0.0233650696f, -0.0417292018f, -0.0362361182f},
|
||||
{-0.0351603342f, -0.0243595924f, -0.00216261038f},
|
||||
{0.01093355288f, -0.0373466924f, 0.00241315350f},
|
||||
{0.01778704744f, -0.00401984678f, -0.0343259192f},
|
||||
{0.0486059334f, 0.0253144f, 0.0672564966f},
|
||||
{0.0309463558f, 0.0277963166f, 0.0520869622f},
|
||||
{0.0374485008f, 0.0551753676f, 0.0225853902f},
|
||||
{-0.0090809962f, -0.004756176f, 0.00636443612f},
|
||||
{-0.0270455652f, -0.0384966954f, -0.00905908082f},
|
||||
{-0.00553493756f, 0.01484553684f, -0.0211502468f},
|
||||
{0.01319502562f, 0.00948005666f, 0.0483789212f},
|
||||
{-0.00931847104f, -0.00276452734f, -0.01011985302f},
|
||||
{0.0180478258f, 0.01614954356f, -0.0209424690f},
|
||||
{-0.0214530434f, -0.00272961176f, 0.0217887476f},
|
||||
{-0.0636772304f, -0.0208893548f, 0.0479167742f},
|
||||
{-0.0250321236f, -0.0286715676f, 0.0530110146f},
|
||||
{-0.01853078078f, 0.01647272818f, -0.00207747588f},
|
||||
{0.0023101082f, 0.01228800748f, 0.01303505006f},
|
||||
{-0.01243671408f, -0.0258638728f, -0.0379116264f},
|
||||
{0.00598934710f, 0.00642563550f, -0.01234514304f},
|
||||
{-0.0296733996f, -0.0234698050f, 0.00060018212f},
|
||||
{-0.0322019498f, -0.0529200462f, -0.00344987414f},
|
||||
{-0.00205026458f, -0.00846599446f, 0.00455971038f},
|
||||
{-0.01082227064f, 0.0315661948f, -0.0677753362f},
|
||||
{0.0645553474f, 0.1109666998f, 0.0674744864f},
|
||||
{0.01036801108f, -0.00484841210f, -0.001529168474f},
|
||||
{0.01264353566f, 0.01548126338f, -0.00966374324f},
|
||||
{-0.0223892408f, -0.00871751526f, -0.000306421670f},
|
||||
{0.0271322742f, 0.03496524f, -0.0089692858f},
|
||||
{0.0512178672f, 0.0173080034f, 0.00804227746f},
|
||||
{0.01210987192f, 0.00758025926f, -0.00281712586f},
|
||||
{0.1897278390f, 0.1210261828f, 0.062603892f},
|
||||
{0.0208058822f, 0.00547548182f, 0.01262955638f},
|
||||
{0.00813332858f, 0.01015930914f, 0.01301771290f},
|
||||
{-0.000927236014f, -0.00152540594f, -0.00599213302f},
|
||||
{0.01663314616f, -0.00582789626f, 0.0163958132f},
|
||||
{-0.0252546342f, -0.0604193732f, -0.1606919922f},
|
||||
{-0.091722686f, -0.0409201224f, -0.0959576198f},
|
||||
{0.0282963112f, -0.01387223872f, -0.01648814464f},
|
||||
{0.0552316818f, 0.0967547788f, 0.0413586632f},
|
||||
{0.00922849292f, 0.00451467542f, -0.0529172378f},
|
||||
{0.0558600768f, 0.0122988308f, -0.01445942422f},
|
||||
{0.000210660902f, -0.01295958782f, -0.01804761764f},
|
||||
{0.0358136250f, -0.0472505970f, -0.1156405142f},
|
||||
{-0.0506390696f, -0.0471914842f, 0.0349791468f},
|
||||
{-0.0480143168f, 0.00628389868f, -0.0545163826f},
|
||||
{0.0315499582f, 0.0564846606f, -0.0430850488f},
|
||||
{-0.0362330316f, -0.01267788554f, 0.0061024772f},
|
||||
{0.0038627542f, 0.00911055916f, -0.00758526008f},
|
||||
{-0.0447103298f, -0.00835411408f, 0.01545872328f},
|
||||
{-0.015006738f, 0.00270612302f, -0.00784361356f},
|
||||
{-0.0221755048f, -0.0513344748f, -0.0475317424f},
|
||||
{-0.01036656294f, -0.00422146068f, -0.0213499052f},
|
||||
{0.01788952706f, 0.01191944190f, 0.0397205238f},
|
||||
};
|
||||
float qwen21_latent_rgb_bias[3] = {-0.043293118f, -0.02695978f, -0.11986706f};
|
||||
|
||||
const float qwen21_latent_alpha_proj[64] = {
|
||||
-0.0416241114f, -0.00678954612f, -0.0169095515f, -0.0230551401f, 0.0100882595f, 0.00655586802f, 0.0401166874f, -0.0055510216f,
|
||||
0.0224234441f, -0.0389640963f, -0.0114492163f, -0.00721128977f, -0.0029064082f, 0.0150300547f, -0.00321615308f, -0.0498856338f,
|
||||
-0.0215251401f, 0.0240220482f, 0.0117338008f, -0.0460420624f, 0.0387872889f, 0.0131517207f, 0.0147100836f, 0.0266985286f,
|
||||
0.0153097324f, -0.0418119757f, 0.0421013917f, 0.0401724499f, 0.00972515915f, 0.011718495f, 0.0117622291f, 0.0136505134f,
|
||||
-0.0350017363f, -0.0100692606f, -0.0131582529f, -0.00660639315f, 0.00253611396f, -0.0195736368f, -0.04240184f, 0.0321299262f,
|
||||
0.0106089414f, -0.0179845306f, -0.00806212708f, 0.0135889057f, -0.0157393098f, -0.0267791344f, 0.0109068534f, 0.0283931966f,
|
||||
-0.0435370078f, 0.00187883536f, -0.0108995378f, -0.0450757676f, -0.0699481501f, 0.0123562106f, -0.0222592249f, 0.0216155907f,
|
||||
0.0563755424f, -0.0073379912f, 0.0160012921f, 0.0411637742f, 0.0189607258f, -0.024025029f, -0.0161487905f, -0.016913203f};
|
||||
const float qwen21_latent_alpha_bias = 0.871268134f;
|
||||
|
||||
const float minimax_latent_rgb_proj[24][3] = {
|
||||
{0.19819857f, 0.11584999f, 0.07929777f},
|
||||
{-0.16047224f, -0.10601170f, -0.15996324f},
|
||||
@@ -324,7 +404,7 @@ const float sd_latent_rgb_proj[4][3] = {
|
||||
{-0.178022f, -0.200862f, -0.678514f}};
|
||||
float sd_latent_rgb_bias[3] = {-0.017478f, -0.055834f, -0.105825f};
|
||||
|
||||
void preview_latent_video(uint8_t* buffer, ggml_tensor* latents, const float (*latent_rgb_proj)[3], const float latent_rgb_bias[3], int patch_size) {
|
||||
void preview_latent_video(uint8_t* buffer, ggml_tensor* latents, const float (*latent_rgb_proj)[3], const float latent_rgb_bias[3], int patch_size, const float* latent_alpha_proj = nullptr, float latent_alpha_bias = 1.f) {
|
||||
size_t buffer_head = 0;
|
||||
|
||||
uint32_t latent_width = static_cast<uint32_t>(latents->ne[0]);
|
||||
@@ -338,7 +418,8 @@ void preview_latent_video(uint8_t* buffer, ggml_tensor* latents, const float (*l
|
||||
uint32_t rgb_width = latent_width * patch_size;
|
||||
uint32_t rgb_height = latent_height * patch_size;
|
||||
|
||||
uint32_t unpatched_dim = dim / (patch_size * patch_size);
|
||||
uint32_t unpatched_dim = dim / (patch_size * patch_size);
|
||||
const uint32_t output_channels = latent_alpha_proj != nullptr ? 4 : 3;
|
||||
|
||||
for (uint32_t k = 0; k < frames; k++) {
|
||||
for (uint32_t rgb_x = 0; rgb_x < rgb_width; rgb_x++) {
|
||||
@@ -356,13 +437,16 @@ void preview_latent_video(uint8_t* buffer, ggml_tensor* latents, const float (*l
|
||||
// should be incremented by 1 for each pixel
|
||||
size_t pixel_id = k * rgb_width * rgb_height + rgb_y * rgb_width + rgb_x;
|
||||
|
||||
float r = 0, g = 0, b = 0;
|
||||
float r = 0, g = 0, b = 0, a = 0;
|
||||
if (latent_rgb_proj != nullptr) {
|
||||
for (uint32_t d = 0; d < unpatched_dim; d++) {
|
||||
float value = *(float*)((char*)latents->data + latent_id + (d * patch_size * patch_size + channel_offset) * latents->nb[ggml_n_dims(latents) - 1]);
|
||||
r += value * latent_rgb_proj[d][0];
|
||||
g += value * latent_rgb_proj[d][1];
|
||||
b += value * latent_rgb_proj[d][2];
|
||||
if (latent_alpha_proj != nullptr) {
|
||||
a += value * latent_alpha_proj[d];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// interpret first 3 channels as RGB
|
||||
@@ -386,9 +470,13 @@ void preview_latent_video(uint8_t* buffer, ggml_tensor* latents, const float (*l
|
||||
g = g >= 0 ? g <= 1 ? g : 1 : 0;
|
||||
b = b >= 0 ? b <= 1 ? b : 1 : 0;
|
||||
|
||||
buffer[pixel_id * 3 + 0] = (uint8_t)(r * 255);
|
||||
buffer[pixel_id * 3 + 1] = (uint8_t)(g * 255);
|
||||
buffer[pixel_id * 3 + 2] = (uint8_t)(b * 255);
|
||||
buffer[pixel_id * output_channels + 0] = (uint8_t)(r * 255);
|
||||
buffer[pixel_id * output_channels + 1] = (uint8_t)(g * 255);
|
||||
buffer[pixel_id * output_channels + 2] = (uint8_t)(b * 255);
|
||||
if (latent_alpha_proj != nullptr) {
|
||||
a = std::min(1.0f, std::max(0.0f, a + latent_alpha_bias));
|
||||
buffer[pixel_id * output_channels + 3] = (uint8_t)(a * 255);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -398,16 +486,17 @@ static inline bool preview_latent_tensor_is_video(const sd::Tensor<float>& laten
|
||||
return latents.dim() == 5;
|
||||
}
|
||||
|
||||
void preview_latent_video(uint8_t* buffer, const sd::Tensor<float>& latents, const float (*latent_rgb_proj)[3], const float latent_rgb_bias[3], int patch_size) {
|
||||
void preview_latent_video(uint8_t* buffer, const sd::Tensor<float>& latents, const float (*latent_rgb_proj)[3], const float latent_rgb_bias[3], int patch_size, const float* latent_alpha_proj = nullptr, float latent_alpha_bias = 1.f) {
|
||||
uint32_t latent_width = static_cast<uint32_t>(latents.shape()[0]);
|
||||
uint32_t latent_height = static_cast<uint32_t>(latents.shape()[1]);
|
||||
bool is_video = preview_latent_tensor_is_video(latents);
|
||||
uint32_t frames = is_video ? static_cast<uint32_t>(latents.shape()[2]) : 1;
|
||||
uint32_t dim = is_video ? static_cast<uint32_t>(latents.shape()[3]) : static_cast<uint32_t>(latents.shape()[2]);
|
||||
|
||||
uint32_t rgb_width = latent_width * patch_size;
|
||||
uint32_t rgb_height = latent_height * patch_size;
|
||||
uint32_t unpatched_dim = dim / (patch_size * patch_size);
|
||||
uint32_t rgb_width = latent_width * patch_size;
|
||||
uint32_t rgb_height = latent_height * patch_size;
|
||||
uint32_t unpatched_dim = dim / (patch_size * patch_size);
|
||||
const uint32_t output_channels = latent_alpha_proj != nullptr ? 4 : 3;
|
||||
|
||||
for (uint32_t k = 0; k < frames; k++) {
|
||||
for (uint32_t rgb_x = 0; rgb_x < rgb_width; rgb_x++) {
|
||||
@@ -427,7 +516,7 @@ void preview_latent_video(uint8_t* buffer, const sd::Tensor<float>& latents, con
|
||||
: latents.values()[latent_x + latent_width * (latent_y + latent_height * latent_channel)];
|
||||
};
|
||||
|
||||
float r = 0.f, g = 0.f, b = 0.f;
|
||||
float r = 0.f, g = 0.f, b = 0.f, a = 0.f;
|
||||
if (latent_rgb_proj != nullptr) {
|
||||
for (uint32_t d = 0; d < unpatched_dim; d++) {
|
||||
uint32_t latent_channel = d * patch_size * patch_size + channel_offset;
|
||||
@@ -435,6 +524,9 @@ void preview_latent_video(uint8_t* buffer, const sd::Tensor<float>& latents, con
|
||||
r += value * latent_rgb_proj[d][0];
|
||||
g += value * latent_rgb_proj[d][1];
|
||||
b += value * latent_rgb_proj[d][2];
|
||||
if (latent_alpha_proj != nullptr) {
|
||||
a += value * latent_alpha_proj[d];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
r = latent_value(0);
|
||||
@@ -450,9 +542,13 @@ void preview_latent_video(uint8_t* buffer, const sd::Tensor<float>& latents, con
|
||||
g = std::min(1.0f, std::max(0.0f, g * .5f + .5f));
|
||||
b = std::min(1.0f, std::max(0.0f, b * .5f + .5f));
|
||||
|
||||
buffer[pixel_id * 3 + 0] = (uint8_t)(r * 255);
|
||||
buffer[pixel_id * 3 + 1] = (uint8_t)(g * 255);
|
||||
buffer[pixel_id * 3 + 2] = (uint8_t)(b * 255);
|
||||
buffer[pixel_id * output_channels + 0] = (uint8_t)(r * 255);
|
||||
buffer[pixel_id * output_channels + 1] = (uint8_t)(g * 255);
|
||||
buffer[pixel_id * output_channels + 2] = (uint8_t)(b * 255);
|
||||
if (latent_alpha_proj != nullptr) {
|
||||
a = std::min(1.0f, std::max(0.0f, a + latent_alpha_bias));
|
||||
buffer[pixel_id * output_channels + 3] = (uint8_t)(a * 255);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,16 +165,18 @@ static inline sd::Tensor<float> convolve_tensor(const sd::Tensor<float>& input,
|
||||
return output;
|
||||
}
|
||||
|
||||
static inline sd::Tensor<float> grayscale_tensor(const sd::Tensor<float>& rgb_img) {
|
||||
GGML_ASSERT(rgb_img.dim() == 4);
|
||||
GGML_ASSERT(rgb_img.shape()[2] >= 3);
|
||||
sd::Tensor<float> grayscale({rgb_img.shape()[0], rgb_img.shape()[1], 1, rgb_img.shape()[3]});
|
||||
for (int64_t iy = 0; iy < rgb_img.shape()[1]; ++iy) {
|
||||
for (int64_t ix = 0; ix < rgb_img.shape()[0]; ++ix) {
|
||||
float r = preprocessing_get_4d(rgb_img, ix, iy, 0, 0);
|
||||
float g = preprocessing_get_4d(rgb_img, ix, iy, 1, 0);
|
||||
float b = preprocessing_get_4d(rgb_img, ix, iy, 2, 0);
|
||||
float gray = 0.2989f * r + 0.5870f * g + 0.1140f * b;
|
||||
static inline sd::Tensor<float> grayscale_tensor(const sd::Tensor<float>& image) {
|
||||
GGML_ASSERT(image.dim() == 4);
|
||||
GGML_ASSERT(image.shape()[2] >= 1);
|
||||
sd::Tensor<float> grayscale({image.shape()[0], image.shape()[1], 1, image.shape()[3]});
|
||||
for (int64_t iy = 0; iy < image.shape()[1]; ++iy) {
|
||||
for (int64_t ix = 0; ix < image.shape()[0]; ++ix) {
|
||||
float gray = preprocessing_get_4d(image, ix, iy, 0, 0);
|
||||
if (image.shape()[2] >= 3) {
|
||||
float g = preprocessing_get_4d(image, ix, iy, 1, 0);
|
||||
float b = preprocessing_get_4d(image, ix, iy, 2, 0);
|
||||
gray = 0.2989f * gray + 0.5870f * g + 0.1140f * b;
|
||||
}
|
||||
preprocessing_set_4d(grayscale, gray, ix, iy, 0, 0);
|
||||
}
|
||||
}
|
||||
@@ -317,11 +319,12 @@ bool preprocess_canny(sd_image_t img, float high_threshold, float low_threshold,
|
||||
image_gray = non_max_supression(G, theta);
|
||||
threshold_hystersis(&image_gray, high_threshold, low_threshold, weak, strong);
|
||||
|
||||
const uint32_t color_channels = img.channel == 2 || img.channel == 4 ? img.channel - 1 : img.channel;
|
||||
for (uint32_t iy = 0; iy < img.height; ++iy) {
|
||||
for (uint32_t ix = 0; ix < img.width; ++ix) {
|
||||
float gray = preprocessing_get_4d(image_gray, ix, iy, 0, 0);
|
||||
gray = inverse ? 1.0f - gray : gray;
|
||||
for (uint32_t c = 0; c < img.channel; ++c) {
|
||||
for (uint32_t c = 0; c < color_channels; ++c) {
|
||||
preprocessing_set_4d(image, gray, ix, iy, c, 0);
|
||||
}
|
||||
}
|
||||
|
||||
+10
-15
@@ -326,6 +326,7 @@ void sd_hires_params_init(sd_hires_params_t* hires_params) {
|
||||
void sd_ctx_params_init(sd_ctx_params_t* sd_ctx_params) {
|
||||
*sd_ctx_params = {};
|
||||
sd_ctx_params->n_threads = sd_get_num_physical_cores();
|
||||
sd_ctx_params->conditioning_cache_size = 4;
|
||||
sd_ctx_params->wtype = SD_TYPE_COUNT;
|
||||
sd_ctx_params->rng_type = CUDA_RNG;
|
||||
sd_ctx_params->sampler_rng_type = RNG_TYPE_COUNT;
|
||||
@@ -378,6 +379,7 @@ char* sd_ctx_params_to_str(const sd_ctx_params_t* sd_ctx_params) {
|
||||
"pulid_weights_path: %s\n"
|
||||
"tensor_type_rules: %s\n"
|
||||
"n_threads: %d\n"
|
||||
"conditioning_cache_size: %d\n"
|
||||
"wtype: %s\n"
|
||||
"rng_type: %s\n"
|
||||
"sampler_rng_type: %s\n"
|
||||
@@ -418,6 +420,7 @@ char* sd_ctx_params_to_str(const sd_ctx_params_t* sd_ctx_params) {
|
||||
SAFE_STR(sd_ctx_params->pulid_weights_path),
|
||||
SAFE_STR(sd_ctx_params->tensor_type_rules),
|
||||
sd_ctx_params->n_threads,
|
||||
sd_ctx_params->conditioning_cache_size,
|
||||
sd_type_name(sd_ctx_params->wtype),
|
||||
sd_rng_type_name(sd_ctx_params->rng_type),
|
||||
sd_rng_type_name(sd_ctx_params->sampler_rng_type),
|
||||
@@ -751,28 +754,20 @@ SD_API bool generate_video(sd_ctx_t* sd_ctx,
|
||||
int* num_frames_out,
|
||||
sd_audio_t** audio_out,
|
||||
int* fps_out) {
|
||||
if (sd_ctx == nullptr || sd_ctx->sd == nullptr || sd_vid_gen_params == nullptr) {
|
||||
if (fps_out != nullptr) {
|
||||
*fps_out = 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (frames_out != nullptr) {
|
||||
if (frames_out != nullptr)
|
||||
*frames_out = nullptr;
|
||||
}
|
||||
if (audio_out != nullptr) {
|
||||
if (audio_out != nullptr)
|
||||
*audio_out = nullptr;
|
||||
}
|
||||
if (num_frames_out != nullptr) {
|
||||
if (num_frames_out != nullptr)
|
||||
*num_frames_out = 0;
|
||||
if (fps_out != nullptr)
|
||||
*fps_out = 0;
|
||||
if (sd_ctx == nullptr || sd_ctx->sd == nullptr || sd_vid_gen_params == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
StableDiffusionGGML::ExecutionScope execution(*sd_ctx->sd);
|
||||
if (!execution.ready) {
|
||||
if (fps_out != nullptr) {
|
||||
*fps_out = 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
+22
-2
@@ -111,10 +111,22 @@ bool UpscalerGGML::load_from_file(const std::string& esrgan_path,
|
||||
|
||||
sd::Tensor<float> UpscalerGGML::upscale_tensor(const sd::Tensor<float>& input_tensor) {
|
||||
sd::ParallelScope tensor_scope(&tensor_executor);
|
||||
if (input_tensor.empty() || input_tensor.dim() != 4 ||
|
||||
(input_tensor.shape()[2] != 3 && input_tensor.shape()[2] != 4)) {
|
||||
LOG_ERROR("esrgan expects a 4D RGB or RGBA image tensor");
|
||||
return {};
|
||||
}
|
||||
|
||||
const bool has_alpha = input_tensor.shape()[2] == 4;
|
||||
sd::Tensor<float> rgb;
|
||||
if (has_alpha) {
|
||||
rgb = sd::ops::slice(input_tensor, 2, 0, 3);
|
||||
}
|
||||
const sd::Tensor<float>& model_input = has_alpha ? rgb : input_tensor;
|
||||
sd::Tensor<float> upscaled;
|
||||
const int scale = esrgan_upscaler->config.scale;
|
||||
if (tile_size <= 0 || (input_tensor.shape()[0] <= tile_size && input_tensor.shape()[1] <= tile_size)) {
|
||||
upscaled = esrgan_upscaler->compute(n_threads, input_tensor);
|
||||
upscaled = esrgan_upscaler->compute(n_threads, model_input);
|
||||
} else {
|
||||
auto on_processing = [&](const sd::Tensor<float>& input_tile) -> sd::Tensor<float> {
|
||||
auto output_tile = esrgan_upscaler->compute(n_threads, input_tile);
|
||||
@@ -125,7 +137,7 @@ sd::Tensor<float> UpscalerGGML::upscale_tensor(const sd::Tensor<float>& input_te
|
||||
return output_tile;
|
||||
};
|
||||
|
||||
upscaled = process_tiles_2d(input_tensor,
|
||||
upscaled = process_tiles_2d(model_input,
|
||||
static_cast<int>(input_tensor.shape()[0] * scale),
|
||||
static_cast<int>(input_tensor.shape()[1] * scale),
|
||||
scale,
|
||||
@@ -141,6 +153,14 @@ sd::Tensor<float> UpscalerGGML::upscale_tensor(const sd::Tensor<float>& input_te
|
||||
LOG_ERROR("esrgan compute failed");
|
||||
return {};
|
||||
}
|
||||
if (has_alpha) {
|
||||
auto alpha = sd::ops::slice(input_tensor, 2, 3, 4);
|
||||
auto alpha_shape = alpha.shape();
|
||||
alpha_shape[0] = upscaled.shape()[0];
|
||||
alpha_shape[1] = upscaled.shape()[1];
|
||||
alpha = sd::ops::interpolate(alpha, alpha_shape, sd::ops::InterpolateMode::Bilinear);
|
||||
upscaled = sd::ops::concat(upscaled, alpha, 2);
|
||||
}
|
||||
return upscaled;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user