mirror of
https://github.com/leejet/stable-diffusion.cpp.git
synced 2026-10-12 04:50:34 +08:00
Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2988060a43 | ||
|
|
1b0ba10893 | ||
|
|
b7a74e68d7 | ||
|
|
05845fde41 | ||
|
|
0e1d733b7a | ||
|
|
ce95c09ce1 | ||
|
|
f89d9b13d7 | ||
|
|
f2f168fa70 | ||
|
|
7867f6da4a | ||
|
|
228c707fde | ||
|
|
e16d26a96a | ||
|
|
a4a966990d | ||
|
|
a1ded76da5 | ||
|
|
8cc5ad8f1c | ||
|
|
4a892293ac | ||
|
|
fd43b542a6 | ||
|
|
0963357214 | ||
|
|
1da55ebde8 | ||
|
|
fb8ee71f55 | ||
|
|
96f67fe631 | ||
|
|
203a882a28 |
@@ -451,8 +451,8 @@ jobs:
|
||||
runs-on: windows-2022
|
||||
|
||||
env:
|
||||
ROCM_VERSION: "7.14.0"
|
||||
GPU_TARGETS: "gfx1010;gfx1011;gfx1012;gfx1030;gfx1031;gfx1032;gfx1033;gfx1034;gfx1035;gfx1036;gfx1100;gfx1101;gfx1102;gfx1103;gfx1150;gfx1151;gfx1152;gfx1153;gfx1200;gfx1201"
|
||||
ROCM_VERSION: "10.1.0"
|
||||
gpu_targets: "gfx1010;gfx1011;gfx1012;gfx1030;gfx1031;gfx1032;gfx1033;gfx1034;gfx1035;gfx1036;gfx1100;gfx1101;gfx1102;gfx1103;gfx1150;gfx1151;gfx1152;gfx1153;gfx1200;gfx1201"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@@ -498,7 +498,7 @@ jobs:
|
||||
|
||||
write-host "Installing ROCm wheels for multi-arch support"
|
||||
# Install ROCm wheels for multi-arch support (this may take several minutes)
|
||||
python -m pip install --index-url https://repo.amd.com/rocm/whl-multi-arch/ "rocm[libraries,devel]==${{env.ROCM_VERSION}}"
|
||||
python -m pip install --index-url https://stable.repo.amd.com/rocm/whl-next/ "rocm[libraries,devel]==${{env.ROCM_VERSION}}"
|
||||
|
||||
# Pre-expand the devel tree so it is included in the cache
|
||||
write-host "Initializing ROCm devel tree"
|
||||
@@ -594,9 +594,9 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- ROCM_VERSION: "7.14.0"
|
||||
- ROCM_VERSION: "10.1.0"
|
||||
gpu_targets: "gfx900;gfx906;gfx908;gfx90a;gfx90c;gfx942;gfx950;gfx1010;gfx1011;gfx1012;gfx1030;gfx1031;gfx1032;gfx1033;gfx1034;gfx1035;gfx1036;gfx1100;gfx1101;gfx1102;gfx1103;gfx1150;gfx1151;gfx1152;gfx1153;gfx1200;gfx1201"
|
||||
build: x64
|
||||
build: 'x64'
|
||||
|
||||
steps:
|
||||
- name: Clone
|
||||
@@ -610,6 +610,16 @@ jobs:
|
||||
with:
|
||||
key: ubuntu-rocm-cmake-${{ matrix.ROCM_VERSION }}-${{ matrix.build }}
|
||||
evict-old-files: 1d
|
||||
max-size: "2G"
|
||||
|
||||
- name: Tune ccache for reinstalled ROCm toolchain
|
||||
run: |
|
||||
# ROCm is pip-installed fresh each run, so the clang binary's mtime
|
||||
# changes every time. With the default compiler_check=mtime that
|
||||
# invalidates the cache; hash compiler contents instead so warm
|
||||
# builds hit.
|
||||
ccache --set-config=compiler_check=content
|
||||
ccache --set-config=sloppiness=time_macros,include_file_mtime,include_file_ctime
|
||||
|
||||
- name: Dependencies
|
||||
id: depends
|
||||
@@ -646,7 +656,7 @@ jobs:
|
||||
# libraries = HIP runtime and CMake configs needed for linking
|
||||
# devel = compilers, headers, static libs
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install --index-url https://repo.amd.com/rocm/whl-multi-arch/ "rocm[libraries,devel]==${{matrix.ROCM_VERSION}}"
|
||||
python -m pip install --index-url https://stable.repo.amd.com/rocm/whl-next/ "rocm[libraries,devel]==${{matrix.ROCM_VERSION}}"
|
||||
|
||||
# Get ROCm installation paths using the rocm-sdk CLI tool
|
||||
ROCM_PATH=$(rocm-sdk path --root)
|
||||
|
||||
@@ -29,6 +29,10 @@ PRs should include:
|
||||
- Linked issue/PR context when applicable.
|
||||
- Screenshots or sample outputs for UI/visual behavior changes.
|
||||
|
||||
## README UI Listings
|
||||
|
||||
The [UIs list in the README](README.md#uis) only includes open-source projects that use `stable-diffusion.cpp` as a backend. Do not submit PRs to add closed-source UIs.
|
||||
|
||||
## Code Style
|
||||
|
||||
Format code according to the repository style before submitting changes.
|
||||
|
||||
@@ -67,6 +67,7 @@ API and command-line option may change frequently.***
|
||||
- [LLaDA-Image](./docs/llada_image.md)
|
||||
- [Ming-Image Design](./docs/ming_image.md)
|
||||
- [PixArt](./docs/pixart.md)
|
||||
- [Iris-3B](./docs/iris.md)
|
||||
- [Image Edit Models](./docs/edit.md)
|
||||
- [FLUX.1-Kontext-dev](./docs/kontext.md)
|
||||
- [Qwen Image Edit series](./docs/qwen_image_edit.md)
|
||||
@@ -182,7 +183,6 @@ These projects wrap `stable-diffusion.cpp` for easier use in other languages/fra
|
||||
These projects use `stable-diffusion.cpp` as a backend for their image generation.
|
||||
|
||||
- [GIMP Plugins](https://github.com/themanyone/gimp-plugins)
|
||||
- [Jellybox](https://jellybox.com)
|
||||
- [Stable Diffusion GUI](https://github.com/fszontagh/sd.cpp.gui.wx)
|
||||
- [Stable Diffusion CLI-GUI](https://github.com/piallai/stable-diffusion.cpp)
|
||||
- [Local Diffusion](https://github.com/rmatif/Local-Diffusion)
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
@@ -0,0 +1,24 @@
|
||||
# How to Use
|
||||
|
||||
Iris-3B generates images directly in pixel space and uses Qwen3-VL-4B-Instruct as the text encoder. No VAE is required.
|
||||
|
||||
## Download weights
|
||||
|
||||
- Download Iris-3B
|
||||
- safetensors: https://huggingface.co/speridlabs/iris-3b/tree/main (`model.safetensors` in the root directory)
|
||||
- Download Qwen3-VL-4B-Instruct
|
||||
- safetensors: https://huggingface.co/Comfy-Org/Krea-2/tree/main/text_encoders
|
||||
- gguf: https://huggingface.co/Qwen/Qwen3-VL-4B-Instruct-GGUF/tree/main
|
||||
|
||||
## Examples
|
||||
|
||||
```
|
||||
.\bin\Release\sd-cli.exe --diffusion-model ..\models\diffusion_models\iris-3b.safetensors --llm ..\models\text_encoders\Qwen3-VL-4B-Instruct-Q4_K_M.gguf -p "a lovely cat" --cfg-scale 3 -H 1024 -W 1024 --diffusion-fa
|
||||
```
|
||||
|
||||
<img width="256" alt="iris-3b example" src="../assets/iris/example.png" />
|
||||
|
||||
## Notes
|
||||
|
||||
- Width and height must be multiples of 16. Do not pass `--vae`.
|
||||
- Captions are limited to 300 tokens including the assistant-turn suffix. Positive and negative prompts use the same template.
|
||||
@@ -21,6 +21,16 @@ CPU fallback. It excludes weights and cache buffers. Within a runner lifecycle,
|
||||
the summary is printed only on the first graph or when backend capacities or the
|
||||
segment count change.
|
||||
|
||||
## Run conditional and unconditional CFG in one batched UNet forward.
|
||||
|
||||
For UNet models, the conditional and unconditional guidance branches are
|
||||
concatenated into a single batch of two and run through one UNet forward per
|
||||
step instead of two separate forwards. This is enabled by default whenever the
|
||||
run qualifies for it.
|
||||
|
||||
Use `--batched-cfg off` to force separate conditional and unconditional
|
||||
forwards.
|
||||
|
||||
## Use VAE tiling to reduce encode and decode memory usage.
|
||||
|
||||
`--vae-tiling` enables spatial tiling for both VAE encoding and decoding. The
|
||||
|
||||
@@ -16,6 +16,8 @@ Qwen Image 2.1 supports text-to-image generation and image editing, using Qwen3-
|
||||
|
||||
Use `qwen_image_2.1_vae_bf16.safetensors` with this model. The earlier Qwen Image and Wan 2.2 VAE weights are not interchangeable with the Qwen Image 2.1 VAE weights.
|
||||
|
||||
For faster encoding, decoding, or previews, see [TAEQI2.1](taesd.md#qwen-image-21-taeqi21).
|
||||
|
||||
## Examples
|
||||
|
||||
Run the following commands from the build directory. Use image dimensions divisible by 32. The resolution-dependent flow schedule is selected automatically.
|
||||
|
||||
@@ -37,3 +37,15 @@ sd.cpp also supports [TAEHV](https://github.com/madebyollin/taehv) (#937), which
|
||||
```
|
||||
|
||||
Then simply replace the `--vae xxx.safetensors` with `--tae xxx.safetensors` in the commands. If it still out of VRAM, add `--vae-conv-direct` to your command though might be slower.
|
||||
|
||||
### Qwen Image 2.1 (TAEQI2.1)
|
||||
|
||||
For Qwen Image 2.1, use [taeqi2_1](https://github.com/madebyollin/taesd), which supports 64-channel latents, 16x spatial scaling, and RGBA images.
|
||||
|
||||
Download the official [safetensors weights](https://huggingface.co/madebyollin/taeqi2_1/blob/main/taeqi2_1.safetensors) directly; no conversion is needed:
|
||||
|
||||
```bash
|
||||
curl -L -o taeqi2_1.safetensors https://huggingface.co/madebyollin/taeqi2_1/resolve/main/taeqi2_1.safetensors
|
||||
```
|
||||
|
||||
Replace `--vae PATH` with `--taesd taeqi2_1.safetensors` in the [Qwen Image 2.1 examples](qwen_image_2.1.md) to use it for encoding and decoding. For previews only, keep `--vae PATH` and add `--taesd taeqi2_1.safetensors --taesd-preview-only --preview tae`.
|
||||
|
||||
+28
-18
@@ -378,6 +378,23 @@ static int parse_scale_override(int argc, const char** argv, int index, float& s
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int parse_on_off_arg(int argc, const char** argv, int index, const char* option, bool& value) {
|
||||
if (++index >= argc) {
|
||||
LOG_ERROR("%s requires 'on' or 'off'", option);
|
||||
return -1;
|
||||
}
|
||||
const std::string arg = argv[index];
|
||||
if (arg == "on") {
|
||||
value = true;
|
||||
} else if (arg == "off") {
|
||||
value = false;
|
||||
} else {
|
||||
LOG_ERROR("invalid %s value '%s'; expected 'on' or 'off'", option, argv[index]);
|
||||
return -1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
ArgOptions SDContextParams::get_options() {
|
||||
ArgOptions options;
|
||||
options.string_options = {
|
||||
@@ -637,23 +654,6 @@ ArgOptions SDContextParams::get_options() {
|
||||
true, &vae_conv_direct},
|
||||
};
|
||||
|
||||
auto on_auto_fit_arg = [&](int argc, const char** argv, int index) {
|
||||
if (++index >= argc) {
|
||||
LOG_ERROR("--auto-fit requires 'on' or 'off'");
|
||||
return -1;
|
||||
}
|
||||
const std::string arg = argv[index];
|
||||
if (arg == "on") {
|
||||
auto_fit = true;
|
||||
} else if (arg == "off") {
|
||||
auto_fit = false;
|
||||
} else {
|
||||
LOG_ERROR("invalid --auto-fit value '%s'; expected 'on' or 'off'", argv[index]);
|
||||
return -1;
|
||||
}
|
||||
return 1;
|
||||
};
|
||||
|
||||
auto on_type_arg = [&](int argc, const char** argv, int index) {
|
||||
if (++index >= argc) {
|
||||
return -1;
|
||||
@@ -742,7 +742,15 @@ ArgOptions SDContextParams::get_options() {
|
||||
"on|off (default: on). Preserve --backend (otherwise select one GPU) and place weights on the compute GPU, "
|
||||
"RAM, another GPU, or disk in that order, according to available memory (--max-vram limits GPU budgets). "
|
||||
"Disabled by explicit --params-backend; uses automatic graph segmentation when needed",
|
||||
on_auto_fit_arg},
|
||||
[this](int argc, const char** argv, int index) {
|
||||
return parse_on_off_arg(argc, argv, index, "--auto-fit", auto_fit);
|
||||
}},
|
||||
{"",
|
||||
"--batched-cfg",
|
||||
"on|off (default: on). Run the conditional and unconditional CFG branches in one batched UNet forward when supported",
|
||||
[this](int argc, const char** argv, int index) {
|
||||
return parse_on_off_arg(argc, argv, index, "--batched-cfg", batched_cfg);
|
||||
}},
|
||||
{"",
|
||||
"--type",
|
||||
"weight type (examples: f32, f16, q4_0, q4_1, q5_0, q5_1, q8_0, q2_K, q3_K, q4_K). "
|
||||
@@ -940,6 +948,7 @@ std::string SDContextParams::to_string() const {
|
||||
<< " max_vram: \"" << max_vram << "\",\n"
|
||||
<< " disable_prefetch: " << (disable_prefetch ? "true" : "false") << ",\n"
|
||||
<< " disable_segmented_compute: " << (disable_segmented_compute ? "true" : "false") << ",\n"
|
||||
<< " batched_cfg: " << (batched_cfg ? "true" : "false") << ",\n"
|
||||
<< " eager_load: " << (eager_load ? "true" : "false") << ",\n"
|
||||
<< " backend: \"" << backend << "\",\n"
|
||||
<< " params_backend: \"" << params_backend << "\",\n"
|
||||
@@ -1022,6 +1031,7 @@ sd_ctx_params_t SDContextParams::to_sd_ctx_params_t(bool taesd_preview) {
|
||||
sd_ctx_params.max_vram = max_vram.c_str();
|
||||
sd_ctx_params.disable_prefetch = disable_prefetch;
|
||||
sd_ctx_params.disable_segmented_compute = disable_segmented_compute;
|
||||
sd_ctx_params.batched_cfg = batched_cfg;
|
||||
sd_ctx_params.eager_load = eager_load;
|
||||
sd_ctx_params.backend = effective_backend.c_str();
|
||||
sd_ctx_params.params_backend = effective_params_backend.c_str();
|
||||
|
||||
@@ -157,6 +157,7 @@ struct SDContextParams {
|
||||
bool disable_prefetch = false;
|
||||
bool disable_segmented_compute = false;
|
||||
bool eager_load = false;
|
||||
bool batched_cfg = true;
|
||||
std::string backend;
|
||||
std::string params_backend;
|
||||
std::string split_mode;
|
||||
|
||||
+18
-21
@@ -91,34 +91,34 @@ void log_print(enum sd_log_level_t level, const char* log, sd_log_level_t min_le
|
||||
switch (level) {
|
||||
case SD_LOG_DEBUG:
|
||||
tag_color = 37;
|
||||
level_str = "DEBUG";
|
||||
level_str = "D";
|
||||
break;
|
||||
case SD_LOG_VERBOSE:
|
||||
tag_color = 37;
|
||||
level_str = "VERBOSE";
|
||||
level_str = "V";
|
||||
break;
|
||||
case SD_LOG_INFO:
|
||||
tag_color = 34;
|
||||
level_str = "INFO";
|
||||
level_str = "I";
|
||||
break;
|
||||
case SD_LOG_WARN:
|
||||
tag_color = 35;
|
||||
level_str = "WARN";
|
||||
level_str = "W";
|
||||
break;
|
||||
case SD_LOG_ERROR:
|
||||
tag_color = 31;
|
||||
level_str = "ERROR";
|
||||
level_str = "E";
|
||||
break;
|
||||
default:
|
||||
tag_color = 33;
|
||||
level_str = "?????";
|
||||
level_str = "?";
|
||||
break;
|
||||
}
|
||||
|
||||
if (color) {
|
||||
fprintf(out_stream, "\033[%d;1m[%-7s]\033[0m ", tag_color, level_str);
|
||||
fprintf(out_stream, "\033[%d;1m[%s]\033[0m ", tag_color, level_str);
|
||||
} else {
|
||||
fprintf(out_stream, "[%-7s] ", level_str);
|
||||
fprintf(out_stream, "[%s] ", level_str);
|
||||
}
|
||||
fflush(out_stream);
|
||||
print_utf8(out_stream, log);
|
||||
@@ -131,18 +131,15 @@ void example_log_printf(sd_log_level_t level, const char* file, int line, const
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
|
||||
static char log_buffer[LOG_BUFFER_SIZE + 1];
|
||||
int written = snprintf(log_buffer, LOG_BUFFER_SIZE, "%s:%-4d - ", sd_basename(file).c_str(), line);
|
||||
|
||||
if (written >= 0 && written < static_cast<int>(LOG_BUFFER_SIZE)) {
|
||||
vsnprintf(log_buffer + written, LOG_BUFFER_SIZE - written, format, args);
|
||||
}
|
||||
size_t len = strlen(log_buffer);
|
||||
if (len == 0 || log_buffer[len - 1] != '\n') {
|
||||
strncat(log_buffer, "\n", LOG_BUFFER_SIZE - len);
|
||||
}
|
||||
|
||||
log_print(level, log_buffer, log_level, log_color);
|
||||
|
||||
char log_buffer[LOG_BUFFER_SIZE + 1] = {};
|
||||
vsnprintf(log_buffer, sizeof(log_buffer), format, args);
|
||||
va_end(args);
|
||||
|
||||
std::string message = log_buffer;
|
||||
while (!message.empty() && (message.back() == '\n' || message.back() == '\r')) {
|
||||
message.pop_back();
|
||||
}
|
||||
message += " --- " + sd_basename(file) + ":" + std::to_string(line) + '\n';
|
||||
|
||||
log_print(level, message.c_str(), log_level, log_color);
|
||||
}
|
||||
|
||||
+68
-1
@@ -37,6 +37,7 @@ Current generation-related endpoints include:
|
||||
|
||||
- `POST /sdapi/v1/txt2img`
|
||||
- `POST /sdapi/v1/img2img`
|
||||
- `GET /sdapi/v1/progress`
|
||||
- `GET /sdapi/v1/loras`
|
||||
- `GET /sdapi/v1/upscalers`
|
||||
- `GET /sdapi/v1/latent-upscale-modes`
|
||||
@@ -278,6 +279,26 @@ Response fields:
|
||||
| `parameters` | `object` | Echo of the parsed outer request body |
|
||||
| `info` | `string` | Currently empty string |
|
||||
|
||||
#### `GET /sdapi/v1/progress`
|
||||
|
||||
Poll this endpoint while a synchronous SDAPI generation request is running.
|
||||
An optional `id_task` query parameter selects the task with the same `id_task`
|
||||
provided in the generation request (default: `"sdapi"`). Set
|
||||
`skip_current_image=true` or `1` to omit the preview image.
|
||||
|
||||
`current_image` contains the latest base64 JPEG latent projection, or `null`
|
||||
when unavailable. `state.sampling_step` is the positive logical step and
|
||||
`state.sampling_steps` is the actual step count for the current sampling pass,
|
||||
including schedule and img2img strength adjustments. `state.job_no` is the
|
||||
zero-based pass index; `state.job_count` includes one pass per batch image and
|
||||
an additional pass per image when highres fix is enabled. `progress` weights
|
||||
these passes equally. Sampling progress can reach `1` before final decoding
|
||||
finishes; `eta_relative` is currently always `0`.
|
||||
|
||||
When no SDAPI task is active, or `id_task` does not match, the endpoint returns
|
||||
`progress=0`, `state.job=""`, zero job/step counts, and `current_image=null`.
|
||||
Completed and failed requests do not retain an active preview.
|
||||
|
||||
#### Discovery / Compatibility Endpoints
|
||||
|
||||
Currently exposed:
|
||||
@@ -410,6 +431,20 @@ Field types:
|
||||
| `queue_position` | `integer` |
|
||||
| `result` | `object \| null` |
|
||||
| `error` | `object \| null` |
|
||||
| `preview` | `object \| null` |
|
||||
|
||||
`preview` sub-fields:
|
||||
|
||||
| Field | Type | Notes |
|
||||
| --- | --- | --- |
|
||||
| `preview.pass` | `integer` | Sampling pass number, starting at 1 within the job |
|
||||
| `preview.step` | `integer` | Positive logical sample step within this pass |
|
||||
| `preview.total_steps` | `integer` | Actual sample steps in this pass, after schedule and strength adjustments |
|
||||
| `preview.b64_json` | `string` | Base64-encoded PNG of the current preview frame |
|
||||
|
||||
The preview is updated in place as sampling progresses; poll `GET /sdcpp/v1/jobs/{id}` to retrieve the latest frame. Only the most recent preview is retained.
|
||||
|
||||
`step / total_steps` describes the current sampling pass, not overall job completion. A new batch item, high/low-noise stage, or highres pass increments `pass` and restarts `step`. Samplers with multiple denoiser evaluations per logical step may update a preview more than once at the same step. The frame and its pass/step metadata are published together.
|
||||
|
||||
### Endpoints
|
||||
|
||||
@@ -437,6 +472,7 @@ Top-level fields:
|
||||
| `loras` | `array<object>` | Available LoRA entries |
|
||||
| `upscalers` | `array<object>` | Available highres upscalers, built-in and model-backed |
|
||||
| `upscale` | `boolean` | Whether a compatible RGB ESRGAN model is available for `POST /sdcpp/v1/upscale` |
|
||||
| `preview_modes` | `array<string>` | Available preview modes, e.g. `["none", "proj", "tae", "vae"]` |
|
||||
| `limits` | `object` | Shared queue and size limits |
|
||||
|
||||
`model`
|
||||
@@ -598,6 +634,7 @@ Fields returned in `features_by_mode.img_gen`:
|
||||
- `cache`
|
||||
- `cancel_queued`
|
||||
- `cancel_generating`
|
||||
- `preview`
|
||||
|
||||
Fields returned in `features_by_mode.vid_gen`:
|
||||
|
||||
@@ -610,6 +647,7 @@ Fields returned in `features_by_mode.vid_gen`:
|
||||
- `cache`
|
||||
- `cancel_queued`
|
||||
- `cancel_generating`
|
||||
- `preview`
|
||||
|
||||
#### `POST /sdcpp/v1/img_gen`
|
||||
|
||||
@@ -782,6 +820,9 @@ Example:
|
||||
"scm_mask": "",
|
||||
"scm_policy_dynamic": true,
|
||||
|
||||
"preview": "none",
|
||||
"preview_interval": 1,
|
||||
|
||||
"output_format": "png",
|
||||
"output_compression": 100
|
||||
}
|
||||
@@ -847,6 +888,8 @@ Top-level scalar fields:
|
||||
| `control_strength` | `number` |
|
||||
| `ip_adapter_strength` | `number` |
|
||||
| `embed_image_metadata` | `boolean` |
|
||||
| `preview` | `string` |
|
||||
| `preview_interval` | `integer` |
|
||||
|
||||
Image fields:
|
||||
|
||||
@@ -934,6 +977,10 @@ When omitted, backend defaults apply to these fields:
|
||||
- `sample_params.flow_shift`
|
||||
- `sample_params.guidance.img_cfg`
|
||||
|
||||
### Preview Interval Semantics
|
||||
|
||||
`preview_interval` controls the period (in sample steps) at which preview frames are generated. The default is `1` (every step). Any non-positive values will be clamped to `1`. Note that if `preview` is set to `"none"`, no previews are produced regardless of the interval.
|
||||
|
||||
### Completion Result
|
||||
|
||||
Example completed job:
|
||||
@@ -1048,7 +1095,8 @@ Response fields:
|
||||
Compared with `img_gen`, the `vid_gen` request body:
|
||||
|
||||
- `vid_gen` is a single video sequence job, so `batch_count` is not part of the request schema
|
||||
- `ref_images`, `mask_image`, `control_image`, `control_strength`, `ip_adapter_image`, `ip_adapter_strength`, and `embed_image_metadata` are not part of the request schema
|
||||
- `mask_image`, `control_image`, `control_strength`, `ip_adapter_image`, `ip_adapter_strength`, and `embed_image_metadata` are not part of the request schema
|
||||
- `ref_images` is accepted for MiniMax-H3 Ref2VA conditioning; other video model families currently ignore it
|
||||
- `vid_gen` adds `end_image`, `control_frames`, `high_noise_sample_params`, `video_frames`, `fps`, `moe_boundary`, and `vace_strength`
|
||||
|
||||
Example:
|
||||
@@ -1069,6 +1117,7 @@ Example:
|
||||
|
||||
"init_image": null,
|
||||
"end_image": null,
|
||||
"ref_images": [],
|
||||
"control_frames": [],
|
||||
|
||||
"sample_params": {
|
||||
@@ -1130,11 +1179,25 @@ Example:
|
||||
"scm_mask": "",
|
||||
"scm_policy_dynamic": true,
|
||||
|
||||
"preview": "none",
|
||||
"preview_interval": 1,
|
||||
|
||||
"output_format": "webm",
|
||||
"output_compression": 100
|
||||
}
|
||||
```
|
||||
|
||||
### Reference Image Rules
|
||||
|
||||
- `ref_images` contains reference images for MiniMax-H3 Ref2VA conditioning.
|
||||
- Images retain request order and correspond to `<Picture 1>`, `<Picture 2>`, and so on in the prompt.
|
||||
- For MiniMax-H3, non-empty `ref_images` cannot be combined with `init_image` or `end_image`. A conflicting API request fails during generation; the WebUI checks this before submission.
|
||||
- MiniMax-H3 does not support `control_frames`; leave that array empty.
|
||||
- Other video model families currently ignore `ref_images`.
|
||||
- `features_by_mode.vid_gen` does not currently advertise `ref_images`; its absence is not an indication that MiniMax-H3 reference images are unsupported.
|
||||
|
||||
See [MiniMax-H3 reference conditioning](../../docs/minimax_h3.md#reference-to-audio-video-conditioning) for model requirements and prompt examples.
|
||||
|
||||
### LoRA Rules
|
||||
|
||||
- The server only accepts explicit LoRA entries from the `lora` field.
|
||||
@@ -1152,6 +1215,7 @@ Channel expectations:
|
||||
|
||||
- `init_image`: 3 channels
|
||||
- `end_image`: 3 channels
|
||||
- `ref_images[]`: decoded with native channels, then converted to RGB by MiniMax-H3
|
||||
- `control_frames[]`: 3 channels
|
||||
|
||||
Frame ordering rules:
|
||||
@@ -1181,6 +1245,8 @@ Top-level scalar fields:
|
||||
| `fps` | `integer` |
|
||||
| `moe_boundary` | `number` |
|
||||
| `vace_strength` | `number` |
|
||||
| `preview` | `string` |
|
||||
| `preview_interval` | `integer` |
|
||||
|
||||
Image and frame fields:
|
||||
|
||||
@@ -1188,6 +1254,7 @@ Image and frame fields:
|
||||
| --- | --- |
|
||||
| `init_image` | `string \| null` |
|
||||
| `end_image` | `string \| null` |
|
||||
| `ref_images` | `array<string>` |
|
||||
| `control_frames` | `array<string>` |
|
||||
|
||||
LoRA fields:
|
||||
|
||||
@@ -10,6 +10,55 @@
|
||||
#include "common/media_io.h"
|
||||
#include "common/resource_owners.hpp"
|
||||
|
||||
static void preview_callback(int step, int frame_count, sd_image_t* frames, bool is_noisy, void* data) {
|
||||
(void)is_noisy;
|
||||
if (frame_count <= 0 || frames == nullptr || frames[0].data == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncGenerationJob* job = static_cast<AsyncGenerationJob*>(data);
|
||||
const sd_image_t& frame = frames[0];
|
||||
const sd_preview_info_t info = sd_get_preview_info();
|
||||
|
||||
auto image_bytes = encode_image_to_vector(EncodedImageFormat::PNG,
|
||||
frame.data,
|
||||
frame.width,
|
||||
frame.height,
|
||||
frame.channel,
|
||||
"",
|
||||
80);
|
||||
if (image_bytes.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::string b64 = base64_encode(image_bytes);
|
||||
std::lock_guard<std::mutex> lock(job->preview_mutex);
|
||||
job->preview_b64 = std::move(b64);
|
||||
job->preview_step = step < 0 ? -step : step;
|
||||
job->preview_pass = info.sample_pass;
|
||||
job->preview_total_steps = info.total_steps;
|
||||
}
|
||||
|
||||
static void clear_preview_callback() {
|
||||
sd_set_preview_callback(nullptr, PREVIEW_NONE, 1, false, false, nullptr);
|
||||
}
|
||||
|
||||
static void set_preview_callback(AsyncGenerationJob& job) {
|
||||
preview_t mode = str_to_preview(job.kind == AsyncJobKind::ImgGen
|
||||
? job.img_gen.preview_mode.c_str()
|
||||
: job.vid_gen.preview_mode.c_str());
|
||||
if (mode == PREVIEW_COUNT || mode == PREVIEW_NONE) {
|
||||
return;
|
||||
}
|
||||
int interval = job.kind == AsyncJobKind::ImgGen
|
||||
? job.img_gen.preview_interval
|
||||
: job.vid_gen.preview_interval;
|
||||
if (interval <= 0) {
|
||||
interval = 1;
|
||||
}
|
||||
sd_set_preview_callback(preview_callback, mode, interval, true, false, &job);
|
||||
}
|
||||
|
||||
const char* async_job_kind_name(AsyncJobKind kind) {
|
||||
switch (kind) {
|
||||
case AsyncJobKind::ImgGen:
|
||||
@@ -161,6 +210,18 @@ json make_async_job_json(const AsyncJobManager& manager, const AsyncGenerationJo
|
||||
result["error"] = nullptr;
|
||||
}
|
||||
|
||||
if (job.status == AsyncJobStatus::Generating) {
|
||||
std::lock_guard<std::mutex> lock(job.preview_mutex);
|
||||
if (!job.preview_b64.empty()) {
|
||||
result["preview"] = {
|
||||
{"pass", job.preview_pass},
|
||||
{"step", job.preview_step},
|
||||
{"total_steps", job.preview_total_steps},
|
||||
{"b64_json", job.preview_b64},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -174,12 +235,14 @@ bool execute_img_gen_job(ServerRuntime& runtime,
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(*runtime.sd_ctx_mutex);
|
||||
set_preview_callback(job);
|
||||
sd_image_t* raw_results = nullptr;
|
||||
int num_results = 0;
|
||||
if (!generate_image(runtime.sd_ctx, ¶ms, &raw_results, &num_results)) {
|
||||
raw_results = nullptr;
|
||||
num_results = 0;
|
||||
}
|
||||
clear_preview_callback();
|
||||
results.adopt(raw_results, num_results);
|
||||
}
|
||||
|
||||
@@ -247,10 +310,12 @@ bool execute_vid_gen_job(ServerRuntime& runtime,
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(*runtime.sd_ctx_mutex);
|
||||
set_preview_callback(job);
|
||||
sd_image_t* raw_results = nullptr;
|
||||
if (!generate_video(runtime.sd_ctx, ¶ms, &raw_results, &num_results, &generated_audio, &output_fps)) {
|
||||
raw_results = nullptr;
|
||||
}
|
||||
clear_preview_callback();
|
||||
results.adopt(raw_results, num_results);
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,11 @@ struct AsyncGenerationJob {
|
||||
int result_fps = 0;
|
||||
std::string error_code;
|
||||
std::string error_message;
|
||||
mutable std::mutex preview_mutex;
|
||||
std::string preview_b64;
|
||||
int preview_pass = 0;
|
||||
int preview_step = 0;
|
||||
int preview_total_steps = 0;
|
||||
};
|
||||
|
||||
struct AsyncJobManager {
|
||||
|
||||
+1
-1
Submodule examples/server/frontend updated: dd74a8e808...8e7c115059
@@ -75,6 +75,8 @@ int main(int argc, const char** argv) {
|
||||
}
|
||||
SDSvrParams svr_params;
|
||||
SDContextParams ctx_params;
|
||||
// Preserve the CLI default while disabling cwd scans for sd-server.
|
||||
ctx_params.lora_model_dir.clear();
|
||||
SDGenerationParams default_gen_params;
|
||||
|
||||
sd_set_log_callback(sd_log_cb, (void*)&svr_params);
|
||||
|
||||
@@ -301,6 +301,76 @@ static nlohmann::json prepare_info_field(const SDContextParams& ctx_params,
|
||||
return jsoninfo;
|
||||
}
|
||||
|
||||
struct SDAPIPreviewState {
|
||||
std::mutex mutex;
|
||||
std::string b64;
|
||||
std::string id_task;
|
||||
std::string job_type;
|
||||
int step = 0;
|
||||
int total = 0;
|
||||
bool active = false;
|
||||
int64_t job_count = 0;
|
||||
int job_no = 0;
|
||||
};
|
||||
|
||||
static SDAPIPreviewState g_preview;
|
||||
|
||||
static void preview_callback(int step, int frame_count, sd_image_t* frames, bool is_noisy, void* data) {
|
||||
(void)is_noisy;
|
||||
(void)data;
|
||||
if (frame_count <= 0 || frames == nullptr || frames[0].data == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sd_image_t& frame = frames[0];
|
||||
const sd_preview_info_t info = sd_get_preview_info();
|
||||
|
||||
auto image_bytes = encode_image_to_vector(EncodedImageFormat::JPEG,
|
||||
frame.data,
|
||||
frame.width,
|
||||
frame.height,
|
||||
frame.channel,
|
||||
"",
|
||||
80);
|
||||
if (image_bytes.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::string b64 = base64_encode(image_bytes);
|
||||
std::lock_guard<std::mutex> lock(g_preview.mutex);
|
||||
g_preview.b64 = std::move(b64);
|
||||
g_preview.step = static_cast<int>(std::min<int64_t>(step < 0 ? -static_cast<int64_t>(step) : step,
|
||||
std::max(0, info.total_steps)));
|
||||
g_preview.total = std::max(0, info.total_steps);
|
||||
g_preview.job_no = std::max(0, info.sample_pass - 1);
|
||||
}
|
||||
|
||||
struct SDAPIPreviewGuard {
|
||||
SDAPIPreviewGuard(const std::string& id_task, const sd_img_gen_params_t& params, const std::string& job_type) {
|
||||
std::lock_guard<std::mutex> lock(g_preview.mutex);
|
||||
g_preview.id_task = id_task;
|
||||
g_preview.total = 0;
|
||||
g_preview.job_type = job_type;
|
||||
g_preview.b64.clear();
|
||||
g_preview.step = 0;
|
||||
g_preview.active = true;
|
||||
// Each batch item has a base pass and, when enabled, a hires pass.
|
||||
g_preview.job_count = static_cast<int64_t>(std::max(1, params.batch_count)) * (params.hires.enabled ? 2 : 1);
|
||||
g_preview.job_no = 0;
|
||||
sd_set_preview_callback(preview_callback, PREVIEW_PROJ, 1, true, false, nullptr);
|
||||
}
|
||||
|
||||
~SDAPIPreviewGuard() {
|
||||
sd_set_preview_callback(nullptr, PREVIEW_NONE, 1, false, false, nullptr);
|
||||
std::lock_guard<std::mutex> lock(g_preview.mutex);
|
||||
g_preview.active = false;
|
||||
g_preview.b64.clear();
|
||||
}
|
||||
|
||||
SDAPIPreviewGuard(const SDAPIPreviewGuard&) = delete;
|
||||
SDAPIPreviewGuard& operator=(const SDAPIPreviewGuard&) = delete;
|
||||
};
|
||||
|
||||
void register_sdapi_endpoints(httplib::Server& svr, ServerRuntime& rt) {
|
||||
ServerRuntime* runtime = &rt;
|
||||
|
||||
@@ -317,7 +387,9 @@ void register_sdapi_endpoints(httplib::Server& svr, ServerRuntime& rt) {
|
||||
return;
|
||||
}
|
||||
|
||||
json j = json::parse(req.body);
|
||||
std::string id_task = "sdapi";
|
||||
json j = json::parse(req.body);
|
||||
id_task = j.value("id_task", id_task);
|
||||
ImgGenJobRequest request;
|
||||
std::string error_message;
|
||||
if (!build_sdapi_img_gen_request(j, *runtime, img2img, request, error_message)) {
|
||||
@@ -334,6 +406,7 @@ void register_sdapi_endpoints(httplib::Server& svr, ServerRuntime& rt) {
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(*runtime->sd_ctx_mutex);
|
||||
SDAPIPreviewGuard preview_guard(id_task, img_gen_params, img2img ? "img2img" : "txt2img");
|
||||
sd_image_t* raw_results = nullptr;
|
||||
if (!generate_image(runtime->sd_ctx, &img_gen_params, &raw_results, &num_results)) {
|
||||
raw_results = nullptr;
|
||||
@@ -528,4 +601,62 @@ void register_sdapi_endpoints(httplib::Server& svr, ServerRuntime& rt) {
|
||||
r["sd_model_checkpoint"] = model_path.stem();
|
||||
res.set_content(r.dump(), "application/json");
|
||||
});
|
||||
|
||||
svr.Get("/sdapi/v1/progress", [](const httplib::Request& req, httplib::Response& res) {
|
||||
bool skip_current_image = false;
|
||||
bool has_id_param = req.has_param("id_task");
|
||||
std::string req_id_task = has_id_param ? req.get_param_value("id_task") : "";
|
||||
|
||||
if (req.has_param("skip_current_image")) {
|
||||
std::string skip_str = req.get_param_value("skip_current_image");
|
||||
skip_current_image = (skip_str == "true" || skip_str == "1");
|
||||
}
|
||||
|
||||
json r;
|
||||
r["eta_relative"] = 0.0f;
|
||||
json state;
|
||||
state["skipped"] = false;
|
||||
state["interrupted"] = false;
|
||||
state["job_timestamp"] = "0";
|
||||
r["textinfo"] = nullptr;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_preview.mutex);
|
||||
|
||||
bool is_matching_task = g_preview.active && (!has_id_param || req_id_task == g_preview.id_task);
|
||||
|
||||
int step = is_matching_task ? g_preview.step : 0;
|
||||
int steps = is_matching_task ? g_preview.total : 0;
|
||||
std::string b64 = is_matching_task ? g_preview.b64 : "";
|
||||
std::string job_type = is_matching_task ? g_preview.job_type : "";
|
||||
int64_t job_count = is_matching_task ? g_preview.job_count : 0;
|
||||
int job_no = is_matching_task ? g_preview.job_no : 0;
|
||||
|
||||
float batch_progress = (steps > 0) ? (static_cast<float>(step) / static_cast<float>(steps)) : 0.0f;
|
||||
if (batch_progress > 1.0f)
|
||||
batch_progress = 1.0f;
|
||||
|
||||
float overall_progress = job_count > 0
|
||||
? (static_cast<float>(job_no) + batch_progress) / static_cast<float>(job_count)
|
||||
: 0.0f;
|
||||
if (overall_progress > 1.0f)
|
||||
overall_progress = 1.0f;
|
||||
|
||||
r["progress"] = overall_progress;
|
||||
state["job"] = job_type;
|
||||
state["sampling_step"] = step;
|
||||
state["sampling_steps"] = steps;
|
||||
state["job_count"] = job_count;
|
||||
state["job_no"] = job_no;
|
||||
r["state"] = state;
|
||||
|
||||
if (!skip_current_image && !b64.empty()) {
|
||||
r["current_image"] = b64;
|
||||
} else {
|
||||
r["current_image"] = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
res.set_content(r.dump(), "application/json");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -205,6 +205,7 @@ static json make_img_gen_features_json() {
|
||||
{"cache", true},
|
||||
{"cancel_queued", true},
|
||||
{"cancel_generating", false},
|
||||
{"preview", true},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -219,9 +220,18 @@ static json make_vid_gen_features_json() {
|
||||
{"cache", true},
|
||||
{"cancel_queued", true},
|
||||
{"cancel_generating", false},
|
||||
{"preview", true},
|
||||
};
|
||||
}
|
||||
|
||||
static json make_preview_modes_json() {
|
||||
json modes = json::array();
|
||||
for (int i = 0; i < PREVIEW_COUNT; ++i) {
|
||||
modes.push_back(sd_preview_name((preview_t)i));
|
||||
}
|
||||
return modes;
|
||||
}
|
||||
|
||||
static json make_capabilities_json(ServerRuntime& runtime) {
|
||||
refresh_lora_cache(runtime);
|
||||
refresh_upscaler_cache(runtime);
|
||||
@@ -396,6 +406,7 @@ static json make_capabilities_json(ServerRuntime& runtime) {
|
||||
result["loras"] = available_loras;
|
||||
result["upscalers"] = available_upscalers;
|
||||
result["upscale"] = have_upscaler_models;
|
||||
result["preview_modes"] = make_preview_modes_json();
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -418,6 +429,8 @@ static bool parse_img_gen_request(const json& body,
|
||||
if (!assign_output_options(request, output_format, output_compression, true, error_message)) {
|
||||
return false;
|
||||
}
|
||||
request.preview_mode = body.value("preview", std::string("none"));
|
||||
request.preview_interval = body.value("preview_interval", 1);
|
||||
// Intentionally disable prompt-embedded LoRA tag parsing for server APIs.
|
||||
if (!request.gen_params.resolve_and_validate(IMG_GEN, "", runtime.ctx_params->hires_upscalers_dir, true)) {
|
||||
error_message = "invalid generation parameters";
|
||||
@@ -445,6 +458,8 @@ static bool parse_vid_gen_request(const json& body,
|
||||
if (!assign_output_options(request, output_format, output_compression, error_message)) {
|
||||
return false;
|
||||
}
|
||||
request.preview_mode = body.value("preview", std::string("none"));
|
||||
request.preview_interval = body.value("preview_interval", 1);
|
||||
// Intentionally disable prompt-embedded LoRA tag parsing for server APIs.
|
||||
if (!request.gen_params.resolve_and_validate(VID_GEN, "", runtime.ctx_params->hires_upscalers_dir, true)) {
|
||||
error_message = "invalid generation parameters";
|
||||
|
||||
@@ -254,6 +254,12 @@ std::string SDSvrParams::to_string() const {
|
||||
void refresh_lora_cache(ServerRuntime& rt) {
|
||||
std::vector<LoraEntry> new_cache;
|
||||
|
||||
if (rt.ctx_params->lora_model_dir.empty()) {
|
||||
std::lock_guard<std::mutex> lock(*rt.lora_mutex);
|
||||
rt.lora_cache->clear();
|
||||
return;
|
||||
}
|
||||
|
||||
fs::path lora_dir = rt.ctx_params->lora_model_dir;
|
||||
std::error_code ec;
|
||||
if (fs::exists(lora_dir, ec) && !ec && fs::is_directory(lora_dir, ec) && !ec) {
|
||||
|
||||
@@ -66,6 +66,8 @@ struct ImgGenJobRequest {
|
||||
SDGenerationParams gen_params;
|
||||
std::string output_format = "png";
|
||||
int output_compression = 100;
|
||||
std::string preview_mode = "none";
|
||||
int preview_interval = 1;
|
||||
|
||||
sd_img_gen_params_t to_sd_img_gen_params_t() {
|
||||
return gen_params.to_sd_img_gen_params_t();
|
||||
@@ -76,6 +78,8 @@ struct VidGenJobRequest {
|
||||
SDGenerationParams gen_params;
|
||||
std::string output_format = "webm";
|
||||
int output_compression = 100;
|
||||
std::string preview_mode = "none";
|
||||
int preview_interval = 1;
|
||||
|
||||
sd_vid_gen_params_t to_sd_vid_gen_params_t() {
|
||||
return gen_params.to_sd_vid_gen_params_t();
|
||||
|
||||
+1
-1
Submodule ggml updated: 89c4413f5d...d25b121ce1
@@ -245,6 +245,7 @@ typedef struct {
|
||||
const char* rpc_servers;
|
||||
const char* model_args;
|
||||
bool disable_segmented_compute; // Force monolithic graph execution even when automatic graph cutting would fit memory better
|
||||
bool batched_cfg; // Run the conditional and unconditional CFG branches in one batched UNet forward when supported
|
||||
float linear_scale; // Override linear input scaling; 0 keeps the model default
|
||||
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
|
||||
@@ -461,6 +462,11 @@ struct ggml_tensor;
|
||||
typedef void (*sd_log_cb_t)(enum sd_log_level_t level, const char* text, void* data);
|
||||
typedef void (*sd_progress_cb_t)(int step, int steps, float time, void* data);
|
||||
typedef void (*sd_preview_cb_t)(int step, int frame_count, sd_image_t* frames, bool is_noisy, void* data);
|
||||
|
||||
typedef struct {
|
||||
int sample_pass;
|
||||
int total_steps;
|
||||
} sd_preview_info_t;
|
||||
typedef bool (*sd_graph_eval_callback_t)(struct ggml_tensor* t, bool ask, void* user_data);
|
||||
|
||||
SD_API void sd_set_log_callback(sd_log_cb_t sd_log_cb, void* data);
|
||||
@@ -469,6 +475,10 @@ SD_API void sd_set_progress_callback(sd_progress_cb_t cb, void* data);
|
||||
// negative interval previews only completed logical step -interval. Zero previews the final
|
||||
// completed step of the first sampling pass (base-resolution or high-noise).
|
||||
SD_API void sd_set_preview_callback(sd_preview_cb_t cb, enum preview_t mode, int interval, bool denoised, bool noisy, void* data);
|
||||
// Query from a preview callback. Passes are numbered from 1 since the last
|
||||
// sd_set_preview_callback call; total_steps is the actual count in that pass.
|
||||
// Both fields are zero before sampling starts.
|
||||
SD_API sd_preview_info_t sd_get_preview_info();
|
||||
SD_API void sd_set_backend_eval_callback(sd_graph_eval_callback_t cb, void* data);
|
||||
SD_API int32_t sd_get_num_physical_cores();
|
||||
SD_API const char* sd_get_system_info();
|
||||
|
||||
@@ -399,7 +399,7 @@ struct FrozenCLIPEmbedderWithCustomWords : public Conditioner {
|
||||
ss << "['" << item.first << "', " << item.second << "], ";
|
||||
}
|
||||
ss << "]";
|
||||
LOG_VERBOSE("parse '%s' to %s", text.c_str(), ss.str().c_str());
|
||||
LOG_VERBOSE("parse '%s' to %s", escape_newlines(text).c_str(), escape_newlines(ss.str()).c_str());
|
||||
}
|
||||
|
||||
auto on_new_token_cb = [&](std::string& str, std::vector<int32_t>& bpe_tokens) -> bool {
|
||||
@@ -817,7 +817,7 @@ struct SD3CLIPEmbedder : public Conditioner {
|
||||
ss << "['" << item.first << "', " << item.second << "], ";
|
||||
}
|
||||
ss << "]";
|
||||
LOG_VERBOSE("parse '%s' to %s", text.c_str(), ss.str().c_str());
|
||||
LOG_VERBOSE("parse '%s' to %s", escape_newlines(text).c_str(), escape_newlines(ss.str()).c_str());
|
||||
}
|
||||
|
||||
auto on_new_token_cb = [&](std::string& str, std::vector<int32_t>& bpe_tokens) -> bool {
|
||||
@@ -909,6 +909,31 @@ struct SD3CLIPEmbedder : public Conditioner {
|
||||
|
||||
size_t chunk_count = std::max(std::max(clip_l_tokens.size(), clip_g_tokens.size()), t5_tokens.size()) / chunk_len;
|
||||
|
||||
// Each active encoder must cover every chunk of the longest token sequence.
|
||||
Tokenizer* tokenizers[] = {
|
||||
clip_l ? clip_l_tokenizer.get() : nullptr,
|
||||
clip_g ? clip_g_tokenizer.get() : nullptr,
|
||||
t5 ? &t5_tokenizer : nullptr,
|
||||
};
|
||||
const size_t target_length = chunk_count * chunk_len;
|
||||
|
||||
for (size_t encoder_idx = 0; encoder_idx < 3; encoder_idx++) {
|
||||
auto& tokens = token_and_weights[encoder_idx].first;
|
||||
auto& weights = token_and_weights[encoder_idx].second;
|
||||
if (!tokenizers[encoder_idx] || tokens.size() >= target_length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::vector<int> empty_tokens;
|
||||
std::vector<float> empty_weights;
|
||||
tokenizers[encoder_idx]->pad_tokens(empty_tokens, &empty_weights, nullptr, chunk_len, chunk_len, false);
|
||||
|
||||
while (tokens.size() < target_length) {
|
||||
tokens.insert(tokens.end(), empty_tokens.begin(), empty_tokens.end());
|
||||
weights.insert(weights.end(), empty_weights.begin(), empty_weights.end());
|
||||
}
|
||||
}
|
||||
|
||||
for (int chunk_idx = 0; chunk_idx < chunk_count; chunk_idx++) {
|
||||
// clip_l
|
||||
sd::Tensor<float> chunk_hidden_states_l;
|
||||
@@ -1207,7 +1232,7 @@ struct FluxCLIPEmbedder : public Conditioner {
|
||||
ss << "['" << item.first << "', " << item.second << "], ";
|
||||
}
|
||||
ss << "]";
|
||||
LOG_VERBOSE("parse '%s' to %s", text.c_str(), ss.str().c_str());
|
||||
LOG_VERBOSE("parse '%s' to %s", escape_newlines(text).c_str(), escape_newlines(ss.str()).c_str());
|
||||
}
|
||||
|
||||
auto on_new_token_cb = [&](std::string& str, std::vector<int32_t>& bpe_tokens) -> bool {
|
||||
@@ -1484,7 +1509,7 @@ struct T5CLIPEmbedder : public Conditioner {
|
||||
ss << "['" << item.first << "', " << item.second << "], ";
|
||||
}
|
||||
ss << "]";
|
||||
LOG_VERBOSE("parse '%s' to %s", text.c_str(), ss.str().c_str());
|
||||
LOG_VERBOSE("parse '%s' to %s", escape_newlines(text).c_str(), escape_newlines(ss.str()).c_str());
|
||||
}
|
||||
|
||||
auto on_new_token_cb = [&](std::string& str, std::vector<int32_t>& bpe_tokens) -> bool {
|
||||
@@ -1873,7 +1898,7 @@ struct AnimaConditioner : public Conditioner {
|
||||
ss << "['" << item.first << "', " << item.second << "], ";
|
||||
}
|
||||
ss << "]";
|
||||
LOG_VERBOSE("parse '%s' to %s", text.c_str(), ss.str().c_str());
|
||||
LOG_VERBOSE("parse '%s' to %s", escape_newlines(text).c_str(), escape_newlines(ss.str()).c_str());
|
||||
}
|
||||
|
||||
std::vector<int> qwen_tokens;
|
||||
@@ -1987,7 +2012,8 @@ struct LLMEmbedder : public Conditioner {
|
||||
sd_version_is_sefi_image(version) ||
|
||||
sd_version_is_krea2(version) ||
|
||||
sd_version_is_minimax_h3(version) ||
|
||||
sd_version_is_mage_flow(version)) {
|
||||
sd_version_is_mage_flow(version) ||
|
||||
version == VERSION_IRIS) {
|
||||
arch = LLM::LLMArch::QWEN3_VL;
|
||||
} else if (sd_version_is_z_image(version) || sd_version_is_z_image_l2p(version) || version == VERSION_OVIS_IMAGE || version == VERSION_FLUX2_KLEIN) {
|
||||
arch = LLM::LLMArch::QWEN3;
|
||||
@@ -2113,9 +2139,10 @@ struct LLMEmbedder : public Conditioner {
|
||||
|
||||
std::tuple<std::vector<int>, std::vector<float>, std::vector<float>> tokenize(std::string text,
|
||||
const std::pair<int, int>& attn_range,
|
||||
size_t min_length = 0,
|
||||
size_t max_length = 100000000,
|
||||
bool spell_quotes = false) {
|
||||
size_t min_length = 0,
|
||||
size_t max_length = 100000000,
|
||||
bool spell_quotes = false,
|
||||
const std::string& suffix = "") {
|
||||
std::vector<std::pair<std::string, float>> parsed_attention;
|
||||
if (attn_range.first >= 0 && attn_range.second > 0) {
|
||||
if (attn_range.first > 0) {
|
||||
@@ -2144,7 +2171,7 @@ struct LLMEmbedder : public Conditioner {
|
||||
ss << "['" << item.first << "', " << item.second << "], ";
|
||||
}
|
||||
ss << "]";
|
||||
LOG_VERBOSE("parse '%s' to %s", text.c_str(), ss.str().c_str());
|
||||
LOG_VERBOSE("parse '%s' to %s", escape_newlines(text).c_str(), escape_newlines(ss.str()).c_str());
|
||||
}
|
||||
|
||||
std::vector<int> tokens;
|
||||
@@ -2160,6 +2187,20 @@ struct LLMEmbedder : public Conditioner {
|
||||
weights.insert(weights.end(), curr_tokens.size(), curr_weight);
|
||||
}
|
||||
|
||||
if (!suffix.empty()) {
|
||||
std::vector<int> suffix_tokens;
|
||||
if (!tokenizer->encode(suffix, suffix_tokens) || suffix_tokens.size() >= max_length) {
|
||||
return {};
|
||||
}
|
||||
// Reserve the assistant-turn marker before truncating the caption.
|
||||
if (tokens.size() > max_length - suffix_tokens.size()) {
|
||||
tokens.resize(max_length - suffix_tokens.size());
|
||||
weights.resize(tokens.size());
|
||||
}
|
||||
tokens.insert(tokens.end(), suffix_tokens.begin(), suffix_tokens.end());
|
||||
weights.insert(weights.end(), suffix_tokens.size(), 1.f);
|
||||
}
|
||||
|
||||
std::vector<float> mask;
|
||||
tokenizer->pad_tokens(tokens, &weights, &mask, min_length, max_length);
|
||||
|
||||
@@ -2182,8 +2223,10 @@ struct LLMEmbedder : public Conditioner {
|
||||
bool spell_quotes = false,
|
||||
int max_length = 100000000,
|
||||
const LLM::DeepStackImageEmbeds& deepstack_image_embeds = {},
|
||||
const std::vector<LLM::ImageGrid>& image_grids = {}) {
|
||||
auto tokens_weights_mask = tokenize(prompt, prompt_attn_range, min_length, max_length, spell_quotes);
|
||||
const std::vector<LLM::ImageGrid>& image_grids = {},
|
||||
const std::string& prompt_suffix = "",
|
||||
sd::Tensor<float>* output_mask = nullptr) {
|
||||
auto tokens_weights_mask = tokenize(prompt, prompt_attn_range, min_length, max_length, spell_quotes, prompt_suffix);
|
||||
auto& tokens = std::get<0>(tokens_weights_mask);
|
||||
auto& weights = std::get<1>(tokens_weights_mask);
|
||||
auto& mask = std::get<2>(tokens_weights_mask);
|
||||
@@ -2246,6 +2289,12 @@ struct LLMEmbedder : public Conditioner {
|
||||
1);
|
||||
}
|
||||
|
||||
if (output_mask != nullptr) {
|
||||
*output_mask = sd::Tensor<float>::zeros({new_hidden_states.shape()[1]});
|
||||
for (size_t i = prompt_template_encode_start_idx; i < mask.size(); ++i) {
|
||||
output_mask->data()[i - prompt_template_encode_start_idx] = mask[i];
|
||||
}
|
||||
}
|
||||
return new_hidden_states;
|
||||
}
|
||||
|
||||
@@ -2312,6 +2361,8 @@ struct LLMEmbedder : public Conditioner {
|
||||
int hidden_states_min_length = 0; // zero pad hidden_states
|
||||
bool spell_quotes = false;
|
||||
std::set<int> out_layers;
|
||||
std::string prompt_suffix;
|
||||
sd::Tensor<float> text_mask;
|
||||
|
||||
int64_t t0 = ggml_time_ms();
|
||||
RefImageResizeMode resize_mode = conditioner_params.ref_image_params.vlm_resize_mode;
|
||||
@@ -3061,6 +3112,23 @@ struct LLMEmbedder : public Conditioner {
|
||||
SDCondition result;
|
||||
result.c_crossattn = std::move(hidden_states);
|
||||
return result;
|
||||
} else if (version == VERSION_IRIS) {
|
||||
prompt =
|
||||
"<|im_start|>system\n"
|
||||
"Describe the image by detailing the color, shape, size, texture, quantity, text, spatial "
|
||||
"relationships of the objects and background:<|im_end|>\n<|im_start|>user\n";
|
||||
auto prefix_tokens = std::get<0>(tokenize(prompt, {0, 0}));
|
||||
if (prefix_tokens.empty()) {
|
||||
return {};
|
||||
}
|
||||
prompt_template_encode_start_idx = static_cast<int>(prefix_tokens.size());
|
||||
hidden_states_min_length = 300;
|
||||
max_length = prompt_template_encode_start_idx + hidden_states_min_length;
|
||||
out_layers = {2, 5, 8, 11, 14, 17, 20, 23, 26, 29, 32, 35};
|
||||
prompt_attn_range.first = static_cast<int>(prompt.size());
|
||||
prompt += conditioner_params.text;
|
||||
prompt_attn_range.second = static_cast<int>(prompt.size());
|
||||
prompt_suffix = "<|im_end|>\n<|im_start|>assistant\n";
|
||||
} else {
|
||||
GGML_ABORT("unknown version %d", version);
|
||||
}
|
||||
@@ -3076,7 +3144,9 @@ struct LLMEmbedder : public Conditioner {
|
||||
spell_quotes,
|
||||
max_length,
|
||||
deepstack_image_embeds,
|
||||
image_grids);
|
||||
image_grids,
|
||||
prompt_suffix,
|
||||
version == VERSION_IRIS ? &text_mask : nullptr);
|
||||
if (hidden_states.empty()) {
|
||||
return {};
|
||||
}
|
||||
@@ -3141,6 +3211,7 @@ struct LLMEmbedder : public Conditioner {
|
||||
SDCondition result;
|
||||
result.c_crossattn = std::move(hidden_states);
|
||||
result.extra_c_crossattns = std::move(extra_hidden_states_vec);
|
||||
result.c_vector = std::move(text_mask);
|
||||
if (version == VERSION_QWEN_IMAGE_2_1) {
|
||||
auto slots = sd::Tensor<int32_t>::zeros({result.c_crossattn.shape()[1]});
|
||||
for (size_t i = 0; i < image_embeds.size(); ++i) {
|
||||
|
||||
@@ -497,11 +497,7 @@ namespace sd::backend_fit {
|
||||
return false;
|
||||
}
|
||||
auto smaller_tile = [&](int size) {
|
||||
int next_size = size / 2;
|
||||
if (!tiling_params.enabled) {
|
||||
next_size = std::min(next_size, 256 / scale_factor);
|
||||
}
|
||||
return std::min(size, std::max(4, next_size));
|
||||
return std::min(size, std::max(4, size / 2));
|
||||
};
|
||||
const int tile_size_w = smaller_tile(latent_tile_size_w);
|
||||
const int tile_size_h = smaller_tile(latent_tile_size_h);
|
||||
|
||||
+11
-11
@@ -223,13 +223,13 @@ ggml_tensor* ggml_ext_linear(ggml_context* ctx,
|
||||
x = ggml_reshape_2d(ctx, x, x->ne[0], x->ne[1] * x->ne[2] * x->ne[3]);
|
||||
x = ggml_mul_mat(ctx, w, x);
|
||||
if (force_prec_f32) {
|
||||
ggml_mul_mat_set_prec(x, GGML_PREC_F32);
|
||||
ggml_prec_set_acc(x, GGML_PREC_F32);
|
||||
}
|
||||
x = ggml_reshape_4d(ctx, x, x->ne[0], x->ne[1] / ne2 / ne3, ne2, ne3);
|
||||
} else {
|
||||
x = ggml_mul_mat(ctx, w, x);
|
||||
if (force_prec_f32) {
|
||||
ggml_mul_mat_set_prec(x, GGML_PREC_F32);
|
||||
ggml_prec_set_acc(x, GGML_PREC_F32);
|
||||
}
|
||||
}
|
||||
if (scale != 1.f) {
|
||||
@@ -488,7 +488,7 @@ ggml_tensor* ggml_ext_conv_3d(ggml_context* ctx,
|
||||
x = ggml_mul_mat(ctx,
|
||||
ggml_reshape_2d(ctx, im2col, im2col->ne[0], im2col->ne[3] * im2col->ne[2] * im2col->ne[1]),
|
||||
ggml_reshape_2d(ctx, w, w->ne[0] * w->ne[1] * w->ne[2] * IC, OC));
|
||||
ggml_mul_mat_set_prec(x, GGML_PREC_F32);
|
||||
ggml_prec_set_acc(x, GGML_PREC_F32);
|
||||
|
||||
int64_t OD = im2col->ne[3] / N;
|
||||
x = ggml_reshape_4d(ctx, x, im2col->ne[1] * im2col->ne[2], OD, N, OC);
|
||||
@@ -683,8 +683,8 @@ ggml_tensor* ggml_ext_attention_ext(ggml_context* ctx,
|
||||
v_in->type == GGML_TYPE_F32 && sd_backend_supports_cuda_mma(backend);
|
||||
if (pad_head) {
|
||||
// CUDA FA MMA starts at 64 channels; keep the original head's attention scale.
|
||||
q_in = ggml_pad(ctx, q_in, 64 - d_head, 0, 0, 0);
|
||||
k_in = ggml_pad(ctx, k_in, 64 - d_head, 0, 0, 0);
|
||||
q_in = ggml_pad(ctx, q_in, static_cast<int>(64 - d_head), 0, 0, 0);
|
||||
k_in = ggml_pad(ctx, k_in, static_cast<int>(64 - d_head), 0, 0, 0);
|
||||
}
|
||||
if (kv_scale != 1.0f) {
|
||||
k_in = ggml_ext_scale(ctx, k_in, kv_scale);
|
||||
@@ -694,7 +694,7 @@ ggml_tensor* ggml_ext_attention_ext(ggml_context* ctx,
|
||||
v_in = ggml_ext_cont(ctx, ggml_permute(ctx, v_in, 0, 2, 1, 3));
|
||||
v_in = ggml_reshape_3d(ctx, v_in, d_head, L_k, n_kv_head * N);
|
||||
if (pad_head) {
|
||||
v_in = ggml_pad(ctx, v_in, 64 - d_head, 0, 0, 0);
|
||||
v_in = ggml_pad(ctx, v_in, static_cast<int>(64 - d_head), 0, 0, 0);
|
||||
}
|
||||
if (kv_scale != 1.0f) {
|
||||
v_in = ggml_ext_scale(ctx, v_in, kv_scale);
|
||||
@@ -721,7 +721,7 @@ ggml_tensor* ggml_ext_attention_ext(ggml_context* ctx,
|
||||
if (!ggml_backend_supports_op(backend, out)) {
|
||||
return nullptr;
|
||||
}
|
||||
ggml_flash_attn_ext_set_prec(out, GGML_PREC_F32);
|
||||
ggml_prec_set_acc(out, GGML_PREC_F32);
|
||||
if (kv_scale != 1.0f) {
|
||||
out = ggml_ext_scale(ctx, out, 1.0f / kv_scale);
|
||||
}
|
||||
@@ -742,9 +742,9 @@ ggml_tensor* ggml_ext_attention_ext(ggml_context* ctx,
|
||||
}
|
||||
if (padded_head != d_head) {
|
||||
// Keep the original head's softmax scale when padding for the CUDA kernel.
|
||||
q_in = ggml_pad(ctx, q_in, padded_head - d_head, 0, 0, 0);
|
||||
k_in = ggml_pad(ctx, k_in, padded_head - d_head, 0, 0, 0);
|
||||
v_in = ggml_pad(ctx, v_in, padded_head - d_head, 0, 0, 0);
|
||||
q_in = ggml_pad(ctx, q_in, static_cast<int>(padded_head - d_head), 0, 0, 0);
|
||||
k_in = ggml_pad(ctx, k_in, static_cast<int>(padded_head - d_head), 0, 0, 0);
|
||||
v_in = ggml_pad(ctx, v_in, static_cast<int>(padded_head - d_head), 0, 0, 0);
|
||||
}
|
||||
if (kv_scale != 1.0f) {
|
||||
k_in = ggml_ext_scale(ctx, k_in, kv_scale);
|
||||
@@ -797,7 +797,7 @@ ggml_tensor* ggml_ext_attention_ext(ggml_context* ctx,
|
||||
v = ggml_reshape_3d(ctx, v, L_k, d_head, n_kv_head * N); // [N * n_kv_head, d_head, L_k]
|
||||
|
||||
auto kq = ggml_mul_mat(ctx, k, q); // [N * n_head, L_q, L_k]
|
||||
ggml_mul_mat_set_prec(kq, GGML_PREC_F32);
|
||||
ggml_prec_set_acc(kq, GGML_PREC_F32);
|
||||
kq = ggml_scale_inplace(ctx, kq, scale);
|
||||
if (mask) {
|
||||
kq = ggml_add_inplace(ctx, kq, mask);
|
||||
|
||||
+43
-10
@@ -62,6 +62,21 @@ void replace_all_chars(std::string& str, char target, char replacement) {
|
||||
}
|
||||
}
|
||||
|
||||
std::string escape_newlines(const std::string& text) {
|
||||
std::string escaped;
|
||||
escaped.reserve(text.size());
|
||||
for (char ch : text) {
|
||||
if (ch == '\n') {
|
||||
escaped += "\\n";
|
||||
} else if (ch == '\r') {
|
||||
escaped += "\\r";
|
||||
} else {
|
||||
escaped += ch;
|
||||
}
|
||||
}
|
||||
return escaped;
|
||||
}
|
||||
|
||||
static std::string sd_vformat(const char* fmt, va_list ap) {
|
||||
char small[128];
|
||||
va_list ap2;
|
||||
@@ -360,12 +375,13 @@ int32_t sd_get_num_physical_cores() {
|
||||
static sd_progress_cb_t sd_progress_cb = nullptr;
|
||||
void* sd_progress_cb_data = nullptr;
|
||||
|
||||
static sd_preview_cb_t sd_preview_cb = nullptr;
|
||||
static void* sd_preview_cb_data = nullptr;
|
||||
preview_t sd_preview_mode = PREVIEW_NONE;
|
||||
int sd_preview_interval = 1;
|
||||
bool sd_preview_denoised = true;
|
||||
bool sd_preview_noisy = false;
|
||||
static sd_preview_info_t sd_preview_info = {};
|
||||
static sd_preview_cb_t sd_preview_cb = nullptr;
|
||||
static void* sd_preview_cb_data = nullptr;
|
||||
preview_t sd_preview_mode = PREVIEW_NONE;
|
||||
int sd_preview_interval = 1;
|
||||
bool sd_preview_denoised = true;
|
||||
bool sd_preview_noisy = false;
|
||||
|
||||
static sd_graph_eval_callback_t sd_backend_eval_cb = nullptr;
|
||||
static void* sd_backend_eval_cb_data = nullptr;
|
||||
@@ -644,9 +660,14 @@ void* sd_log_cb_data = nullptr;
|
||||
static void sd_log_dispatch(sd_log_level_t level, const std::string& origin, const std::string& text) {
|
||||
if (sd_log_cb == nullptr)
|
||||
return;
|
||||
std::string message = origin + " - " + text;
|
||||
if (message.back() != '\n') {
|
||||
message += '\n';
|
||||
std::string message = text;
|
||||
while (!message.empty() && (message.back() == '\n' || message.back() == '\r')) {
|
||||
message.pop_back();
|
||||
}
|
||||
if (origin == "ggml") {
|
||||
message = origin + ": " + message + '\n';
|
||||
} else {
|
||||
message += " --- " + origin + '\n';
|
||||
}
|
||||
sd_log_cb(level, message.c_str(), sd_log_cb_data);
|
||||
}
|
||||
@@ -656,7 +677,7 @@ void log_printf(sd_log_level_t level, const char* file, int line, const char* fo
|
||||
va_start(args, format);
|
||||
std::string message = sd_vformat(format, args);
|
||||
va_end(args);
|
||||
std::string origin = sd_format("%s:%-4d", sd_basename(file).c_str(), line);
|
||||
std::string origin = sd_format("%s:%d", sd_basename(file).c_str(), line);
|
||||
sd_log_dispatch(level, origin, message);
|
||||
}
|
||||
|
||||
@@ -691,6 +712,7 @@ void sd_set_progress_callback(sd_progress_cb_t cb, void* data) {
|
||||
sd_progress_cb_data = data;
|
||||
}
|
||||
void sd_set_preview_callback(sd_preview_cb_t cb, preview_t mode, int interval, bool denoised, bool noisy, void* data) {
|
||||
sd_preview_info = {};
|
||||
sd_preview_cb = cb;
|
||||
sd_preview_cb_data = data;
|
||||
sd_preview_mode = mode;
|
||||
@@ -704,6 +726,17 @@ void sd_set_backend_eval_callback(sd_graph_eval_callback_t cb, void* data) {
|
||||
sd_backend_eval_cb_data = data;
|
||||
}
|
||||
|
||||
void sd_begin_preview_pass(int total_steps) {
|
||||
if (sd_preview_cb != nullptr) {
|
||||
++sd_preview_info.sample_pass;
|
||||
sd_preview_info.total_steps = total_steps;
|
||||
}
|
||||
}
|
||||
|
||||
sd_preview_info_t sd_get_preview_info() {
|
||||
return sd_preview_info;
|
||||
}
|
||||
|
||||
sd_preview_cb_t sd_get_preview_callback() {
|
||||
return sd_preview_cb;
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ bool contains(const std::string& str, const std::string& substr);
|
||||
std::string sd_format(const char* fmt, ...);
|
||||
|
||||
void replace_all_chars(std::string& str, char target, char replacement);
|
||||
std::string escape_newlines(const std::string& text);
|
||||
|
||||
int round_up_to(int value, int base);
|
||||
|
||||
@@ -101,6 +102,7 @@ std::vector<std::pair<std::string, float>> split_quotation_attention(
|
||||
sd_progress_cb_t sd_get_progress_callback();
|
||||
void* sd_get_progress_callback_data();
|
||||
|
||||
void sd_begin_preview_pass(int total_steps);
|
||||
sd_preview_cb_t sd_get_preview_callback();
|
||||
void* sd_get_preview_callback_data();
|
||||
preview_t sd_get_preview_mode();
|
||||
|
||||
+3
-1
@@ -65,6 +65,7 @@ enum SDVersion {
|
||||
VERSION_PIXART,
|
||||
VERSION_MING_IMAGE,
|
||||
VERSION_Z_IMAGE_L2P,
|
||||
VERSION_IRIS,
|
||||
VERSION_COUNT,
|
||||
};
|
||||
|
||||
@@ -334,7 +335,8 @@ static inline bool sd_version_is_dit(SDVersion version) {
|
||||
sd_version_is_krea2(version) ||
|
||||
sd_version_is_mage_flow(version) ||
|
||||
sd_version_is_sensenova_u1(version) ||
|
||||
sd_version_is_pixart(version)) {
|
||||
sd_version_is_pixart(version) ||
|
||||
version == VERSION_IRIS) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -11,7 +11,9 @@ ggml_tensor* ggml_ext_merge_lora(ggml_context* ctx,
|
||||
ggml_tensor* lora_mid) {
|
||||
ggml_tensor* updown;
|
||||
// flat lora tensors to multiply it
|
||||
int64_t lora_up_rows = lora_up->ne[ggml_n_dims(lora_up) - 1];
|
||||
// ggml_n_dims drops the output dimension when a linear layer has one output.
|
||||
int lora_up_n_dims = ggml_n_dims(lora_up);
|
||||
int64_t lora_up_rows = lora_up_n_dims == 1 ? 1 : lora_up->ne[lora_up_n_dims - 1];
|
||||
lora_up = ggml_reshape_2d(ctx, lora_up, ggml_nelements(lora_up) / lora_up_rows, lora_up_rows);
|
||||
auto lora_down_n_dims = ggml_n_dims(lora_down);
|
||||
// assume n_dims should always be a multiple of 2 (otherwise rank 1 doesn't work)
|
||||
|
||||
@@ -1067,10 +1067,22 @@ namespace Rope {
|
||||
return result;
|
||||
}
|
||||
|
||||
__STATIC_INLINE__ ggml_tensor* apply_rope(ggml_context* ctx,
|
||||
__STATIC_INLINE__ ggml_tensor* apply_rope(GGMLRunnerContext* runner_ctx,
|
||||
ggml_tensor* x,
|
||||
ggml_tensor* pe,
|
||||
bool rope_interleaved = true) {
|
||||
auto ctx = runner_ctx->ggml_ctx;
|
||||
#ifndef SD_USE_UPSTREAM_GGML
|
||||
auto backend = runner_ctx->backend;
|
||||
if (backend != nullptr && x->type == GGML_TYPE_F32 && pe->type == GGML_TYPE_F32 &&
|
||||
x->ne[0] % 2 == 0 && pe->ne[0] == 2 && pe->ne[1] == 2 &&
|
||||
pe->ne[2] == x->ne[0] / 2 && pe->ne[3] == x->ne[2]) {
|
||||
auto out = ggml_rope_apply(ctx, x, pe, rope_interleaved);
|
||||
if (ggml_backend_supports_op(backend, out)) {
|
||||
return out;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
// x: [N, L, n_head, d_head]
|
||||
// pe: [L, d_head/2, 2, 2], [[cos, -sin], [sin, cos]]
|
||||
int64_t d_head = x->ne[0];
|
||||
@@ -1126,8 +1138,8 @@ namespace Rope {
|
||||
// return: [N, L, n_head*d_head]
|
||||
int64_t n_head = q->ne[1];
|
||||
|
||||
q = apply_rope(ctx->ggml_ctx, q, pe, rope_interleaved); // [N*n_head, L, d_head]
|
||||
k = apply_rope(ctx->ggml_ctx, k, pe, rope_interleaved); // [N*n_head, L, d_head]
|
||||
q = apply_rope(ctx, q, pe, rope_interleaved); // [N*n_head, L, d_head]
|
||||
k = apply_rope(ctx, k, pe, rope_interleaved); // [N*n_head, L, d_head]
|
||||
|
||||
auto x = ggml_ext_attention_ext(ctx, q, k, v, n_head, mask, true, ctx->flash_attn_enabled, kv_scale); // [N, L, n_head*d_head]
|
||||
return x;
|
||||
|
||||
@@ -235,8 +235,8 @@ namespace Anima {
|
||||
if (pe_k == nullptr) {
|
||||
pe_k = pe_q;
|
||||
}
|
||||
auto q_rope = Rope::apply_rope(ctx->ggml_ctx, q4, pe_q, false);
|
||||
auto k_rope = Rope::apply_rope(ctx->ggml_ctx, k4, pe_k, false);
|
||||
auto q_rope = Rope::apply_rope(ctx, q4, pe_q, false);
|
||||
auto k_rope = Rope::apply_rope(ctx, k4, pe_k, false);
|
||||
attn_out = ggml_ext_attention_ext(ctx,
|
||||
q_rope,
|
||||
k_rope,
|
||||
|
||||
@@ -0,0 +1,450 @@
|
||||
#ifndef __SD_MODEL_DIFFUSION_IRIS_HPP__
|
||||
#define __SD_MODEL_DIFFUSION_IRIS_HPP__
|
||||
|
||||
#include <array>
|
||||
#include <stdexcept>
|
||||
#include "model/diffusion/pid.hpp"
|
||||
|
||||
namespace Iris {
|
||||
struct IrisConfig {
|
||||
int64_t hidden_size = 2560;
|
||||
int64_t depth = 24;
|
||||
int64_t dual_depth = 8;
|
||||
int64_t head_dim = 128;
|
||||
int64_t num_heads = 20;
|
||||
int64_t kv_heads = 5;
|
||||
int64_t mlp_dim = 6826;
|
||||
int64_t patch_size = 16;
|
||||
int64_t text_dim = 2560;
|
||||
int64_t text_len = 300;
|
||||
int64_t text_layers = 12;
|
||||
int64_t layer_heads = 32;
|
||||
int64_t layer_mlp_dim = 3328;
|
||||
int64_t pixel_dim = 16;
|
||||
int64_t pixel_attn_dim = 1280;
|
||||
int64_t pixel_heads = 10;
|
||||
int64_t pixel_depth = 4;
|
||||
|
||||
static IrisConfig detect_from_weights(const String2TensorStorage& weights, const std::string& prefix) {
|
||||
IrisConfig config;
|
||||
auto find = [&](const std::string& name) -> const TensorStorage& {
|
||||
auto it = weights.find(prefix + "." + name);
|
||||
if (it == weights.end()) {
|
||||
throw std::runtime_error("Iris-3B: missing weight " + prefix + "." + name);
|
||||
}
|
||||
return it->second;
|
||||
};
|
||||
config.hidden_size = find("s_embedder.proj.weight").ne[1];
|
||||
config.patch_size = static_cast<int64_t>(std::sqrt(find("s_embedder.proj.weight").ne[0] / 3));
|
||||
config.head_dim = find("blocks.0.attn.q_norm_x.weight").ne[0];
|
||||
config.num_heads = config.hidden_size / config.head_dim;
|
||||
config.kv_heads = find("blocks.0.attn.k_proj_x.weight").ne[1] / config.head_dim;
|
||||
config.mlp_dim = find("blocks.0.mlp_x.w1.weight").ne[1];
|
||||
config.text_dim = find("y_embedder.refiner.proj.weight").ne[0];
|
||||
config.text_len = find("y_pos_embedding").ne[1];
|
||||
config.text_layers = find("y_embedder.layer_pool.weight").ne[0];
|
||||
config.layer_mlp_dim = find("y_embedder.layer_blocks.0.mlp.0.weight").ne[1];
|
||||
config.pixel_dim = find("pixel_embedder.proj.weight").ne[1];
|
||||
config.pixel_attn_dim = find("pixel_blocks.0.compress.weight").ne[1];
|
||||
config.pixel_heads = config.pixel_attn_dim / find("pixel_blocks.0.attn.q_norm.weight").ne[0];
|
||||
config.depth = config.dual_depth = config.pixel_depth = 0;
|
||||
for (const auto& [name, tensor] : weights) {
|
||||
if (!starts_with(name, prefix + ".")) {
|
||||
continue;
|
||||
}
|
||||
auto parts = split_string(name.substr(prefix.size() + 1), '.');
|
||||
if (parts.size() > 2 && parts[0] == "blocks") {
|
||||
int64_t index = std::stoi(parts[1]);
|
||||
config.depth = std::max(config.depth, index + 1);
|
||||
if (parts[2] == "adaln_img") {
|
||||
config.dual_depth = std::max(config.dual_depth, index + 1);
|
||||
}
|
||||
} else if (parts.size() > 2 && parts[0] == "pixel_blocks") {
|
||||
config.pixel_depth = std::max(config.pixel_depth, int64_t(std::stoi(parts[1]) + 1));
|
||||
}
|
||||
}
|
||||
if (config.text_layers != 12 || config.text_dim != 2560 || config.text_len != 300 ||
|
||||
find("modulation_cores.adaln_img.weight").ne[1] != 6 * config.hidden_size ||
|
||||
find("pixel_blocks.0.adaln.weight").ne[1] != 4 * config.pixel_dim * config.patch_size * config.patch_size) {
|
||||
throw std::runtime_error("Iris-3B: unsupported checkpoint configuration");
|
||||
}
|
||||
LOG_VERBOSE("iris: hidden_size = %" PRId64 ", depth = %" PRId64 ", dual_depth = %" PRId64 ", heads = %" PRId64 "/%" PRId64 ", pixel_depth = %" PRId64,
|
||||
config.hidden_size, config.depth, config.dual_depth, config.num_heads, config.kv_heads, config.pixel_depth);
|
||||
return config;
|
||||
}
|
||||
};
|
||||
|
||||
struct SelfAttention : public GGMLBlock {
|
||||
int64_t dim;
|
||||
int64_t heads;
|
||||
bool qk_norm;
|
||||
|
||||
SelfAttention(int64_t dim, int64_t heads, bool qk_norm = true)
|
||||
: dim(dim), heads(heads), qk_norm(qk_norm) {
|
||||
blocks["qkv"] = std::make_shared<Linear>(dim, 3 * dim, false);
|
||||
blocks["proj"] = std::make_shared<Linear>(dim, dim, true);
|
||||
if (qk_norm) {
|
||||
blocks["q_norm"] = std::make_shared<RMSNorm>(dim / heads, 1e-6f);
|
||||
blocks["k_norm"] = std::make_shared<RMSNorm>(dim / heads, 1e-6f);
|
||||
}
|
||||
}
|
||||
|
||||
ggml_tensor* forward(GGMLRunnerContext* ctx, ggml_tensor* x, ggml_tensor* pe = nullptr, ggml_tensor* mask = nullptr) {
|
||||
auto c = ctx->ggml_ctx;
|
||||
auto qkv = split_qkv(c, std::dynamic_pointer_cast<Linear>(blocks["qkv"])->forward(ctx, x));
|
||||
auto q = ggml_reshape_4d(c, qkv[0], dim / heads, heads, x->ne[1], x->ne[2]);
|
||||
auto k = ggml_reshape_4d(c, qkv[1], dim / heads, heads, x->ne[1], x->ne[2]);
|
||||
auto v = ggml_reshape_4d(c, qkv[2], dim / heads, heads, x->ne[1], x->ne[2]);
|
||||
if (qk_norm) {
|
||||
q = std::dynamic_pointer_cast<RMSNorm>(blocks["q_norm"])->forward(ctx, q);
|
||||
k = std::dynamic_pointer_cast<RMSNorm>(blocks["k_norm"])->forward(ctx, k);
|
||||
}
|
||||
if (pe) {
|
||||
x = Rope::attention(ctx, q, k, v, pe, mask);
|
||||
} else {
|
||||
q = ggml_reshape_3d(c, q, dim, x->ne[1], x->ne[2]);
|
||||
k = ggml_reshape_3d(c, k, dim, x->ne[1], x->ne[2]);
|
||||
v = ggml_reshape_3d(c, v, dim, x->ne[1], x->ne[2]);
|
||||
x = ggml_ext_attention_ext(ctx, q, k, v, heads, mask, false, ctx->flash_attn_enabled);
|
||||
}
|
||||
return std::dynamic_pointer_cast<Linear>(blocks["proj"])->forward(ctx, x);
|
||||
}
|
||||
};
|
||||
|
||||
struct TextBlock : public GGMLBlock {
|
||||
bool layerwise;
|
||||
|
||||
TextBlock(int64_t dim, int64_t heads, int64_t mlp_dim, bool layerwise)
|
||||
: layerwise(layerwise) {
|
||||
blocks["norm1"] = std::make_shared<RMSNorm>(dim, 1e-6f);
|
||||
blocks["norm2"] = std::make_shared<RMSNorm>(dim, 1e-6f);
|
||||
blocks["attn"] = std::make_shared<SelfAttention>(dim, heads, !layerwise);
|
||||
if (layerwise) {
|
||||
blocks["mlp.0"] = std::make_shared<Linear>(dim, mlp_dim, true);
|
||||
blocks["mlp.2"] = std::make_shared<Linear>(mlp_dim, dim, true);
|
||||
} else {
|
||||
blocks["mlp"] = std::make_shared<Pid::FeedForward>(dim, mlp_dim);
|
||||
}
|
||||
}
|
||||
|
||||
ggml_tensor* forward(GGMLRunnerContext* ctx, ggml_tensor* x, ggml_tensor* mask = nullptr) {
|
||||
auto h = std::dynamic_pointer_cast<RMSNorm>(blocks["norm1"])->forward(ctx, x);
|
||||
h = std::dynamic_pointer_cast<SelfAttention>(blocks["attn"])->forward(ctx, h, nullptr, mask);
|
||||
x = ggml_add(ctx->ggml_ctx, x, h);
|
||||
h = std::dynamic_pointer_cast<RMSNorm>(blocks["norm2"])->forward(ctx, x);
|
||||
if (layerwise) {
|
||||
h = std::dynamic_pointer_cast<Linear>(blocks["mlp.0"])->forward(ctx, h);
|
||||
h = ggml_silu(ctx->ggml_ctx, h);
|
||||
h = std::dynamic_pointer_cast<Linear>(blocks["mlp.2"])->forward(ctx, h);
|
||||
} else {
|
||||
h = std::dynamic_pointer_cast<Pid::FeedForward>(blocks["mlp"])->forward(ctx, h);
|
||||
}
|
||||
return ggml_add(ctx->ggml_ctx, x, h);
|
||||
}
|
||||
};
|
||||
|
||||
struct TextEmbedder : public GGMLBlock {
|
||||
IrisConfig config;
|
||||
|
||||
TextEmbedder(const IrisConfig& config)
|
||||
: config(config) {
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
blocks["layer_blocks." + std::to_string(i)] = std::make_shared<TextBlock>(config.text_dim, config.layer_heads, config.layer_mlp_dim, true);
|
||||
blocks["refiner.blocks." + std::to_string(i)] = std::make_shared<TextBlock>(config.hidden_size, config.num_heads, config.mlp_dim, false);
|
||||
}
|
||||
blocks["layer_pool"] = std::make_shared<Linear>(config.text_layers, 1, true);
|
||||
blocks["refiner.proj"] = std::make_shared<Linear>(config.text_dim, config.hidden_size, true);
|
||||
blocks["refiner.norm"] = std::make_shared<RMSNorm>(config.hidden_size, 1e-6f);
|
||||
}
|
||||
|
||||
ggml_tensor* forward(GGMLRunnerContext* ctx, ggml_tensor* x, ggml_tensor* mask) {
|
||||
auto c = ctx->ggml_ctx;
|
||||
int64_t tokens = x->ne[1];
|
||||
int64_t batch = x->ne[2];
|
||||
// The encoder concatenates selected layers within each token's feature dimension.
|
||||
x = ggml_reshape_3d(c, x, config.text_dim, config.text_layers, tokens * batch);
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
x = std::dynamic_pointer_cast<TextBlock>(blocks["layer_blocks." + std::to_string(i)])->forward(ctx, x);
|
||||
}
|
||||
x = ggml_cont(c, ggml_permute(c, x, 1, 0, 2, 3));
|
||||
x = std::dynamic_pointer_cast<Linear>(blocks["layer_pool"])->forward(ctx, x);
|
||||
x = ggml_reshape_3d(c, x, config.text_dim, tokens, batch);
|
||||
x = std::dynamic_pointer_cast<Linear>(blocks["refiner.proj"])->forward(ctx, x);
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
x = std::dynamic_pointer_cast<TextBlock>(blocks["refiner.blocks." + std::to_string(i)])->forward(ctx, x, mask);
|
||||
}
|
||||
return std::dynamic_pointer_cast<RMSNorm>(blocks["refiner.norm"])->forward(ctx, x);
|
||||
}
|
||||
};
|
||||
|
||||
struct TrunkBlock : public GGMLBlock {
|
||||
IrisConfig config;
|
||||
bool dual;
|
||||
|
||||
TrunkBlock(const IrisConfig& config, bool dual)
|
||||
: config(config), dual(dual) {
|
||||
int64_t d = config.hidden_size;
|
||||
for (const std::string stream : dual ? std::vector<std::string>{"x", "y"} : std::vector<std::string>{""}) {
|
||||
std::string suffix = stream.empty() ? "" : "_" + stream;
|
||||
std::string attn = dual ? "attn." : "";
|
||||
blocks[attn + "q_proj" + suffix] = std::make_shared<Linear>(d, d, false);
|
||||
blocks[attn + "k_proj" + suffix] = std::make_shared<Linear>(d, config.kv_heads * config.head_dim, false);
|
||||
blocks[attn + "v_proj" + suffix] = std::make_shared<Linear>(d, config.kv_heads * config.head_dim, false);
|
||||
blocks[attn + "q_norm" + suffix] = std::make_shared<RMSNorm>(config.head_dim, 1e-6f);
|
||||
blocks[attn + "k_norm" + suffix] = std::make_shared<RMSNorm>(config.head_dim, 1e-6f);
|
||||
blocks[dual ? "attn.proj" + suffix : "attn_proj"] = std::make_shared<Linear>(d, d, true);
|
||||
blocks["attn_gate" + suffix] = std::make_shared<Linear>(d, d, false);
|
||||
blocks["norm" + suffix + "1"] = std::make_shared<RMSNorm>(d, 1e-6f);
|
||||
blocks["norm" + suffix + "2"] = std::make_shared<RMSNorm>(d, 1e-6f);
|
||||
blocks["attn_post_norm" + suffix] = std::make_shared<RMSNorm>(d, 1e-6f);
|
||||
blocks["mlp_post_norm" + suffix] = std::make_shared<RMSNorm>(d, 1e-6f);
|
||||
blocks["mlp" + suffix] = std::make_shared<Pid::FeedForward>(d, config.mlp_dim);
|
||||
}
|
||||
}
|
||||
|
||||
void init_params(ggml_context* ctx, const String2TensorStorage& = {}, const std::string = "") override {
|
||||
if (dual) {
|
||||
params["adaln_img.bias"] = ggml_new_tensor_1d(ctx, GGML_TYPE_F32, 6 * config.hidden_size);
|
||||
params["adaln_txt.bias"] = ggml_new_tensor_1d(ctx, GGML_TYPE_F32, 6 * config.hidden_size);
|
||||
} else {
|
||||
params["adaln.bias"] = ggml_new_tensor_1d(ctx, GGML_TYPE_F32, 6 * config.hidden_size);
|
||||
}
|
||||
}
|
||||
|
||||
ggml_tensor* norm(GGMLRunnerContext* ctx, const std::string& name, ggml_tensor* x) {
|
||||
return std::dynamic_pointer_cast<RMSNorm>(blocks[name])->forward(ctx, x);
|
||||
}
|
||||
|
||||
ggml_tensor* linear(GGMLRunnerContext* ctx, const std::string& name, ggml_tensor* x) {
|
||||
return std::dynamic_pointer_cast<Linear>(blocks[name])->forward(ctx, x);
|
||||
}
|
||||
|
||||
std::array<ggml_tensor*, 3> project(GGMLRunnerContext* ctx, ggml_tensor* h, const std::string& suffix) {
|
||||
auto c = ctx->ggml_ctx;
|
||||
std::string a = dual ? "attn." : "";
|
||||
auto q = linear(ctx, a + "q_proj" + suffix, h);
|
||||
auto k = linear(ctx, a + "k_proj" + suffix, h);
|
||||
auto v = linear(ctx, a + "v_proj" + suffix, h);
|
||||
q = ggml_reshape_4d(c, q, config.head_dim, config.num_heads, h->ne[1], h->ne[2]);
|
||||
k = ggml_reshape_4d(c, k, config.head_dim, config.kv_heads, h->ne[1], h->ne[2]);
|
||||
v = ggml_reshape_4d(c, v, config.head_dim, config.kv_heads, h->ne[1], h->ne[2]);
|
||||
return {norm(ctx, a + "q_norm" + suffix, q), norm(ctx, a + "k_norm" + suffix, k), v};
|
||||
}
|
||||
|
||||
ggml_tensor* finish(GGMLRunnerContext* ctx, ggml_tensor* x, ggml_tensor* h, ggml_tensor* out, const std::vector<ggml_tensor*>& mods, const std::string& suffix) {
|
||||
auto c = ctx->ggml_ctx;
|
||||
out = ggml_mul(c, out, ggml_sigmoid(c, linear(ctx, "attn_gate" + suffix, h)));
|
||||
out = linear(ctx, dual ? "attn.proj" + suffix : "attn_proj", out);
|
||||
out = norm(ctx, "attn_post_norm" + suffix, out);
|
||||
x = ggml_add(c, x, ggml_mul(c, out, mods[2]));
|
||||
h = Pid::apply_adaln(c, norm(ctx, "norm" + suffix + "2", x), mods[3], mods[4]);
|
||||
h = std::dynamic_pointer_cast<Pid::FeedForward>(blocks["mlp" + suffix])->forward(ctx, h);
|
||||
h = norm(ctx, "mlp_post_norm" + suffix, h);
|
||||
return ggml_add(c, x, ggml_mul(c, h, mods[5]));
|
||||
}
|
||||
|
||||
std::pair<ggml_tensor*, ggml_tensor*> forward(GGMLRunnerContext* ctx, ggml_tensor* x, ggml_tensor* y, ggml_tensor* img_mod, ggml_tensor* txt_mod, ggml_tensor* pe) {
|
||||
auto c = ctx->ggml_ctx;
|
||||
int64_t nt = y->ne[1];
|
||||
auto mx = ggml_ext_chunk(c, ggml_add(c, img_mod, params[dual ? "adaln_img.bias" : "adaln.bias"]), 6, 0);
|
||||
if (!dual) {
|
||||
auto tokens = ggml_concat(c, y, x, 1);
|
||||
auto h = Pid::apply_adaln(c, norm(ctx, "norm1", tokens), mx[0], mx[1]);
|
||||
auto qkv = project(ctx, h, "");
|
||||
auto out = Rope::attention(ctx, qkv[0], qkv[1], qkv[2], pe, nullptr);
|
||||
tokens = finish(ctx, tokens, h, out, mx, "");
|
||||
return {ggml_ext_slice(c, tokens, 1, nt, tokens->ne[1]), ggml_ext_slice(c, tokens, 1, 0, nt)};
|
||||
}
|
||||
auto my = ggml_ext_chunk(c, ggml_add(c, txt_mod, params["adaln_txt.bias"]), 6, 0);
|
||||
auto hx = Pid::apply_adaln(c, norm(ctx, "norm_x1", x), mx[0], mx[1]);
|
||||
auto hy = Pid::apply_adaln(c, norm(ctx, "norm_y1", y), my[0], my[1]);
|
||||
auto qx = project(ctx, hx, "_x");
|
||||
auto qy = project(ctx, hy, "_y");
|
||||
auto out = Rope::attention(ctx, ggml_concat(c, qy[0], qx[0], 2), ggml_concat(c, qy[1], qx[1], 2),
|
||||
ggml_concat(c, qy[2], qx[2], 2), pe, nullptr);
|
||||
x = finish(ctx, x, hx, ggml_ext_slice(c, out, 1, nt, out->ne[1]), mx, "_x");
|
||||
y = finish(ctx, y, hy, ggml_ext_slice(c, out, 1, 0, nt), my, "_y");
|
||||
return {x, y};
|
||||
}
|
||||
};
|
||||
|
||||
struct PixelBlock : public GGMLBlock {
|
||||
IrisConfig config;
|
||||
|
||||
PixelBlock(const IrisConfig& config)
|
||||
: config(config) {
|
||||
auto d = config.pixel_dim;
|
||||
auto flat = d * config.patch_size * config.patch_size;
|
||||
blocks["norm1"] = std::make_shared<RMSNorm>(d, 1e-6f);
|
||||
blocks["norm2"] = std::make_shared<RMSNorm>(d, 1e-6f);
|
||||
blocks["compress"] = std::make_shared<Linear>(flat, config.pixel_attn_dim, true);
|
||||
blocks["expand"] = std::make_shared<Linear>(config.pixel_attn_dim, flat, true);
|
||||
blocks["adaln"] = std::make_shared<Linear>(config.hidden_size, 4 * flat, true);
|
||||
blocks["attn"] = std::make_shared<SelfAttention>(config.pixel_attn_dim, config.pixel_heads);
|
||||
blocks["mlp.fc1"] = std::make_shared<Linear>(d, d * 4, true);
|
||||
blocks["mlp.fc2"] = std::make_shared<Linear>(d * 4, d, true);
|
||||
}
|
||||
|
||||
ggml_tensor* forward(GGMLRunnerContext* ctx, ggml_tensor* x, ggml_tensor* cond, ggml_tensor* pe) {
|
||||
auto c = ctx->ggml_ctx;
|
||||
auto d = config.pixel_dim;
|
||||
auto p2 = config.patch_size * config.patch_size;
|
||||
auto patches = cond->ne[1];
|
||||
auto batch = cond->ne[2];
|
||||
auto mods = std::dynamic_pointer_cast<Linear>(blocks["adaln"])->forward(ctx, cond);
|
||||
mods = ggml_reshape_3d(c, mods, 4 * d, p2, patches * batch);
|
||||
auto m = ggml_ext_chunk(c, mods, 4, 0);
|
||||
auto h = std::dynamic_pointer_cast<RMSNorm>(blocks["norm1"])->forward(ctx, x);
|
||||
h = ggml_reshape_3d(c, h, d * p2, patches, batch);
|
||||
h = std::dynamic_pointer_cast<Linear>(blocks["compress"])->forward(ctx, h);
|
||||
h = std::dynamic_pointer_cast<SelfAttention>(blocks["attn"])->forward(ctx, h, pe);
|
||||
h = std::dynamic_pointer_cast<Linear>(blocks["expand"])->forward(ctx, h);
|
||||
h = ggml_reshape_3d(c, h, d, p2, patches * batch);
|
||||
x = ggml_add(c, x, Pid::apply_adaln(c, h, m[1], m[0]));
|
||||
h = std::dynamic_pointer_cast<RMSNorm>(blocks["norm2"])->forward(ctx, x);
|
||||
h = std::dynamic_pointer_cast<Linear>(blocks["mlp.fc1"])->forward(ctx, h);
|
||||
h = ggml_gelu_erf(c, h);
|
||||
h = std::dynamic_pointer_cast<Linear>(blocks["mlp.fc2"])->forward(ctx, h);
|
||||
return ggml_add(c, x, Pid::apply_adaln(c, h, m[3], m[2]));
|
||||
}
|
||||
};
|
||||
|
||||
struct IrisModel : public GGMLBlock {
|
||||
IrisConfig config;
|
||||
|
||||
IrisModel(const IrisConfig& config)
|
||||
: config(config) {
|
||||
blocks["s_embedder"] = std::make_shared<Pid::PatchTokenEmbedder>(3 * config.patch_size * config.patch_size, config.hidden_size);
|
||||
blocks["t_embedder"] = std::make_shared<Pid::PixelDiTTimestepEmbedder>(config.hidden_size);
|
||||
blocks["y_embedder"] = std::make_shared<TextEmbedder>(config);
|
||||
blocks["modulation_cores.adaln_img"] = std::make_shared<Linear>(config.hidden_size, 6 * config.hidden_size, true);
|
||||
blocks["modulation_cores.adaln_txt"] = std::make_shared<Linear>(config.hidden_size, 6 * config.hidden_size, true);
|
||||
for (int64_t i = 0; i < config.depth; ++i) {
|
||||
blocks["blocks." + std::to_string(i)] = std::make_shared<TrunkBlock>(config, i < config.dual_depth);
|
||||
}
|
||||
blocks["pixel_embedder"] = std::make_shared<Pid::PixelTokenEmbedder>(3, config.pixel_dim);
|
||||
for (int64_t i = 0; i < config.pixel_depth; ++i) {
|
||||
blocks["pixel_blocks." + std::to_string(i)] = std::make_shared<PixelBlock>(config);
|
||||
}
|
||||
blocks["final_layer"] = std::make_shared<Pid::FinalLayer>(config.pixel_dim, 3);
|
||||
}
|
||||
|
||||
void init_params(ggml_context* ctx, const String2TensorStorage& = {}, const std::string = "") override {
|
||||
params["y_pos_embedding"] = ggml_new_tensor_3d(ctx, GGML_TYPE_F32, config.hidden_size, config.text_len, 1);
|
||||
}
|
||||
|
||||
ggml_tensor* forward(GGMLRunnerContext* ctx, ggml_tensor* image, ggml_tensor* t, ggml_tensor* text, ggml_tensor* mask, ggml_tensor* joint_pe, ggml_tensor* pixel_pe, ggml_tensor* pixel_pos) {
|
||||
auto c = ctx->ggml_ctx;
|
||||
int p = static_cast<int>(config.patch_size);
|
||||
auto x = DiT::patchify(c, image, p, p, true);
|
||||
x = std::dynamic_pointer_cast<Pid::PatchTokenEmbedder>(blocks["s_embedder"])->forward(ctx, x);
|
||||
auto te = std::dynamic_pointer_cast<Pid::PixelDiTTimestepEmbedder>(blocks["t_embedder"])->forward(ctx, t);
|
||||
te = ggml_reshape_3d(c, te, config.hidden_size, 1, image->ne[3]);
|
||||
auto cond = ggml_silu(c, te);
|
||||
auto im = std::dynamic_pointer_cast<Linear>(blocks["modulation_cores.adaln_img"])->forward(ctx, cond);
|
||||
auto tm = std::dynamic_pointer_cast<Linear>(blocks["modulation_cores.adaln_txt"])->forward(ctx, cond);
|
||||
auto y = std::dynamic_pointer_cast<TextEmbedder>(blocks["y_embedder"])->forward(ctx, text, mask);
|
||||
y = ggml_add(c, y, params["y_pos_embedding"]);
|
||||
for (int64_t i = 0; i < config.depth; ++i) {
|
||||
auto block = std::dynamic_pointer_cast<TrunkBlock>(blocks["blocks." + std::to_string(i)]);
|
||||
std::tie(x, y) = block->forward(ctx, x, y, im, tm, joint_pe);
|
||||
sd::ggml_graph_cut::mark_graph_cut(x, "iris.blocks." + std::to_string(i), "x");
|
||||
sd::ggml_graph_cut::mark_graph_cut(y, "iris.blocks." + std::to_string(i), "y");
|
||||
}
|
||||
cond = ggml_silu(c, ggml_add(c, x, te));
|
||||
x = std::dynamic_pointer_cast<Pid::PixelTokenEmbedder>(blocks["pixel_embedder"])->forward(ctx, image, p, pixel_pos);
|
||||
for (int64_t i = 0; i < config.pixel_depth; ++i) {
|
||||
x = std::dynamic_pointer_cast<PixelBlock>(blocks["pixel_blocks." + std::to_string(i)])->forward(ctx, x, cond, pixel_pe);
|
||||
sd::ggml_graph_cut::mark_graph_cut(x, "iris.pixel_blocks." + std::to_string(i), "x");
|
||||
}
|
||||
x = std::dynamic_pointer_cast<Pid::FinalLayer>(blocks["final_layer"])->forward(ctx, x);
|
||||
x = ggml_reshape_3d(c, x, 3 * p * p, cond->ne[1], image->ne[3]);
|
||||
return DiT::unpatchify(c, x, image->ne[1] / p, image->ne[0] / p, p, p, false);
|
||||
}
|
||||
};
|
||||
|
||||
inline std::vector<float> image_rope(int64_t height, int64_t width, int64_t dim) {
|
||||
std::vector<float> pe;
|
||||
pe.reserve(height * width * dim * 2);
|
||||
float step = 16.f / static_cast<float>(std::max<int64_t>(std::max(height, width) - 1, 1));
|
||||
for (int64_t y = 0; y < height; ++y) {
|
||||
for (int64_t x = 0; x < width; ++x) {
|
||||
for (int64_t i = 0; i < dim / 4; ++i) {
|
||||
float frequency = std::pow(10000.f, -4.f * static_cast<float>(i) / static_cast<float>(dim));
|
||||
for (int64_t pos : {x, y}) {
|
||||
float angle = static_cast<float>(pos) * step * frequency;
|
||||
pe.insert(pe.end(), {std::cos(angle), -std::sin(angle), std::sin(angle), std::cos(angle)});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return pe;
|
||||
}
|
||||
|
||||
struct IrisRunner : public DiffusionModelRunner {
|
||||
IrisConfig config;
|
||||
IrisModel model;
|
||||
std::vector<float> joint_pe_data;
|
||||
std::vector<float> pixel_pe_data;
|
||||
std::vector<float> pixel_pos_data;
|
||||
std::vector<float> text_mask_data;
|
||||
|
||||
IrisRunner(ggml_backend_t backend, const String2TensorStorage& weights, const std::string& prefix, std::shared_ptr<RunnerWeightManager> weight_manager = nullptr)
|
||||
: DiffusionModelRunner(backend, prefix, weight_manager),
|
||||
config(IrisConfig::detect_from_weights(weights, prefix)),
|
||||
model(config) {
|
||||
model.init(params_ctx, weights, prefix);
|
||||
}
|
||||
|
||||
std::string get_desc() override { return "Iris-3B"; }
|
||||
|
||||
void get_param_tensors(std::map<std::string, ggml_tensor*>& tensors, const std::string& prefix) override {
|
||||
model.get_param_tensors(tensors, prefix);
|
||||
}
|
||||
|
||||
sd::Tensor<float> compute(int n_threads, const DiffusionParams& inputs) override {
|
||||
if (!inputs.x || !inputs.timesteps || !inputs.context || !inputs.y || inputs.y->empty()) {
|
||||
LOG_ERROR("Iris-3B requires text features and their padding mask");
|
||||
return {};
|
||||
}
|
||||
auto get_graph = [&]() {
|
||||
auto gf = new_graph_custom(196608);
|
||||
auto x = make_input(*inputs.x);
|
||||
auto t = make_input(*inputs.timesteps);
|
||||
auto text = make_input(*inputs.context);
|
||||
int h = static_cast<int>(x->ne[1]);
|
||||
int w = static_cast<int>(x->ne[0]);
|
||||
int nt = static_cast<int>(config.text_len);
|
||||
GGML_ASSERT(h % config.patch_size == 0 && w % config.patch_size == 0);
|
||||
GGML_ASSERT(text->ne[0] == config.text_dim * config.text_layers && text->ne[1] == nt);
|
||||
GGML_ASSERT(inputs.y->numel() == nt && x->ne[3] == 1);
|
||||
joint_pe_data = Pid::make_rope_1d(nt, static_cast<int>(config.head_dim), 10000.f);
|
||||
auto img = image_rope(h / config.patch_size, w / config.patch_size, config.head_dim);
|
||||
joint_pe_data.insert(joint_pe_data.end(), img.begin(), img.end());
|
||||
pixel_pe_data = image_rope(h / config.patch_size, w / config.patch_size, config.pixel_attn_dim / config.pixel_heads);
|
||||
pixel_pos_data = Pid::make_pixel_abs_pos(h, w, static_cast<int>(config.pixel_dim));
|
||||
text_mask_data.resize(nt * nt);
|
||||
for (int q = 0; q < nt; ++q) {
|
||||
for (int k = 0; k < nt; ++k) {
|
||||
text_mask_data[q * nt + k] = inputs.y->values()[k] != 0.f || q == k ? 0.f : -INFINITY;
|
||||
}
|
||||
}
|
||||
auto pe = ggml_new_tensor_4d(compute_ctx, GGML_TYPE_F32, 2, 2, config.head_dim / 2, nt + img.size() / (2 * config.head_dim));
|
||||
auto pp = ggml_new_tensor_4d(compute_ctx, GGML_TYPE_F32, 2, 2, config.pixel_attn_dim / config.pixel_heads / 2, (h / config.patch_size) * (w / config.patch_size));
|
||||
auto pos = ggml_new_tensor_3d(compute_ctx, GGML_TYPE_F32, config.pixel_dim, h * w, 1);
|
||||
auto mask = ggml_new_tensor_2d(compute_ctx, GGML_TYPE_F32, nt, nt);
|
||||
set_backend_tensor_data(pe, joint_pe_data.data());
|
||||
set_backend_tensor_data(pp, pixel_pe_data.data());
|
||||
set_backend_tensor_data(pos, pixel_pos_data.data());
|
||||
set_backend_tensor_data(mask, text_mask_data.data());
|
||||
auto ctx = get_context();
|
||||
auto out = model.forward(&ctx, x, t, text, mask, pe, pp, pos);
|
||||
ggml_build_forward_expand(gf, out);
|
||||
return gf;
|
||||
};
|
||||
return restore_trailing_singleton_dims(GGMLRunner::compute(get_graph, n_threads, false), inputs.x->dim());
|
||||
}
|
||||
};
|
||||
} // namespace Iris
|
||||
|
||||
#endif // __SD_MODEL_DIFFUSION_IRIS_HPP__
|
||||
@@ -548,21 +548,22 @@ namespace LTXV {
|
||||
return build_rope_matrix_from_frequencies(freqs, dim);
|
||||
}
|
||||
|
||||
__STATIC_INLINE__ ggml_tensor* apply_hidden_rope(ggml_context* ctx,
|
||||
__STATIC_INLINE__ ggml_tensor* apply_hidden_rope(GGMLRunnerContext* runner_ctx,
|
||||
ggml_tensor* x,
|
||||
ggml_tensor* pe,
|
||||
int64_t heads,
|
||||
int64_t dim_head,
|
||||
bool rope_interleaved) {
|
||||
auto ctx = runner_ctx->ggml_ctx;
|
||||
GGML_ASSERT(x->ne[0] == heads * dim_head);
|
||||
auto x4 = ggml_reshape_4d(ctx, x, dim_head, heads, x->ne[1], x->ne[2]);
|
||||
if (pe != nullptr && pe->ne[3] == x->ne[1] * heads) {
|
||||
auto x_flat = ggml_reshape_4d(ctx, x4, dim_head, 1, x->ne[1] * heads, x->ne[2]);
|
||||
auto out_flat = Rope::apply_rope(ctx, x_flat, pe, rope_interleaved);
|
||||
auto out_flat = Rope::apply_rope(runner_ctx, x_flat, pe, rope_interleaved);
|
||||
auto out4 = ggml_reshape_4d(ctx, out_flat, dim_head, heads, x->ne[1], x->ne[2]);
|
||||
return ggml_reshape_3d(ctx, out4, heads * dim_head, x->ne[1], x->ne[2]);
|
||||
}
|
||||
return Rope::apply_rope(ctx, x4, pe, rope_interleaved);
|
||||
return Rope::apply_rope(runner_ctx, x4, pe, rope_interleaved);
|
||||
}
|
||||
|
||||
struct TimestepEmbedder : public GGMLBlock {
|
||||
@@ -705,8 +706,8 @@ namespace LTXV {
|
||||
if (k_pe == nullptr) {
|
||||
k_pe = pe;
|
||||
}
|
||||
q = apply_hidden_rope(ctx->ggml_ctx, q, pe, heads, dim_head, rope_interleaved);
|
||||
k = apply_hidden_rope(ctx->ggml_ctx, k, k_pe, heads, dim_head, rope_interleaved);
|
||||
q = apply_hidden_rope(ctx, q, pe, heads, dim_head, rope_interleaved);
|
||||
k = apply_hidden_rope(ctx, k, k_pe, heads, dim_head, rope_interleaved);
|
||||
}
|
||||
|
||||
auto out = ggml_ext_attention_ext(ctx,
|
||||
|
||||
@@ -160,12 +160,13 @@ namespace MiniMaxH3 {
|
||||
return ggml_reshape_3d(ctx, x, x->ne[0], x->ne[1], x->ne[2] * x->ne[3]);
|
||||
}
|
||||
|
||||
static ggml_tensor* apply_partial_rope(ggml_context* ctx,
|
||||
static ggml_tensor* apply_partial_rope(GGMLRunnerContext* runner_ctx,
|
||||
ggml_tensor* x,
|
||||
ggml_tensor* pe) {
|
||||
auto ctx = runner_ctx->ggml_ctx;
|
||||
int64_t rot_dim = pe->ne[2] * 2;
|
||||
GGML_ASSERT(rot_dim <= x->ne[0]);
|
||||
auto rotated = Rope::apply_rope(ctx,
|
||||
auto rotated = Rope::apply_rope(runner_ctx,
|
||||
ggml_ext_slice(ctx, x, 0, 0, rot_dim),
|
||||
pe,
|
||||
false);
|
||||
@@ -209,8 +210,8 @@ namespace MiniMaxH3 {
|
||||
q = q_norm->forward(ctx, q);
|
||||
k = k_norm->forward(ctx, k);
|
||||
if (pe != nullptr) {
|
||||
q = apply_partial_rope(ctx->ggml_ctx, q, pe);
|
||||
k = apply_partial_rope(ctx->ggml_ctx, k, pe);
|
||||
q = apply_partial_rope(ctx, q, pe);
|
||||
k = apply_partial_rope(ctx, k, pe);
|
||||
} else {
|
||||
q = attention_layout(ctx->ggml_ctx, q);
|
||||
k = attention_layout(ctx->ggml_ctx, k);
|
||||
|
||||
@@ -188,8 +188,8 @@ namespace Qwen {
|
||||
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);
|
||||
q = Rope::apply_rope(ctx, q, pe);
|
||||
k = Rope::apply_rope(ctx, k, pe);
|
||||
if (cache.mode == QwenImage21PrefixCache::Mode::STORE) {
|
||||
// Preserve query-first attention evaluation while writing each layer's
|
||||
// prefix before its full-sequence K/V can accumulate across layers.
|
||||
|
||||
@@ -587,7 +587,9 @@ public:
|
||||
label_emb = ggml_silu_inplace(ctx->ggml_ctx, label_emb);
|
||||
label_emb = label_embed_2->forward(ctx, label_emb); // [N, time_embed_dim]
|
||||
|
||||
emb = ggml_add(ctx->ggml_ctx, emb, label_emb); // [N, time_embed_dim]
|
||||
emb = label_emb->ne[1] > emb->ne[1]
|
||||
? ggml_add(ctx->ggml_ctx, label_emb, emb)
|
||||
: ggml_add(ctx->ggml_ctx, emb, label_emb); // [N, time_embed_dim]
|
||||
}
|
||||
// sd::ggml_graph_cut::mark_graph_cut(emb, "unet.prelude", "emb");
|
||||
|
||||
|
||||
@@ -228,11 +228,12 @@ namespace MiniMaxH3VAE {
|
||||
return ggml_reshape_3d(ctx, x, x->ne[0], x->ne[1], x->ne[2] * x->ne[3]);
|
||||
}
|
||||
|
||||
static ggml_tensor* apply_partial_rope(ggml_context* ctx,
|
||||
static ggml_tensor* apply_partial_rope(GGMLRunnerContext* runner_ctx,
|
||||
ggml_tensor* x,
|
||||
ggml_tensor* pe) {
|
||||
auto ctx = runner_ctx->ggml_ctx;
|
||||
int64_t rot_dim = pe->ne[2] * 2;
|
||||
auto rotated = Rope::apply_rope(ctx,
|
||||
auto rotated = Rope::apply_rope(runner_ctx,
|
||||
ggml_ext_slice(ctx, x, 0, 0, rot_dim),
|
||||
pe,
|
||||
false);
|
||||
@@ -289,8 +290,8 @@ namespace MiniMaxH3VAE {
|
||||
batch_size);
|
||||
q = ggml_rms_norm(ctx->ggml_ctx, q, 1e-5f);
|
||||
k = ggml_rms_norm(ctx->ggml_ctx, k, 1e-5f);
|
||||
q = apply_partial_rope(ctx->ggml_ctx, q, pe);
|
||||
k = apply_partial_rope(ctx->ggml_ctx, k, pe);
|
||||
q = apply_partial_rope(ctx, q, pe);
|
||||
k = apply_partial_rope(ctx, k, pe);
|
||||
auto out = ggml_ext_attention_ext(ctx,
|
||||
q,
|
||||
k,
|
||||
@@ -633,8 +634,11 @@ namespace MiniMaxH3VAE {
|
||||
auto plan = make_vae_temporal_tile_plan(input.shape()[2], {17, 0});
|
||||
auto result = process_vae_temporal_tiles(input, plan, [&](const sd::Tensor<float>& chunk, const VAETemporalTile& tile) {
|
||||
SD_UNUSED(tile);
|
||||
return VAE::encode(n_threads, chunk, tiling, circular_x, circular_y);
|
||||
// keep the runner alive across chunks; ending it here would
|
||||
// evict and reload the encoder weights every chunk
|
||||
return VAE::encode(n_threads, chunk, tiling, circular_x, circular_y, false);
|
||||
});
|
||||
runner_end();
|
||||
if (result.empty()) {
|
||||
return {};
|
||||
}
|
||||
@@ -695,13 +699,16 @@ namespace MiniMaxH3VAE {
|
||||
{static_cast<int>(tokens_per_chunk + token_overlap), static_cast<int>(token_overlap)});
|
||||
GGML_ASSERT(plan.tiles.size() == static_cast<size_t>(num_chunks));
|
||||
auto result = process_vae_temporal_tiles(input, plan, [&](const sd::Tensor<float>& chunk, const VAETemporalTile& tile) {
|
||||
// keep the runner alive across chunks; ending it here would
|
||||
// evict and reload the decoder weights every chunk
|
||||
auto decoded = VAE::decode(n_threads,
|
||||
chunk,
|
||||
tiling,
|
||||
true,
|
||||
circular_x,
|
||||
circular_y,
|
||||
silent);
|
||||
silent,
|
||||
false);
|
||||
if (decoded.empty()) {
|
||||
return sd::Tensor<float>();
|
||||
}
|
||||
@@ -728,6 +735,7 @@ namespace MiniMaxH3VAE {
|
||||
}
|
||||
return first;
|
||||
});
|
||||
runner_end();
|
||||
if (result.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
+38
-13
@@ -84,12 +84,17 @@ class TinyEncoder : public UnaryBlock {
|
||||
int channels = 64;
|
||||
int z_channels = 4;
|
||||
int num_blocks = 3;
|
||||
bool f16;
|
||||
|
||||
public:
|
||||
TinyEncoder(int z_channels = 4, bool use_midblock_gn = false)
|
||||
: z_channels(z_channels) {
|
||||
int index = 0;
|
||||
TinyEncoder(int z_channels = 4, bool use_midblock_gn = false, bool f16 = false)
|
||||
: z_channels(z_channels), f16(f16) {
|
||||
in_channels = f16 ? 16 : 3;
|
||||
int index = f16 ? 1 : 0;
|
||||
blocks[std::to_string(index++)] = std::shared_ptr<GGMLBlock>(new Conv2d(in_channels, channels, {3, 3}, {1, 1}, {1, 1}));
|
||||
if (f16) {
|
||||
index++; // nn.ReLU()
|
||||
}
|
||||
blocks[std::to_string(index++)] = std::shared_ptr<GGMLBlock>(new TAEBlock(channels, channels));
|
||||
|
||||
blocks[std::to_string(index++)] = std::shared_ptr<GGMLBlock>(new Conv2d(channels, channels, {3, 3}, {2, 2}, {1, 1}, {1, 1}, false));
|
||||
@@ -97,12 +102,14 @@ public:
|
||||
blocks[std::to_string(index++)] = std::shared_ptr<GGMLBlock>(new TAEBlock(channels, channels));
|
||||
}
|
||||
|
||||
blocks[std::to_string(index++)] = std::shared_ptr<GGMLBlock>(new Conv2d(channels, channels, {3, 3}, {2, 2}, {1, 1}, {1, 1}, false));
|
||||
blocks[std::to_string(index++)] = std::shared_ptr<GGMLBlock>(new Conv2d(channels, f16 ? channels * 2 : channels, {3, 3}, {2, 2}, {1, 1}, {1, 1}, false));
|
||||
channels *= f16 ? 2 : 1;
|
||||
for (int i = 0; i < num_blocks; i++) {
|
||||
blocks[std::to_string(index++)] = std::shared_ptr<GGMLBlock>(new TAEBlock(channels, channels));
|
||||
}
|
||||
|
||||
blocks[std::to_string(index++)] = std::shared_ptr<GGMLBlock>(new Conv2d(channels, channels, {3, 3}, {2, 2}, {1, 1}, {1, 1}, false));
|
||||
blocks[std::to_string(index++)] = std::shared_ptr<GGMLBlock>(new Conv2d(channels, f16 ? channels * 2 : channels, {3, 3}, {2, 2}, {1, 1}, {1, 1}, false));
|
||||
channels *= f16 ? 2 : 1;
|
||||
for (int i = 0; i < num_blocks; i++) {
|
||||
blocks[std::to_string(index++)] = std::shared_ptr<GGMLBlock>(new TAEBlock(channels, channels, use_midblock_gn));
|
||||
}
|
||||
@@ -114,7 +121,11 @@ public:
|
||||
// x: [n, in_channels, h, w]
|
||||
// return: [n, z_channels, h/8, w/8]
|
||||
|
||||
for (int i = 0; i < num_blocks * 3 + 6; i++) {
|
||||
for (int i = f16 ? 1 : 0; i < num_blocks * 3 + 6 + (f16 ? 2 : 0); i++) {
|
||||
if (f16 && i == 2) {
|
||||
x = ggml_relu_inplace(ctx->ggml_ctx, x);
|
||||
continue;
|
||||
}
|
||||
auto block = std::dynamic_pointer_cast<UnaryBlock>(blocks[std::to_string(i)]);
|
||||
|
||||
x = block->forward(ctx, x);
|
||||
@@ -131,9 +142,11 @@ class TinyDecoder : public UnaryBlock {
|
||||
int num_blocks = 3;
|
||||
|
||||
public:
|
||||
TinyDecoder(int z_channels = 4, bool use_midblock_gn = false)
|
||||
TinyDecoder(int z_channels = 4, bool use_midblock_gn = false, bool f16 = false)
|
||||
: z_channels(z_channels) {
|
||||
int index = 0;
|
||||
channels = f16 ? 256 : 64;
|
||||
out_channels = f16 ? 16 : 3;
|
||||
int index = 0;
|
||||
|
||||
blocks[std::to_string(index++)] = std::shared_ptr<GGMLBlock>(new Conv2d(z_channels, channels, {3, 3}, {1, 1}, {1, 1}));
|
||||
index++; // nn.ReLU()
|
||||
@@ -142,13 +155,15 @@ public:
|
||||
blocks[std::to_string(index++)] = std::shared_ptr<GGMLBlock>(new TAEBlock(channels, channels, use_midblock_gn));
|
||||
}
|
||||
index++; // nn.Upsample()
|
||||
blocks[std::to_string(index++)] = std::shared_ptr<GGMLBlock>(new Conv2d(channels, channels, {3, 3}, {1, 1}, {1, 1}, {1, 1}, false));
|
||||
blocks[std::to_string(index++)] = std::shared_ptr<GGMLBlock>(new Conv2d(channels, f16 ? channels / 2 : channels, {3, 3}, {1, 1}, {1, 1}, {1, 1}, false));
|
||||
channels /= f16 ? 2 : 1;
|
||||
|
||||
for (int i = 0; i < num_blocks; i++) {
|
||||
blocks[std::to_string(index++)] = std::shared_ptr<GGMLBlock>(new TAEBlock(channels, channels));
|
||||
}
|
||||
index++; // nn.Upsample()
|
||||
blocks[std::to_string(index++)] = std::shared_ptr<GGMLBlock>(new Conv2d(channels, channels, {3, 3}, {1, 1}, {1, 1}, {1, 1}, false));
|
||||
blocks[std::to_string(index++)] = std::shared_ptr<GGMLBlock>(new Conv2d(channels, f16 ? channels / 2 : channels, {3, 3}, {1, 1}, {1, 1}, {1, 1}, false));
|
||||
channels /= f16 ? 2 : 1;
|
||||
|
||||
for (int i = 0; i < num_blocks; i++) {
|
||||
blocks[std::to_string(index++)] = std::shared_ptr<GGMLBlock>(new TAEBlock(channels, channels));
|
||||
@@ -691,6 +706,7 @@ class TAESD : public GGMLBlock {
|
||||
protected:
|
||||
bool decode_only;
|
||||
bool taef2 = false;
|
||||
bool f16 = false;
|
||||
|
||||
public:
|
||||
int z_channels = 4;
|
||||
@@ -708,10 +724,14 @@ public:
|
||||
z_channels = 32;
|
||||
use_midblock_gn = true;
|
||||
}
|
||||
blocks["decoder.layers"] = std::shared_ptr<GGMLBlock>(new TinyDecoder(z_channels, use_midblock_gn));
|
||||
f16 = version == VERSION_QWEN_IMAGE_2_1;
|
||||
if (f16) {
|
||||
z_channels = 64;
|
||||
}
|
||||
blocks["decoder.layers"] = std::shared_ptr<GGMLBlock>(new TinyDecoder(z_channels, use_midblock_gn, f16));
|
||||
|
||||
if (!decode_only) {
|
||||
blocks["encoder.layers"] = std::shared_ptr<GGMLBlock>(new TinyEncoder(z_channels, use_midblock_gn));
|
||||
blocks["encoder.layers"] = std::shared_ptr<GGMLBlock>(new TinyEncoder(z_channels, use_midblock_gn, f16));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -720,10 +740,15 @@ public:
|
||||
if (taef2) {
|
||||
z = unpatchify(ctx->ggml_ctx, z, 2);
|
||||
}
|
||||
return decoder->forward(ctx, z);
|
||||
auto x = decoder->forward(ctx, z);
|
||||
return f16 ? unpatchify(ctx->ggml_ctx, x, 2) : x;
|
||||
}
|
||||
|
||||
ggml_tensor* encode(GGMLRunnerContext* ctx, ggml_tensor* x) {
|
||||
if (f16) {
|
||||
GGML_ASSERT(x->ne[0] % 2 == 0 && x->ne[1] % 2 == 0);
|
||||
x = patchify(ctx->ggml_ctx, x, 2);
|
||||
}
|
||||
auto encoder = std::dynamic_pointer_cast<TinyEncoder>(blocks["encoder.layers"]);
|
||||
auto z = encoder->forward(ctx, x);
|
||||
if (taef2) {
|
||||
|
||||
+37
-4
@@ -176,11 +176,20 @@ public:
|
||||
int scale_factor = 8;
|
||||
if (version == VERSION_LTXAV) {
|
||||
scale_factor = 32;
|
||||
} else if (version == VERSION_WAN2_2_TI2V || version == VERSION_QWEN_IMAGE_2_1 || sd_version_is_hunyuan_video(version) || sd_version_is_mage_flow(version) || sd_version_is_minimax_h3(version)) {
|
||||
} else if (version == VERSION_WAN2_2_TI2V ||
|
||||
version == VERSION_QWEN_IMAGE_2_1 ||
|
||||
sd_version_is_hunyuan_video(version) ||
|
||||
sd_version_is_mage_flow(version) ||
|
||||
sd_version_is_minimax_h3(version)) {
|
||||
scale_factor = 16;
|
||||
} else if (sd_version_uses_flux2_vae(version)) {
|
||||
scale_factor = 16;
|
||||
} else if (version == VERSION_CHROMA_RADIANCE || version == VERSION_HIDREAM_O1 || sd_version_is_minit2i(version) || sd_version_is_sensenova_u1(version) || sd_version_is_z_image_l2p(version)) {
|
||||
} else if (version == VERSION_CHROMA_RADIANCE ||
|
||||
version == VERSION_HIDREAM_O1 ||
|
||||
sd_version_is_minit2i(version) ||
|
||||
sd_version_is_sensenova_u1(version) ||
|
||||
sd_version_is_z_image_l2p(version) ||
|
||||
version == VERSION_IRIS) {
|
||||
scale_factor = 1;
|
||||
}
|
||||
return scale_factor;
|
||||
@@ -241,6 +250,15 @@ public:
|
||||
sd_tiling_params_t tiling_params,
|
||||
bool circular_x = false,
|
||||
bool circular_y = false) {
|
||||
return encode(n_threads, x, tiling_params, circular_x, circular_y, true);
|
||||
}
|
||||
|
||||
sd::Tensor<float> encode(int n_threads,
|
||||
const sd::Tensor<float>& x,
|
||||
sd_tiling_params_t tiling_params,
|
||||
bool circular_x,
|
||||
bool circular_y,
|
||||
bool auto_end) {
|
||||
int64_t t0 = ggml_time_ms();
|
||||
tiling_params = resolve_tiling_params(tiling_params);
|
||||
sd::Tensor<float> input = x;
|
||||
@@ -280,7 +298,9 @@ public:
|
||||
tiling_params);
|
||||
}
|
||||
|
||||
runner_end();
|
||||
if (auto_end) {
|
||||
runner_end();
|
||||
}
|
||||
|
||||
if (output.empty()) {
|
||||
LOG_ERROR("vae encode compute failed");
|
||||
@@ -298,6 +318,17 @@ public:
|
||||
bool circular_x = false,
|
||||
bool circular_y = false,
|
||||
bool silent = false) {
|
||||
return decode(n_threads, x, tiling_params, decode_video, circular_x, circular_y, silent, true);
|
||||
}
|
||||
|
||||
sd::Tensor<float> decode(int n_threads,
|
||||
const sd::Tensor<float>& x,
|
||||
sd_tiling_params_t tiling_params,
|
||||
bool decode_video,
|
||||
bool circular_x,
|
||||
bool circular_y,
|
||||
bool silent,
|
||||
bool auto_end) {
|
||||
int64_t t0 = ggml_time_ms();
|
||||
tiling_params = resolve_tiling_params(tiling_params);
|
||||
sd::Tensor<float> input = x;
|
||||
@@ -338,7 +369,9 @@ public:
|
||||
tiling_params);
|
||||
}
|
||||
|
||||
runner_end();
|
||||
if (auto_end) {
|
||||
runner_end();
|
||||
}
|
||||
|
||||
if (output.empty()) {
|
||||
LOG_ERROR("vae decode compute failed");
|
||||
|
||||
@@ -556,6 +556,10 @@ SDVersion ModelLoader::get_sd_version() const {
|
||||
tensor_storage_map.find("model.diffusion_model.audio_patch_proj.weight") != tensor_storage_map.end()) {
|
||||
return VERSION_MINIMAX_H3;
|
||||
}
|
||||
if (name == "model.diffusion_model.y_embedder.layer_pool.weight" &&
|
||||
tensor_storage_map.count("model.diffusion_model.modulation_cores.adaln_img.weight")) {
|
||||
return VERSION_IRIS;
|
||||
}
|
||||
if (tensor_storage.name.find("model.diffusion_model.blocks.0.cross_attn.norm_k.weight") != std::string::npos) {
|
||||
is_wan = true;
|
||||
}
|
||||
|
||||
@@ -1904,6 +1904,49 @@ bool ModelManager::ensure_compute_backend_capacity(
|
||||
}
|
||||
}
|
||||
|
||||
// The loader retains mmap buffers after storage blocks are released, so
|
||||
// demoting mapped states would only force duplicate allocations on reload.
|
||||
std::unordered_set<TensorState*> mapped_states;
|
||||
for (const auto& block : params_storage_blocks_) {
|
||||
if (!block->mmap_tensor_stores.empty()) {
|
||||
mapped_states.insert(block->states.begin(), block->states.end());
|
||||
}
|
||||
}
|
||||
std::unordered_set<TensorState*> required_set(required_states.begin(), required_states.end());
|
||||
std::vector<TensorState*> demoted_states;
|
||||
auto revert_demotions = [&]() {
|
||||
for (TensorState* state : demoted_states) {
|
||||
if (state->loaded_to_params_backend) {
|
||||
state->residency_mode = ResidencyMode::ParamBackend;
|
||||
}
|
||||
}
|
||||
};
|
||||
auto can_demote_to_disk = [&](TensorState* state) {
|
||||
return state != nullptr && state->tensor != nullptr && state->tensor->view_src == nullptr &&
|
||||
state->residency_mode == ResidencyMode::ParamBackend &&
|
||||
state->params_backend != nullptr && state->params_backend == state->compute_backend &&
|
||||
state->compute_backend == compute_backend && !sd_backend_is_cpu(state->params_backend) &&
|
||||
state->pin_count == 0 && !state->staged_to_compute_backend && state->loaded_to_params_backend &&
|
||||
protected_states.find(state) == protected_states.end() &&
|
||||
required_set.find(state) == required_set.end() &&
|
||||
mapped_states.find(state) == mapped_states.end() &&
|
||||
!state->sources.empty() && state->applied_lora_epoch == UINT64_MAX &&
|
||||
state->split_buffer_type == nullptr;
|
||||
};
|
||||
for (TensorState* state : global_candidates) {
|
||||
if (!can_demote_to_disk(state)) {
|
||||
continue;
|
||||
}
|
||||
state->residency_mode = ResidencyMode::Disk;
|
||||
demoted_states.push_back(state);
|
||||
add_evictable_state(state);
|
||||
if (release_eviction_states()) {
|
||||
revert_demotions();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
revert_demotions();
|
||||
|
||||
const auto capacity = check_capacity(request, required_states, true);
|
||||
const std::string available_device = capacity.available_device_bytes == SIZE_MAX
|
||||
? "unknown"
|
||||
|
||||
@@ -108,6 +108,7 @@ const char* model_version_to_str[] = {
|
||||
"PixArt",
|
||||
"Ming-Image",
|
||||
"Z-Image L2P",
|
||||
"Iris-3B",
|
||||
};
|
||||
|
||||
static_assert(VERSION_COUNT == sizeof(model_version_to_str) / sizeof(model_version_to_str[0]),
|
||||
@@ -1382,7 +1383,8 @@ bool StableDiffusionGGML::build_denoiser() {
|
||||
sd_version_is_llada_image(version) ||
|
||||
sd_version_is_boogu_image(version) ||
|
||||
sd_version_is_pid(version) ||
|
||||
sd_version_is_ideogram4(version)) {
|
||||
sd_version_is_ideogram4(version) ||
|
||||
version == VERSION_IRIS) {
|
||||
pred_type = FLOW_PRED;
|
||||
if (sd_version_is_wan(version)) {
|
||||
default_flow_shift = 5.f;
|
||||
@@ -1390,7 +1392,7 @@ bool StableDiffusionGGML::build_denoiser() {
|
||||
default_flow_shift = 7.f;
|
||||
} else if (sd_version_is_minimax_h3(version)) {
|
||||
default_flow_shift = 12.f;
|
||||
} else if (sd_version_is_ernie_image(version)) {
|
||||
} else if (sd_version_is_ernie_image(version) || version == VERSION_IRIS) {
|
||||
default_flow_shift = 4.f;
|
||||
} else if (sd_version_is_pid(version)) {
|
||||
default_flow_shift = 1.5f;
|
||||
@@ -2225,6 +2227,27 @@ void StableDiffusionGGML::report_sample_progress(int step,
|
||||
}
|
||||
}
|
||||
|
||||
static sd::Tensor<float> batch_two_condition_tensors(const sd::Tensor<float>& a, const sd::Tensor<float>& b) {
|
||||
if (a.empty() || b.empty() || a.dim() != b.dim()) {
|
||||
return {};
|
||||
}
|
||||
if (a.dim() == 1) {
|
||||
if (a.shape() != b.shape()) {
|
||||
return {};
|
||||
}
|
||||
auto batched = sd::ops::concat(a, b, 0);
|
||||
batched.reshape_({a.shape()[0], 2});
|
||||
return batched;
|
||||
}
|
||||
const int64_t batch_dim = a.dim() - 1;
|
||||
for (int64_t d = 0; d < batch_dim; d++) {
|
||||
if (a.shape()[d] != b.shape()[d]) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
return sd::ops::concat(a, b, static_cast<size_t>(batch_dim));
|
||||
}
|
||||
|
||||
void StableDiffusionGGML::compute_sample_controls(const sd::Tensor<float>& control_image,
|
||||
const sd::Tensor<float>& noised_input,
|
||||
const sd::Tensor<float>& timesteps_tensor,
|
||||
@@ -2373,7 +2396,8 @@ sd::Tensor<float> StableDiffusionGGML::sample(const std::shared_ptr<DiffusionMod
|
||||
noise *= eta;
|
||||
}
|
||||
|
||||
int64_t last_progress_us = ggml_time_us();
|
||||
int64_t last_progress_us = ggml_time_us();
|
||||
sd_begin_preview_pass(static_cast<int>(steps));
|
||||
SamplePreviewContext preview = prepare_sample_preview_context();
|
||||
|
||||
sd::Tensor<float> processed_init_latent = denoiser->process_latent_in(init_latent);
|
||||
@@ -2601,6 +2625,57 @@ sd::Tensor<float> StableDiffusionGGML::sample(const std::shared_ptr<DiffusionMod
|
||||
return output_opt;
|
||||
};
|
||||
|
||||
auto run_batched_condition = [&](const SDCondition& condition,
|
||||
const sd::Tensor<float>* c_concat_override) -> sd::Tensor<float> {
|
||||
const sd::Tensor<float>& condition_concat =
|
||||
c_concat_override != nullptr ? *c_concat_override : condition.c_concat;
|
||||
|
||||
sd::Tensor<float> batched_context = batch_two_condition_tensors(condition.c_crossattn, uncond.c_crossattn);
|
||||
sd::Tensor<float> batched_y = batch_two_condition_tensors(condition.c_vector, uncond.c_vector);
|
||||
sd::Tensor<float> batched_concat = batch_two_condition_tensors(condition_concat, uncond.c_concat);
|
||||
if (!condition.c_crossattn.empty() && batched_context.empty()) {
|
||||
return {};
|
||||
}
|
||||
if ((!condition.c_vector.empty() || !uncond.c_vector.empty()) && batched_y.empty()) {
|
||||
return {};
|
||||
}
|
||||
if ((!condition_concat.empty() || !uncond.c_concat.empty()) && batched_concat.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<sd::Tensor<float>> uncond_controls;
|
||||
compute_sample_controls(control_image, noised_input, timesteps_tensor, uncond, &uncond_controls);
|
||||
if (controls.size() != uncond_controls.size()) {
|
||||
return {};
|
||||
}
|
||||
std::vector<sd::Tensor<float>> batched_controls;
|
||||
batched_controls.reserve(controls.size());
|
||||
for (size_t i = 0; i < controls.size(); i++) {
|
||||
sd::Tensor<float> batched_control = batch_two_condition_tensors(controls[i], uncond_controls[i]);
|
||||
if (batched_control.empty()) {
|
||||
return {};
|
||||
}
|
||||
batched_controls.push_back(std::move(batched_control));
|
||||
}
|
||||
|
||||
sd::Tensor<float> batched_x =
|
||||
sd::ops::concat(noised_input, noised_input, static_cast<size_t>(noised_input.dim() - 1));
|
||||
|
||||
DiffusionParams batched_params = diffusion_params;
|
||||
batched_params.x = &batched_x;
|
||||
batched_params.context = batched_context.empty() ? nullptr : &batched_context;
|
||||
batched_params.c_concat = batched_concat.empty() ? nullptr : &batched_concat;
|
||||
batched_params.y = batched_y.empty() ? nullptr : &batched_y;
|
||||
batched_params.ref_latents = nullptr;
|
||||
batched_params.extra = UNetDiffusionExtra{1, &batched_controls, control_strength};
|
||||
|
||||
sd::Tensor<float> output = work_diffusion_model->compute(n_threads, batched_params);
|
||||
if (output.empty()) {
|
||||
LOG_ERROR("batched diffusion model compute failed");
|
||||
}
|
||||
return output;
|
||||
};
|
||||
|
||||
const SDCondition* positive_condition = &cond;
|
||||
const sd::Tensor<float>* c_concat_override = nullptr;
|
||||
for (const auto& extension : generation_extensions) {
|
||||
@@ -2640,12 +2715,40 @@ sd::Tensor<float> StableDiffusionGGML::sample(const std::shared_ptr<DiffusionMod
|
||||
}
|
||||
}
|
||||
|
||||
cond_out = run_condition(*positive_condition, c_concat_override);
|
||||
if (cond_out.empty()) {
|
||||
return {};
|
||||
const bool batch_cfg_ok = config_->params.batched_cfg &&
|
||||
sd_version_is_unet(version) &&
|
||||
!uncond.empty() &&
|
||||
img_uncond.empty() &&
|
||||
!skip_uncond &&
|
||||
!cache_runtime.ucache_enabled() &&
|
||||
!(is_skiplayer_step && slg_uncond) &&
|
||||
ip_adapter_tokens.empty() &&
|
||||
ip_adapter_uncond_tokens.empty() &&
|
||||
!config_->animatediff_loaded &&
|
||||
(noised_input.dim() < 4 || noised_input.shape()[3] <= 1) &&
|
||||
std::none_of(generation_extensions.begin(),
|
||||
generation_extensions.end(),
|
||||
[](const std::shared_ptr<GenerationExtension>& extension) {
|
||||
return extension->is_enabled();
|
||||
});
|
||||
|
||||
if (batch_cfg_ok) {
|
||||
sd::Tensor<float> batched_out = run_batched_condition(*positive_condition, c_concat_override);
|
||||
if (!batched_out.empty() && batched_out.dim() >= 4 && batched_out.shape()[3] == 2) {
|
||||
auto parts = sd::ops::chunk(batched_out, 2, 3);
|
||||
cond_out = std::move(parts[0]);
|
||||
uncond_out = std::move(parts[1]);
|
||||
}
|
||||
}
|
||||
|
||||
if (!uncond.empty()) {
|
||||
if (cond_out.empty()) {
|
||||
cond_out = run_condition(*positive_condition, c_concat_override);
|
||||
if (cond_out.empty()) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
if (uncond_out.empty() && !uncond.empty()) {
|
||||
if (!skip_uncond) {
|
||||
const std::vector<int>* uncond_skip_layers = nullptr;
|
||||
if (is_skiplayer_step && slg_uncond) {
|
||||
@@ -2764,7 +2867,7 @@ int StableDiffusionGGML::get_diffusion_model_down_factor() {
|
||||
if (sd_version_is_dit(version)) {
|
||||
if (sd_version_is_sensenova_u1(version)) {
|
||||
down_factor = 32;
|
||||
} else if (sd_version_is_z_image_l2p(version)) {
|
||||
} else if (sd_version_is_z_image_l2p(version) || version == VERSION_IRIS) {
|
||||
down_factor = 16;
|
||||
} else if (version == VERSION_QWEN_IMAGE_2_1 || version == VERSION_MING_IMAGE || sd_version_is_wan(version) || sd_version_is_lingbot_video(version) || sd_version_is_minimax_h3(version) || sd_version_is_pixart(version)) {
|
||||
down_factor = 2;
|
||||
@@ -2798,7 +2901,7 @@ int StableDiffusionGGML::get_latent_channel() {
|
||||
latent_channel = 3;
|
||||
} else if (sd_version_is_z_image_l2p(version)) {
|
||||
latent_channel = 3;
|
||||
} else if (sd_version_is_pid(version)) {
|
||||
} else if (sd_version_is_pid(version) || version == VERSION_IRIS) {
|
||||
latent_channel = 3;
|
||||
} else if (sd_version_is_sefi_image(version)) {
|
||||
latent_channel = 144;
|
||||
@@ -2899,7 +3002,7 @@ sd::Tensor<float> StableDiffusionGGML::encode_first_stage(const sd::Tensor<float
|
||||
}
|
||||
|
||||
sd::Tensor<float> StableDiffusionGGML::decode_first_stage(const sd::Tensor<float>& x, bool decode_video) {
|
||||
if (sd_version_is_pid(version) || sd_version_is_minit2i(version)) {
|
||||
if (sd_version_is_pid(version) || sd_version_is_minit2i(version) || version == VERSION_IRIS) {
|
||||
return sd::ops::clamp((x + 1.f) * 0.5f, 0.0f, 1.0f);
|
||||
}
|
||||
auto latents = first_stage_model->diffusion_to_vae_latents(x);
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "model/diffusion/hidream_o1.hpp"
|
||||
#include "model/diffusion/hunyuan.hpp"
|
||||
#include "model/diffusion/ideogram4.hpp"
|
||||
#include "model/diffusion/iris.hpp"
|
||||
#include "model/diffusion/krea2.hpp"
|
||||
#include "model/diffusion/lens.hpp"
|
||||
#include "model/diffusion/lingbot_video.hpp"
|
||||
@@ -456,6 +457,11 @@ namespace sd::model_builders {
|
||||
tensor_storage_map,
|
||||
"model.diffusion_model",
|
||||
weight_manager);
|
||||
} else if (version == VERSION_IRIS) {
|
||||
result.conditioner = std::make_shared<LLMEmbedder>(ctx.backends.runtime_backend(SDBackendModule::TE),
|
||||
tensor_storage_map, version, "", false, weight_manager, tokenizers);
|
||||
result.diffusion = std::make_shared<Iris::IrisRunner>(ctx.backends.runtime_backend(SDBackendModule::DIFFUSION),
|
||||
tensor_storage_map, "model.diffusion_model", weight_manager);
|
||||
} else { // SD1.x SD2.x SDXL
|
||||
std::map<std::string, std::string> embbeding_map;
|
||||
for (uint32_t i = 0; i < sd_ctx_params->embedding_count; i++) {
|
||||
@@ -527,7 +533,7 @@ namespace sd::model_builders {
|
||||
}
|
||||
|
||||
auto create_tae = [&](bool decode_only) -> std::shared_ptr<VAE> {
|
||||
if (sd_version_uses_wan_vae(version) || sd_version_is_hunyuan_video(version) || sd_version_is_ltxav(version) || sd_version_is_minimax_h3(version)) {
|
||||
if ((sd_version_uses_wan_vae(version) && version != VERSION_QWEN_IMAGE_2_1) || sd_version_is_hunyuan_video(version) || sd_version_is_ltxav(version) || sd_version_is_minimax_h3(version)) {
|
||||
return std::make_shared<TinyVideoAutoEncoder>(ctx.backends.runtime_backend(SDBackendModule::VAE),
|
||||
tensor_storage_map,
|
||||
"decoder",
|
||||
@@ -626,7 +632,12 @@ namespace sd::model_builders {
|
||||
}
|
||||
};
|
||||
|
||||
if (version == VERSION_CHROMA_RADIANCE || version == VERSION_HIDREAM_O1 || sd_version_is_minit2i(version) || sd_version_is_sensenova_u1(version) || sd_version_is_z_image_l2p(version)) {
|
||||
if (version == VERSION_CHROMA_RADIANCE ||
|
||||
version == VERSION_HIDREAM_O1 ||
|
||||
sd_version_is_minit2i(version) ||
|
||||
sd_version_is_sensenova_u1(version) ||
|
||||
sd_version_is_z_image_l2p(version) ||
|
||||
version == VERSION_IRIS) {
|
||||
LOG_INFO("using FakeVAE");
|
||||
result.vae = std::make_shared<FakeVAE>(version,
|
||||
ctx.backends.runtime_backend(SDBackendModule::VAE),
|
||||
|
||||
+74
-247
@@ -8,60 +8,51 @@
|
||||
#include "core/util.h"
|
||||
#include "ggml.h"
|
||||
|
||||
static int sd_tiling_calc_num_tiles(int dimension, int tile_size, float target_overlap_factor) {
|
||||
if (dimension <= tile_size) {
|
||||
return 1;
|
||||
} else if (dimension < 2 * tile_size) {
|
||||
return 2;
|
||||
} else if (dimension == 2 * tile_size) {
|
||||
return 3;
|
||||
} else {
|
||||
float target_num_tiles = 1.0f + (dimension - tile_size) / ((1.0f - target_overlap_factor) * tile_size);
|
||||
int num_tiles_lower = static_cast<int>(std::floor(target_num_tiles));
|
||||
int num_tiles_upper = static_cast<int>(std::ceil(target_num_tiles));
|
||||
int num_tiles_min = 1 + (dimension - 2) / (tile_size - 1); // positive adjacent overlap
|
||||
int num_tiles_max = 2 * dimension / tile_size - 1; // no triple overlap under Bresenham placement
|
||||
num_tiles_lower = std::clamp(num_tiles_lower, num_tiles_min, num_tiles_max);
|
||||
num_tiles_upper = std::clamp(num_tiles_upper, num_tiles_min, num_tiles_max);
|
||||
auto overlap_error = [target_num_tiles](int num_tiles) -> float {
|
||||
return std::abs(1.0f / (num_tiles - 1.0f) - 1.0f / (target_num_tiles - 1.0f));
|
||||
}; // (dimension - tile_size) / tile_size factors out
|
||||
return (overlap_error(num_tiles_upper) < overlap_error(num_tiles_lower)) ? num_tiles_upper : num_tiles_lower; // use lower if tie
|
||||
struct TileSpan {
|
||||
int offset;
|
||||
int overlap_before;
|
||||
int overlap_after;
|
||||
};
|
||||
|
||||
static std::vector<TileSpan> sd_tiling_plan_axis(int dimension,
|
||||
int tile_size,
|
||||
float target_overlap,
|
||||
bool circular) {
|
||||
if (tile_size >= dimension) {
|
||||
return {{0, 0, 0}};
|
||||
}
|
||||
}
|
||||
|
||||
static float sd_tiling_calc_average_stride_factor(int dimension, int tile_size, int num_tiles) {
|
||||
return static_cast<float>(dimension - tile_size) / static_cast<float>(tile_size * (num_tiles - 1));
|
||||
}
|
||||
|
||||
static void sd_tiling_calc_tiles(int& num_tiles_dim,
|
||||
float& tile_overlap_factor_dim,
|
||||
int small_dim,
|
||||
int tile_size,
|
||||
const float tile_overlap_factor,
|
||||
bool circular) {
|
||||
int tile_overlap = static_cast<int>(tile_size * tile_overlap_factor);
|
||||
int non_tile_overlap = tile_size - tile_overlap;
|
||||
|
||||
if (circular) {
|
||||
// circular means the last and first tile are overlapping (wraping around)
|
||||
num_tiles_dim = small_dim / non_tile_overlap;
|
||||
|
||||
if (num_tiles_dim < 1) {
|
||||
num_tiles_dim = 1;
|
||||
}
|
||||
|
||||
tile_overlap_factor_dim = (tile_size - small_dim / num_tiles_dim) / (float)tile_size;
|
||||
|
||||
// if single tile and tile_overlap_factor is not 0, add one to ensure we have at least two overlapping tiles
|
||||
if (num_tiles_dim == 1 && tile_overlap_factor_dim > 0) {
|
||||
num_tiles_dim++;
|
||||
tile_overlap_factor_dim = 0.5;
|
||||
}
|
||||
} else {
|
||||
num_tiles_dim = sd_tiling_calc_num_tiles(small_dim, tile_size, tile_overlap_factor);
|
||||
tile_overlap_factor_dim = (num_tiles_dim == 1) ? 0 : (1.0f - sd_tiling_calc_average_stride_factor(small_dim, tile_size, num_tiles_dim));
|
||||
const int extent = circular ? dimension : dimension - tile_size;
|
||||
int intervals = extent;
|
||||
if (tile_size > 1) {
|
||||
const float target = extent / ((1.0f - target_overlap) * tile_size);
|
||||
// Adjacent tiles must overlap, but three tiles must not cover the same point.
|
||||
const int min_intervals = 1 + (extent - 1) / (tile_size - 1);
|
||||
const int max_intervals = std::max(1, static_cast<int>(2LL * extent / tile_size));
|
||||
const int lower = std::clamp(static_cast<int>(std::floor(target)), min_intervals, max_intervals);
|
||||
const int upper = std::clamp(static_cast<int>(std::ceil(target)), min_intervals, max_intervals);
|
||||
auto error = [&](int count) {
|
||||
return std::abs(1.0f / count - 1.0f / target);
|
||||
};
|
||||
intervals = error(upper) < error(lower) ? upper : lower;
|
||||
}
|
||||
|
||||
const int num_tiles = circular ? intervals : intervals + 1;
|
||||
auto position = [&](int index) -> int {
|
||||
return static_cast<int>(int64_t(index) * extent / intervals);
|
||||
};
|
||||
std::vector<TileSpan> tiles;
|
||||
tiles.reserve(num_tiles);
|
||||
for (int i = 0; i < num_tiles; ++i) {
|
||||
const int offset = position(i);
|
||||
// Keep wrapped neighbors in the same unwrapped coordinate space.
|
||||
const int previous = i > 0 ? position(i - 1) : position(num_tiles - 1) - dimension;
|
||||
const int next = i + 1 < num_tiles ? position(i + 1) : dimension;
|
||||
const int before = i > 0 || circular ? previous + tile_size - offset : 0;
|
||||
const int after = i + 1 < num_tiles || circular ? offset + tile_size - next : 0;
|
||||
tiles.push_back({offset, before, after});
|
||||
}
|
||||
return tiles;
|
||||
}
|
||||
|
||||
static int64_t sd_tensor_plane_size(const sd::Tensor<float>& tensor) {
|
||||
@@ -96,62 +87,10 @@ static void sd_tensor_merge_2d(const sd::Tensor<float>& input,
|
||||
sd::Tensor<float>* output,
|
||||
int x,
|
||||
int y,
|
||||
int overlap_x,
|
||||
int overlap_y,
|
||||
bool circular_x,
|
||||
bool circular_y,
|
||||
int x_skip,
|
||||
int y_skip) {
|
||||
GGML_ASSERT(output != nullptr);
|
||||
int64_t width = input.shape()[0];
|
||||
int64_t height = input.shape()[1];
|
||||
int64_t img_width = output->shape()[0];
|
||||
int64_t img_height = output->shape()[1];
|
||||
int64_t input_plane = sd_tensor_plane_size(input);
|
||||
int64_t output_plane = sd_tensor_plane_size(*output);
|
||||
int64_t plane_count = input.numel() / input_plane;
|
||||
GGML_ASSERT(output->numel() / output_plane == plane_count);
|
||||
|
||||
// unclamped -> expects x in the range [0-1]
|
||||
auto smootherstep_f32 = [](const float x) -> float {
|
||||
GGML_ASSERT(x >= 0.f && x <= 1.f);
|
||||
return x * x * x * (x * (6.0f * x - 15.0f) + 10.0f);
|
||||
};
|
||||
|
||||
for (int iy = y_skip; iy < height; iy++) {
|
||||
for (int ix = x_skip; ix < width; ix++) {
|
||||
int64_t src_xy = ix + width * iy;
|
||||
int64_t ox = (x + ix) % img_width;
|
||||
int64_t oy = (y + iy) % img_height;
|
||||
int64_t dst_xy = ox + img_width * oy;
|
||||
for (int64_t plane = 0; plane < plane_count; ++plane) {
|
||||
float new_value = input[plane * input_plane + src_xy];
|
||||
if (overlap_x > 0 || overlap_y > 0) {
|
||||
float old_value = (*output)[plane * output_plane + dst_xy];
|
||||
const float x_f_0 = (circular_x || (overlap_x > 0 && x > 0)) ? (ix - x_skip) / float(overlap_x) : 1.f;
|
||||
const float x_f_1 = (circular_x || (overlap_x > 0 && x < (img_width - width))) ? (width - ix) / float(overlap_x) : 1.f;
|
||||
const float y_f_0 = (circular_y || (overlap_y > 0 && y > 0)) ? (iy - y_skip) / float(overlap_y) : 1.f;
|
||||
const float y_f_1 = (circular_y || (overlap_y > 0 && y < (img_height - height))) ? (height - iy) / float(overlap_y) : 1.f;
|
||||
const float x_f = std::min(std::min(x_f_0, x_f_1), 1.f);
|
||||
const float y_f = std::min(std::min(y_f_0, y_f_1), 1.f);
|
||||
(*output)[plane * output_plane + dst_xy] =
|
||||
old_value + new_value * smootherstep_f32(y_f) * smootherstep_f32(x_f);
|
||||
} else {
|
||||
(*output)[plane * output_plane + dst_xy] = new_value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void sd_tensor_merge_2d_non_circular(const sd::Tensor<float>& input,
|
||||
sd::Tensor<float>* output,
|
||||
int x,
|
||||
int y,
|
||||
int overlap_left,
|
||||
int overlap_right,
|
||||
int overlap_top,
|
||||
int overlap_bottom) {
|
||||
int overlap_left,
|
||||
int overlap_right,
|
||||
int overlap_top,
|
||||
int overlap_bottom) {
|
||||
GGML_ASSERT(output != nullptr);
|
||||
|
||||
int64_t in_width = input.shape()[0];
|
||||
@@ -164,8 +103,8 @@ static void sd_tensor_merge_2d_non_circular(const sd::Tensor<float>& input,
|
||||
|
||||
GGML_ASSERT(output->numel() == plane_count * out_size);
|
||||
GGML_ASSERT(x >= 0 && y >= 0);
|
||||
GGML_ASSERT(x + in_width <= out_width);
|
||||
GGML_ASSERT(y + in_height <= out_height);
|
||||
GGML_ASSERT(x < out_width && in_width <= out_width);
|
||||
GGML_ASSERT(y < out_height && in_height <= out_height);
|
||||
GGML_ASSERT(overlap_left >= 0 && overlap_right >= 0);
|
||||
GGML_ASSERT(overlap_top >= 0 && overlap_bottom >= 0);
|
||||
|
||||
@@ -191,7 +130,7 @@ static void sd_tensor_merge_2d_non_circular(const sd::Tensor<float>& input,
|
||||
x_f = static_cast<float>(in_width - ix) / overlap_right;
|
||||
}
|
||||
float x_weight = smootherstep_f32(std::clamp(x_f, 0.0f, 1.0f));
|
||||
(*output)[plane * out_size + out_width * (y + iy) + (x + ix)] += x_weight * y_weight * input[plane * in_size + in_width * iy + ix];
|
||||
(*output)[plane * out_size + out_width * ((y + iy) % out_height) + ((x + ix) % out_width)] += x_weight * y_weight * input[plane * in_size + in_width * iy + ix];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -223,13 +162,8 @@ sd::Tensor<float> process_tiles_2d(const sd::Tensor<float>& input,
|
||||
int scale_in = decode ? 1 : scale;
|
||||
int scale_out = decode ? scale : 1;
|
||||
|
||||
int num_tiles_x;
|
||||
float tile_overlap_factor_x;
|
||||
sd_tiling_calc_tiles(num_tiles_x, tile_overlap_factor_x, small_width, p_tile_size_w, tile_overlap_factor, circular_x);
|
||||
|
||||
int num_tiles_y;
|
||||
float tile_overlap_factor_y;
|
||||
sd_tiling_calc_tiles(num_tiles_y, tile_overlap_factor_y, small_height, p_tile_size_h, tile_overlap_factor, circular_y);
|
||||
const auto tiles_x = sd_tiling_plan_axis(small_width, p_tile_size_w, tile_overlap_factor, circular_x);
|
||||
const auto tiles_y = sd_tiling_plan_axis(small_height, p_tile_size_h, tile_overlap_factor, circular_y);
|
||||
|
||||
int tile_width = std::min(p_tile_size_w, small_width);
|
||||
int tile_height = std::min(p_tile_size_h, small_height);
|
||||
@@ -238,144 +172,37 @@ sd::Tensor<float> process_tiles_2d(const sd::Tensor<float>& input,
|
||||
int output_tile_width = tile_width * scale_out;
|
||||
int output_tile_height = tile_height * scale_out;
|
||||
|
||||
int num_tiles = num_tiles_x * num_tiles_y;
|
||||
int tile_count = 1;
|
||||
float last_time = 0.0f;
|
||||
const int num_tiles = static_cast<int>(tiles_x.size() * tiles_y.size());
|
||||
int tile_count = 0;
|
||||
if (!silent) {
|
||||
LOG_VERBOSE("num tiles : %d, %d ", num_tiles_x, num_tiles_y);
|
||||
LOG_VERBOSE("optimal overlap : %f, %f (targeting %f)", tile_overlap_factor_x, tile_overlap_factor_y, tile_overlap_factor);
|
||||
LOG_VERBOSE("num tiles : %d, %d ", static_cast<int>(tiles_x.size()), static_cast<int>(tiles_y.size()));
|
||||
LOG_VERBOSE("processing %i tiles", num_tiles);
|
||||
pretty_progress(0, num_tiles, 0.0f);
|
||||
}
|
||||
if (circular_x || circular_y) {
|
||||
int tile_overlap_x = static_cast<int32_t>(p_tile_size_w * tile_overlap_factor_x);
|
||||
int non_tile_overlap_x = p_tile_size_w - tile_overlap_x;
|
||||
int tile_overlap_y = static_cast<int32_t>(p_tile_size_h * tile_overlap_factor_y);
|
||||
int non_tile_overlap_y = p_tile_size_h - tile_overlap_y;
|
||||
|
||||
bool last_y = false;
|
||||
bool last_x = false;
|
||||
|
||||
for (int y = 0; y < small_height && !last_y; y += non_tile_overlap_y) {
|
||||
int dy = 0;
|
||||
if (!circular_y && y + tile_height >= small_height) {
|
||||
int original_y = y;
|
||||
y = small_height - tile_height;
|
||||
dy = original_y - y;
|
||||
if (decode) {
|
||||
dy *= scale;
|
||||
}
|
||||
last_y = true;
|
||||
for (const auto& y : tiles_y) {
|
||||
for (const auto& x : tiles_x) {
|
||||
int64_t t1 = ggml_time_ms();
|
||||
auto input_tile = sd_tensor_split_2d(input, input_tile_width, input_tile_height, x.offset * scale_in, y.offset * scale_in);
|
||||
auto output_tile = on_processing(input_tile);
|
||||
if (output_tile.empty()) {
|
||||
return {};
|
||||
}
|
||||
for (int x = 0; x < small_width && !last_x; x += non_tile_overlap_x) {
|
||||
int dx = 0;
|
||||
if (!circular_x && x + tile_width >= small_width) {
|
||||
int original_x = x;
|
||||
x = small_width - tile_width;
|
||||
dx = original_x - x;
|
||||
if (decode) {
|
||||
dx *= scale;
|
||||
}
|
||||
last_x = true;
|
||||
}
|
||||
|
||||
int x_in = decode ? x : scale * x;
|
||||
int y_in = decode ? y : scale * y;
|
||||
int x_out = decode ? x * scale : x;
|
||||
int y_out = decode ? y * scale : y;
|
||||
|
||||
int overlap_x_out = decode ? tile_overlap_x * scale : tile_overlap_x;
|
||||
int overlap_y_out = decode ? tile_overlap_y * scale : tile_overlap_y;
|
||||
|
||||
int64_t t1 = ggml_time_ms();
|
||||
auto input_tile = sd_tensor_split_2d(input, input_tile_width, input_tile_height, x_in, y_in);
|
||||
auto output_tile = on_processing(input_tile);
|
||||
if (output_tile.empty()) {
|
||||
return {};
|
||||
}
|
||||
GGML_ASSERT(output_tile.shape()[0] == output_tile_width && output_tile.shape()[1] == output_tile_height);
|
||||
if (output.empty()) {
|
||||
std::vector<int64_t> output_shape = output_tile.shape();
|
||||
output_shape[0] = output_width;
|
||||
output_shape[1] = output_height;
|
||||
output = sd::Tensor<float>::zeros(std::move(output_shape));
|
||||
}
|
||||
sd_tensor_merge_2d(output_tile, &output, x_out, y_out, overlap_x_out, overlap_y_out, circular_x, circular_y, dx, dy);
|
||||
|
||||
if (!silent) {
|
||||
int64_t t2 = ggml_time_ms();
|
||||
last_time = (t2 - t1) / 1000.0f;
|
||||
pretty_progress(tile_count, num_tiles, last_time);
|
||||
}
|
||||
tile_count++;
|
||||
GGML_ASSERT(output_tile.shape()[0] == output_tile_width && output_tile.shape()[1] == output_tile_height);
|
||||
if (output.empty()) {
|
||||
std::vector<int64_t> output_shape = output_tile.shape();
|
||||
output_shape[0] = output_width;
|
||||
output_shape[1] = output_height;
|
||||
output = sd::Tensor<float>::zeros(std::move(output_shape));
|
||||
}
|
||||
last_x = false;
|
||||
}
|
||||
} else {
|
||||
for (int j = 0; j < num_tiles_y; ++j) {
|
||||
int y = 0;
|
||||
int overlap_top = 0;
|
||||
int overlap_bottom = 0;
|
||||
if (num_tiles_y > 1) {
|
||||
y = j * (small_height - tile_height) / (num_tiles_y - 1);
|
||||
if (j > 0) {
|
||||
int y_prev = (j - 1) * (small_height - tile_height) / (num_tiles_y - 1);
|
||||
overlap_top = y_prev + tile_height - y;
|
||||
}
|
||||
if (j < num_tiles_y - 1) {
|
||||
int y_next = (j + 1) * (small_height - tile_height) / (num_tiles_y - 1);
|
||||
overlap_bottom = y + tile_height - y_next;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < num_tiles_x; ++i) {
|
||||
int x = 0;
|
||||
int overlap_left = 0;
|
||||
int overlap_right = 0;
|
||||
if (num_tiles_x > 1) {
|
||||
x = i * (small_width - tile_width) / (num_tiles_x - 1);
|
||||
if (i > 0) {
|
||||
int x_prev = (i - 1) * (small_width - tile_width) / (num_tiles_x - 1);
|
||||
overlap_left = x_prev + tile_width - x;
|
||||
}
|
||||
if (i < num_tiles_x - 1) {
|
||||
int x_next = (i + 1) * (small_width - tile_width) / (num_tiles_x - 1);
|
||||
overlap_right = x + tile_width - x_next;
|
||||
}
|
||||
}
|
||||
|
||||
int64_t t1 = ggml_time_ms();
|
||||
auto input_tile = sd_tensor_split_2d(input, input_tile_width, input_tile_height, x * scale_in, y * scale_in);
|
||||
auto output_tile = on_processing(input_tile);
|
||||
if (output_tile.empty()) {
|
||||
return {};
|
||||
}
|
||||
GGML_ASSERT(output_tile.shape()[0] == output_tile_width && output_tile.shape()[1] == output_tile_height);
|
||||
if (output.empty()) {
|
||||
std::vector<int64_t> output_shape = output_tile.shape();
|
||||
output_shape[0] = output_width;
|
||||
output_shape[1] = output_height;
|
||||
output = sd::Tensor<float>::zeros(std::move(output_shape));
|
||||
}
|
||||
sd_tensor_merge_2d_non_circular(
|
||||
output_tile, &output,
|
||||
x * scale_out, y * scale_out,
|
||||
overlap_left * scale_out,
|
||||
overlap_right * scale_out,
|
||||
overlap_top * scale_out,
|
||||
overlap_bottom * scale_out);
|
||||
if (!silent) {
|
||||
last_time = (ggml_time_ms() - t1) / 1000.0f;
|
||||
pretty_progress(tile_count, num_tiles, last_time);
|
||||
}
|
||||
tile_count++;
|
||||
sd_tensor_merge_2d(output_tile, &output,
|
||||
x.offset * scale_out, y.offset * scale_out,
|
||||
x.overlap_before * scale_out, x.overlap_after * scale_out,
|
||||
y.overlap_before * scale_out, y.overlap_after * scale_out);
|
||||
++tile_count;
|
||||
if (!silent) {
|
||||
pretty_progress(tile_count, num_tiles, (ggml_time_ms() - t1) / 1000.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!silent && tile_count < num_tiles) {
|
||||
pretty_progress(num_tiles, num_tiles, last_time);
|
||||
}
|
||||
if (output.empty()) {
|
||||
return {};
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
@@ -335,6 +335,7 @@ void sd_ctx_params_init(sd_ctx_params_t* sd_ctx_params) {
|
||||
sd_ctx_params->max_vram = nullptr;
|
||||
sd_ctx_params->disable_prefetch = false;
|
||||
sd_ctx_params->disable_segmented_compute = false;
|
||||
sd_ctx_params->batched_cfg = true;
|
||||
sd_ctx_params->eager_load = false;
|
||||
sd_ctx_params->enable_mmap = false;
|
||||
sd_ctx_params->diffusion_flash_attn = false;
|
||||
|
||||
@@ -242,7 +242,7 @@ bool BPETokenizer::encode(const std::string& text, std::vector<int>& result, on_
|
||||
ss << "\"" << token << "\", ";
|
||||
}
|
||||
ss << "]";
|
||||
LOG_VERBOSE("split prompt \"%s\" to %zu tokens %s", text.c_str(), bpe_tokens.size(), ss.str().c_str());
|
||||
LOG_VERBOSE("split prompt \"%s\" to %zu tokens %s", escape_newlines(text).c_str(), bpe_tokens.size(), escape_newlines(ss.str()).c_str());
|
||||
result = std::move(bpe_tokens);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -735,7 +735,7 @@ bool HFTokenizer::encode(const std::string& text, std::vector<int>& tokens, on_n
|
||||
}
|
||||
}
|
||||
ss << "]";
|
||||
LOG_VERBOSE("split prompt \"%s\" to %zu tokens %s", text.c_str(), result.size(), ss.str().c_str());
|
||||
LOG_VERBOSE("split prompt \"%s\" to %zu tokens %s", escape_newlines(text).c_str(), result.size(), escape_newlines(ss.str()).c_str());
|
||||
tokens = std::move(result);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -343,7 +343,7 @@ bool T5UniGramTokenizer::encode(const std::string& input, std::vector<int>& resu
|
||||
ss << "\"" << token_str << "\", ";
|
||||
}
|
||||
ss << "]";
|
||||
LOG_VERBOSE("split prompt \"%s\" to tokens %s", input.c_str(), ss.str().c_str());
|
||||
LOG_VERBOSE("split prompt \"%s\" to tokens %s", escape_newlines(input).c_str(), escape_newlines(ss.str()).c_str());
|
||||
|
||||
result = std::move(tokens);
|
||||
return true;
|
||||
|
||||
Reference in New Issue
Block a user