Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e790073e1c | ||
|
|
b11c95a41c | ||
|
|
68f3d6df9f | ||
|
|
7bcd189639 | ||
|
|
2574f59365 | ||
|
|
7dab3668d0 | ||
|
|
556f04bb3f | ||
|
|
3590aa8d62 | ||
|
|
1a13107bac | ||
|
|
484baa41e5 | ||
|
|
2bb0389683 | ||
|
|
ccda89e09c | ||
|
|
f0271076ad | ||
|
|
3b6c9ca97c | ||
|
|
57e19fa954 | ||
|
|
61a637bfd5 | ||
|
|
3ec374aecc | ||
|
|
0484600284 | ||
|
|
c1790754d3 | ||
|
|
9f855c933b | ||
|
|
7b5f34d93e | ||
|
|
d77b8f5ee8 | ||
|
|
03e9a22f4d | ||
|
|
f54e45e81c | ||
|
|
9956436c92 | ||
|
|
ec4cb8104b | ||
|
|
3973015ed7 | ||
|
|
9ee77fc227 | ||
|
|
39f7962d69 | ||
|
|
8caa3f908a | ||
|
|
2938272d82 | ||
|
|
f440ad9c29 | ||
|
|
41f7acbfb0 | ||
|
|
b395a6972d | ||
|
|
854bebfe02 | ||
|
|
787d229d84 | ||
|
|
b12098f5d0 | ||
|
|
2bd249c971 | ||
|
|
e9e952462f | ||
|
|
e8e012eef2 | ||
|
|
7f0e728b7d | ||
|
|
92a3b73cdb | ||
|
|
710bc91c8f | ||
|
|
5a34bc7f6e | ||
|
|
146b6cc49e | ||
|
|
93527fda74 | ||
|
|
6e66a1a4a4 | ||
|
|
bb90bfa00f | ||
|
|
517abc777d | ||
|
|
6f00939f75 | ||
|
|
c2df4e1228 | ||
|
|
9838264c49 | ||
|
|
17d70b91e6 | ||
|
|
5db680c2c7 | ||
|
|
749186c0eb | ||
|
|
bdb431ad95 | ||
|
|
276025e054 | ||
|
|
8d4c7af95b | ||
|
|
9b0fceb41b | ||
|
|
563137a592 | ||
|
|
3a54597776 | ||
|
|
1365008348 | ||
|
|
1fb6b22850 | ||
|
|
c20769b2c8 | ||
|
|
1b702a51e7 | ||
|
|
19bdfe22d2 | ||
|
|
138da14cc3 | ||
|
|
17a2b4a315 | ||
|
|
b3d56d0ba1 | ||
|
|
2a07540c2a | ||
|
|
81abfb2548 | ||
|
|
f3fd359b58 | ||
|
|
dfb2390dd4 | ||
|
|
cfbc19d186 | ||
|
|
b9254dda0d | ||
|
|
0648f4426b | ||
|
|
74f513d512 | ||
|
|
064001b524 | ||
|
|
1f9ee88e09 | ||
|
|
a7f2e03da4 | ||
|
|
4513e3fda9 | ||
|
|
2d40a8b2ad | ||
|
|
9c7f9a20b3 | ||
|
|
ed74577c40 | ||
|
|
7948df8ac1 | ||
|
|
02f06370a7 | ||
|
|
f8935d6f25 | ||
|
|
be65ac7511 | ||
|
|
20901f6d8e | ||
|
|
0982807139 | ||
|
|
d2797b8667 | ||
|
|
d3b2cb047e | ||
|
|
b4ba55d8d7 | ||
|
|
b54bd83a3f | ||
|
|
0e4ee04488 | ||
|
|
29ab511fc7 | ||
|
|
55c2aed52c | ||
|
|
8eded497e5 | ||
|
|
92dc7268fc | ||
|
|
07b2b18e70 | ||
|
|
1ceb5bd9df | ||
|
|
202c6154a2 | ||
|
|
a397e03488 | ||
|
|
72e512a0cc | ||
|
|
0baf721215 | ||
|
|
645e6e9089 | ||
|
|
cbf92191c3 | ||
|
|
8cf55a3b3b | ||
|
|
3a8788cb7d | ||
|
|
449165caf5 | ||
|
|
adaa599a3b | ||
|
|
2e3514625a | ||
|
|
47d8198b69 | ||
|
|
ef92a0027e | ||
|
|
b3374e6a71 | ||
|
|
bdd937f29a | ||
|
|
c51ec7cad9 | ||
|
|
5b0267e941 | ||
|
|
0045a72b96 | ||
|
|
99bd062546 | ||
|
|
9d8c9e4279 | ||
|
|
caa823a8c0 | ||
|
|
22c8c40b0d | ||
|
|
b706d682ad | ||
|
|
b758b7de13 | ||
|
|
f683c88a28 | ||
|
|
21fd4e6788 | ||
|
|
830804262b | ||
|
|
82e03ef137 | ||
|
|
baf7eda1e4 | ||
|
|
e7eb92fd84 | ||
|
|
50134e51dd | ||
|
|
e43b24cf48 | ||
|
|
06accf2b39 | ||
|
|
cde20d5ef0 | ||
|
|
67dda3f897 | ||
|
|
3b4d26f3d9 | ||
|
|
bd17f53b73 | ||
|
|
d7ecbe1d01 | ||
|
|
36330724bd | ||
|
|
0c1ca170ca | ||
|
|
839f6a94d2 | ||
|
|
38b14adb67 | ||
|
|
fd1a2794f3 | ||
|
|
db08b84607 | ||
|
|
686856edca | ||
|
|
0b8296915c | ||
|
|
381e0df50f | ||
|
|
0665a7f8bf | ||
|
|
eeac950b44 | ||
|
|
57ff2eb0f4 | ||
|
|
9d683417cb | ||
|
|
60477fd50f | ||
|
|
6ee0684d74 | ||
|
|
90e87bc846 | ||
|
|
586b6f1481 | ||
|
|
9097ce5211 | ||
|
|
3d6064b37e | ||
|
|
b8079e253d | ||
|
|
331cfa5387 | ||
|
|
a81677f59c | ||
|
|
f40a707d0f | ||
|
|
970c4a3312 | ||
|
|
b8bdffc199 | ||
|
|
c97702e105 | ||
|
|
44cca3d626 | ||
|
|
0a7ae07f94 | ||
|
|
66143340b6 | ||
|
|
7023fc4cfb | ||
|
|
e77e4c46bf | ||
|
|
7d33d4b2dd | ||
|
|
3c99f700de | ||
|
|
4d626d24b2 | ||
|
|
f3f69e2fbe | ||
|
|
6a9cb31150 | ||
|
|
2bcff67480 | ||
|
|
a564fdf642 | ||
|
|
84fc5446d2 | ||
|
|
1b4e9be643 | ||
|
|
d73b4198a4 | ||
|
|
5c243db9a8 | ||
|
|
c41c5ded7a | ||
|
|
9ac7b672c2 | ||
|
|
ee5bf956b0 | ||
|
|
6b675a5ede | ||
|
|
12a369cc67 | ||
|
|
fd3504760f | ||
|
|
7ade90e478 | ||
|
|
118489eb5c | ||
|
|
be9f51b25c | ||
|
|
e8323cabb0 | ||
|
|
dd753729cc | ||
|
|
8afbeb6ba9 | ||
|
|
5bf438d568 | ||
|
|
359eb8b8de | ||
|
|
7397ddaa86 | ||
|
|
9369ab759f | ||
|
|
687a81f251 | ||
|
|
87ecb95cbc | ||
|
|
99c1de379b | ||
|
|
09b12d5f6d | ||
|
|
6dfe945958 | ||
|
|
bf0216765a | ||
|
|
4fe7a35939 | ||
|
|
4d5232083f | ||
|
|
1d6cb0f8c3 | ||
|
|
83e8f6f0af | ||
|
|
8d878872d9 | ||
|
|
02dd5e5dd2 | ||
|
|
8f2967c006 | ||
|
|
f16a110f87 | ||
|
|
ed88e215a2 | ||
|
|
6293ab5aaf | ||
|
|
545fac4f3f | ||
|
|
5265a5efa1 | ||
|
|
84cbd88df1 | ||
|
|
997bb11fb6 | ||
|
|
862a6586cb | ||
|
|
61d8331ef3 | ||
|
|
acc3bf1fdc | ||
|
|
83eabd7c01 | ||
|
|
630ee03f23 | ||
|
|
f6968bc589 | ||
|
|
adfef62900 | ||
|
|
6fa7ca9317 | ||
|
|
d6dd6d7b55 | ||
|
|
dea4980f4e | ||
|
|
c8fb3d2458 | ||
|
|
3d33caaef8 | ||
|
|
9b424db0f4 | ||
|
|
d95062737e | ||
|
|
7c880f80c7 | ||
|
|
aaa8a51bd8 | ||
|
|
ba35dd734e | ||
|
|
d41f5fff69 | ||
|
|
810ef0cf76 | ||
|
|
5792c66879 | ||
|
|
39d54702a6 | ||
|
|
60889bc9a1 | ||
|
|
e64baa3611 | ||
|
|
cec4aedcfd | ||
|
|
4cdfff5ff2 | ||
|
|
0752cc9d3a | ||
|
|
b314d80ad0 | ||
|
|
c9cd49701a | ||
|
|
c5eb1e4137 | ||
|
|
636d3cb6ff | ||
|
|
adea272225 | ||
|
|
45ce78a3ae | ||
|
|
28ef93c0e1 | ||
|
|
3296545090 | ||
|
|
d60fb27560 | ||
|
|
c7ccafbd6f | ||
|
|
aa0b899397 | ||
|
|
5e264372ce | ||
|
|
f0f641a142 | ||
|
|
9f56833e14 | ||
|
|
65891d74cc | ||
|
|
f957fa3d2a | ||
|
|
c252e03c6b | ||
|
|
e63daba33d | ||
|
|
3959109281 | ||
|
|
e411520407 | ||
|
|
43e829f219 | ||
|
|
7837232631 | ||
|
|
4ccce027b2 | ||
|
|
fa61ea744d | ||
|
|
5e4579c11d | ||
|
|
329571131d | ||
|
|
a48b4a3ade | ||
|
|
b87fe13afd | ||
|
|
e50e1f253d | ||
|
|
c6206fb351 | ||
|
|
639091fbe9 | ||
|
|
9293016c9d | ||
|
|
2efd19978d | ||
|
|
61659ef299 | ||
|
|
9565c7f6bd | ||
|
|
fbce16e02d | ||
|
|
7010bb4dff | ||
|
|
48d3161a8d | ||
|
|
271b594e74 |
@@ -0,0 +1,15 @@
|
||||
## Summary
|
||||
|
||||
<!-- Describe what changed and why. Keep the PR focused on one clear change. -->
|
||||
|
||||
## Related Issue / Discussion
|
||||
|
||||
<!-- Link related issues, discussions, or previous PRs if applicable. -->
|
||||
|
||||
## Additional Information
|
||||
|
||||
<!-- Add verification notes, screenshots, sample output, or other context when applicable. -->
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] I have read and confirmed this PR follows the [contribution guidelines](https://github.com/leejet/stable-diffusion.cpp/blob/master/CONTRIBUTING.md).
|
||||
@@ -14,6 +14,8 @@ on:
|
||||
paths:
|
||||
[
|
||||
".github/workflows/**",
|
||||
".dockerignore",
|
||||
"Dockerfile*",
|
||||
"**/CMakeLists.txt",
|
||||
"**/Makefile",
|
||||
"**/*.h",
|
||||
@@ -21,11 +23,16 @@ on:
|
||||
"**/*.c",
|
||||
"**/*.cpp",
|
||||
"**/*.cu",
|
||||
"examples/server/frontend",
|
||||
"examples/server/frontend/**",
|
||||
]
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
paths:
|
||||
[
|
||||
".github/workflows/**",
|
||||
".dockerignore",
|
||||
"Dockerfile*",
|
||||
"**/CMakeLists.txt",
|
||||
"**/Makefile",
|
||||
"**/*.h",
|
||||
@@ -33,6 +40,8 @@ on:
|
||||
"**/*.c",
|
||||
"**/*.cpp",
|
||||
"**/*.cu",
|
||||
"examples/server/frontend",
|
||||
"examples/server/frontend/**",
|
||||
]
|
||||
|
||||
env:
|
||||
@@ -53,6 +62,16 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10.15.1
|
||||
|
||||
- name: Dependencies
|
||||
id: depends
|
||||
run: |
|
||||
@@ -64,13 +83,13 @@ jobs:
|
||||
run: |
|
||||
mkdir build
|
||||
cd build
|
||||
cmake .. -DGGML_AVX2=ON -DSD_BUILD_SHARED_LIBS=ON
|
||||
cmake .. -DSD_BUILD_SHARED_LIBS=ON -DGGML_NATIVE=OFF -DSD_BUILD_SHARED_GGML_LIB=ON -DGGML_BACKEND_DL=ON -DGGML_CPU_ALL_VARIANTS=ON -DCMAKE_BUILD_WITH_INSTALL_RPATH=ON -DCMAKE_INSTALL_RPATH='$ORIGIN'
|
||||
cmake --build . --config Release
|
||||
|
||||
- name: Get commit hash
|
||||
id: commit
|
||||
if: ${{ ( github.event_name == 'push' && github.ref == 'refs/heads/master' ) || github.event.inputs.create_release == 'true' }}
|
||||
uses: pr-mpt/actions-commit-hash@v2
|
||||
uses: prompt/actions-commit-hash@v2
|
||||
|
||||
- name: Fetch system info
|
||||
id: system-info
|
||||
@@ -106,24 +125,34 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10.15.1
|
||||
|
||||
- name: Dependencies
|
||||
id: depends
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install build-essential libvulkan-dev glslc
|
||||
sudo apt-get install build-essential libvulkan-dev glslc spirv-headers
|
||||
|
||||
- name: Build
|
||||
id: cmake_build
|
||||
run: |
|
||||
mkdir build
|
||||
cd build
|
||||
cmake .. -DSD_BUILD_SHARED_LIBS=ON -DSD_VULKAN=ON
|
||||
cmake .. -DSD_BUILD_SHARED_LIBS=ON -DSD_VULKAN=ON -DGGML_NATIVE=OFF -DSD_BUILD_SHARED_GGML_LIB=ON -DGGML_BACKEND_DL=ON -DGGML_CPU_ALL_VARIANTS=ON -DCMAKE_BUILD_WITH_INSTALL_RPATH=ON -DCMAKE_INSTALL_RPATH='$ORIGIN'
|
||||
cmake --build . --config Release
|
||||
|
||||
- name: Get commit hash
|
||||
id: commit
|
||||
if: ${{ ( github.event_name == 'push' && github.ref == 'refs/heads/master' ) || github.event.inputs.create_release == 'true' }}
|
||||
uses: pr-mpt/actions-commit-hash@v2
|
||||
uses: prompt/actions-commit-hash@v2
|
||||
|
||||
- name: Fetch system info
|
||||
id: system-info
|
||||
@@ -151,7 +180,8 @@ jobs:
|
||||
|
||||
build-and-push-docker-images:
|
||||
name: Build and push container images
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
runs-on: ${{ matrix.runner }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -162,7 +192,22 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
variant: [musa, sycl, vulkan]
|
||||
variant: [musa, sycl, vulkan, cuda]
|
||||
platform: [linux/amd64]
|
||||
runner: [ubuntu-latest]
|
||||
build-args: [""]
|
||||
tag-suffix: [""]
|
||||
include:
|
||||
- variant: cuda
|
||||
platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
tag-suffix: "-spark"
|
||||
build-args: |
|
||||
CUDA_VERSION=13.0.0
|
||||
UBUNTU_VERSION=24.04
|
||||
CUDA_ARCHITECTURES=121
|
||||
GGML_CUDA_FA_ALL_QUANTS=ON
|
||||
GGML_CUDA_ENABLE_DYNAMIC_CPU_BACKENDS=OFF
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
@@ -174,10 +219,20 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10.15.1
|
||||
|
||||
- name: Get commit hash
|
||||
id: commit
|
||||
if: ${{ ( github.event_name == 'push' && github.ref == 'refs/heads/master' ) || github.event.inputs.create_release == 'true' }}
|
||||
uses: pr-mpt/actions-commit-hash@v2
|
||||
uses: prompt/actions-commit-hash@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
@@ -206,12 +261,14 @@ jobs:
|
||||
id: build-push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
platforms: linux/amd64
|
||||
context: .
|
||||
platforms: ${{ matrix.platform }}
|
||||
push: ${{ ( github.event_name == 'push' && github.ref == 'refs/heads/master' ) || github.event.inputs.create_release == 'true' }}
|
||||
file: Dockerfile.${{ matrix.variant }}
|
||||
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.BRANCH_NAME }}-${{ matrix.variant }}
|
||||
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.BRANCH_NAME }}-${{ matrix.variant }}${{ matrix.tag-suffix }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
annotations: ${{ steps.meta.outputs.annotations }}
|
||||
build-args: ${{ matrix.build-args }}
|
||||
|
||||
macOS-latest-cmake:
|
||||
runs-on: macos-latest
|
||||
@@ -223,6 +280,16 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10.15.1
|
||||
|
||||
- name: Dependencies
|
||||
id: depends
|
||||
run: |
|
||||
@@ -240,7 +307,7 @@ jobs:
|
||||
- name: Get commit hash
|
||||
id: commit
|
||||
if: ${{ ( github.event_name == 'push' && github.ref == 'refs/heads/master' ) || github.event.inputs.create_release == 'true' }}
|
||||
uses: pr-mpt/actions-commit-hash@v2
|
||||
uses: prompt/actions-commit-hash@v2
|
||||
|
||||
- name: Fetch system info
|
||||
id: system-info
|
||||
@@ -275,18 +342,12 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- build: "noavx"
|
||||
defines: "-DGGML_NATIVE=OFF -DGGML_AVX=OFF -DGGML_AVX2=OFF -DGGML_FMA=OFF -DSD_BUILD_SHARED_LIBS=ON"
|
||||
- build: "avx2"
|
||||
defines: "-DGGML_NATIVE=OFF -DGGML_AVX2=ON -DSD_BUILD_SHARED_LIBS=ON"
|
||||
- build: "avx"
|
||||
defines: "-DGGML_NATIVE=OFF -DGGML_AVX=ON -DGGML_AVX2=OFF -DSD_BUILD_SHARED_LIBS=ON"
|
||||
- build: "avx512"
|
||||
defines: "-DGGML_NATIVE=OFF -DGGML_AVX512=ON -DGGML_AVX=ON -DGGML_AVX2=ON -DSD_BUILD_SHARED_LIBS=ON"
|
||||
- build: "cpu"
|
||||
defines: "-DGGML_NATIVE=OFF -DSD_BUILD_SHARED_LIBS=ON -DSD_BUILD_SHARED_GGML_LIB=ON -DGGML_BACKEND_DL=ON -DGGML_CPU_ALL_VARIANTS=ON"
|
||||
- build: "cuda12"
|
||||
defines: "-DSD_CUDA=ON -DSD_BUILD_SHARED_LIBS=ON -DCMAKE_CUDA_ARCHITECTURES='61;70;75;80;86;89;90;100;120' -DCMAKE_CUDA_FLAGS='-Xcudafe \"--diag_suppress=177\" -Xcudafe \"--diag_suppress=550\"'"
|
||||
defines: "-DSD_CUDA=ON -DSD_BUILD_SHARED_LIBS=ON -DCMAKE_CUDA_ARCHITECTURES='61;70;75;80;86;89;90;100;120' -DCMAKE_CUDA_FLAGS='-Xcudafe \"--diag_suppress=177\" -Xcudafe \"--diag_suppress=550\"' -DGGML_NATIVE=OFF -DSD_BUILD_SHARED_GGML_LIB=ON -DGGML_BACKEND_DL=ON -DGGML_CPU_ALL_VARIANTS=ON"
|
||||
- build: "vulkan"
|
||||
defines: "-DSD_VULKAN=ON -DSD_BUILD_SHARED_LIBS=ON"
|
||||
defines: "-DSD_VULKAN=ON -DSD_BUILD_SHARED_LIBS=ON -DGGML_NATIVE=OFF -DSD_BUILD_SHARED_GGML_LIB=ON -DGGML_BACKEND_DL=ON -DGGML_CPU_ALL_VARIANTS=ON"
|
||||
steps:
|
||||
- name: Clone
|
||||
id: checkout
|
||||
@@ -294,6 +355,16 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10.15.1
|
||||
|
||||
- name: Install cuda-toolkit
|
||||
id: cuda-toolkit
|
||||
if: ${{ matrix.build == 'cuda12' }}
|
||||
@@ -324,23 +395,10 @@ jobs:
|
||||
cmake .. -DCMAKE_CXX_FLAGS='/bigobj' -G Ninja -DCMAKE_C_COMPILER=cl.exe -DCMAKE_CXX_COMPILER=cl.exe -DCMAKE_BUILD_TYPE=Release ${{ matrix.defines }}
|
||||
cmake --build .
|
||||
|
||||
- name: Check AVX512F support
|
||||
id: check_avx512f
|
||||
if: ${{ matrix.build == 'avx512' }}
|
||||
continue-on-error: true
|
||||
run: |
|
||||
cd build
|
||||
$vcdir = $(vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath)
|
||||
$msvc = $(join-path $vcdir $('VC\Tools\MSVC\'+$(gc -raw $(join-path $vcdir 'VC\Auxiliary\Build\Microsoft.VCToolsVersion.default.txt')).Trim()))
|
||||
$cl = $(join-path $msvc 'bin\Hostx64\x64\cl.exe')
|
||||
echo 'int main(void){unsigned int a[4];__cpuid(a,7);return !(a[1]&65536);}' >> avx512f.c
|
||||
& $cl /O2 /GS- /kernel avx512f.c /link /nodefaultlib /entry:main
|
||||
.\avx512f.exe && echo "AVX512F: YES" && ( echo HAS_AVX512F=1 >> $env:GITHUB_ENV ) || echo "AVX512F: NO"
|
||||
|
||||
- name: Get commit hash
|
||||
id: commit
|
||||
if: ${{ ( github.event_name == 'push' && github.ref == 'refs/heads/master' ) || github.event.inputs.create_release == 'true' }}
|
||||
uses: pr-mpt/actions-commit-hash@v2
|
||||
uses: prompt/actions-commit-hash@v2
|
||||
|
||||
- name: Pack artifacts
|
||||
id: pack_artifacts
|
||||
@@ -387,18 +445,145 @@ jobs:
|
||||
path: |
|
||||
sd-${{ env.BRANCH_NAME }}-${{ steps.commit.outputs.short }}-bin-win-${{ matrix.build }}-x64.zip
|
||||
|
||||
windows-latest-cmake-hip:
|
||||
windows-latest-rocm:
|
||||
runs-on: windows-2022
|
||||
|
||||
env:
|
||||
HIPSDK_INSTALLER_VERSION: "25.Q3"
|
||||
GPU_TARGETS: "gfx1151;gfx1200;gfx1201;gfx1100;gfx1101;gfx1102;gfx1030;gfx1031;gfx1032"
|
||||
ROCM_VERSION: "7.13.0"
|
||||
GPU_TARGETS: "gfx906;gfx908;gfx90a;gfx942;gfx950;gfx1010;gfx1011;gfx1012;gfx1030;gfx1031;gfx1032;gfx1033;gfx1034;gfx1035;gfx1036;gfx1100;gfx1101;gfx1102;gfx1150;gfx1151;gfx1152;gfx1200;gfx1201"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10.15.1
|
||||
|
||||
- name: Cache ROCm Installation
|
||||
id: cache-rocm
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: C:\TheRock\build
|
||||
key: rocm-${{ env.ROCM_VERSION }}-gfx1151-${{ runner.os }}
|
||||
|
||||
- name: ccache
|
||||
uses: ggml-org/ccache-action@v1.2.16
|
||||
with:
|
||||
key: windows-latest-rocm-${{ env.ROCM_VERSION }}-x64
|
||||
evict-old-files: 1d
|
||||
|
||||
- name: Install ROCm
|
||||
if: steps.cache-rocm.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
write-host "Downloading AMD ROCm ${{ env.ROCM_VERSION }} tarball"
|
||||
Invoke-WebRequest -Uri "https://repo.amd.com/rocm/tarball/therock-dist-windows-gfx1151-${{ env.ROCM_VERSION }}.tar.gz" -OutFile "${env:RUNNER_TEMP}\rocm.tar.gz"
|
||||
write-host "Extracting ROCm tarball"
|
||||
mkdir C:\TheRock\build -Force
|
||||
tar -xzf "${env:RUNNER_TEMP}\rocm.tar.gz" -C C:\TheRock\build --strip-components=1
|
||||
write-host "Completed ROCm extraction"
|
||||
|
||||
- name: Setup ROCm Environment
|
||||
run: |
|
||||
$rocmPath = "C:\TheRock\build"
|
||||
echo "HIP_PATH=$rocmPath" >> $env:GITHUB_ENV
|
||||
echo "HIP_DEVICE_LIB_PATH=$rocmPath\lib\llvm\amdgcn\bitcode" >> $env:GITHUB_ENV
|
||||
echo "HIP_PLATFORM=amd" >> $env:GITHUB_ENV
|
||||
echo "LLVM_PATH=$rocmPath\lib\llvm" >> $env:GITHUB_ENV
|
||||
echo "$rocmPath\bin" >> $env:GITHUB_PATH
|
||||
echo "$rocmPath\lib\llvm\bin" >> $env:GITHUB_PATH
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
mkdir build
|
||||
cd build
|
||||
cmake .. `
|
||||
-G "Unix Makefiles" `
|
||||
-DCMAKE_PREFIX_PATH="${env:HIP_PATH}" `
|
||||
-DSD_HIPBLAS=ON `
|
||||
-DSD_BUILD_SHARED_LIBS=ON `
|
||||
-DGGML_NATIVE=OFF `
|
||||
-DCMAKE_C_COMPILER="${env:HIP_PATH}\lib\llvm\bin\clang.exe" `
|
||||
-DCMAKE_CXX_COMPILER="${env:HIP_PATH}\lib\llvm\bin\clang++.exe" `
|
||||
-DCMAKE_HIP_COMPILER="${env:HIP_PATH}\lib\llvm\bin\clang.exe" `
|
||||
-DHIP_PATH="${env:HIP_PATH}" `
|
||||
-DCMAKE_BUILD_TYPE=Release `
|
||||
-DGPU_TARGETS="${{ env.GPU_TARGETS }}"
|
||||
cmake --build . --config Release --parallel ${env:NUMBER_OF_PROCESSORS}
|
||||
|
||||
- name: Get commit hash
|
||||
id: commit
|
||||
if: ${{ ( github.event_name == 'push' && github.ref == 'refs/heads/master' ) || github.event.inputs.create_release == 'true' }}
|
||||
uses: pr-mpt/actions-commit-hash@v2
|
||||
|
||||
- name: Pack artifacts
|
||||
if: ${{ ( github.event_name == 'push' && github.ref == 'refs/heads/master' ) || github.event.inputs.create_release == 'true' }}
|
||||
run: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
$dst = "build\bin"
|
||||
$rocmBin = Join-Path "${env:HIP_PATH}" "bin"
|
||||
$requiredRocmPaths = @(
|
||||
(Join-Path $rocmBin "rocblas.dll"),
|
||||
(Join-Path $rocmBin "rocblas\library")
|
||||
)
|
||||
foreach ($path in $requiredRocmPaths) {
|
||||
if (!(Test-Path $path)) {
|
||||
throw "Missing ROCm runtime dependency: $path"
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($pattern in @("rocblas*.dll", "hipblas*.dll", "libhipblas*.dll")) {
|
||||
Copy-Item -Path (Join-Path $rocmBin $pattern) -Destination $dst -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
foreach ($dir in @("rocblas", "hipblaslt")) {
|
||||
$src = Join-Path $rocmBin $dir
|
||||
if (Test-Path $src) {
|
||||
Copy-Item -Path $src -Destination $dst -Recurse -Force
|
||||
}
|
||||
}
|
||||
|
||||
7z a sd-${{ env.BRANCH_NAME }}-${{ steps.commit.outputs.short }}-bin-win-rocm-${{ env.ROCM_VERSION }}-x64.zip .\build\bin\*
|
||||
|
||||
- name: Upload artifacts
|
||||
if: ${{ ( github.event_name == 'push' && github.ref == 'refs/heads/master' ) || github.event.inputs.create_release == 'true' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: sd-${{ env.BRANCH_NAME }}-${{ steps.commit.outputs.short }}-bin-win-rocm-${{ env.ROCM_VERSION }}-x64.zip
|
||||
path: |
|
||||
sd-${{ env.BRANCH_NAME }}-${{ steps.commit.outputs.short }}-bin-win-rocm-${{ env.ROCM_VERSION }}-x64.zip
|
||||
|
||||
windows-latest-cmake-hip:
|
||||
runs-on: windows-2022
|
||||
|
||||
env:
|
||||
HIPSDK_INSTALLER_VERSION: "26.Q1"
|
||||
ROCM_VERSION: "7.1.1"
|
||||
GPU_TARGETS: "gfx1150;gfx1151;gfx1200;gfx1201;gfx1100;gfx1101;gfx1102;gfx1030;gfx1031;gfx1032"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10.15.1
|
||||
|
||||
- name: Cache ROCm Installation
|
||||
id: cache-rocm
|
||||
uses: actions/cache@v4
|
||||
@@ -417,7 +602,7 @@ jobs:
|
||||
run: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
write-host "Downloading AMD HIP SDK Installer"
|
||||
Invoke-WebRequest -Uri "https://download.amd.com/developer/eula/rocm-hub/AMD-Software-PRO-Edition-${{ env.HIPSDK_INSTALLER_VERSION }}-WinSvr2022-For-HIP.exe" -OutFile "${env:RUNNER_TEMP}\rocm-install.exe"
|
||||
Invoke-WebRequest -Uri "https://download.amd.com/developer/eula/rocm-hub/AMD-Software-PRO-Edition-${{ env.HIPSDK_INSTALLER_VERSION }}-Win11-For-HIP.exe" -OutFile "${env:RUNNER_TEMP}\rocm-install.exe"
|
||||
write-host "Installing AMD HIP SDK"
|
||||
$proc = Start-Process "${env:RUNNER_TEMP}\rocm-install.exe" -ArgumentList '-install' -NoNewWindow -PassThru
|
||||
$completed = $proc.WaitForExit(600000)
|
||||
@@ -463,27 +648,184 @@ jobs:
|
||||
- name: Get commit hash
|
||||
id: commit
|
||||
if: ${{ ( github.event_name == 'push' && github.ref == 'refs/heads/master' ) || github.event.inputs.create_release == 'true' }}
|
||||
uses: pr-mpt/actions-commit-hash@v2
|
||||
uses: prompt/actions-commit-hash@v2
|
||||
|
||||
- name: Pack artifacts
|
||||
if: ${{ ( github.event_name == 'push' && github.ref == 'refs/heads/master' ) || github.event.inputs.create_release == 'true' }}
|
||||
run: |
|
||||
md "build\bin\rocblas\library\"
|
||||
md "build\bin\hipblaslt\library"
|
||||
cp "${env:HIP_PATH}\bin\hipblas.dll" "build\bin\"
|
||||
cp "${env:HIP_PATH}\bin\hipblaslt.dll" "build\bin\"
|
||||
cp "${env:HIP_PATH}\bin\libhipblas.dll" "build\bin\"
|
||||
cp "${env:HIP_PATH}\bin\libhipblaslt.dll" "build\bin\"
|
||||
cp "${env:HIP_PATH}\bin\rocblas.dll" "build\bin\"
|
||||
cp "${env:HIP_PATH}\bin\rocblas\library\*" "build\bin\rocblas\library\"
|
||||
cp "${env:HIP_PATH}\bin\hipblaslt\library\*" "build\bin\hipblaslt\library\"
|
||||
7z a sd-${{ env.BRANCH_NAME }}-${{ steps.commit.outputs.short }}-bin-win-rocm-x64.zip .\build\bin\*
|
||||
7z a sd-${{ env.BRANCH_NAME }}-${{ steps.commit.outputs.short }}-bin-win-rocm-${{ env.ROCM_VERSION }}-x64.zip .\build\bin\*
|
||||
|
||||
- name: Upload artifacts
|
||||
if: ${{ ( github.event_name == 'push' && github.ref == 'refs/heads/master' ) || github.event.inputs.create_release == 'true' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: sd-${{ env.BRANCH_NAME }}-${{ steps.commit.outputs.short }}-bin-win-rocm-x64.zip
|
||||
name: sd-${{ env.BRANCH_NAME }}-${{ steps.commit.outputs.short }}-bin-win-rocm-${{ env.ROCM_VERSION }}-x64.zip
|
||||
path: |
|
||||
sd-${{ env.BRANCH_NAME }}-${{ steps.commit.outputs.short }}-bin-win-rocm-x64.zip
|
||||
sd-${{ env.BRANCH_NAME }}-${{ steps.commit.outputs.short }}-bin-win-rocm-${{ env.ROCM_VERSION }}-x64.zip
|
||||
|
||||
ubuntu-latest-rocm:
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
env:
|
||||
UBUNTU_VERSION: "24.04"
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- ROCM_VERSION: "7.2.1"
|
||||
gpu_targets: "gfx908;gfx90a;gfx942;gfx1030;gfx1031;gfx1032;gfx1100;gfx1101;gfx1102;gfx1151;gfx1150;gfx1200;gfx1201"
|
||||
build: 'x64'
|
||||
- ROCM_VERSION: "7.13.0"
|
||||
gpu_targets: "gfx906;gfx908;gfx90a;gfx942;gfx950;gfx1010;gfx1011;gfx1012;gfx1030;gfx1031;gfx1032;gfx1033;gfx1034;gfx1035;gfx1036;gfx1100;gfx1101;gfx1102;gfx1150;gfx1151;gfx1152;gfx1200;gfx1201"
|
||||
build: x64
|
||||
|
||||
steps:
|
||||
- name: Clone
|
||||
id: checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: ccache
|
||||
uses: ggml-org/ccache-action@v1.2.16
|
||||
with:
|
||||
key: ubuntu-rocm-cmake-${{ matrix.ROCM_VERSION }}-${{ matrix.build }}
|
||||
evict-old-files: 1d
|
||||
|
||||
- name: Dependencies
|
||||
id: depends
|
||||
run: |
|
||||
sudo apt install -y build-essential cmake wget zip ninja-build
|
||||
|
||||
- name: Free disk space
|
||||
run: |
|
||||
df -h
|
||||
|
||||
# Remove preinstalled SDKs and caches not needed for this job before
|
||||
# installing ROCm. The legacy ROCm apt packages are large enough to
|
||||
# exhaust ubuntu-latest if cleanup runs after installation.
|
||||
sudo rm -rf /usr/share/dotnet || true
|
||||
sudo rm -rf /usr/local/lib/android || true
|
||||
sudo rm -rf /opt/ghc || true
|
||||
sudo rm -rf /usr/local/.ghcup || true
|
||||
sudo rm -rf /opt/hostedtoolcache || true
|
||||
sudo rm -rf /usr/share/swift || true
|
||||
sudo rm -rf /usr/local/share/boost || true
|
||||
docker system prune -af || true
|
||||
|
||||
sudo apt clean
|
||||
df -h
|
||||
|
||||
- name: Setup Legacy ROCm
|
||||
if: matrix.ROCM_VERSION == '7.2.1'
|
||||
id: legacy_env
|
||||
run: |
|
||||
sudo mkdir --parents --mode=0755 /etc/apt/keyrings
|
||||
wget https://repo.radeon.com/rocm/rocm.gpg.key -O - | \
|
||||
gpg --dearmor | sudo tee /etc/apt/keyrings/rocm.gpg > /dev/null
|
||||
|
||||
sudo tee /etc/apt/sources.list.d/rocm.list << EOF
|
||||
deb [arch=amd64 signed-by=/etc/apt/keyrings/rocm.gpg] https://repo.radeon.com/rocm/apt/${{ matrix.ROCM_VERSION }} noble main
|
||||
EOF
|
||||
|
||||
sudo tee /etc/apt/preferences.d/rocm-pin-600 << EOF
|
||||
Package: *
|
||||
Pin: release o=repo.radeon.com
|
||||
Pin-Priority: 600
|
||||
EOF
|
||||
|
||||
sudo apt update
|
||||
sudo apt-get install -y libssl-dev rocm-hip-sdk
|
||||
|
||||
- name: Setup TheRock
|
||||
if: matrix.ROCM_VERSION != '7.2.1'
|
||||
id: therock_env
|
||||
run: |
|
||||
wget https://repo.amd.com/rocm/tarball/therock-dist-linux-gfx1151-${{ matrix.ROCM_VERSION }}.tar.gz
|
||||
mkdir install
|
||||
tar -xf *.tar.gz -C install
|
||||
export ROCM_PATH=$(pwd)/install
|
||||
echo ROCM_PATH=$ROCM_PATH >> $GITHUB_ENV
|
||||
echo PATH=$PATH:$ROCM_PATH/bin >> $GITHUB_ENV
|
||||
echo LD_LIBRARY_PATH=$ROCM_PATH/lib:$ROCM_PATH/llvm/lib:$ROCM_PATH/lib/rocprofiler-systems >> $GITHUB_ENV
|
||||
|
||||
# setup-node installs into /opt/hostedtoolcache, which is removed above.
|
||||
# Keep Node/pnpm setup after disk cleanup so the server frontend can be embedded.
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10.15.1
|
||||
|
||||
- name: Build
|
||||
id: cmake_build
|
||||
run: |
|
||||
mkdir build
|
||||
cd build
|
||||
cmake .. -G Ninja \
|
||||
-DCMAKE_HIP_COMPILER="$(hipconfig -l)/clang" \
|
||||
-DCMAKE_HIP_FLAGS="-mllvm --amdgpu-unroll-threshold-local=600" \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DSD_HIPBLAS=ON \
|
||||
-DHIP_PLATFORM=amd \
|
||||
-DGPU_TARGETS="${{ matrix.gpu_targets }}" \
|
||||
-DCMAKE_BUILD_WITH_INSTALL_RPATH=ON \
|
||||
-DCMAKE_POSITION_INDEPENDENT_CODE=ON \
|
||||
-DCMAKE_INSTALL_RPATH='$ORIGIN' \
|
||||
-DGGML_NATIVE=OFF \
|
||||
-DSD_BUILD_SHARED_GGML_LIB=ON \
|
||||
-DGGML_BACKEND_DL=ON \
|
||||
-DGGML_CPU_ALL_VARIANTS=ON \
|
||||
-DSD_BUILD_SHARED_LIBS=ON
|
||||
cmake --build . --config Release
|
||||
|
||||
- name: Get commit hash
|
||||
id: commit
|
||||
if: ${{ ( github.event_name == 'push' && github.ref == 'refs/heads/master' ) || github.event.inputs.create_release == 'true' }}
|
||||
uses: prompt/actions-commit-hash@v2
|
||||
|
||||
- name: Prepare artifacts
|
||||
id: prepare_artifacts
|
||||
if: ${{ ( github.event_name == 'push' && github.ref == 'refs/heads/master' ) || github.event.inputs.create_release == 'true' }}
|
||||
run: |
|
||||
# Copy licenses
|
||||
cp ggml/LICENSE ./build/bin/ggml.txt
|
||||
cp LICENSE ./build/bin/stable-diffusion.cpp.txt
|
||||
|
||||
- name: Fetch system info
|
||||
id: system-info
|
||||
run: |
|
||||
echo "CPU_ARCH=`uname -m`" >> "$GITHUB_OUTPUT"
|
||||
echo "OS_NAME=`lsb_release -s -i`" >> "$GITHUB_OUTPUT"
|
||||
echo "OS_VERSION=`lsb_release -s -r`" >> "$GITHUB_OUTPUT"
|
||||
echo "OS_TYPE=`uname -s`" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Pack artifacts
|
||||
id: pack_artifacts
|
||||
if: ${{ ( github.event_name == 'push' && github.ref == 'refs/heads/master' ) || github.event.inputs.create_release == 'true' }}
|
||||
run: |
|
||||
cp ggml/LICENSE ./build/bin/ggml.txt
|
||||
cp LICENSE ./build/bin/stable-diffusion.cpp.txt
|
||||
zip -y -r sd-${{ env.BRANCH_NAME }}-${{ steps.commit.outputs.short }}-bin-${{ steps.system-info.outputs.OS_TYPE }}-Ubuntu-${{ env.UBUNTU_VERSION }}-${{ steps.system-info.outputs.CPU_ARCH }}-rocm-${{ matrix.ROCM_VERSION }}.zip ./build/bin
|
||||
|
||||
- name: Upload artifacts
|
||||
if: ${{ ( github.event_name == 'push' && github.ref == 'refs/heads/master' ) || github.event.inputs.create_release == 'true' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: sd-${{ env.BRANCH_NAME }}-${{ steps.commit.outputs.short }}-bin-${{ steps.system-info.outputs.OS_TYPE }}-Ubuntu-${{ env.UBUNTU_VERSION }}-${{ steps.system-info.outputs.CPU_ARCH }}-rocm-${{ matrix.ROCM_VERSION }}.zip
|
||||
path: |
|
||||
sd-${{ env.BRANCH_NAME }}-${{ steps.commit.outputs.short }}-bin-${{ steps.system-info.outputs.OS_TYPE }}-Ubuntu-${{ env.UBUNTU_VERSION }}-${{ steps.system-info.outputs.CPU_ARCH }}-rocm-${{ matrix.ROCM_VERSION }}.zip
|
||||
|
||||
release:
|
||||
if: ${{ ( github.event_name == 'push' && github.ref == 'refs/heads/master' ) || github.event.inputs.create_release == 'true' }}
|
||||
@@ -493,10 +835,12 @@ jobs:
|
||||
needs:
|
||||
- ubuntu-latest-cmake
|
||||
- ubuntu-latest-cmake-vulkan
|
||||
- ubuntu-latest-rocm
|
||||
- build-and-push-docker-images
|
||||
- macOS-latest-cmake
|
||||
- windows-latest-cmake
|
||||
- windows-latest-cmake-hip
|
||||
- windows-latest-rocm
|
||||
|
||||
steps:
|
||||
- name: Clone
|
||||
@@ -519,7 +863,7 @@ jobs:
|
||||
|
||||
- name: Get commit hash
|
||||
id: commit
|
||||
uses: pr-mpt/actions-commit-hash@v2
|
||||
uses: prompt/actions-commit-hash@v2
|
||||
|
||||
- name: Create release
|
||||
id: create_release
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
name: Close inactive PRs
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run daily. GitHub cron schedules use UTC.
|
||||
- cron: "30 1 * * *"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
debug_only:
|
||||
description: "Dry run: log intended actions without changing PRs"
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
stale-prs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Mark and close inactive PRs
|
||||
uses: actions/stale@v10
|
||||
with:
|
||||
days-before-issue-stale: -1
|
||||
days-before-issue-close: -1
|
||||
|
||||
days-before-pr-stale: 365
|
||||
days-before-pr-close: 7
|
||||
|
||||
stale-pr-label: pr:inactive
|
||||
close-pr-label: pr:auto-closed
|
||||
exempt-pr-labels: pr:keep-open
|
||||
|
||||
stale-pr-message: >
|
||||
This PR has been inactive for 365 days. If there is no new activity
|
||||
within 7 days, it will be closed automatically. Comment, push new
|
||||
commits, or remove the pr:inactive label to keep it open. Add
|
||||
pr:keep-open to exempt it from future inactive PR cleanup.
|
||||
|
||||
close-pr-message: >
|
||||
Closing this PR because it has had no activity for 7 days after
|
||||
being marked inactive. If this is still useful or ready to move
|
||||
forward, feel free to reopen it with fresh context or updated
|
||||
details. Sorry for any inconvenience.
|
||||
|
||||
remove-pr-stale-when-updated: true
|
||||
delete-branch: false
|
||||
operations-per-run: 100
|
||||
debug-only: ${{ github.event_name == 'workflow_dispatch' && inputs.debug_only || false }}
|
||||
@@ -1,3 +1,12 @@
|
||||
[submodule "ggml"]
|
||||
path = ggml
|
||||
url = https://github.com/ggml-org/ggml.git
|
||||
url = https://github.com/leejet/ggml.git
|
||||
[submodule "examples/server/frontend"]
|
||||
path = examples/server/frontend
|
||||
url = https://github.com/leejet/sdcpp-webui.git
|
||||
[submodule "thirdparty/libwebp"]
|
||||
path = thirdparty/libwebp
|
||||
url = https://github.com/webmproject/libwebp.git
|
||||
[submodule "thirdparty/libwebm"]
|
||||
path = thirdparty/libwebm
|
||||
url = https://github.com/webmproject/libwebm.git
|
||||
|
||||
@@ -11,17 +11,68 @@ endif()
|
||||
if (MSVC)
|
||||
add_compile_definitions(_CRT_SECURE_NO_WARNINGS)
|
||||
add_compile_definitions(_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING)
|
||||
add_compile_options(
|
||||
$<$<COMPILE_LANGUAGE:C>:/MP>
|
||||
$<$<COMPILE_LANGUAGE:C>:/utf-8>
|
||||
$<$<COMPILE_LANGUAGE:CXX>:/MP>
|
||||
$<$<COMPILE_LANGUAGE:CXX>:/utf-8>
|
||||
)
|
||||
endif()
|
||||
|
||||
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
|
||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
|
||||
|
||||
if(APPLE)
|
||||
function(sd_set_macos_rpaths target)
|
||||
get_target_property(target_type ${target} TYPE)
|
||||
if(target_type STREQUAL "EXECUTABLE")
|
||||
set(runtime_paths "@executable_path" "@executable_path/../lib")
|
||||
elseif(target_type STREQUAL "SHARED_LIBRARY" OR target_type STREQUAL "MODULE_LIBRARY")
|
||||
set(runtime_paths "@loader_path" "@loader_path/../lib")
|
||||
set_target_properties(${target} PROPERTIES
|
||||
MACOSX_RPATH ON
|
||||
INSTALL_NAME_DIR "@rpath"
|
||||
BUILD_WITH_INSTALL_NAME_DIR ON
|
||||
)
|
||||
else()
|
||||
return()
|
||||
endif()
|
||||
|
||||
# Release artifacts zip the build output directly, so keep macOS rpaths relocatable.
|
||||
set_target_properties(${target} PROPERTIES
|
||||
BUILD_RPATH "${runtime_paths}"
|
||||
INSTALL_RPATH "${runtime_paths}"
|
||||
BUILD_WITH_INSTALL_RPATH ON
|
||||
)
|
||||
endfunction()
|
||||
endif()
|
||||
|
||||
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
|
||||
set(SD_STANDALONE ON)
|
||||
else()
|
||||
set(SD_STANDALONE OFF)
|
||||
endif()
|
||||
|
||||
set(SD_SUBMODULE_WEBP FALSE)
|
||||
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/thirdparty/libwebp/CMakeLists.txt")
|
||||
set(SD_SUBMODULE_WEBP TRUE)
|
||||
endif()
|
||||
if(SD_SUBMODULE_WEBP)
|
||||
set(SD_WEBP_DEFAULT ON)
|
||||
else()
|
||||
set(SD_WEBP_DEFAULT ${SD_USE_SYSTEM_WEBP})
|
||||
endif()
|
||||
|
||||
set(SD_SUBMODULE_WEBM FALSE)
|
||||
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/thirdparty/libwebm/CMakeLists.txt")
|
||||
set(SD_SUBMODULE_WEBM TRUE)
|
||||
endif()
|
||||
if(SD_SUBMODULE_WEBM)
|
||||
set(SD_WEBM_DEFAULT ON)
|
||||
else()
|
||||
set(SD_WEBM_DEFAULT ${SD_USE_SYSTEM_WEBM})
|
||||
endif()
|
||||
|
||||
#
|
||||
# Option list
|
||||
#
|
||||
@@ -29,6 +80,10 @@ endif()
|
||||
# general
|
||||
#option(SD_BUILD_TESTS "sd: build tests" ${SD_STANDALONE})
|
||||
option(SD_BUILD_EXAMPLES "sd: build examples" ${SD_STANDALONE})
|
||||
option(SD_WEBP "sd: enable WebP image I/O support" ${SD_WEBP_DEFAULT})
|
||||
option(SD_USE_SYSTEM_WEBP "sd: link against system libwebp" OFF)
|
||||
option(SD_WEBM "sd: enable WebM video output support" ${SD_WEBM_DEFAULT})
|
||||
option(SD_USE_SYSTEM_WEBM "sd: link against system libwebm" OFF)
|
||||
option(SD_CUDA "sd: cuda backend" OFF)
|
||||
option(SD_HIPBLAS "sd: rocm backend" OFF)
|
||||
option(SD_METAL "sd: metal backend" OFF)
|
||||
@@ -36,60 +91,152 @@ option(SD_VULKAN "sd: vulkan backend" OFF)
|
||||
option(SD_OPENCL "sd: opencl backend" OFF)
|
||||
option(SD_SYCL "sd: sycl backend" OFF)
|
||||
option(SD_MUSA "sd: musa backend" OFF)
|
||||
option(SD_FAST_SOFTMAX "sd: x1.5 faster softmax, indeterministic (sometimes, same seed don't generate same image), cuda only" OFF)
|
||||
option(SD_BUILD_SHARED_LIBS "sd: build shared libs" OFF)
|
||||
option(SD_BUILD_SHARED_GGML_LIB "sd: build ggml as a separate shared lib" OFF)
|
||||
option(SD_USE_SYSTEM_GGML "sd: use system-installed GGML library" OFF)
|
||||
#option(SD_BUILD_SERVER "sd: build server example" ON)
|
||||
|
||||
set(CMAKE_C_STANDARD 11)
|
||||
set(CMAKE_C_STANDARD_REQUIRED true)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED true)
|
||||
|
||||
if(SD_CUDA)
|
||||
message("-- Use CUDA as backend stable-diffusion")
|
||||
set(GGML_CUDA ON)
|
||||
add_definitions(-DSD_USE_CUDA)
|
||||
endif()
|
||||
|
||||
if(SD_METAL)
|
||||
message("-- Use Metal as backend stable-diffusion")
|
||||
set(GGML_METAL ON)
|
||||
add_definitions(-DSD_USE_METAL)
|
||||
endif()
|
||||
|
||||
if (SD_VULKAN)
|
||||
message("-- Use Vulkan as backend stable-diffusion")
|
||||
set(GGML_VULKAN ON)
|
||||
add_definitions(-DSD_USE_VULKAN)
|
||||
endif ()
|
||||
|
||||
if (SD_OPENCL)
|
||||
message("-- Use OpenCL as backend stable-diffusion")
|
||||
set(GGML_OPENCL ON)
|
||||
add_definitions(-DSD_USE_OPENCL)
|
||||
endif ()
|
||||
|
||||
if (SD_HIPBLAS)
|
||||
message("-- Use HIPBLAS as backend stable-diffusion")
|
||||
set(GGML_HIP ON)
|
||||
add_definitions(-DSD_USE_CUDA)
|
||||
if(SD_FAST_SOFTMAX)
|
||||
set(GGML_CUDA_FAST_SOFTMAX ON)
|
||||
endif()
|
||||
# ggml-hip's device-stub objects must be position-independent, or the
|
||||
# default-PIE sd-cli link fails with `relocation R_X86_64_32 ... cannot be
|
||||
# used when making a PIE object` on distros that default to PIE
|
||||
# (Ubuntu 24.04, Fedora 40+, Debian 12+). The shared-library branch below
|
||||
# already sets this; the static build (the HIP default) did not.
|
||||
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
||||
endif ()
|
||||
|
||||
if(SD_MUSA)
|
||||
message("-- Use MUSA as backend stable-diffusion")
|
||||
set(GGML_MUSA ON)
|
||||
add_definitions(-DSD_USE_CUDA)
|
||||
if(SD_FAST_SOFTMAX)
|
||||
set(GGML_CUDA_FAST_SOFTMAX ON)
|
||||
endif()
|
||||
|
||||
if(SD_WEBP)
|
||||
if(NOT SD_SUBMODULE_WEBP AND NOT SD_USE_SYSTEM_WEBP)
|
||||
message(FATAL_ERROR "WebP support enabled but no source found.
|
||||
Either initialize the submodule:\n git submodule update --init thirdparty/libwebp\n\n"
|
||||
"Or link against system library:\n cmake (...) -DSD_USE_SYSTEM_WEBP=ON")
|
||||
endif()
|
||||
if(SD_USE_SYSTEM_WEBP)
|
||||
find_package(WebP)
|
||||
if(WebP_FOUND)
|
||||
add_library(webp ALIAS WebP::webp)
|
||||
# libwebp CMake target naming is not consistent across versions/distros.
|
||||
# Some export WebP::libwebpmux, others export WebP::webpmux.
|
||||
if(TARGET WebP::libwebpmux)
|
||||
add_library(libwebpmux ALIAS WebP::libwebpmux)
|
||||
elseif(TARGET WebP::webpmux)
|
||||
add_library(libwebpmux ALIAS WebP::webpmux)
|
||||
else()
|
||||
message(FATAL_ERROR
|
||||
"Could not find a compatible webpmux target in system WebP package. "
|
||||
"Expected WebP::libwebpmux or WebP::webpmux."
|
||||
)
|
||||
endif()
|
||||
else()
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(WebP REQUIRED IMPORTED_TARGET GLOBAL libwebp)
|
||||
pkg_check_modules(WebPMux REQUIRED IMPORTED_TARGET GLOBAL libwebpmux)
|
||||
link_libraries(PkgConfig::WebP)
|
||||
link_libraries(PkgConfig::WebPMux)
|
||||
add_library(libwebpmux ALIAS PkgConfig::WebPMux)
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(SD_WEBM)
|
||||
if(NOT SD_WEBP)
|
||||
message(FATAL_ERROR "SD_WEBM requires SD_WEBP because WebM output reuses libwebp VP8 encoding.")
|
||||
endif()
|
||||
if(NOT SD_SUBMODULE_WEBM AND NOT SD_USE_SYSTEM_WEBM)
|
||||
message(FATAL_ERROR "WebM support enabled but no source found.
|
||||
Either initialize the submodule:\n git submodule update --init thirdparty/libwebm\n\n"
|
||||
"Or link against system library:\n cmake (...) -DSD_USE_SYSTEM_WEBM=ON")
|
||||
endif()
|
||||
if(SD_USE_SYSTEM_WEBM)
|
||||
find_package(PkgConfig)
|
||||
if(PkgConfig_FOUND)
|
||||
pkg_check_modules(WebM REQUIRED IMPORTED_TARGET GLOBAL libwebm)
|
||||
endif()
|
||||
if(PkgConfig_FOUND AND WebM_FOUND)
|
||||
link_libraries(PkgConfig::WebM)
|
||||
else()
|
||||
find_path(WEBM_INCLUDE_DIR
|
||||
NAMES mkvmuxer/mkvmuxer.h mkvparser/mkvparser.h common/webmids.h
|
||||
PATH_SUFFIXES webm
|
||||
REQUIRED)
|
||||
find_library(WEBM_LIBRARY
|
||||
NAMES webm libwebm
|
||||
REQUIRED)
|
||||
|
||||
add_library(webm UNKNOWN IMPORTED)
|
||||
set_target_properties(webm PROPERTIES
|
||||
IMPORTED_LOCATION "${WEBM_LIBRARY}"
|
||||
INTERFACE_INCLUDE_DIRECTORIES "${WEBM_INCLUDE_DIR}")
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if (SD_RPC)
|
||||
message("-- Use RPC as backend stable-diffusion")
|
||||
set(GGML_RPC ON)
|
||||
add_definitions(-DSD_USE_RPC)
|
||||
endif ()
|
||||
|
||||
set(SD_LIB stable-diffusion)
|
||||
|
||||
file(GLOB SD_LIB_SOURCES
|
||||
"*.h"
|
||||
"*.cpp"
|
||||
"*.hpp"
|
||||
file(GLOB SD_LIB_SOURCES CONFIGURE_DEPENDS
|
||||
"src/*.h"
|
||||
"src/*.cpp"
|
||||
"src/*.hpp"
|
||||
"src/conditioning/*.h"
|
||||
"src/conditioning/*.cpp"
|
||||
"src/conditioning/*.hpp"
|
||||
"src/core/*.h"
|
||||
"src/core/*.cpp"
|
||||
"src/core/*.hpp"
|
||||
"src/extensions/*.h"
|
||||
"src/extensions/*.cpp"
|
||||
"src/extensions/*.hpp"
|
||||
"src/model/*/*.h"
|
||||
"src/model/*/*.cpp"
|
||||
"src/model/*/*.hpp"
|
||||
"src/runtime/*.h"
|
||||
"src/runtime/*.cpp"
|
||||
"src/runtime/*.hpp"
|
||||
"src/model_io/*.h"
|
||||
"src/model_io/*.cpp"
|
||||
"src/tokenizers/*.h"
|
||||
"src/tokenizers/*.cpp"
|
||||
"src/tokenizers/vocab/*.h"
|
||||
"src/tokenizers/vocab/*.cpp"
|
||||
)
|
||||
|
||||
find_program(GIT_EXE NAMES git git.exe NO_CMAKE_FIND_ROOT_PATH)
|
||||
@@ -119,7 +266,7 @@ endif()
|
||||
message(STATUS "stable-diffusion.cpp commit ${SDCPP_BUILD_COMMIT}")
|
||||
|
||||
set_property(
|
||||
SOURCE ${CMAKE_CURRENT_SOURCE_DIR}/version.cpp
|
||||
SOURCE ${CMAKE_CURRENT_SOURCE_DIR}/src/version.cpp
|
||||
APPEND PROPERTY COMPILE_DEFINITIONS
|
||||
SDCPP_BUILD_COMMIT=${SDCPP_BUILD_COMMIT} SDCPP_BUILD_VERSION=${SDCPP_BUILD_VERSION}
|
||||
)
|
||||
@@ -142,11 +289,14 @@ else()
|
||||
add_library(${SD_LIB} STATIC ${SD_LIB_SOURCES})
|
||||
endif()
|
||||
|
||||
if(APPLE)
|
||||
sd_set_macos_rpaths(${SD_LIB})
|
||||
endif()
|
||||
|
||||
if(SD_SYCL)
|
||||
message("-- Use SYCL as backend stable-diffusion")
|
||||
set(GGML_SYCL ON)
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-narrowing -fsycl")
|
||||
add_definitions(-DSD_USE_SYCL)
|
||||
# disable fast-math on host, see:
|
||||
# https://www.intel.com/content/www/us/en/docs/cpp-compiler/developer-guide-reference/2021-10/fp-model-fp.html
|
||||
if (WIN32)
|
||||
@@ -181,7 +331,10 @@ endif()
|
||||
|
||||
add_subdirectory(thirdparty)
|
||||
|
||||
target_link_libraries(${SD_LIB} PUBLIC ggml zip)
|
||||
target_sources(${SD_LIB} PRIVATE $<TARGET_OBJECTS:zip>)
|
||||
target_link_libraries(${SD_LIB} PUBLIC ggml)
|
||||
target_include_directories(${SD_LIB} PUBLIC . src include)
|
||||
target_include_directories(${SD_LIB} PRIVATE src/core)
|
||||
target_include_directories(${SD_LIB} PUBLIC . thirdparty)
|
||||
target_compile_features(${SD_LIB} PUBLIC c_std_11 cxx_std_17)
|
||||
|
||||
@@ -190,7 +343,58 @@ if (SD_BUILD_EXAMPLES)
|
||||
add_subdirectory(examples)
|
||||
endif()
|
||||
|
||||
set(SD_PUBLIC_HEADERS stable-diffusion.h)
|
||||
set_target_properties(${SD_LIB} PROPERTIES PUBLIC_HEADER "${SD_PUBLIC_HEADERS}")
|
||||
|
||||
install(TARGETS ${SD_LIB} LIBRARY PUBLIC_HEADER)
|
||||
|
||||
#
|
||||
# install
|
||||
#
|
||||
|
||||
include(CMakePackageConfigHelpers)
|
||||
include(GNUInstallDirs)
|
||||
|
||||
set(SD_INSTALL_VERSION "${SDCPP_BUILD_VERSION}")
|
||||
set(SD_INSTALL_COMMIT "${SDCPP_BUILD_COMMIT}")
|
||||
set(SD_SHARED_LIB ${SD_BUILD_SHARED_LIBS})
|
||||
|
||||
set(SD_INCLUDE_INSTALL_DIR ${CMAKE_INSTALL_INCLUDEDIR} CACHE PATH "Location of header files")
|
||||
set(SD_LIB_INSTALL_DIR ${CMAKE_INSTALL_LIBDIR} CACHE PATH "Location of library files")
|
||||
set(SD_BIN_INSTALL_DIR ${CMAKE_INSTALL_BINDIR} CACHE PATH "Location of binary files")
|
||||
|
||||
set(SD_PUBLIC_HEADERS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/stable-diffusion.h)
|
||||
|
||||
set_target_properties(${SD_LIB}
|
||||
PROPERTIES
|
||||
PUBLIC_HEADER "${SD_PUBLIC_HEADERS}")
|
||||
|
||||
|
||||
install(TARGETS ${SD_LIB}
|
||||
ARCHIVE
|
||||
LIBRARY
|
||||
RUNTIME
|
||||
PUBLIC_HEADER)
|
||||
|
||||
|
||||
configure_package_config_file(
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/cmake/stable-diffusion-config.cmake.in
|
||||
${CMAKE_CURRENT_BINARY_DIR}/stable-diffusion-config.cmake
|
||||
INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/stable-diffusion
|
||||
PATH_VARS SD_INCLUDE_INSTALL_DIR
|
||||
SD_LIB_INSTALL_DIR
|
||||
SD_BIN_INSTALL_DIR )
|
||||
|
||||
write_basic_package_version_file(
|
||||
${CMAKE_CURRENT_BINARY_DIR}/stable-diffusion-version.cmake
|
||||
VERSION ${SD_INSTALL_VERSION}
|
||||
COMPATIBILITY SameMajorVersion)
|
||||
|
||||
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/stable-diffusion-config.cmake
|
||||
${CMAKE_CURRENT_BINARY_DIR}/stable-diffusion-version.cmake
|
||||
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/stable-diffusion)
|
||||
|
||||
configure_file(cmake/stable-diffusion.pc.in
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/stable-diffusion.pc"
|
||||
@ONLY)
|
||||
|
||||
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/stable-diffusion.pc"
|
||||
DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# Contributing
|
||||
|
||||
This document collects general contribution conventions for this repository.
|
||||
|
||||
## Before You Start
|
||||
|
||||
Before opening a PR, please search existing PRs to avoid duplicating ongoing work.
|
||||
|
||||
For large-scale refactors or changes with broad impact, please open an issue first to discuss the approach before submitting a PR.
|
||||
|
||||
If you want to update a third-party dependency, please open an issue first instead of submitting a direct PR. See [Dependency Updates](#dependency-updates) for details.
|
||||
|
||||
## Pull Requests
|
||||
|
||||
Keep each PR focused on one clear change. Large or overly complex PRs are harder to review and may not be merged.
|
||||
|
||||
Follow Conventional Commit-style subjects seen in history: `feat:`, `fix:`, `refactor:`, `ci:`, `docs:`, `chore:`. Keep subjects imperative and scoped.
|
||||
|
||||
PRs should include:
|
||||
|
||||
- What changed and why (short problem/solution summary).
|
||||
- Verification evidence when applicable (commands and key outputs).
|
||||
- Linked issue/PR context when applicable.
|
||||
- Screenshots or sample outputs for UI/visual behavior changes.
|
||||
|
||||
## Code Style
|
||||
|
||||
Format code according to the repository style before submitting changes.
|
||||
|
||||
Formatting follows `.clang-format` (Chromium base, 4-space indent, no tabs). Run `format-code.sh` before opening a PR. Keep C++ standard at C++17-compatible patterns used in this repo.
|
||||
|
||||
Naming conventions:
|
||||
|
||||
- Use `PascalCase` for class/struct/type names.
|
||||
- In `PascalCase` names, preserve common abbreviations in uppercase, for example `SD`, `API`, `HTTP`, `JSON`, `RGB`, `VAE`, `TAE`, `LoRA`, and `WebP`.
|
||||
- Use `snake_case` for functions, methods, variables, and file names unless an existing API requires a different style.
|
||||
- Use a trailing underscore for private data member names, for example `hidden_size_` or `tokenizer_`.
|
||||
- Use `.h` for C and C++ header files. Do not introduce new `.hpp` headers.
|
||||
- Use macro-based header include guards instead of `#pragma once`.
|
||||
- Format header include guards as `__SD_{PATH}__`, where `{PATH}` is the header path in uppercase snake case without the file extension. For example, `src/sample.h` should use `__SD_SAMPLE_H__`.
|
||||
- Do not introduce anonymous namespaces in new or modified code; prefer `static` file-local functions/variables or an explicit named namespace when scoping is needed.
|
||||
- In `class`/`struct` definitions, place data members before member functions unless an existing type already clearly follows a different pattern.
|
||||
- Keep `test_*.cpp` / `test_*.py` naming for tests.
|
||||
|
||||
Some older code in the project may not fully follow the current conventions. Please do not submit PRs that only rewrite existing code to match style rules.
|
||||
|
||||
When adding or modifying model implementations, follow the model config and weight detection conventions in [docs/model_config.md](docs/model_config.md).
|
||||
|
||||
## AI-Assisted Contributions
|
||||
|
||||
AI tools may be used to assist development, but contributors are responsible for the quality and correctness of the submitted code.
|
||||
|
||||
If any part of a contribution was generated with AI assistance, the contributor must perform a thorough human review before submitting the PR and understand every changed line.
|
||||
|
||||
Do not list AI tools as co-authors. The human contributor is the sole responsible author of the submitted code.
|
||||
|
||||
Please do not submit AI-generated code that you do not understand, and do not include meaningless experiments, temporary test code, or unrelated generated output in a PR.
|
||||
|
||||
## Dependency Updates
|
||||
|
||||
Do not submit PRs that update `ggml`. `ggml` updates are performed only after local validation by the maintainer.
|
||||
|
||||
Other third-party dependencies are not updated unless necessary. If you want to update a dependency, please open an issue first instead of submitting a direct PR.
|
||||
|
||||
## Security & Configuration
|
||||
|
||||
Do not commit model weights, secrets, or local absolute paths. Keep large binaries out of git unless intentionally tracked release assets.
|
||||
@@ -2,13 +2,31 @@ ARG UBUNTU_VERSION=24.04
|
||||
|
||||
FROM ubuntu:$UBUNTU_VERSION AS build
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends build-essential git cmake
|
||||
# sd-server embeds the web UI at build time, so the build image needs Node/pnpm.
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends build-essential git cmake ca-certificates curl gnupg && \
|
||||
mkdir -p /etc/apt/keyrings && \
|
||||
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key -o /tmp/nodesource-repo.gpg.key && \
|
||||
gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg /tmp/nodesource-repo.gpg.key && \
|
||||
rm /tmp/nodesource-repo.gpg.key && \
|
||||
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" > /etc/apt/sources.list.d/nodesource.list && \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends nodejs && \
|
||||
npm install -g pnpm@10.15.1 && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /sd.cpp
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN cmake . -B ./build
|
||||
RUN cmake . -B ./build \
|
||||
-DSD_BUILD_SHARED_LIBS=ON \
|
||||
-DGGML_NATIVE=OFF \
|
||||
-DSD_BUILD_SHARED_GGML_LIB=ON \
|
||||
-DGGML_BACKEND_DL=ON \
|
||||
-DGGML_CPU_ALL_VARIANTS=ON \
|
||||
-DCMAKE_BUILD_WITH_INSTALL_RPATH=ON \
|
||||
-DCMAKE_INSTALL_RPATH='$ORIGIN'
|
||||
RUN cmake --build ./build --config Release --parallel
|
||||
|
||||
FROM ubuntu:$UBUNTU_VERSION AS runtime
|
||||
@@ -17,7 +35,9 @@ RUN apt-get update && \
|
||||
apt-get install --yes --no-install-recommends libgomp1 && \
|
||||
apt-get clean
|
||||
|
||||
COPY --from=build /sd.cpp/build/bin/sd-cli /sd-cli
|
||||
COPY --from=build /sd.cpp/build/bin/sd-server /sd-server
|
||||
COPY --from=build /sd.cpp/build/bin /sd.cpp/bin
|
||||
RUN printf '#!/bin/sh\nexec /sd.cpp/bin/sd-cli "$@"\n' > /sd-cli && \
|
||||
printf '#!/bin/sh\nexec /sd.cpp/bin/sd-server "$@"\n' > /sd-server && \
|
||||
chmod +x /sd-cli /sd-server
|
||||
|
||||
ENTRYPOINT [ "/sd-cli" ]
|
||||
ENTRYPOINT [ "/sd-cli" ]
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
ARG CUDA_VERSION=12.6.3
|
||||
ARG UBUNTU_VERSION=24.04
|
||||
|
||||
FROM nvidia/cuda:${CUDA_VERSION}-cudnn-devel-ubuntu${UBUNTU_VERSION} AS build
|
||||
|
||||
# sd-server embeds the web UI at build time, so the build image needs Node/pnpm.
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends build-essential git ccache cmake ca-certificates curl gnupg && \
|
||||
mkdir -p /etc/apt/keyrings && \
|
||||
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key -o /tmp/nodesource-repo.gpg.key && \
|
||||
gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg /tmp/nodesource-repo.gpg.key && \
|
||||
rm /tmp/nodesource-repo.gpg.key && \
|
||||
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" > /etc/apt/sources.list.d/nodesource.list && \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends nodejs && \
|
||||
npm install -g pnpm@10.15.1 && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /sd.cpp
|
||||
|
||||
COPY . .
|
||||
|
||||
ARG CUDACXX=/usr/local/cuda/bin/nvcc
|
||||
ARG CUDA_ARCHITECTURES=""
|
||||
ARG GGML_CUDA_FA_ALL_QUANTS=""
|
||||
ARG GGML_CUDA_ENABLE_DYNAMIC_CPU_BACKENDS=ON
|
||||
|
||||
RUN set -- \
|
||||
-DSD_CUDA=ON; \
|
||||
if [ "${GGML_CUDA_ENABLE_DYNAMIC_CPU_BACKENDS}" = "ON" ]; then \
|
||||
set -- "$@" \
|
||||
-DSD_BUILD_SHARED_LIBS=ON \
|
||||
-DGGML_NATIVE=OFF \
|
||||
-DSD_BUILD_SHARED_GGML_LIB=ON \
|
||||
-DGGML_BACKEND_DL=ON \
|
||||
-DGGML_CPU_ALL_VARIANTS=ON \
|
||||
-DCMAKE_BUILD_WITH_INSTALL_RPATH=ON \
|
||||
"-DCMAKE_INSTALL_RPATH=\$ORIGIN"; \
|
||||
fi; \
|
||||
if [ -n "${CUDA_ARCHITECTURES}" ]; then \
|
||||
set -- "$@" "-DCMAKE_CUDA_ARCHITECTURES=${CUDA_ARCHITECTURES}"; \
|
||||
fi; \
|
||||
if [ -n "${GGML_CUDA_FA_ALL_QUANTS}" ]; then \
|
||||
set -- "$@" "-DGGML_CUDA_FA_ALL_QUANTS=${GGML_CUDA_FA_ALL_QUANTS}"; \
|
||||
fi; \
|
||||
cmake . -B ./build "$@"
|
||||
RUN cmake --build ./build --config Release -j$(nproc)
|
||||
|
||||
FROM nvidia/cuda:${CUDA_VERSION}-cudnn-runtime-ubuntu${UBUNTU_VERSION} AS runtime
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install --yes --no-install-recommends libgomp1 && \
|
||||
apt-get clean
|
||||
|
||||
COPY --from=build /sd.cpp/build/bin /sd.cpp/bin
|
||||
RUN printf '#!/bin/sh\nexec /sd.cpp/bin/sd-cli "$@"\n' > /sd-cli && \
|
||||
printf '#!/bin/sh\nexec /sd.cpp/bin/sd-server "$@"\n' > /sd-server && \
|
||||
chmod +x /sd-cli /sd-server
|
||||
|
||||
ENTRYPOINT [ "/sd-cli" ]
|
||||
@@ -3,7 +3,18 @@ ARG UBUNTU_VERSION=22.04
|
||||
|
||||
FROM mthreads/musa:${MUSA_VERSION}-devel-ubuntu${UBUNTU_VERSION}-amd64 as build
|
||||
|
||||
RUN apt-get update && apt-get install -y ccache cmake git
|
||||
# sd-server embeds the web UI at build time, so the build image needs Node/pnpm.
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends ccache cmake git ca-certificates curl gnupg && \
|
||||
mkdir -p /etc/apt/keyrings && \
|
||||
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key -o /tmp/nodesource-repo.gpg.key && \
|
||||
gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg /tmp/nodesource-repo.gpg.key && \
|
||||
rm /tmp/nodesource-repo.gpg.key && \
|
||||
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" > /etc/apt/sources.list.d/nodesource.list && \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends nodejs && \
|
||||
npm install -g pnpm@10.15.1 && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /sd.cpp
|
||||
|
||||
@@ -13,12 +24,22 @@ RUN mkdir build && cd build && \
|
||||
cmake .. -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ \
|
||||
-DCMAKE_C_FLAGS="${CMAKE_C_FLAGS} -fopenmp -I/usr/lib/llvm-14/lib/clang/14.0.0/include -L/usr/lib/llvm-14/lib" \
|
||||
-DCMAKE_CXX_FLAGS="${CMAKE_CXX_FLAGS} -fopenmp -I/usr/lib/llvm-14/lib/clang/14.0.0/include -L/usr/lib/llvm-14/lib" \
|
||||
-DSD_MUSA=ON -DCMAKE_BUILD_TYPE=Release && \
|
||||
-DSD_MUSA=ON \
|
||||
-DSD_BUILD_SHARED_LIBS=ON \
|
||||
-DGGML_NATIVE=OFF \
|
||||
-DSD_BUILD_SHARED_GGML_LIB=ON \
|
||||
-DGGML_BACKEND_DL=ON \
|
||||
-DGGML_CPU_ALL_VARIANTS=ON \
|
||||
-DCMAKE_BUILD_WITH_INSTALL_RPATH=ON \
|
||||
-DCMAKE_INSTALL_RPATH='$ORIGIN' \
|
||||
-DCMAKE_BUILD_TYPE=Release && \
|
||||
cmake --build . --config Release
|
||||
|
||||
FROM mthreads/musa:${MUSA_VERSION}-runtime-ubuntu${UBUNTU_VERSION}-amd64 as runtime
|
||||
|
||||
COPY --from=build /sd.cpp/build/bin/sd-cli /sd-cli
|
||||
COPY --from=build /sd.cpp/build/bin/sd-server /sd-server
|
||||
COPY --from=build /sd.cpp/build/bin /sd.cpp/bin
|
||||
RUN printf '#!/bin/sh\nexec /sd.cpp/bin/sd-cli "$@"\n' > /sd-cli && \
|
||||
printf '#!/bin/sh\nexec /sd.cpp/bin/sd-server "$@"\n' > /sd-server && \
|
||||
chmod +x /sd-cli /sd-server
|
||||
|
||||
ENTRYPOINT [ "/sd-cli" ]
|
||||
ENTRYPOINT [ "/sd-cli" ]
|
||||
|
||||
@@ -1,8 +1,20 @@
|
||||
ARG SYCL_VERSION=2025.1.0-0
|
||||
# ggml SYCL hardware detection uses BMG G31/WCL architecture enums added in oneAPI 2025.3.
|
||||
ARG SYCL_VERSION=2025.3.2-0
|
||||
|
||||
FROM intel/oneapi-basekit:${SYCL_VERSION}-devel-ubuntu24.04 AS build
|
||||
|
||||
RUN apt-get update && apt-get install -y cmake
|
||||
# sd-server embeds the web UI at build time, so the build image needs Node/pnpm.
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends cmake ca-certificates curl gnupg && \
|
||||
mkdir -p /etc/apt/keyrings && \
|
||||
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key -o /tmp/nodesource-repo.gpg.key && \
|
||||
gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg /tmp/nodesource-repo.gpg.key && \
|
||||
rm /tmp/nodesource-repo.gpg.key && \
|
||||
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" > /etc/apt/sources.list.d/nodesource.list && \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends nodejs && \
|
||||
npm install -g pnpm@10.15.1 && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /sd.cpp
|
||||
|
||||
@@ -17,4 +29,4 @@ FROM intel/oneapi-basekit:${SYCL_VERSION}-devel-ubuntu24.04 AS runtime
|
||||
COPY --from=build /sd.cpp/build/bin/sd-cli /sd-cli
|
||||
COPY --from=build /sd.cpp/build/bin/sd-server /sd-server
|
||||
|
||||
ENTRYPOINT [ "/sd-cli" ]
|
||||
ENTRYPOINT [ "/sd-cli" ]
|
||||
@@ -2,14 +2,33 @@ ARG UBUNTU_VERSION=24.04
|
||||
|
||||
FROM ubuntu:$UBUNTU_VERSION AS build
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends build-essential git cmake libvulkan-dev glslc
|
||||
# sd-server embeds the web UI at build time, so the build image needs Node/pnpm.
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends build-essential git cmake libvulkan-dev glslc spirv-headers ca-certificates curl gnupg && \
|
||||
mkdir -p /etc/apt/keyrings && \
|
||||
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key -o /tmp/nodesource-repo.gpg.key && \
|
||||
gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg /tmp/nodesource-repo.gpg.key && \
|
||||
rm /tmp/nodesource-repo.gpg.key && \
|
||||
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" > /etc/apt/sources.list.d/nodesource.list && \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends nodejs && \
|
||||
npm install -g pnpm@10.15.1 && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /sd.cpp
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN cmake . -B ./build -DSD_VULKAN=ON
|
||||
RUN cmake --build ./build --config Release --parallel
|
||||
RUN cmake . -B ./build \
|
||||
-DSD_VULKAN=ON \
|
||||
-DSD_BUILD_SHARED_LIBS=ON \
|
||||
-DGGML_NATIVE=OFF \
|
||||
-DSD_BUILD_SHARED_GGML_LIB=ON \
|
||||
-DGGML_BACKEND_DL=ON \
|
||||
-DGGML_CPU_ALL_VARIANTS=ON \
|
||||
-DCMAKE_BUILD_WITH_INSTALL_RPATH=ON \
|
||||
-DCMAKE_INSTALL_RPATH='$ORIGIN'
|
||||
RUN cmake --build ./build --config Release -j$(nproc)
|
||||
|
||||
FROM ubuntu:$UBUNTU_VERSION AS runtime
|
||||
|
||||
@@ -17,7 +36,9 @@ RUN apt-get update && \
|
||||
apt-get install --yes --no-install-recommends libgomp1 libvulkan1 mesa-vulkan-drivers && \
|
||||
apt-get clean
|
||||
|
||||
COPY --from=build /sd.cpp/build/bin/sd-cli /sd-cli
|
||||
COPY --from=build /sd.cpp/build/bin/sd-server /sd-server
|
||||
COPY --from=build /sd.cpp/build/bin /sd.cpp/bin
|
||||
RUN printf '#!/bin/sh\nexec /sd.cpp/bin/sd-cli "$@"\n' > /sd-cli && \
|
||||
printf '#!/bin/sh\nexec /sd.cpp/bin/sd-server "$@"\n' > /sd-server && \
|
||||
chmod +x /sd-cli /sd-server
|
||||
|
||||
ENTRYPOINT [ "/sd-cli" ]
|
||||
|
||||
@@ -15,23 +15,19 @@ API and command-line option may change frequently.***
|
||||
|
||||
## 🔥Important News
|
||||
|
||||
* **2026/06/25** 🚀 stable-diffusion.cpp now supports **Krea2**
|
||||
* **2026/06/04** 🚀 stable-diffusion.cpp now supports **Ideogram4**
|
||||
* **2026/05/31** 🚀 stable-diffusion.cpp now supports **PiD**
|
||||
* **2026/05/27** 🚀 stable-diffusion.cpp now supports **Lens**
|
||||
* **2026/05/17** 🚀 stable-diffusion.cpp now supports **LTX-2.3**
|
||||
* **2026/04/11** 🚀 stable-diffusion.cpp now uses a brand-new embedded web UI.
|
||||
* **2026/01/18** 🚀 stable-diffusion.cpp now supports **FLUX.2-klein**
|
||||
* **2025/12/01** 🚀 stable-diffusion.cpp now supports **Z-Image**
|
||||
👉 Details: [PR #1020](https://github.com/leejet/stable-diffusion.cpp/pull/1020)
|
||||
|
||||
* **2025/11/30** 🚀 stable-diffusion.cpp now supports **FLUX.2-dev**
|
||||
👉 Details: [PR #1016](https://github.com/leejet/stable-diffusion.cpp/pull/1016)
|
||||
|
||||
* **2025/10/13** 🚀 stable-diffusion.cpp now supports **Qwen-Image-Edit / Qwen-Image-Edit 2509**
|
||||
👉 Details: [PR #877](https://github.com/leejet/stable-diffusion.cpp/pull/877)
|
||||
|
||||
* **2025/10/12** 🚀 stable-diffusion.cpp now supports **Qwen-Image**
|
||||
👉 Details: [PR #851](https://github.com/leejet/stable-diffusion.cpp/pull/851)
|
||||
|
||||
* **2025/09/14** 🚀 stable-diffusion.cpp now supports **Wan2.1 Vace**
|
||||
👉 Details: [PR #819](https://github.com/leejet/stable-diffusion.cpp/pull/819)
|
||||
|
||||
* **2025/09/06** 🚀 stable-diffusion.cpp now supports **Wan2.1 / Wan2.2**
|
||||
👉 Details: [PR #778](https://github.com/leejet/stable-diffusion.cpp/pull/778)
|
||||
|
||||
## Features
|
||||
|
||||
@@ -39,28 +35,42 @@ API and command-line option may change frequently.***
|
||||
- Super lightweight and without external dependencies
|
||||
- Supported models
|
||||
- Image Models
|
||||
- SD1.x, SD2.x, [SD-Turbo](https://huggingface.co/stabilityai/sd-turbo)
|
||||
- SDXL, [SDXL-Turbo](https://huggingface.co/stabilityai/sdxl-turbo)
|
||||
- [SD1.x, SD2.x, SD-Turbo](./docs/sd.md)
|
||||
- [SDXL, SDXL-Turbo](./docs/sd.md)
|
||||
- [Some SD1.x and SDXL distilled models](./docs/distilled_sd.md)
|
||||
- [SD3/SD3.5](./docs/sd3.md)
|
||||
- [FlUX.1-dev/FlUX.1-schnell](./docs/flux.md)
|
||||
- [FLUX.2-dev](./docs/flux2.md)
|
||||
- [FLUX.1-dev/FLUX.1-schnell](./docs/flux.md)
|
||||
- [FLUX.2-dev/FLUX.2-klein](./docs/flux2.md)
|
||||
- [Lens](./docs/lens.md)
|
||||
- [Chroma](./docs/chroma.md)
|
||||
- [Chroma1-Radiance](./docs/chroma_radiance.md)
|
||||
- [Qwen Image](./docs/qwen_image.md)
|
||||
- [PiD](./docs/pid.md)
|
||||
- [LongCat Image](./docs/longcat_image.md)
|
||||
- [Z-Image](./docs/z_image.md)
|
||||
- [MiniT2I](./docs/minit2i.md)
|
||||
- [Ovis-Image](./docs/ovis_image.md)
|
||||
- [Anima](./docs/anima.md)
|
||||
- [ERNIE-Image](./docs/ernie_image.md)
|
||||
- [Boogu Image](./docs/boogu_image.md)
|
||||
- [Krea2](./docs/krea2.md)
|
||||
- [SeFi-Image](./docs/sefi_image.md)
|
||||
- [HiDream-O1-Image](./docs/hidream_o1_image.md)
|
||||
- [Ideogram4](./docs/ideogram4.md)
|
||||
- Image Edit Models
|
||||
- [FLUX.1-Kontext-dev](./docs/kontext.md)
|
||||
- [Qwen Image Edit series](./docs/qwen_image_edit.md)
|
||||
- [LongCat Image Edit](./docs/longcat_image.md)
|
||||
- [Boogu Image Edit](./docs/boogu_image.md)
|
||||
- Video Models
|
||||
- [Wan2.1/Wan2.2](./docs/wan.md)
|
||||
- [PhotoMaker](https://github.com/TencentARC/PhotoMaker) support.
|
||||
- [LTX-2.3](./docs/ltx2.md)
|
||||
- [PhotoMaker](./docs/photo_maker.md) support.
|
||||
- Control Net support with SD 1.5
|
||||
- LoRA support, same as [stable-diffusion-webui](https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki/Features#lora)
|
||||
- Latent Consistency Models support (LCM/LCM-LoRA)
|
||||
- Faster and memory efficient latent decoding with [TAESD](https://github.com/madebyollin/taesd)
|
||||
- Upscale images generated with [ESRGAN](https://github.com/xinntao/Real-ESRGAN)
|
||||
- Faster and memory efficient latent decoding with [TAESD](./docs/taesd.md)
|
||||
- Upscale images generated with [ESRGAN](./docs/esrgan.md)
|
||||
- Supported backends
|
||||
- CPU (AVX, AVX2 and AVX512 support for x86 architectures)
|
||||
- CUDA
|
||||
@@ -69,9 +79,10 @@ API and command-line option may change frequently.***
|
||||
- OpenCL
|
||||
- SYCL
|
||||
- Supported weight formats
|
||||
- Pytorch checkpoint (`.ckpt` or `.pth`)
|
||||
- Pytorch checkpoint (`.ckpt` or `.pth` or `.pt`)
|
||||
- Safetensors (`.safetensors`)
|
||||
- GGUF (`.gguf`)
|
||||
- Convert mode supports converting model weights to `.gguf` or `.safetensors`
|
||||
- Supported platforms
|
||||
- Linux
|
||||
- Mac OS
|
||||
@@ -89,6 +100,7 @@ API and command-line option may change frequently.***
|
||||
- `DPM++ 2M`
|
||||
- [`DPM++ 2M v2`](https://github.com/AUTOMATIC1111/stable-diffusion-webui/discussions/8457)
|
||||
- `DPM++ 2S a`
|
||||
- `ER-SDE`
|
||||
- [`LCM`](https://github.com/AUTOMATIC1111/stable-diffusion-webui/issues/13952)
|
||||
- Cross-platform reproducibility
|
||||
- `--rng cuda`, default, consistent with the `stable-diffusion-webui GPU RNG`
|
||||
@@ -122,25 +134,14 @@ API and command-line option may change frequently.***
|
||||
## Performance
|
||||
|
||||
If you want to improve performance or reduce VRAM/RAM usage, please refer to [performance guide](./docs/performance.md).
|
||||
For runtime and parameter backend placement, see the [backend selection guide](./docs/backend.md).
|
||||
|
||||
## More Guides
|
||||
|
||||
- [SD1.x/SD2.x/SDXL](./docs/sd.md)
|
||||
- [SD3/SD3.5](./docs/sd3.md)
|
||||
- [FlUX.1-dev/FlUX.1-schnell](./docs/flux.md)
|
||||
- [FLUX.2-dev](./docs/flux2.md)
|
||||
- [FLUX.1-Kontext-dev](./docs/kontext.md)
|
||||
- [Chroma](./docs/chroma.md)
|
||||
- [🔥Qwen Image](./docs/qwen_image.md)
|
||||
- [🔥Qwen Image Edit series](./docs/qwen_image_edit.md)
|
||||
- [🔥Wan2.1/Wan2.2](./docs/wan.md)
|
||||
- [🔥Z-Image](./docs/z_image.md)
|
||||
- [Ovis-Image](./docs/ovis_image.md)
|
||||
- [Backend selection](./docs/backend.md)
|
||||
- [RPC](./docs/rpc.md)
|
||||
- [LoRA](./docs/lora.md)
|
||||
- [LCM/LCM-LoRA](./docs/lcm.md)
|
||||
- [Using PhotoMaker to personalize image generation](./docs/photo_maker.md)
|
||||
- [Using ESRGAN to upscale results](./docs/esrgan.md)
|
||||
- [Using TAESD to faster decoding](./docs/taesd.md)
|
||||
- [Docker](./docs/docker.md)
|
||||
- [Quantization and GGUF](./docs/quantization_and_gguf.md)
|
||||
- [Inference acceleration via caching](./docs/caching.md)
|
||||
@@ -151,6 +152,7 @@ These projects wrap `stable-diffusion.cpp` for easier use in other languages/fra
|
||||
|
||||
* Golang (non-cgo): [seasonjs/stable-diffusion](https://github.com/seasonjs/stable-diffusion)
|
||||
* Golang (cgo): [Binozo/GoStableDiffusion](https://github.com/Binozo/GoStableDiffusion)
|
||||
* Golang (non-cgo): [l8bloom/gosd](https://github.com/l8bloom/gosd)
|
||||
* C#: [DarthAffe/StableDiffusion.NET](https://github.com/DarthAffe/StableDiffusion.NET)
|
||||
* Python: [william-murray1204/stable-diffusion-cpp-python](https://github.com/william-murray1204/stable-diffusion-cpp-python)
|
||||
* Rust: [newfla/diffusion-rs](https://github.com/newfla/diffusion-rs)
|
||||
|
||||
|
After Width: | Height: | Size: 230 KiB |
|
After Width: | Height: | Size: 477 KiB |
|
After Width: | Height: | Size: 489 KiB |
|
After Width: | Height: | Size: 595 KiB |
|
After Width: | Height: | Size: 562 KiB |
|
After Width: | Height: | Size: 510 KiB |
|
After Width: | Height: | Size: 455 KiB |
|
After Width: | Height: | Size: 511 KiB |
|
After Width: | Height: | Size: 491 KiB |
|
After Width: | Height: | Size: 464 KiB |
|
After Width: | Height: | Size: 552 KiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 289 KiB |
|
After Width: | Height: | Size: 630 KiB |
|
After Width: | Height: | Size: 555 KiB |
|
After Width: | Height: | Size: 423 KiB |
|
After Width: | Height: | Size: 9.0 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 870 KiB |
@@ -0,0 +1,37 @@
|
||||
set(SD_VERSION "@SD_INSTALL_VERSION@")
|
||||
set(SD_BUILD_COMMIT "@SD_INSTALL_COMMIT@")
|
||||
set(SD_SHARED_LIB @SD_SHARED_LIB@)
|
||||
|
||||
@PACKAGE_INIT@
|
||||
|
||||
set_and_check(SD_INCLUDE_DIR "@PACKAGE_SD_INCLUDE_INSTALL_DIR@")
|
||||
set_and_check(SD_LIB_DIR "@PACKAGE_SD_LIB_INSTALL_DIR@")
|
||||
set(SD_BIN_DIR "@PACKAGE_SD_BIN_INSTALL_DIR@")
|
||||
|
||||
include(CMakeFindDependencyMacro)
|
||||
find_dependency(ggml REQUIRED HINTS "${SD_LIB_DIR}/cmake")
|
||||
|
||||
if(NOT TARGET stable-diffusion)
|
||||
find_library(stable-diffusion_LIBRARY stable-diffusion
|
||||
REQUIRED
|
||||
HINTS "${SD_LIB_DIR}"
|
||||
NO_CMAKE_FIND_ROOT_PATH
|
||||
)
|
||||
|
||||
add_library(stable-diffusion UNKNOWN IMPORTED)
|
||||
set_target_properties(stable-diffusion
|
||||
PROPERTIES
|
||||
INTERFACE_INCLUDE_DIRECTORIES "${SD_INCLUDE_DIR}"
|
||||
INTERFACE_LINK_LIBRARIES "ggml::ggml"
|
||||
IMPORTED_LINK_INTERFACE_LANGUAGES "CXX"
|
||||
IMPORTED_LOCATION "${stable-diffusion_LIBRARY}"
|
||||
INTERFACE_COMPILE_FEATURES "c_std_11;cxx_std_17"
|
||||
POSITION_INDEPENDENT_CODE ON)
|
||||
|
||||
if(SD_SHARED_LIB)
|
||||
target_compile_definitions(stable-diffusion
|
||||
INTERFACE SD_BUILD_SHARED_LIB)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
check_required_components(stable-diffusion)
|
||||
@@ -0,0 +1,11 @@
|
||||
prefix=@CMAKE_INSTALL_PREFIX@
|
||||
exec_prefix=${prefix}
|
||||
libdir=@CMAKE_INSTALL_FULL_LIBDIR@
|
||||
includedir=@CMAKE_INSTALL_FULL_INCLUDEDIR@
|
||||
|
||||
Name: stable-diffusion
|
||||
Description: Diffusion model(SD,Flux,Wan,Qwen Image,Z-Image,...) inference in pure C/C++
|
||||
Version: @SDCPP_BUILD_VERSION@
|
||||
Libs: -L${libdir} -lstable-diffusion
|
||||
Libs.private: -lggml -lggml-base
|
||||
Cflags: -I${includedir}
|
||||
@@ -1,450 +0,0 @@
|
||||
#ifndef __DIFFUSION_MODEL_H__
|
||||
#define __DIFFUSION_MODEL_H__
|
||||
|
||||
#include "flux.hpp"
|
||||
#include "mmdit.hpp"
|
||||
#include "qwen_image.hpp"
|
||||
#include "unet.hpp"
|
||||
#include "wan.hpp"
|
||||
#include "z_image.hpp"
|
||||
|
||||
struct DiffusionParams {
|
||||
struct ggml_tensor* x = nullptr;
|
||||
struct ggml_tensor* timesteps = nullptr;
|
||||
struct ggml_tensor* context = nullptr;
|
||||
struct ggml_tensor* c_concat = nullptr;
|
||||
struct ggml_tensor* y = nullptr;
|
||||
struct ggml_tensor* guidance = nullptr;
|
||||
std::vector<ggml_tensor*> ref_latents = {};
|
||||
bool increase_ref_index = false;
|
||||
int num_video_frames = -1;
|
||||
std::vector<struct ggml_tensor*> controls = {};
|
||||
float control_strength = 0.f;
|
||||
struct ggml_tensor* vace_context = nullptr;
|
||||
float vace_strength = 1.f;
|
||||
std::vector<int> skip_layers = {};
|
||||
};
|
||||
|
||||
struct DiffusionModel {
|
||||
virtual std::string get_desc() = 0;
|
||||
virtual bool compute(int n_threads,
|
||||
DiffusionParams diffusion_params,
|
||||
struct ggml_tensor** output = nullptr,
|
||||
struct ggml_context* output_ctx = nullptr) = 0;
|
||||
virtual void alloc_params_buffer() = 0;
|
||||
virtual void free_params_buffer() = 0;
|
||||
virtual void free_compute_buffer() = 0;
|
||||
virtual void get_param_tensors(std::map<std::string, struct ggml_tensor*>& tensors) = 0;
|
||||
virtual size_t get_params_buffer_size() = 0;
|
||||
virtual void set_weight_adapter(const std::shared_ptr<WeightAdapter>& adapter){};
|
||||
virtual int64_t get_adm_in_channels() = 0;
|
||||
virtual void set_flash_attn_enabled(bool enabled) = 0;
|
||||
virtual void set_circular_axes(bool circular_x, bool circular_y) = 0;
|
||||
};
|
||||
|
||||
struct UNetModel : public DiffusionModel {
|
||||
UNetModelRunner unet;
|
||||
|
||||
UNetModel(ggml_backend_t backend,
|
||||
bool offload_params_to_cpu,
|
||||
const String2TensorStorage& tensor_storage_map = {},
|
||||
SDVersion version = VERSION_SD1)
|
||||
: unet(backend, offload_params_to_cpu, tensor_storage_map, "model.diffusion_model", version) {
|
||||
}
|
||||
|
||||
std::string get_desc() override {
|
||||
return unet.get_desc();
|
||||
}
|
||||
|
||||
void alloc_params_buffer() override {
|
||||
unet.alloc_params_buffer();
|
||||
}
|
||||
|
||||
void free_params_buffer() override {
|
||||
unet.free_params_buffer();
|
||||
}
|
||||
|
||||
void free_compute_buffer() override {
|
||||
unet.free_compute_buffer();
|
||||
}
|
||||
|
||||
void get_param_tensors(std::map<std::string, struct ggml_tensor*>& tensors) override {
|
||||
unet.get_param_tensors(tensors, "model.diffusion_model");
|
||||
}
|
||||
|
||||
size_t get_params_buffer_size() override {
|
||||
return unet.get_params_buffer_size();
|
||||
}
|
||||
|
||||
void set_weight_adapter(const std::shared_ptr<WeightAdapter>& adapter) override {
|
||||
unet.set_weight_adapter(adapter);
|
||||
}
|
||||
|
||||
int64_t get_adm_in_channels() override {
|
||||
return unet.unet.adm_in_channels;
|
||||
}
|
||||
|
||||
void set_flash_attn_enabled(bool enabled) {
|
||||
unet.set_flash_attention_enabled(enabled);
|
||||
}
|
||||
|
||||
void set_circular_axes(bool circular_x, bool circular_y) override {
|
||||
unet.set_circular_axes(circular_x, circular_y);
|
||||
}
|
||||
|
||||
bool compute(int n_threads,
|
||||
DiffusionParams diffusion_params,
|
||||
struct ggml_tensor** output = nullptr,
|
||||
struct ggml_context* output_ctx = nullptr) override {
|
||||
return unet.compute(n_threads,
|
||||
diffusion_params.x,
|
||||
diffusion_params.timesteps,
|
||||
diffusion_params.context,
|
||||
diffusion_params.c_concat,
|
||||
diffusion_params.y,
|
||||
diffusion_params.num_video_frames,
|
||||
diffusion_params.controls,
|
||||
diffusion_params.control_strength, output, output_ctx);
|
||||
}
|
||||
};
|
||||
|
||||
struct MMDiTModel : public DiffusionModel {
|
||||
MMDiTRunner mmdit;
|
||||
|
||||
MMDiTModel(ggml_backend_t backend,
|
||||
bool offload_params_to_cpu,
|
||||
const String2TensorStorage& tensor_storage_map = {})
|
||||
: mmdit(backend, offload_params_to_cpu, tensor_storage_map, "model.diffusion_model") {
|
||||
}
|
||||
|
||||
std::string get_desc() override {
|
||||
return mmdit.get_desc();
|
||||
}
|
||||
|
||||
void alloc_params_buffer() override {
|
||||
mmdit.alloc_params_buffer();
|
||||
}
|
||||
|
||||
void free_params_buffer() override {
|
||||
mmdit.free_params_buffer();
|
||||
}
|
||||
|
||||
void free_compute_buffer() override {
|
||||
mmdit.free_compute_buffer();
|
||||
}
|
||||
|
||||
void get_param_tensors(std::map<std::string, struct ggml_tensor*>& tensors) override {
|
||||
mmdit.get_param_tensors(tensors, "model.diffusion_model");
|
||||
}
|
||||
|
||||
size_t get_params_buffer_size() override {
|
||||
return mmdit.get_params_buffer_size();
|
||||
}
|
||||
|
||||
void set_weight_adapter(const std::shared_ptr<WeightAdapter>& adapter) override {
|
||||
mmdit.set_weight_adapter(adapter);
|
||||
}
|
||||
|
||||
int64_t get_adm_in_channels() override {
|
||||
return 768 + 1280;
|
||||
}
|
||||
|
||||
void set_flash_attn_enabled(bool enabled) {
|
||||
mmdit.set_flash_attention_enabled(enabled);
|
||||
}
|
||||
|
||||
void set_circular_axes(bool circular_x, bool circular_y) override {
|
||||
mmdit.set_circular_axes(circular_x, circular_y);
|
||||
}
|
||||
|
||||
bool compute(int n_threads,
|
||||
DiffusionParams diffusion_params,
|
||||
struct ggml_tensor** output = nullptr,
|
||||
struct ggml_context* output_ctx = nullptr) override {
|
||||
return mmdit.compute(n_threads,
|
||||
diffusion_params.x,
|
||||
diffusion_params.timesteps,
|
||||
diffusion_params.context,
|
||||
diffusion_params.y,
|
||||
output,
|
||||
output_ctx,
|
||||
diffusion_params.skip_layers);
|
||||
}
|
||||
};
|
||||
|
||||
struct FluxModel : public DiffusionModel {
|
||||
Flux::FluxRunner flux;
|
||||
|
||||
FluxModel(ggml_backend_t backend,
|
||||
bool offload_params_to_cpu,
|
||||
const String2TensorStorage& tensor_storage_map = {},
|
||||
SDVersion version = VERSION_FLUX,
|
||||
bool use_mask = false)
|
||||
: flux(backend, offload_params_to_cpu, tensor_storage_map, "model.diffusion_model", version, use_mask) {
|
||||
}
|
||||
|
||||
std::string get_desc() override {
|
||||
return flux.get_desc();
|
||||
}
|
||||
|
||||
void alloc_params_buffer() override {
|
||||
flux.alloc_params_buffer();
|
||||
}
|
||||
|
||||
void free_params_buffer() override {
|
||||
flux.free_params_buffer();
|
||||
}
|
||||
|
||||
void free_compute_buffer() override {
|
||||
flux.free_compute_buffer();
|
||||
}
|
||||
|
||||
void get_param_tensors(std::map<std::string, struct ggml_tensor*>& tensors) override {
|
||||
flux.get_param_tensors(tensors, "model.diffusion_model");
|
||||
}
|
||||
|
||||
size_t get_params_buffer_size() override {
|
||||
return flux.get_params_buffer_size();
|
||||
}
|
||||
|
||||
void set_weight_adapter(const std::shared_ptr<WeightAdapter>& adapter) override {
|
||||
flux.set_weight_adapter(adapter);
|
||||
}
|
||||
|
||||
int64_t get_adm_in_channels() override {
|
||||
return 768;
|
||||
}
|
||||
|
||||
void set_flash_attn_enabled(bool enabled) {
|
||||
flux.set_flash_attention_enabled(enabled);
|
||||
}
|
||||
|
||||
void set_circular_axes(bool circular_x, bool circular_y) override {
|
||||
flux.set_circular_axes(circular_x, circular_y);
|
||||
}
|
||||
|
||||
bool compute(int n_threads,
|
||||
DiffusionParams diffusion_params,
|
||||
struct ggml_tensor** output = nullptr,
|
||||
struct ggml_context* output_ctx = nullptr) override {
|
||||
return flux.compute(n_threads,
|
||||
diffusion_params.x,
|
||||
diffusion_params.timesteps,
|
||||
diffusion_params.context,
|
||||
diffusion_params.c_concat,
|
||||
diffusion_params.y,
|
||||
diffusion_params.guidance,
|
||||
diffusion_params.ref_latents,
|
||||
diffusion_params.increase_ref_index,
|
||||
output,
|
||||
output_ctx,
|
||||
diffusion_params.skip_layers);
|
||||
}
|
||||
};
|
||||
|
||||
struct WanModel : public DiffusionModel {
|
||||
std::string prefix;
|
||||
WAN::WanRunner wan;
|
||||
|
||||
WanModel(ggml_backend_t backend,
|
||||
bool offload_params_to_cpu,
|
||||
const String2TensorStorage& tensor_storage_map = {},
|
||||
const std::string prefix = "model.diffusion_model",
|
||||
SDVersion version = VERSION_WAN2)
|
||||
: prefix(prefix), wan(backend, offload_params_to_cpu, tensor_storage_map, prefix, version) {
|
||||
}
|
||||
|
||||
std::string get_desc() override {
|
||||
return wan.get_desc();
|
||||
}
|
||||
|
||||
void alloc_params_buffer() override {
|
||||
wan.alloc_params_buffer();
|
||||
}
|
||||
|
||||
void free_params_buffer() override {
|
||||
wan.free_params_buffer();
|
||||
}
|
||||
|
||||
void free_compute_buffer() override {
|
||||
wan.free_compute_buffer();
|
||||
}
|
||||
|
||||
void get_param_tensors(std::map<std::string, struct ggml_tensor*>& tensors) override {
|
||||
wan.get_param_tensors(tensors, prefix);
|
||||
}
|
||||
|
||||
size_t get_params_buffer_size() override {
|
||||
return wan.get_params_buffer_size();
|
||||
}
|
||||
|
||||
void set_weight_adapter(const std::shared_ptr<WeightAdapter>& adapter) override {
|
||||
wan.set_weight_adapter(adapter);
|
||||
}
|
||||
|
||||
int64_t get_adm_in_channels() override {
|
||||
return 768;
|
||||
}
|
||||
|
||||
void set_flash_attn_enabled(bool enabled) {
|
||||
wan.set_flash_attention_enabled(enabled);
|
||||
}
|
||||
|
||||
void set_circular_axes(bool circular_x, bool circular_y) override {
|
||||
wan.set_circular_axes(circular_x, circular_y);
|
||||
}
|
||||
|
||||
bool compute(int n_threads,
|
||||
DiffusionParams diffusion_params,
|
||||
struct ggml_tensor** output = nullptr,
|
||||
struct ggml_context* output_ctx = nullptr) override {
|
||||
return wan.compute(n_threads,
|
||||
diffusion_params.x,
|
||||
diffusion_params.timesteps,
|
||||
diffusion_params.context,
|
||||
diffusion_params.y,
|
||||
diffusion_params.c_concat,
|
||||
nullptr,
|
||||
diffusion_params.vace_context,
|
||||
diffusion_params.vace_strength,
|
||||
output,
|
||||
output_ctx);
|
||||
}
|
||||
};
|
||||
|
||||
struct QwenImageModel : public DiffusionModel {
|
||||
std::string prefix;
|
||||
Qwen::QwenImageRunner qwen_image;
|
||||
|
||||
QwenImageModel(ggml_backend_t backend,
|
||||
bool offload_params_to_cpu,
|
||||
const String2TensorStorage& tensor_storage_map = {},
|
||||
const std::string prefix = "model.diffusion_model",
|
||||
SDVersion version = VERSION_QWEN_IMAGE,
|
||||
bool zero_cond_t = false)
|
||||
: prefix(prefix), qwen_image(backend, offload_params_to_cpu, tensor_storage_map, prefix, version, zero_cond_t) {
|
||||
}
|
||||
|
||||
std::string get_desc() override {
|
||||
return qwen_image.get_desc();
|
||||
}
|
||||
|
||||
void alloc_params_buffer() override {
|
||||
qwen_image.alloc_params_buffer();
|
||||
}
|
||||
|
||||
void free_params_buffer() override {
|
||||
qwen_image.free_params_buffer();
|
||||
}
|
||||
|
||||
void free_compute_buffer() override {
|
||||
qwen_image.free_compute_buffer();
|
||||
}
|
||||
|
||||
void get_param_tensors(std::map<std::string, struct ggml_tensor*>& tensors) override {
|
||||
qwen_image.get_param_tensors(tensors, prefix);
|
||||
}
|
||||
|
||||
size_t get_params_buffer_size() override {
|
||||
return qwen_image.get_params_buffer_size();
|
||||
}
|
||||
|
||||
void set_weight_adapter(const std::shared_ptr<WeightAdapter>& adapter) override {
|
||||
qwen_image.set_weight_adapter(adapter);
|
||||
}
|
||||
|
||||
int64_t get_adm_in_channels() override {
|
||||
return 768;
|
||||
}
|
||||
|
||||
void set_flash_attn_enabled(bool enabled) {
|
||||
qwen_image.set_flash_attention_enabled(enabled);
|
||||
}
|
||||
|
||||
void set_circular_axes(bool circular_x, bool circular_y) override {
|
||||
qwen_image.set_circular_axes(circular_x, circular_y);
|
||||
}
|
||||
|
||||
bool compute(int n_threads,
|
||||
DiffusionParams diffusion_params,
|
||||
struct ggml_tensor** output = nullptr,
|
||||
struct ggml_context* output_ctx = nullptr) override {
|
||||
return qwen_image.compute(n_threads,
|
||||
diffusion_params.x,
|
||||
diffusion_params.timesteps,
|
||||
diffusion_params.context,
|
||||
diffusion_params.ref_latents,
|
||||
true, // increase_ref_index
|
||||
output,
|
||||
output_ctx);
|
||||
}
|
||||
};
|
||||
|
||||
struct ZImageModel : public DiffusionModel {
|
||||
std::string prefix;
|
||||
ZImage::ZImageRunner z_image;
|
||||
|
||||
ZImageModel(ggml_backend_t backend,
|
||||
bool offload_params_to_cpu,
|
||||
const String2TensorStorage& tensor_storage_map = {},
|
||||
const std::string prefix = "model.diffusion_model",
|
||||
SDVersion version = VERSION_Z_IMAGE)
|
||||
: prefix(prefix), z_image(backend, offload_params_to_cpu, tensor_storage_map, prefix, version) {
|
||||
}
|
||||
|
||||
std::string get_desc() override {
|
||||
return z_image.get_desc();
|
||||
}
|
||||
|
||||
void alloc_params_buffer() override {
|
||||
z_image.alloc_params_buffer();
|
||||
}
|
||||
|
||||
void free_params_buffer() override {
|
||||
z_image.free_params_buffer();
|
||||
}
|
||||
|
||||
void free_compute_buffer() override {
|
||||
z_image.free_compute_buffer();
|
||||
}
|
||||
|
||||
void get_param_tensors(std::map<std::string, struct ggml_tensor*>& tensors) override {
|
||||
z_image.get_param_tensors(tensors, prefix);
|
||||
}
|
||||
|
||||
size_t get_params_buffer_size() override {
|
||||
return z_image.get_params_buffer_size();
|
||||
}
|
||||
|
||||
void set_weight_adapter(const std::shared_ptr<WeightAdapter>& adapter) override {
|
||||
z_image.set_weight_adapter(adapter);
|
||||
}
|
||||
|
||||
int64_t get_adm_in_channels() override {
|
||||
return 768;
|
||||
}
|
||||
|
||||
void set_flash_attn_enabled(bool enabled) {
|
||||
z_image.set_flash_attention_enabled(enabled);
|
||||
}
|
||||
|
||||
void set_circular_axes(bool circular_x, bool circular_y) override {
|
||||
z_image.set_circular_axes(circular_x, circular_y);
|
||||
}
|
||||
|
||||
bool compute(int n_threads,
|
||||
DiffusionParams diffusion_params,
|
||||
struct ggml_tensor** output = nullptr,
|
||||
struct ggml_context* output_ctx = nullptr) override {
|
||||
return z_image.compute(n_threads,
|
||||
diffusion_params.x,
|
||||
diffusion_params.timesteps,
|
||||
diffusion_params.context,
|
||||
diffusion_params.ref_latents,
|
||||
true, // increase_ref_index
|
||||
output,
|
||||
output_ctx);
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,21 @@
|
||||
# How to Use
|
||||
|
||||
## Download weights
|
||||
|
||||
- Download Anima
|
||||
- safetensors: https://huggingface.co/circlestone-labs/Anima/tree/main/split_files/diffusion_models
|
||||
- gguf: https://huggingface.co/Bedovyy/Anima-GGUF/tree/main
|
||||
- gguf Anima2: https://huggingface.co/JusteLeo/Anima2-GGUF/tree/main
|
||||
- Download vae
|
||||
- safetensors: https://huggingface.co/circlestone-labs/Anima/tree/main/split_files/vae
|
||||
- Download Qwen3-0.6B-Base
|
||||
- safetensors: https://huggingface.co/circlestone-labs/Anima/tree/main/split_files/text_encoders
|
||||
- gguf: https://huggingface.co/mradermacher/Qwen3-0.6B-Base-GGUF/tree/main
|
||||
|
||||
## Examples
|
||||
|
||||
```sh
|
||||
.\bin\Release\sd-cli.exe --diffusion-model ..\..\ComfyUI\models\diffusion_models\anima-preview.safetensors --vae ..\..\ComfyUI\models\vae\qwen_image_vae.safetensors --llm ..\..\ComfyUI\models\text_encoders\qwen_3_06b_base.safetensors -p "a lovely cat holding a sign says 'anima.cpp'" --cfg-scale 6.0 --sampling-method euler -v --offload-to-cpu --diffusion-fa
|
||||
```
|
||||
|
||||
<img alt="anima image example" src="../assets/anima/example.png" />
|
||||
@@ -0,0 +1,243 @@
|
||||
# Backend selection
|
||||
|
||||
`stable-diffusion.cpp` has two backend assignments:
|
||||
|
||||
- `--backend` selects the runtime backend used to execute model graphs.
|
||||
- `--params-backend` selects where model parameters are kept.
|
||||
|
||||
If `--params-backend` is not set, parameters use the same backend as their module runtime backend.
|
||||
|
||||
## Syntax
|
||||
|
||||
A backend assignment can be a single backend name:
|
||||
|
||||
```shell
|
||||
sd-cli -m model.safetensors -p "a cat" --backend cpu
|
||||
```
|
||||
|
||||
This applies to every module that does not have a more specific assignment.
|
||||
|
||||
Assignments can also target individual modules:
|
||||
|
||||
```shell
|
||||
sd-cli -m model.safetensors -p "a cat" --backend te=cpu,vae=cuda0,diffusion=vulkan0
|
||||
```
|
||||
|
||||
The same syntax is used for parameter placement:
|
||||
|
||||
```shell
|
||||
sd-cli -m model.safetensors -p "a cat" --backend cuda0 --params-backend te=cpu,vae=cpu
|
||||
```
|
||||
|
||||
`--params-backend` also accepts the special value `disk`:
|
||||
|
||||
```shell
|
||||
sd-cli -m model.safetensors -p "a cat" --backend cuda0 --params-backend disk
|
||||
```
|
||||
|
||||
`--max-vram` can target resolved backend/device names:
|
||||
|
||||
```shell
|
||||
sd-cli -m model.safetensors -p "a cat" --backend diffusion=cuda0,vae=vulkan0 --max-vram cuda0=6,vulkan0=2
|
||||
```
|
||||
|
||||
The budget applies to every module running on that backend.
|
||||
|
||||
Module names are case-insensitive. Hyphens and underscores in module names are ignored, so `clip_vision`, `clip-vision`, and `clipvision` are equivalent.
|
||||
|
||||
`all=`, `default=`, and `*=` can be used to set the default backend inside a mixed assignment:
|
||||
|
||||
```shell
|
||||
sd-cli -m model.safetensors -p "a cat" --backend all=cuda0,te=cpu
|
||||
```
|
||||
|
||||
## Multiple devices per module (layer split)
|
||||
|
||||
A `--backend` module assignment can list several devices separated by `&`:
|
||||
|
||||
```shell
|
||||
sd-cli -m model.safetensors -p "a cat" --backend "diffusion=cuda0&cuda1"
|
||||
```
|
||||
|
||||
The module's transformer blocks are then distributed across the listed devices
|
||||
in contiguous ranges sized proportionally to each device's free memory (minus a
|
||||
compute-buffer headroom of about 2 GiB per device), and the
|
||||
module's graphs are executed with a `ggml_backend_sched` that runs each block
|
||||
on the device holding its weights, copying the residual stream at the range
|
||||
boundaries. The first device in the list is the module's main device: it also
|
||||
holds the non-block tensors (embeddings, final norms, small sub-runners such as
|
||||
CLIP models or projectors) and the graph inputs/outputs.
|
||||
|
||||
Layer split is supported for the `diffusion` and `te` modules. For `te` it
|
||||
applies to the dominant text encoder (`t5xxl` or the LLM); other modules accept
|
||||
only a single device. If the module has no recognizable transformer blocks, the
|
||||
assignment falls back to the first listed device.
|
||||
|
||||
`--params-backend` accepts no device lists. If the module has no explicit
|
||||
params assignment, each block range's parameters are loaded directly to (and,
|
||||
with `--params-backend diffusion=disk`, released directly from) its own device;
|
||||
an explicit assignment such as `te=cpu` keeps the parameters on that backend
|
||||
and stages each range to its device on demand.
|
||||
|
||||
Layer split cannot be combined with `--max-vram` graph-cut segmentation or
|
||||
`--stream-layers` for the split module; those are single-device mechanisms and
|
||||
are disabled for it.
|
||||
|
||||
Use `--list-devices` to see the device names available on the system.
|
||||
|
||||
### Row split (`--split-mode row`)
|
||||
|
||||
`--split-mode` selects how a multi-device module distributes its weights:
|
||||
`layer` (the default, described above) or `row`. It accepts a single mode or
|
||||
per-module assignments:
|
||||
|
||||
```shell
|
||||
sd-cli -m model.safetensors -p "a cat" --backend "diffusion=cuda0&cuda1" --split-mode row
|
||||
sd-cli -m model.safetensors -p "a cat" --backend "diffusion=cuda0&cuda1,te=cuda0&cuda1" --split-mode diffusion=row,te=layer
|
||||
```
|
||||
|
||||
In row mode the module keeps executing on its main (first listed) device, but
|
||||
its transformer-block matmul weights are allocated in the backend's row-split
|
||||
buffer type, which slices each weight's rows across the listed devices in
|
||||
proportion to free memory and runs those matmuls on all devices in parallel.
|
||||
Compared to a layer split this uses all GPUs within every layer (instead of
|
||||
sequentially device by device) at the cost of a cross-device reduction per
|
||||
matmul - usually the faster option when the devices have fast interconnect.
|
||||
|
||||
Row split requires backend support for split buffers and is currently
|
||||
available on CUDA only; on other backends (or when the listed devices belong
|
||||
to different backend registries) the module falls back to a layer split.
|
||||
Embeddings, normalization weights, biases and other non-block tensors stay in
|
||||
regular buffers on the main device.
|
||||
|
||||
Direct ("immediately") LoRA application cannot patch row-split tensors; with
|
||||
`--split-mode row` the automatic LoRA mode selects runtime application, and an
|
||||
explicit `--lora-apply-mode immediately` skips the split tensors with a
|
||||
warning.
|
||||
|
||||
## Automatic placement (`--auto-fit`)
|
||||
|
||||
`--auto-fit` derives the `diffusion` / `te` / `vae` placements from the model
|
||||
metadata and the per-device memory budgets, then feeds them into the same
|
||||
backend assignment mechanism described above (the chosen specs are printed).
|
||||
`--backend` and `--params-backend` are ignored while auto-fit is enabled.
|
||||
|
||||
```shell
|
||||
sd-cli -m model.safetensors -p "a cat" --auto-fit
|
||||
sd-cli -m model.safetensors -p "a cat" --auto-fit --max-vram cuda0=8,cuda1=14
|
||||
sd-cli -m model.safetensors -p "a cat" --auto-fit --split-mode row
|
||||
```
|
||||
|
||||
Budgets reuse `--max-vram`: a positive per-device value caps what auto-fit
|
||||
plans with on that device, a negative value means "free memory minus that many
|
||||
GiB", and with no budget set each device's free memory minus a 512 MiB margin
|
||||
is used. (The same values still drive graph-cut segmented execution for
|
||||
modules that end up on a single device.)
|
||||
|
||||
When everything fits resident, components are simply spread across the
|
||||
available GPUs. When it does not, auto-fit switches to time-share mode: the
|
||||
heavy components get `disk` params residency (loaded for their phase, freed
|
||||
after), and a component too large for any single device is split across all
|
||||
GPUs with the layer/row split mechanism (`--split-mode` selects which, layer
|
||||
by default). Components that fit nowhere fall back to the CPU. If a VAE decode
|
||||
still runs out of memory, tiling is enabled and the decode retried once.
|
||||
|
||||
## Modules
|
||||
|
||||
| Module | Purpose | Accepted names |
|
||||
| --- | --- | --- |
|
||||
| `diffusion` | UNet, DiT, MMDiT, Flux, Wan, Qwen Image, and other diffusion models | `diffusion`, `model`, `unet`, `dit` |
|
||||
| `te` | Text encoders and conditioners | `te`, `clip`, `text`, `textencoder`, `textencoders`, `conditioner`, `cond`, `llm`, `t5`, `t5xxl` |
|
||||
| `clip_vision` | CLIP vision encoder | `clip_vision`, `clipvision`, `clip-vision`, `vision` |
|
||||
| `vae` | VAE and TAE | `vae`, `firststage`, `autoencoder`, `tae` |
|
||||
| `controlnet` | ControlNet | `controlnet`, `control` |
|
||||
| `photomaker` | PhotoMaker ID encoder and PhotoMaker LoRA | `photomaker`, `photomakerid`, `pmid`, `photo` |
|
||||
| `upscaler` | ESRGAN upscaler | `upscaler`, `esrgan`, `hires` |
|
||||
|
||||
`te` is the preferred module name for text encoders. `clip` is kept as an accepted alias because many existing commands and model names use CLIP terminology.
|
||||
|
||||
## Backend names
|
||||
|
||||
Backend names are resolved against the GGML backend device list. Matching is case-insensitive and accepts exact names or unique prefixes, so common values include names such as:
|
||||
|
||||
- `cpu`
|
||||
- `cuda0`
|
||||
- `vulkan0`
|
||||
- `metal`
|
||||
|
||||
The special values `auto`, `default`, and an empty backend name select the default backend. The default preference is GPU, then integrated GPU, then CPU.
|
||||
|
||||
The special value `gpu` selects the first GPU backend, falling back to the first integrated GPU backend.
|
||||
|
||||
The special value `disk` is accepted only by `--params-backend`. `--backend disk` is invalid because `disk` is a parameter residency mode, not a runtime compute backend.
|
||||
|
||||
## Runtime backend vs. parameter backend
|
||||
|
||||
The runtime backend controls where graph execution runs. The parameter backend controls where model weights are allocated or whether they are reloaded from disk on demand.
|
||||
|
||||
For example:
|
||||
|
||||
```shell
|
||||
sd-cli -m model.safetensors -p "a cat" --backend cuda0 --params-backend cpu
|
||||
```
|
||||
|
||||
This runs all modules on `cuda0`, but stores parameters in CPU RAM. During execution, parameters are moved to the runtime backend as needed.
|
||||
|
||||
For example:
|
||||
|
||||
```shell
|
||||
sd-cli -m model.safetensors -p "a cat" --backend cuda0 --params-backend disk
|
||||
```
|
||||
|
||||
This runs all modules on `cuda0`, reloads parameters from the model file as needed, and releases those parameter buffers after use.
|
||||
|
||||
`disk` is never selected implicitly. If `--params-backend` is not set, parameters use the runtime backend.
|
||||
|
||||
Per-module assignments can be mixed:
|
||||
|
||||
```shell
|
||||
sd-cli -m model.safetensors -p "a cat" --backend diffusion=cuda0,te=cpu,vae=cpu --params-backend diffusion=cuda0,te=cpu,vae=cpu
|
||||
```
|
||||
|
||||
This keeps text encoding and VAE execution on CPU while the diffusion model runs on GPU.
|
||||
|
||||
## Backend sharing and lifetime
|
||||
|
||||
Backends are managed by `SDBackendManager`.
|
||||
|
||||
Within one manager, backend instances are cached by resolved backend device name. If multiple modules request the same backend, they share the same `ggml_backend_t`.
|
||||
|
||||
For example:
|
||||
|
||||
```shell
|
||||
--backend te=cpu,vae=cpu
|
||||
```
|
||||
|
||||
uses one shared CPU backend for both `te` and `vae` runtime execution.
|
||||
|
||||
Runtime and parameter assignments also share the same backend cache. If `--backend diffusion=cuda0` and `--params-backend diffusion=cuda0` resolve to the same device, both use the same backend instance.
|
||||
|
||||
`--params-backend disk` does not create a separate backend instance. Parameters are loaded lazily using the module runtime backend.
|
||||
|
||||
`SDBackendManager` owns the backend instances and frees them when the context or upscaler is destroyed. Model runners receive non-owning runtime and parameter backend pointers and do not free them.
|
||||
|
||||
## Compatibility flags
|
||||
|
||||
The example CLI/server still accepts these older CPU placement flags as compatibility aliases:
|
||||
|
||||
- `--clip-on-cpu`
|
||||
- `--vae-on-cpu`
|
||||
- `--control-net-cpu`
|
||||
- `--offload-to-cpu`
|
||||
|
||||
`--clip-on-cpu`, `--vae-on-cpu`, and `--control-net-cpu` are deprecated. The example argument layer prepends `te=cpu`, `vae=cpu`, and `controlnet=cpu` to `--backend` before creating the context.
|
||||
|
||||
`--offload-to-cpu` prepends a CPU default to the parameter assignment in the caller before creating the context:
|
||||
|
||||
```shell
|
||||
--params-backend '*=cpu'
|
||||
```
|
||||
|
||||
Because this default is inserted first, later explicit `--params-backend` entries can still override it, for example `--offload-to-cpu --params-backend te=disk` keeps non-TE parameters on CPU and reloads TE parameters from disk.
|
||||
|
||||
Library callers should set `backend` and `params_backend` directly. The old CPU/offload fields are no longer part of the C API. Explicit `--backend` and `--params-backend` assignments are preferred for new commands.
|
||||
@@ -0,0 +1,31 @@
|
||||
# How to Use
|
||||
|
||||
Boogu Image uses a Boogu diffusion transformer, the FLUX VAE, and Qwen3-VL as the LLM text and vision encoder.
|
||||
|
||||
## Download weights
|
||||
|
||||
- Download Boogu Image
|
||||
- safetensors: https://huggingface.co/Comfy-Org/Boogu-Image/tree/main/diffusion_models
|
||||
- Download vae
|
||||
- safetensors: https://huggingface.co/black-forest-labs/FLUX.1-dev/blob/main/ae.safetensors
|
||||
- Download Qwen3-VL 8B
|
||||
- gguf: https://huggingface.co/unsloth/Qwen3-VL-8B-Instruct-GGUF/tree/main
|
||||
- For image editing with GGUF text encoders, also download the matching mmproj file and pass it with `--llm_vision`.
|
||||
|
||||
## Examples
|
||||
|
||||
### Boogu Image Base
|
||||
|
||||
```
|
||||
.\bin\Release\sd-cli.exe --diffusion-model ..\..\ComfyUI\models\diffusion_models\boogu_image_base_bf16.safetensors --llm ..\..\llm\Qwen3VL-8B-Instruct-Q4_K_M.gguf --vae ..\..\ComfyUI\models\vae\ae.sft -p "a lovely cat" --diffusion-fa -v --offload-to-cpu
|
||||
```
|
||||
|
||||
<img width="256" alt="Boogu Image Base example" src="../assets/boogu/example.png" />
|
||||
|
||||
### Boogu Image Edit
|
||||
|
||||
```
|
||||
.\bin\Release\sd-cli.exe --diffusion-model ..\..\ComfyUI\models\diffusion_models\boogu_image_edit_bf16.safetensors --llm ..\..\llm\Qwen3VL-8B-Instruct-Q4_K_M.gguf --llm_vision ..\..\llm\mmproj-Qwen3VL-8B-Instruct-F16.gguf --vae ..\..\ComfyUI\models\vae\ae.sft --diffusion-fa -v --offload-to-cpu -r ..\assets\flux\flux1-dev-q8_0.png -p "change 'flux.cpp' to 'boogu.cpp'"
|
||||
```
|
||||
|
||||
<img width="256" alt="Boogu Image Edit example" src="../assets/boogu/edit_example.png" />
|
||||
@@ -16,6 +16,26 @@ git submodule init
|
||||
git submodule update
|
||||
```
|
||||
|
||||
## WebP and WebM Support in Examples
|
||||
|
||||
The example applications (`examples/cli` and `examples/server`) use `libwebp` to support WebP image I/O, and `examples/cli` can also use `libwebm` for `.webm` video output. Both are enabled by default. WebM output currently reuses `libwebp` to encode each frame as VP8 before muxing with `libwebm`.
|
||||
|
||||
If you do not want WebP/WebM support, you can disable them at configure time:
|
||||
|
||||
```shell
|
||||
mkdir build && cd build
|
||||
cmake .. -DSD_WEBP=OFF -DSD_WEBM=OFF
|
||||
cmake --build . --config Release
|
||||
```
|
||||
|
||||
If the submodules are not available, you can also link against system packages instead:
|
||||
|
||||
```shell
|
||||
mkdir build && cd build
|
||||
cmake .. -DSD_USE_SYSTEM_WEBP=ON -DSD_USE_SYSTEM_WEBM=ON
|
||||
cmake --build . --config Release
|
||||
```
|
||||
|
||||
## Build (CPU only)
|
||||
|
||||
If you don't have a GPU or CUDA installed, you can build a CPU-only version.
|
||||
@@ -82,6 +102,11 @@ cmake --build . --config Release
|
||||
## Build with Vulkan
|
||||
|
||||
Install Vulkan SDK from https://www.lunarg.com/vulkan-sdk/.
|
||||
On Ubuntu, install the Vulkan development packages and SPIR-V headers:
|
||||
|
||||
```shell
|
||||
sudo apt-get install build-essential libvulkan-dev glslc spirv-headers
|
||||
```
|
||||
|
||||
```shell
|
||||
mkdir build && cd build
|
||||
|
||||
@@ -11,6 +11,7 @@ Caching methods accelerate diffusion inference by reusing intermediate computati
|
||||
| `dbcache` | DiT models | Block-level L1 residual threshold |
|
||||
| `taylorseer` | DiT models | Taylor series approximation |
|
||||
| `cache-dit` | DiT models | Combined DBCache + TaylorSeer |
|
||||
| `spectrum` | UNET and DiT models | Chebyshev + Taylor output forecasting |
|
||||
|
||||
### UCache (UNET Models)
|
||||
|
||||
@@ -79,7 +80,7 @@ Uses Taylor series approximation to predict block outputs:
|
||||
Combines DBCache and TaylorSeer:
|
||||
|
||||
```bash
|
||||
--cache-mode cache-dit --cache-preset fast
|
||||
--cache-mode cache-dit
|
||||
```
|
||||
|
||||
#### Parameters
|
||||
@@ -91,14 +92,6 @@ Combines DBCache and TaylorSeer:
|
||||
| `threshold` | L1 residual difference threshold | 0.08 |
|
||||
| `warmup` | Steps before caching starts | 8 |
|
||||
|
||||
#### Presets
|
||||
|
||||
Available presets: `slow`, `medium`, `fast`, `ultra` (or `s`, `m`, `f`, `u`).
|
||||
|
||||
```bash
|
||||
--cache-mode cache-dit --cache-preset fast
|
||||
```
|
||||
|
||||
#### SCM Options
|
||||
|
||||
Steps Computation Mask controls which steps can be cached:
|
||||
@@ -118,6 +111,26 @@ Mask values: `1` = compute, `0` = can cache.
|
||||
--scm-policy dynamic
|
||||
```
|
||||
|
||||
### Spectrum (UNET and DiT Models)
|
||||
|
||||
Spectrum uses Chebyshev polynomial fitting blended with Taylor extrapolation to predict denoised outputs, skipping entire forward passes. Based on the paper [Spectrum: Adaptive Spectral Feature Forecasting for Efficient Diffusion Sampling](https://github.com/tingyu215/Spectrum).
|
||||
|
||||
```bash
|
||||
sd-cli -m model.safetensors -p "a cat" --cache-mode spectrum
|
||||
```
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Parameter | Description | Default |
|
||||
|-----------|-------------|---------|
|
||||
| `w` | Chebyshev vs Taylor blend weight (0=Taylor, 1=Chebyshev) | 0.40 |
|
||||
| `m` | Chebyshev polynomial degree | 3 |
|
||||
| `lam` | Ridge regression regularization | 1.0 |
|
||||
| `window` | Initial window size (compute every N steps) | 2 |
|
||||
| `flex` | Window growth per computed step after warmup | 0.50 |
|
||||
| `warmup` | Steps to always compute before caching starts | 4 |
|
||||
| `stop` | Stop caching at this fraction of total steps | 0.9 |
|
||||
|
||||
### Performance Tips
|
||||
|
||||
- Start with default thresholds and adjust based on output quality
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Running distilled models: SSD1B and SDx.x with tiny U-Nets
|
||||
# Running distilled models: SSD1B, Vega and SDx.x with tiny U-Nets
|
||||
|
||||
## Preface
|
||||
|
||||
These models feature a reduced U-Net architecture. Unlike standard SDXL models, the SSD-1B U-Net contains only one middle block and fewer attention layers in its up- and down-blocks, resulting in significantly smaller file sizes. Using these models can reduce inference time by more than 33%. For more details, refer to Segmind's paper: https://arxiv.org/abs/2401.02677v1.
|
||||
These models feature a reduced U-Net architecture. Unlike standard SDXL models, the SSD-1B and Vega U-Net contains only one middle block and fewer attention layers in its up- and down-blocks, resulting in significantly smaller file sizes. Using these models can reduce inference time by more than 33%. For more details, refer to Segmind's paper: https://arxiv.org/abs/2401.02677v1.
|
||||
Similarly, SD1.x- and SD2.x-style models with a tiny U-Net consist of only 6 U-Net blocks, leading to very small files and time savings of up to 50%. For more information, see the paper: https://arxiv.org/pdf/2305.15798.pdf.
|
||||
|
||||
## SSD1B
|
||||
@@ -17,7 +17,17 @@ Useful LoRAs are also available:
|
||||
* https://huggingface.co/seungminh/lora-swarovski-SSD-1B/resolve/main/pytorch_lora_weights.safetensors
|
||||
* https://huggingface.co/kylielee505/mylcmlorassd/resolve/main/pytorch_lora_weights.safetensors
|
||||
|
||||
These files can be used out-of-the-box, unlike the models described in the next section.
|
||||
## Vega
|
||||
|
||||
Segmind's Vega model is available online here:
|
||||
|
||||
* https://huggingface.co/segmind/Segmind-Vega/resolve/main/segmind-vega.safetensors
|
||||
|
||||
VegaRT is an example for an LCM-LoRA:
|
||||
|
||||
* https://huggingface.co/segmind/Segmind-VegaRT/resolve/main/pytorch_lora_weights.safetensors
|
||||
|
||||
Both files can be used out-of-the-box, unlike the models described in next sections.
|
||||
|
||||
|
||||
## SD1.x, SD2.x with tiny U-Nets
|
||||
@@ -77,23 +87,32 @@ pipe.save_pretrained("segmindtiny-sd", safe_serialization=True)
|
||||
```bash
|
||||
python convert_diffusers_to_original_stable_diffusion.py \
|
||||
--model_path ./segmindtiny-sd \
|
||||
--checkpoint_path ./segmind_tiny-sd.ckpt --half
|
||||
--checkpoint_path ./segmind_tiny-sd.safetensors --half --use_safetensors
|
||||
```
|
||||
|
||||
The file segmind_tiny-sd.ckpt will be generated and is now ready for use with sd.cpp. You can follow a similar process for the other models mentioned above.
|
||||
The file segmind_tiny-sd.safetensors will be generated and is now ready for use with sd.cpp. You can follow a similar process for the other models mentioned above.
|
||||
|
||||
|
||||
### Another available .ckpt file:
|
||||
### SDXS-512-DreamShaper
|
||||
|
||||
* https://huggingface.co/ClashSAN/small-sd/resolve/main/tinySDdistilled.ckpt
|
||||
Another very tiny and **incredibly fast** model is SDXS by IDKiro et al. The authors refer to it as *"Real-Time One-Step Latent Diffusion Models with Image Conditions"*. For details read the paper: https://arxiv.org/pdf/2403.16627 . Once again the authors removed some more blocks of U-Net part and unlike other SD1 models they use an adjusted _AutoEncoderTiny_ instead of default _AutoEncoderKL_ for the VAE part.
|
||||
##### Some ready-to-run SDXS-512 model files are available online, such as:
|
||||
|
||||
To use this file, you must first adjust its non-contiguous tensors:
|
||||
* https://huggingface.co/akleine/sdxs-512
|
||||
* https://huggingface.co/concedo/sdxs-512-tinySDdistilled-GGUF
|
||||
|
||||
```python
|
||||
import torch
|
||||
ckpt = torch.load("tinySDdistilled.ckpt", map_location=torch.device('cpu'))
|
||||
for key, value in ckpt['state_dict'].items():
|
||||
if isinstance(value, torch.Tensor):
|
||||
ckpt['state_dict'][key] = value.contiguous()
|
||||
torch.save(ckpt, "tinySDdistilled_fixed.ckpt")
|
||||
##### Run the model as follows:
|
||||
```bash
|
||||
~/stable-diffusion.cpp/build/bin/sd-cli -m sdxs.safetensors -p "portrait of a lovely cat" \
|
||||
--cfg-scale 1 --steps 1
|
||||
```
|
||||
Both options: ``` --cfg-scale 1 ``` and ``` --steps 1 ``` are mandatory here.
|
||||
|
||||
### SDXS-512-0.9
|
||||
|
||||
Even though the name "SDXS-512-0.9" is similar to "SDXS-512-DreamShaper", it is *completely different* but also **incredibly fast**. Sometimes it is preferred, so try it yourself.
|
||||
##### Download a ready-to-run file from here:
|
||||
|
||||
* https://huggingface.co/akleine/sdxs-09
|
||||
|
||||
For the use of this model, both options ``` --cfg-scale 1 ``` and ``` --steps 1 ``` are again absolutely necessary.
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
# How to Use
|
||||
|
||||
You can run ERNIE-Image with stable-diffusion.cpp on GPUs with 4GB of VRAM — or even less.
|
||||
|
||||
## Download weights
|
||||
|
||||
- Download ERNIE-Image-Turbo
|
||||
- safetensors: https://huggingface.co/Comfy-Org/ERNIE-Image/tree/main/diffusion_models
|
||||
- gguf: https://huggingface.co/unsloth/ERNIE-Image-Turbo-GGUF/tree/main
|
||||
- Download ERNIE-Image
|
||||
- safetensors: https://huggingface.co/Comfy-Org/ERNIE-Image/tree/main/diffusion_models
|
||||
- gguf: https://huggingface.co/unsloth/ERNIE-Image-GGUF/tree/main
|
||||
- Download vae
|
||||
- safetensors: https://huggingface.co/Comfy-Org/ERNIE-Image/tree/main/vae
|
||||
- Download ministral 3b
|
||||
- safetensors: https://huggingface.co/Comfy-Org/ERNIE-Image/tree/main/text_encoders
|
||||
- gguf: https://huggingface.co/unsloth/Ministral-3-3B-Instruct-2512-GGUF/tree/main
|
||||
|
||||
## Examples
|
||||
|
||||
### ERNIE-Image-Turbo
|
||||
|
||||
```
|
||||
.\bin\Release\sd-cli.exe --diffusion-model ..\..\ComfyUI\models\diffusion_models\ernie-image-turbo.safetensors --vae ..\..\ComfyUI\models\vae\flux2_ae.safetensors --llm ..\..\ComfyUI\models\text_encoders\ministral-3-3b.safetensors -p "a lovely cat" --cfg-scale 1.0 --steps 8 -v --offload-to-cpu --diffusion-fa
|
||||
```
|
||||
|
||||
<img width="256" alt="ERNIE-Image Turbo example" src="../assets/ernie_image/turbo_example.png" />
|
||||
|
||||
### ERNIE-Image
|
||||
|
||||
```
|
||||
.\bin\Release\sd-cli.exe --diffusion-model ..\..\ComfyUI\models\diffusion_models\ernie-image-UD-Q4_K_M.gguf --vae ..\..\ComfyUI\models\vae\flux2_ae.safetensors --llm ..\..\ComfyUI\models\text_encoders\ministral-3-3b.safetensors -p "a lovely cat" --cfg-scale 5.0 -v --offload-to-cpu --diffusion-fa
|
||||
```
|
||||
|
||||
<img width="256" alt="ERNIE-Image example" src="../assets/ernie_image/example.png" />
|
||||
@@ -1,6 +1,6 @@
|
||||
## Using ESRGAN to upscale results
|
||||
|
||||
You can use ESRGAN to upscale the generated images. At the moment, only the [RealESRGAN_x4plus_anime_6B.pth](https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.2.4/RealESRGAN_x4plus_anime_6B.pth) model is supported. Support for more models of this architecture will be added soon.
|
||||
You can use ESRGAN—such as the model [RealESRGAN_x4plus_anime_6B.pth](https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.2.4/RealESRGAN_x4plus_anime_6B.pth)—to upscale the generated images and improve their overall resolution and clarity.
|
||||
|
||||
- Specify the model path using the `--upscale-model PATH` parameter. example:
|
||||
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
# How to Use
|
||||
|
||||
## Download weights
|
||||
## Flux.2-dev
|
||||
|
||||
### Download weights
|
||||
|
||||
- Download FLUX.2-dev
|
||||
- gguf: https://huggingface.co/city96/FLUX.2-dev-gguf/tree/main
|
||||
- Download vae
|
||||
- safetensors: https://huggingface.co/black-forest-labs/FLUX.2-dev/tree/main
|
||||
- Download FLUX.2-small-decoder (full_encoder_small_decoder.safetensors) as an alternative VAE option
|
||||
- safetensors: https://huggingface.co/black-forest-labs/FLUX.2-small-decoder/tree/main
|
||||
- Download Mistral-Small-3.2-24B-Instruct-2506-GGUF
|
||||
- gguf: https://huggingface.co/unsloth/Mistral-Small-3.2-24B-Instruct-2506-GGUF/tree/main
|
||||
|
||||
## Examples
|
||||
### Examples
|
||||
|
||||
```
|
||||
.\bin\Release\sd-cli.exe --diffusion-model ..\..\ComfyUI\models\diffusion_models\flux2-dev-Q4_K_S.gguf --vae ..\..\ComfyUI\models\vae\flux2_ae.safetensors --llm ..\..\ComfyUI\models\text_encoders\Mistral-Small-3.2-24B-Instruct-2506-Q4_K_M.gguf -r .\kontext_input.png -p "change 'flux.cpp' to 'flux2-dev.cpp'" --cfg-scale 1.0 --sampling-method euler -v --diffusion-fa --offload-to-cpu
|
||||
@@ -17,5 +21,76 @@
|
||||
|
||||
<img alt="flux2 example" src="../assets/flux2/example.png" />
|
||||
|
||||
## Flux.2 klein 4B / Flux.2 klein base 4B
|
||||
|
||||
### Download weights
|
||||
|
||||
- Download FLUX.2-klein-4B
|
||||
- safetensors: https://huggingface.co/black-forest-labs/FLUX.2-klein-4B
|
||||
- gguf: https://huggingface.co/leejet/FLUX.2-klein-4B-GGUF/tree/main
|
||||
- Download FLUX.2-klein-base-4B
|
||||
- safetensors: https://huggingface.co/black-forest-labs/FLUX.2-klein-base-4B
|
||||
- gguf: https://huggingface.co/leejet/FLUX.2-klein-base-4B-GGUF/tree/main
|
||||
- Download vae
|
||||
- safetensors: https://huggingface.co/black-forest-labs/FLUX.2-dev/tree/main
|
||||
- Download FLUX.2-small-decoder (full_encoder_small_decoder.safetensors) as an alternative VAE option
|
||||
- safetensors: https://huggingface.co/black-forest-labs/FLUX.2-small-decoder/tree/main
|
||||
- Download Qwen3 4b
|
||||
- safetensors: https://huggingface.co/Comfy-Org/flux2-klein-4B/tree/main/split_files/text_encoders
|
||||
- gguf: https://huggingface.co/unsloth/Qwen3-4B-GGUF/tree/main
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
.\bin\Release\sd-cli.exe --diffusion-model ..\..\ComfyUI\models\diffusion_models\flux-2-klein-4b.safetensors --vae ..\..\ComfyUI\models\vae\flux2_ae.safetensors --llm ..\..\ComfyUI\models\text_encoders\qwen_3_4b.safetensors -p "a lovely cat" --cfg-scale 1.0 --steps 4 -v --offload-to-cpu --diffusion-fa
|
||||
```
|
||||
|
||||
<img alt="flux2-klein-4b" src="../assets/flux2/flux2-klein-4b.png" />
|
||||
|
||||
```
|
||||
.\bin\Release\sd-cli.exe --diffusion-model ..\..\ComfyUI\models\diffusion_models\flux-2-klein-4b.safetensors --vae ..\..\ComfyUI\models\vae\flux2_ae.safetensors --llm ..\..\ComfyUI\models\text_encoders\qwen_3_4b.safetensors -r .\kontext_input.png -p "change 'flux.cpp' to 'klein.cpp'" --cfg-scale 1.0 --sampling-method euler -v --diffusion-fa --offload-to-cpu --steps 4
|
||||
```
|
||||
|
||||
<img alt="flux2-klein-4b-edit" src="../assets/flux2/flux2-klein-4b-edit.png" />
|
||||
|
||||
```
|
||||
.\bin\Release\sd-cli.exe --diffusion-model ..\..\ComfyUI\models\diffusion_models\flux-2-klein-base-4b.safetensors --vae ..\..\ComfyUI\models\vae\flux2_ae.safetensors --llm ..\..\ComfyUI\models\text_encoders\qwen_3_4b.safetensors -p "a lovely cat" --cfg-scale 4.0 --steps 20 -v --offload-to-cpu --diffusion-fa
|
||||
```
|
||||
|
||||
<img alt="flux2-klein-base-4b" src="../assets/flux2/flux2-klein-base-4b.png" />
|
||||
|
||||
## Flux.2 klein 9B / Flux.2 klein base 9B
|
||||
|
||||
### Download weights
|
||||
|
||||
- Download FLUX.2-klein-9B
|
||||
- safetensors: https://huggingface.co/black-forest-labs/FLUX.2-klein-9B
|
||||
- gguf: https://huggingface.co/leejet/FLUX.2-klein-9B-GGUF/tree/main
|
||||
- Download FLUX.2-klein-base-9B
|
||||
- safetensors: https://huggingface.co/black-forest-labs/FLUX.2-klein-base-9B
|
||||
- gguf: https://huggingface.co/leejet/FLUX.2-klein-base-9B-GGUF/tree/main
|
||||
- Download vae
|
||||
- safetensors: https://huggingface.co/black-forest-labs/FLUX.2-dev/tree/main
|
||||
- Download Qwen3 8B
|
||||
- safetensors: https://huggingface.co/Comfy-Org/flux2-klein-9B/tree/main/split_files/text_encoders
|
||||
- gguf: https://huggingface.co/unsloth/Qwen3-8B-GGUF/tree/main
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
.\bin\Release\sd-cli.exe --diffusion-model ..\..\ComfyUI\models\diffusion_models\flux-2-klein-9b.safetensors --vae ..\..\ComfyUI\models\vae\flux2_ae.safetensors --llm ..\..\ComfyUI\models\text_encoders\qwen_3_8b.safetensors -p "a lovely cat" --cfg-scale 1.0 --steps 4 -v --offload-to-cpu --diffusion-fa
|
||||
```
|
||||
|
||||
<img alt="flux2-klein-9b" src="../assets/flux2/flux2-klein-9b.png" />
|
||||
|
||||
```
|
||||
.\bin\Release\sd-cli.exe --diffusion-model ..\..\ComfyUI\models\diffusion_models\flux-2-klein-9b.safetensors --vae ..\..\ComfyUI\models\vae\flux2_ae.safetensors --llm ..\..\ComfyUI\models\text_encoders\qwen_3_8b.safetensors -r .\kontext_input.png -p "change 'flux.cpp' to 'klein.cpp'" --cfg-scale 1.0 --sampling-method euler -v --diffusion-fa --offload-to-cpu --steps 4
|
||||
```
|
||||
|
||||
<img alt="flux2-klein-9b-edit" src="../assets/flux2/flux2-klein-9b-edit.png" />
|
||||
|
||||
```
|
||||
.\bin\Release\sd-cli.exe --diffusion-model ..\..\ComfyUI\models\diffusion_models\flux-2-klein-base-9b.safetensors --vae ..\..\ComfyUI\models\vae\flux2_ae.safetensors --llm ..\..\ComfyUI\models\text_encoders\qwen_3_8b.safetensors -p "a lovely cat" --cfg-scale 4.0 --steps 20 -v --offload-to-cpu --diffusion-fa
|
||||
```
|
||||
|
||||
<img alt="flux2-klein-base-9b" src="../assets/flux2/flux2-klein-base-9b.png" />
|
||||
@@ -0,0 +1,20 @@
|
||||
# How to Use
|
||||
|
||||
## Download weights
|
||||
|
||||
- Download HiDream-O1-Image-Dev
|
||||
- safetensors: https://huggingface.co/Comfy-Org/HiDream-O1-Image/tree/main/checkpoints
|
||||
- Download HiDream-O1-Image
|
||||
- safetensors: https://huggingface.co/Comfy-Org/HiDream-O1-Image/tree/main/checkpoints
|
||||
|
||||
## Examples
|
||||
|
||||
### HiDream-O1-Image-Dev
|
||||
|
||||
```
|
||||
.\bin\Release\sd-cli.exe -m ..\..\ComfyUI\models\diffusion_models\hidream_o1_image_dev_bf16.safetensors -p "a lovely cat holding a sign says
|
||||
'hidream o1 cpp'" --cfg-scale 1.0 -v -H 1024 -W 1024
|
||||
```
|
||||
|
||||
<img width="256" alt="HiDream-O1-Image-Dev example" src="../assets/hidream-o1/dev_example.png" />
|
||||
|
||||
@@ -26,12 +26,12 @@ Fortunately, `AMD` provides complete help documentation, you can use the help do
|
||||
|
||||
Then we must set `ROCM` as environment variables before running cmake.
|
||||
|
||||
Usually if you install according to the official tutorial and do not modify the ROCM path, then there is a high probability that it is here `C:\Program Files\AMD\ROCm\5.5\bin`
|
||||
Usually if you install according to the official tutorial and do not modify the ROCM path, then there is a high probability that it is here `C:\Program Files\AMD\ROCm\7.1.1\bin`
|
||||
|
||||
This is what I use to set the clang:
|
||||
```Commandline
|
||||
set CC=C:\Program Files\AMD\ROCm\5.5\bin\clang.exe
|
||||
set CXX=C:\Program Files\AMD\ROCm\5.5\bin\clang++.exe
|
||||
set CC=C:\Program Files\AMD\ROCm\7.1.1\bin\clang.exe
|
||||
set CXX=C:\Program Files\AMD\ROCm\7.1.1\bin\clang++.exe
|
||||
```
|
||||
|
||||
## Ninja
|
||||
@@ -46,7 +46,7 @@ set ninja=C:\Program Files\ninja\ninja.exe
|
||||
## Building stable-diffusion.cpp
|
||||
|
||||
The thing different from the regular CPU build is `-DSD_HIPBLAS=ON` ,
|
||||
`-G "Ninja"`, `-DCMAKE_C_COMPILER=clang`, `-DCMAKE_CXX_COMPILER=clang++`, `-DAMDGPU_TARGETS=gfx1100`
|
||||
`-G "Ninja"`, `-DCMAKE_C_COMPILER=clang`, `-DCMAKE_CXX_COMPILER=clang++`, `-DAMDGPU_TARGETS=gfx1150;gfx1151;gfx1200;gfx1201;gfx1100;gfx1101;gfx1102;gfx1030;gfx1031;gfx1032`
|
||||
|
||||
>**Notice**: check the `clang` and `clang++` information:
|
||||
```Commandline
|
||||
@@ -59,26 +59,29 @@ If you see like this, we can continue:
|
||||
clang version 17.0.0 (git@github.amd.com:Compute-Mirrors/llvm-project e3201662d21c48894f2156d302276eb1cf47c7be)
|
||||
Target: x86_64-pc-windows-msvc
|
||||
Thread model: posix
|
||||
InstalledDir: C:\Program Files\AMD\ROCm\5.5\bin
|
||||
InstalledDir: C:\Program Files\AMD\ROCm\7.1.1\bin
|
||||
```
|
||||
|
||||
```
|
||||
clang version 17.0.0 (git@github.amd.com:Compute-Mirrors/llvm-project e3201662d21c48894f2156d302276eb1cf47c7be)
|
||||
Target: x86_64-pc-windows-msvc
|
||||
Thread model: posix
|
||||
InstalledDir: C:\Program Files\AMD\ROCm\5.5\bin
|
||||
InstalledDir: C:\Program Files\AMD\ROCm\7.1.1\bin
|
||||
```
|
||||
|
||||
>**Notice** that the `gfx1100` is the GPU architecture of my GPU, you can change it to your GPU architecture. Click here to see your architecture [LLVM Target](https://rocm.docs.amd.com/en/latest/release/windows_support.html#windows-supported-gpus)
|
||||
>**Notice** that the GPU targets are now compatible with multiple GPU architectures (ROCm 7.1.1 targets). You can change them to match your GPU architecture. Click here to see your architecture [LLVM Target](https://rocm.docs.amd.com/en/latest/release/windows_support.html#windows-supported-gpus)
|
||||
|
||||
My GPU is AMD Radeon™ RX 7900 XTX Graphics, so I set it to `gfx1100`.
|
||||
Examples:
|
||||
- AMD Radeon™ RX 7900 XTX Graphics: `gfx1100`
|
||||
- AMD Radeon™ RX 7900 XT Graphics: `gfx1101`
|
||||
- AMD Radeon™ RX 7900 GRE Graphics: `gfx1102`
|
||||
|
||||
option:
|
||||
|
||||
```commandline
|
||||
mkdir build
|
||||
cd build
|
||||
cmake .. -G "Ninja" -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DSD_HIPBLAS=ON -DCMAKE_BUILD_TYPE=Release -DAMDGPU_TARGETS=gfx1100
|
||||
cmake .. -G "Ninja" -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DSD_HIPBLAS=ON -DCMAKE_BUILD_TYPE=Release -DAMDGPU_TARGETS="gfx1150;gfx1151;gfx1200;gfx1201;gfx1100;gfx1101;gfx1102;gfx1030;gfx1031;gfx1032"
|
||||
cmake --build . --config Release
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# How to Use
|
||||
|
||||
## Download weights
|
||||
|
||||
- Download Ideogram4
|
||||
- safetensors: https://huggingface.co/ideogram-ai/ideogram-4-fp8/tree/main/transformer
|
||||
- Download Ideogram4 uncond
|
||||
- safetensors: https://huggingface.co/ideogram-ai/ideogram-4-fp8/tree/main/unconditional_transformer
|
||||
- Download vae
|
||||
- safetensors: https://huggingface.co/black-forest-labs/FLUX.2-dev/tree/main
|
||||
- Download Qwen3-VL-8B-Instruct
|
||||
- gguf: https://huggingface.co/unsloth/Qwen3-VL-8B-Instruct-GGUF/tree/main
|
||||
|
||||
## Convert weights
|
||||
|
||||
fp8 scale -> bf16
|
||||
|
||||
```
|
||||
python .\convert_fp8_scale_to_bf16.py --input .\ideogram4_fp8.safetensors --output ideogram4_bf16.safetensors
|
||||
python .\convert_fp8_scale_to_bf16.py --input .\ideogram4_uncond_fp8.safetensors --output ideogram4_uncond_bf16.safetensors
|
||||
```
|
||||
|
||||
bf16 -> q8
|
||||
|
||||
```
|
||||
.\bin\Release\sd-cli.exe -M convert -m ideogram4_bf16.safetensors -o ideogram4-Q8_0.gguf --tensor-type-rules "^layers.*adaln_modulation.*weight=q8_0,layers.*attention.o.*weight=q8_0,layers.*attention.qkv.*weight=q8_0,layers.*feed_forward.*weight=q8_0" -v
|
||||
|
||||
.\bin\Release\sd-cli.exe -M convert -m ideogram4_uncond_bf16.safetensors -o ideogram4_uncond-Q8_0.gguf --tensor-type-rules "^layers.*adaln_modulation.*weight=q8_0,layers.*attention.o.*weight=q8_0,layers.*attention.qkv.*weight=q8_0,layers.*feed_forward.*weight=q8_0" -v
|
||||
```
|
||||
|
||||
If you want lower VRAM usage, you can change the quantization from q8_0 to a lower-level quantization, such as q4_0.
|
||||
|
||||
|
||||
## Examples
|
||||
|
||||
```sh
|
||||
.\bin\Release\sd-cli.exe --diffusion-model ideogram4-Q8_0.gguf --uncond-diffusion-model ideogram4_uncond-Q8_0.gguf --llm ..\..\llm\Qwen3VL-8B-Instruct-Q4_K_M.gguf --vae ..\..\ComfyUI\models\vae\flux2_ae.safetensors -p '{"high_level_description":"A square 1024 x 1024 luxury fashion magazine cover featuring exactly one short chubby fluffy cat as the main model. The cat sits on a soft ivory studio floor, facing the viewer with a stylish calm expression, wearing tiny black sunglasses, a red silk scarf, and a small gold collar charm. In front of the cat on the floor is a wide horizontal luxury nameplate that clearly reads ideogram4.cpp. The whole design feels premium, fashionable, clean, and editorial.","style_description":{"aesthetics":"luxury fashion magazine cover, high-end pet couture campaign, minimalist editorial design, elegant studio photography, soft paper texture, refined typography, fashionable and polished","lighting":"Soft diffused studio lighting, gentle spotlight on the cat, subtle floor shadow, warm ivory highlights, clean separation between subject and background","photo":"high-resolution fashion editorial photography look, front-facing cat portrait, crisp fur details, glossy sunglasses, clear readable nameplate text, shallow depth of field","medium":"mixed media fashion photography and premium editorial graphic design","color_palette":["#F4EFE7","#111111","#D8B56D","#B73A3A","#FFFFFF","#8A7A6A"]},"compositional_deconstruction":{"canvas":"Square 1024 x 1024 canvas with a normal upright orientation. Do not rotate the poster or any text. Use a clean fashion magazine cover layout.","background":"Warm ivory studio backdrop with subtle paper grain, a soft spotlight gradient, faint floor shadow, and a few minimal gold editorial lines. The background is spacious, premium, and uncluttered.","layout":"Top center has a small elegant headline. Center area features one cat as the main fashion model. Lower foreground has a wide horizontal luxury nameplate placed on the floor in front of the cat. Bottom center has a small footer. All text is horizontal, upright, and readable left to right.","elements":[{"type":"text","desc":"Top center headline reading LOOK WHAT I FOUND in a refined high-fashion serif font. The headline is horizontal, centered, elegant, and secondary to the nameplate text."},{"type":"obj","desc":"Exactly one short chubby fluffy cat sitting in the center like a luxury fashion model. The cat has a large round head, compact body, short legs, soft detailed fur, expressive eyes, and a calm confident pose. The cat is cute and rounded, not tall, not stretched, not duplicated."},{"type":"obj","desc":"Tiny glossy black sunglasses worn naturally by the cat, slightly oversized but still showing the cat face clearly. The sunglasses add a chic fashion-editorial attitude."},{"type":"obj","desc":"A red silk scarf tied neatly around the cat neck, with soft folds and a couture feeling. The scarf must not cover the cat face or the nameplate."},{"type":"obj","desc":"A small gold collar charm or fashion accessory under the scarf, subtle and premium, adding a luxury campaign detail."},{"type":"obj","desc":"In the lower foreground, place a wide horizontal luxury nameplate on the floor in front of the cat. The nameplate is low, flat, landscape-oriented, much wider than tall, like a fashion show seat card or premium display plaque. It is centered, front-facing, level, and fully visible. It must not become vertical, tall, standing, rotated, or side-facing."},{"type":"text","desc":"Print the exact text ideogram4.cpp only on the wide horizontal nameplate. Use clean bold black lettering, perfectly spelled, lowercase, with the number 4 and .cpp extension. The text must fit completely inside the nameplate, stay horizontal, and be readable from left to right."},{"type":"obj","desc":"Add sparse premium editorial accents around the edges: thin gold lines, small code brackets, tiny cursor marks, subtle dots, and minimal geometric details. No extra cats, no stickers, no animal faces, no busy decorations."},{"type":"text","desc":"Bottom center footer reading tiny paws, big compile energy in a small refined monospace or editorial font. The footer is horizontal, centered, understated, and much smaller than the nameplate text."}]}}' --diffusion-fa -v --offload-to-cpu -H 1024 -W 1024
|
||||
```
|
||||
|
||||
<img alt="ideogram4 image example" src="../assets/ideogram4/example.png" />
|
||||
@@ -0,0 +1,59 @@
|
||||
# Importance Matrix (imatrix) Quantization
|
||||
|
||||
## What is an Importance Matrix?
|
||||
|
||||
Quantization reduces the precision of a model's weights, decreasing its size and computational requirements. However, this can lead to a loss of quality. An importance matrix helps mitigate this by identifying which weights are *most* important for the model's performance. During quantization, these important weights are preserved with higher precision, while less important weights are quantized more aggressively. This allows for better overall quality at a given quantization level.
|
||||
|
||||
This originates from work done with language models in [llama.cpp](https://github.com/ggml-org/llama.cpp/blob/master/tools/imatrix/README.md).
|
||||
|
||||
## Usage
|
||||
|
||||
The imatrix feature involves two main steps: *training* the matrix and *using* it during quantization.
|
||||
|
||||
### Training the Importance Matrix
|
||||
|
||||
To generate an imatrix, run stable-diffusion.cpp with the `--imat-out` flag, specifying the output filename. This process runs alongside normal image generation.
|
||||
|
||||
```bash
|
||||
sd.exe [same exact parameters as normal generation] --imat-out imatrix.dat
|
||||
```
|
||||
|
||||
* **`[same exact parameters as normal generation]`**: Use the same command-line arguments you would normally use for image generation (e.g., prompt, dimensions, sampling method, etc.).
|
||||
* **`--imat-out imatrix.dat`**: Specifies the output file for the generated imatrix.
|
||||
|
||||
You can generate multiple images at once using the `-b` flag to speed up the training process.
|
||||
|
||||
### Continuing Training an Existing Matrix
|
||||
|
||||
If you want to refine an existing imatrix, use the `--imat-in` flag *in addition* to `--imat-out`. This will load the existing matrix and continue training it.
|
||||
|
||||
```bash
|
||||
sd.exe [same exact parameters as normal generation] --imat-out imatrix.dat --imat-in imatrix.dat
|
||||
```
|
||||
With that, you can train and refine the imatrix while generating images like you'd normally do.
|
||||
|
||||
### Using Multiple Matrices
|
||||
|
||||
You can load and merge multiple imatrices together:
|
||||
|
||||
```bash
|
||||
sd.exe [same exact parameters as normal generation] --imat-out imatrix.dat --imat-in imatrix.dat --imat-in imatrix2.dat
|
||||
```
|
||||
|
||||
### Quantizing with an Importance Matrix
|
||||
|
||||
To quantize a model using a trained imatrix, use the `-M convert` option (or equivalent quantization command) and the `--imat-in` flag, specifying the imatrix file.
|
||||
|
||||
```bash
|
||||
sd.exe -M convert [same exact parameters as normal quantization] --imat-in imatrix.dat
|
||||
```
|
||||
|
||||
* **`[same exact parameters as normal quantization]`**: Use the same command-line arguments you would normally use for quantization (e.g., target quantization method, input/output filenames).
|
||||
* **`--imat-in imatrix.dat`**: Specifies the imatrix file to use during quantization. You can specify multiple `--imat-in` flags to combine multiple matrices.
|
||||
|
||||
## Important Considerations
|
||||
|
||||
* The quality of the imatrix depends on the prompts and settings used during training. Use prompts and settings representative of the types of images you intend to generate for the best results.
|
||||
* Experiment with different training parameters (e.g., number of images, prompt variations) to optimize the imatrix for your specific use case.
|
||||
* The performance impact of training an imatrix during image generation or using an imatrix for quantization is negligible.
|
||||
* Using already quantized models to train the imatrix seems to be working fine.
|
||||
@@ -0,0 +1,27 @@
|
||||
# How to Use
|
||||
|
||||
Krea2 uses a Krea2 diffusion transformer, the Wan2.1 VAE, and Qwen3-VL 4B as the LLM text encoder.
|
||||
|
||||
## Download weights
|
||||
|
||||
- Download Krea2 Raw
|
||||
- safetensors: https://huggingface.co/krea/Krea-2-Raw/tree/main
|
||||
- gguf: https://huggingface.co/realrebelai/KREA-2_GGUFs/tree/main/BASE
|
||||
- Download Krea2 Turbo
|
||||
- safetensors: https://huggingface.co/krea/Krea-2-Turbo/tree/main
|
||||
- gguf: https://huggingface.co/realrebelai/KREA-2_GGUFs/tree/main/TURBO
|
||||
- Download vae
|
||||
- safetensors: https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/blob/main/split_files/vae/wan_2.1_vae.safetensors
|
||||
- Download Qwen3-VL 4B
|
||||
- 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
|
||||
|
||||
### Krea2
|
||||
|
||||
```
|
||||
.\bin\Release\sd-cli.exe --diffusion-model ..\..\ComfyUI\models\diffusion_models\Krea-2-Raw-Q8_0.gguf --llm ..\..\ComfyUI\models\text_encoders\Qwen3-VL-4B-Instruct-Q4_K_M.gguf --vae ..\..\ComfyUI\models\vae\wan_2.1_vae.safetensors -p "a lovely cat holding a sign says 'krea2.cpp'" --diffusion-fa -v --offload-to-cpu
|
||||
```
|
||||
|
||||
<img width="256" alt="Krea2 Raw example" src="../assets/krea2/example.png" />
|
||||
@@ -0,0 +1,32 @@
|
||||
# How to Use
|
||||
|
||||
Lens uses a Lens diffusion transformer, the FLUX.2 VAE, and GPT-OSS-20B as the LLM text encoder.
|
||||
|
||||
## Download weights
|
||||
|
||||
- Download Lens
|
||||
- safetensors: https://huggingface.co/Comfy-Org/Lens/tree/main/diffusion_models
|
||||
- Download Lens Turbo
|
||||
- safetensors: https://huggingface.co/Comfy-Org/Lens/tree/main/diffusion_models
|
||||
- Download vae
|
||||
- safetensors: https://huggingface.co/black-forest-labs/FLUX.2-dev/tree/main
|
||||
- Download GPT-OSS-20B
|
||||
- gguf: https://huggingface.co/unsloth/gpt-oss-20b-GGUF/tree/main
|
||||
|
||||
## Examples
|
||||
|
||||
### Lens
|
||||
|
||||
```
|
||||
.\bin\Release\sd-cli.exe --diffusion-model ..\..\ComfyUI\models\diffusion_models\lens_bf16.safetensors --llm "..\..\llm\gpt-oss-20b-UD-Q8_K_XL.gguf" --vae ..\..\ComfyUI\models\vae\flux2_ae.safetensors --cfg-scale 5.0 -p "A crystal dragon soaring through an aurora borealis sky, its entire body made of transparent faceted crystal refracting the green and purple aurora light into rainbow spectra, ice particles trailing from its wings, high fantasy digital art" --diffusion-fa -v
|
||||
```
|
||||
|
||||
<img width="256" alt="Lens example" src="../assets/lens/example.png" />
|
||||
|
||||
### Lens Turbo
|
||||
|
||||
```
|
||||
.\bin\Release\sd-cli.exe --diffusion-model ..\..\ComfyUI\models\diffusion_models\lens_turbo_bf16.safetensors --llm "..\..\llm\gpt-oss-20b-UD-Q8_K_XL.gguf" --vae ..\..\ComfyUI\models\vae\flux2_ae.safetensors --cfg-scale 1.0 -p "A crystal dragon soaring through an aurora borealis sky, its entire body made of transparent faceted crystal refracting the green and purple aurora light into rainbow spectra, ice particles trailing from its wings, high fantasy digital art" --diffusion-fa -v --steps 4
|
||||
```
|
||||
|
||||
<img width="256" alt="Lens Turbo example" src="../assets/lens/turbo_example.png" />
|
||||
@@ -0,0 +1,30 @@
|
||||
# How to Use
|
||||
|
||||
LongCat-Image uses a LongCat diffusion transformer, the FLUX VAE, and Qwen2.5-VL as the LLM text encoder.
|
||||
|
||||
## Download weights
|
||||
|
||||
- Download LongCat Image
|
||||
- safetensors: https://huggingface.co/Comfy-Org/LongCat-Image/tree/main/split_files/diffusion_models
|
||||
- gguf: https://huggingface.co/vantagewithai/LongCat-Image-GGUF/tree/main/comfy
|
||||
- Download LongCat Image Edit
|
||||
- LongCat Image Edit Turbo: https://huggingface.co/meituan-longcat/LongCat-Image-Edit-Turbo
|
||||
- gguf: https://huggingface.co/vantagewithai/LongCat-Image-Edit-GGUF/tree/main
|
||||
- Download vae
|
||||
- safetensors: https://huggingface.co/black-forest-labs/FLUX.1-dev/blob/main/ae.safetensors
|
||||
- Download qwen_2.5_vl 7b
|
||||
- safetensors: https://huggingface.co/Comfy-Org/Qwen-Image_ComfyUI/tree/main/split_files/text_encoders
|
||||
- gguf: https://huggingface.co/mradermacher/Qwen2.5-VL-7B-Instruct-GGUF/tree/main
|
||||
- For image editing with GGUF text encoders, also download the matching mmproj file and pass it with `--llm_vision`.
|
||||
|
||||
## Run
|
||||
|
||||
LongCat uses quoted text for character-level text rendering. Put target text inside single quotes, double quotes, or Chinese quotes.
|
||||
|
||||
### LongCat Image
|
||||
|
||||
```
|
||||
.\bin\Release\sd-cli.exe --diffusion-model ..\..\ComfyUI\models\diffusion_models\LongCat-Image-Q4_K_M.gguf --vae ..\..\ComfyUI\models\vae\ae.sft --llm ..\..\ComfyUI\models\text_encoders\Qwen2.5-VL-7B-Instruct-Q8_0.gguf -p "a lovely cat holding a sign says 'longcat.cpp'" --cfg-scale 5.0 --sampling-method euler --flow-shift 3 -v --offload-to-cpu --diffusion-fa
|
||||
```
|
||||
|
||||
<img alt="longcat example" src="../assets/longcat/example.png" />
|
||||
@@ -0,0 +1,77 @@
|
||||
# How to Use
|
||||
|
||||
## Download weights
|
||||
|
||||
- Download LTX-2.3
|
||||
- safetensors: https://huggingface.co/Kijai/LTX2.3_comfy/tree/main/diffusion_models
|
||||
- gguf: https://huggingface.co/unsloth/LTX-2.3-GGUF/tree/main
|
||||
- Download gemma-3-12b-it
|
||||
- gguf: https://huggingface.co/unsloth/gemma-3-12b-it-GGUF/tree/main
|
||||
- Download embeddings connectors
|
||||
- safetensors: https://huggingface.co/unsloth/LTX-2.3-GGUF/tree/main/text_encoders
|
||||
- Download vae
|
||||
- safetensors: https://huggingface.co/unsloth/LTX-2.3-GGUF/tree/main/vae
|
||||
- Download audio vae
|
||||
- safetensors: https://huggingface.co/unsloth/LTX-2.3-GGUF/tree/main/vae
|
||||
- Download LTX spatial latent upscaler
|
||||
- safetensors: https://huggingface.co/Lightricks/LTX-2.3/resolve/main/ltx-2.3-spatial-upscaler-x2-1.1.safetensors
|
||||
|
||||
## Examples
|
||||
|
||||
### LTX-2.3 dev T2V
|
||||
|
||||
```
|
||||
.\bin\Release\sd-cli.exe -M vid_gen --diffusion-model ..\..\ComfyUI\models\diffusion_models\ltx-2.3-22b-dev-UD-Q4_K_M.gguf --vae ..\..\ComfyUI\models\vae\ltx-2.3-22b-dev_video_vae.safetensors --audio-vae ..\..\ComfyUI\models\vae\ltx-2.3-22b-dev_audio_vae.safetensors --llm ..\..\ComfyUI\models\text_encoders\gemma-3-12b-it-qat-UD-Q4_K_XL.gguf --embeddings-connectors ..\..\ComfyUI\models\text_encoders\ltx-2.3-22b-dev_embeddings_connectors.safetensors -p "a lovely cat" --cfg-scale 6.0 --sampling-method euler -v -n "worst quality, low quality, blurry, distorted, artifacts" -W 1280 -H 720 --diffusion-fa --offload-to-cpu --video-frames 33 --fps 24 -o t2v.webm
|
||||
```
|
||||
|
||||
<video
|
||||
src="../assets/ltx2/t2v.webm"
|
||||
controls
|
||||
muted
|
||||
style="max-width: 100%; height: auto;"></video>
|
||||
|
||||
### LTX-2.3 dev I2V
|
||||
|
||||
```
|
||||
.\bin\Release\sd-cli.exe -M vid_gen --diffusion-model ..\..\ComfyUI\models\diffusion_models\ltx-2.3-22b-dev-UD-Q4_K_M.gguf --vae ..\..\ComfyUI\models\vae\ltx-2.3-22b-dev_video_vae.safetensors --audio-vae ..\..\ComfyUI\models\vae\ltx-2.3-22b-dev_audio_vae.safetensors --llm ..\..\ComfyUI\models\text_encoders\gemma-3-12b-it-qat-UD-Q4_K_XL.gguf --embeddings-connectors ..\..\ComfyUI\models\text_encoders\ltx-2.3-22b-dev_embeddings_connectors.safetensors -p "a lovely cat" --cfg-scale 6.0 --sampling-method euler -v -W 1280 -H 720 --diffusion-fa --offload-to-cpu --video-frames 33 -i ..\assets\ernie_image\turbo_example.png -o i2v.webm
|
||||
```
|
||||
|
||||
<video
|
||||
src="../assets/ltx2/i2v.webm"
|
||||
controls
|
||||
muted
|
||||
style="max-width: 100%; height: auto;"></video>
|
||||
|
||||
### LTX-2.3 dev FLF2V
|
||||
|
||||
```
|
||||
.\bin\Release\sd-cli.exe -M vid_gen --diffusion-model ..\..\ComfyUI\models\diffusion_models\ltx-2.3-22b-dev-UD-Q4_K_M.gguf --vae ..\..\ComfyUI\models\vae\ltx-2.3-22b-dev_video_vae.safetensors --audio-vae ..\..\ComfyUI\models\vae\ltx-2.3-22b-dev_audio_vae.safetensors --llm ..\..\ComfyUI\models\text_encoders\gemma-3-12b-it-qat-UD-Q4_K_XL.gguf --embeddings-connectors ..\..\ComfyUI\models\text_encoders\ltx-2.3-22b-dev_embeddings_connectors.safetensors -p "glass flower blossom" --cfg-scale 6.0 --sampling-method euler -v -W 1280 -H 720 --diffusion-fa --offload-to-cpu --video-frames 33 --init-img ..\..\ComfyUI\input\start_image.png --end-img ..\..\ComfyUI\input\end_image.png -o flf2v.webm
|
||||
```
|
||||
|
||||
<video
|
||||
src="../assets/ltx2/flf2v.webm"
|
||||
controls
|
||||
muted
|
||||
style="max-width: 100%; height: auto;"></video>
|
||||
|
||||
### LTX-2.3 spatial latent upscale
|
||||
|
||||
LTX spatial latent upscale runs a model-backed x2 latent upsampler between the low-resolution video pass and the high-resolution refine pass. `-W` and `-H` are the pre-upscale generation size; the spatial upsampler produces x2 latent dimensions.
|
||||
|
||||
Put `ltx-2.3-spatial-upscaler-x2-1.1.safetensors` under the directory passed to `--hires-upscalers-dir`, then use the model name without path or extension in `--hires-upscaler`.
|
||||
|
||||
```
|
||||
.\bin\Release\sd-cli.exe -M vid_gen --diffusion-model ..\..\ComfyUI\models\diffusion_models\ltx-2.3-22b-dev-UD-Q4_K_M.gguf --vae ..\..\ComfyUI\models\vae\ltx-2.3-22b-dev_video_vae.safetensors --audio-vae ..\..\ComfyUI\models\vae\ltx-2.3-22b-dev_audio_vae.safetensors --llm ..\..\ComfyUI\models\text_encoders\gemma-3-12b-it-qat-UD-Q4_K_XL.gguf --embeddings-connectors ..\..\ComfyUI\models\text_encoders\ltx-2.3-22b-dev_embeddings_connectors.safetensors --hires-upscalers-dir ..\..\ComfyUI\models\latent_upscale_models --hires-upscaler ltx-2.3-spatial-upscaler-x2-1.1 --hires --hires-steps 4 -p "a lovely cat" --cfg-scale 6.0 --sampling-method euler -v -W 640 -H 360 --diffusion-fa --offload-to-cpu --video-frames 33 -i ..\assets\ernie_image\turbo_example.png -o hires_i2v.webm
|
||||
```
|
||||
|
||||
By default, the hires refine pass uses the main sampler and scheduler, then trims the second-pass sigma schedule by `--hires-denoising-strength` (`0.7` by default). To reproduce a ComfyUI-style explicit refine schedule, pass custom hires sigmas:
|
||||
|
||||
```
|
||||
--hires-sigmas "0.85,0.725,0.421875,0.0"
|
||||
```
|
||||
|
||||
<video
|
||||
src="../assets/ltx2/hires_i2v.webm"
|
||||
controls
|
||||
muted
|
||||
style="max-width: 100%; height: auto;"></video>
|
||||
@@ -0,0 +1,48 @@
|
||||
# How to Use
|
||||
|
||||
MiniT2I uses a MiniT2I diffusion transformer and `google/flan-t5-large` as the text encoder.
|
||||
|
||||
## Download weights
|
||||
|
||||
- Download MiniT2I diffusion model
|
||||
- safetensors: https://huggingface.co/MiniT2I/MiniT2I/tree/main/minit2i-b-16/transformer (`diffusion_pytorch_model.safetensors`)
|
||||
- Download flan-t5-large text encoder
|
||||
- safetensors: https://huggingface.co/google/flan-t5-large/tree/main (`model.safetensors`)
|
||||
|
||||
## Examples
|
||||
|
||||
### Mac Metal
|
||||
|
||||
```
|
||||
./bin/sd-cli \
|
||||
--backend metal \
|
||||
--diffusion-model ../models/minit2i/diffusion_pytorch_model.safetensors \
|
||||
--t5xxl ../models/flan-t5-large/model.safetensors \
|
||||
--prompt "a cat" \
|
||||
--steps 100 \
|
||||
--cfg-scale 6 \
|
||||
--width 512 \
|
||||
--height 512 \
|
||||
--seed 42 \
|
||||
--sampling-method euler \
|
||||
--rng cpu \
|
||||
--output minit2i_metal.png \
|
||||
--threads 8
|
||||
```
|
||||
|
||||
### CUDA with diffusion flash attention
|
||||
|
||||
```
|
||||
./bin/sd-cli \
|
||||
--diffusion-model ../models/minit2i/diffusion_pytorch_model.safetensors \
|
||||
--t5xxl ../models/flan-t5-large/model.safetensors \
|
||||
--prompt "a cat" \
|
||||
--steps 100 \
|
||||
--cfg-scale 6 \
|
||||
--width 512 \
|
||||
--height 512 \
|
||||
--seed 42 \
|
||||
--sampling-method euler \
|
||||
--diffusion-fa \
|
||||
--output minit2i_cuda.png
|
||||
```
|
||||
@@ -0,0 +1,118 @@
|
||||
# Model Configuration Conventions
|
||||
|
||||
This document describes the conventions for model configuration structs and
|
||||
weight-based configuration detection.
|
||||
|
||||
## Config Types
|
||||
|
||||
Model configuration should live in a model-specific `*Config` struct.
|
||||
|
||||
Examples:
|
||||
|
||||
- `ZImageConfig`
|
||||
- `UNetConfig`
|
||||
- `MMDiTConfig`
|
||||
- `LLMConfig`
|
||||
|
||||
Preserve established acronym casing in type names, such as `UNet`, `MMDiT`,
|
||||
`LLM`, `VAE`, and `T5`.
|
||||
|
||||
Place the config struct near the top of the model header, before the main model
|
||||
blocks and runner types that consume it.
|
||||
|
||||
## Config Variables
|
||||
|
||||
Variables and members that hold a config should be named `config`.
|
||||
|
||||
Examples:
|
||||
|
||||
```cpp
|
||||
UNetConfig config;
|
||||
UnetModelBlock unet;
|
||||
|
||||
MMDiTRunner(...)
|
||||
: DiffusionModelRunner(backend, params_backend, prefix),
|
||||
config(MMDiTConfig::detect_from_weights(tensor_storage_map, prefix)),
|
||||
mmdit(config) {
|
||||
}
|
||||
```
|
||||
|
||||
Avoid alternate names such as `params`, `params_cfg`, `model_params`, or
|
||||
model-specific aliases unless an existing public API requires them.
|
||||
|
||||
## Weight Detection
|
||||
|
||||
If a model can derive configuration from loaded weight metadata, expose that
|
||||
logic as a static method on the config type:
|
||||
|
||||
```cpp
|
||||
static XxxConfig detect_from_weights(const String2TensorStorage& tensor_storage_map,
|
||||
const std::string& prefix);
|
||||
```
|
||||
|
||||
Additional selector arguments are allowed when required by an existing model
|
||||
family, for example `SDVersion version` or an architecture enum:
|
||||
|
||||
```cpp
|
||||
static UNetConfig detect_from_weights(const String2TensorStorage& tensor_storage_map,
|
||||
const std::string& prefix,
|
||||
SDVersion version = VERSION_SD1);
|
||||
```
|
||||
|
||||
Use `TensorStorage` metadata, especially `n_dims` and `ne`, to infer shapes.
|
||||
Do not load or parse tensor data for config detection.
|
||||
|
||||
Detection should respect `prefix`. For nested weights, construct full names from
|
||||
`prefix + "." + suffix` or filter entries with `starts_with(name, prefix)`.
|
||||
|
||||
Do not add persistent config fields such as `inferred_from_weights` only to
|
||||
record whether detection happened. If the function needs to decide whether to
|
||||
print a debug line, keep that as local control flow inside `detect_from_weights`.
|
||||
|
||||
## Logging
|
||||
|
||||
When config values are inferred from weights, print one `LOG_DEBUG` line at the
|
||||
end of `detect_from_weights`.
|
||||
|
||||
Example:
|
||||
|
||||
```cpp
|
||||
LOG_DEBUG("llm: num_layers = %" PRId64 ", vocab_size = %" PRId64 ", hidden_size = %" PRId64 ", intermediate_size = %" PRId64,
|
||||
config.num_layers,
|
||||
config.vocab_size,
|
||||
config.hidden_size,
|
||||
config.intermediate_size);
|
||||
```
|
||||
|
||||
Only print the config detection log when the function actually inferred values
|
||||
from weights. Do not duplicate the same config summary in runner constructors or
|
||||
model loading code.
|
||||
|
||||
Use the correct format specifiers for field types, such as `%" PRId64 "` for
|
||||
`int64_t` and `%d` for `int`.
|
||||
|
||||
## Runner And Model Responsibilities
|
||||
|
||||
Runners should detect the config once and pass it into the model block:
|
||||
|
||||
```cpp
|
||||
struct XxxRunner : public DiffusionModelRunner {
|
||||
XxxConfig config;
|
||||
XxxModel model;
|
||||
|
||||
XxxRunner(..., const String2TensorStorage& tensor_storage_map, const std::string prefix)
|
||||
: DiffusionModelRunner(backend, params_backend, prefix),
|
||||
config(XxxConfig::detect_from_weights(tensor_storage_map, prefix)),
|
||||
model(config) {
|
||||
model.init(params_ctx, tensor_storage_map, prefix);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
Model blocks should consume `config` directly instead of re-scanning weights in
|
||||
their constructors. Keep config-derived behavior centralized in the config
|
||||
struct.
|
||||
|
||||
If a model has no weight-derived config today, it may still provide
|
||||
`detect_from_weights` for API consistency, but it should not print a config
|
||||
detection log unless it actually derives values from weights.
|
||||
@@ -21,6 +21,38 @@ and the compute buffer shrink in the debug log:
|
||||
|
||||
Using `--offload-to-cpu` allows you to offload weights to the CPU, saving VRAM without reducing generation speed.
|
||||
|
||||
## Use params backend to reduce VRAM or RAM usage.
|
||||
|
||||
`--params-backend` controls where model parameters are kept. If it is not set, parameters use the same backend as `--backend`, so a GPU runtime backend also keeps parameters in VRAM.
|
||||
|
||||
Use CPU params to reduce VRAM usage:
|
||||
|
||||
```shell
|
||||
--backend cuda0 --params-backend cpu
|
||||
```
|
||||
|
||||
This keeps model weights in system RAM and moves them to the runtime backend when needed. In the example CLI/server, `--offload-to-cpu` is a compatibility shortcut that prepends `*=cpu` to `--params-backend` before creating the context, so explicit module assignments can still override it:
|
||||
|
||||
```shell
|
||||
--offload-to-cpu --params-backend te=disk
|
||||
```
|
||||
|
||||
Use disk params to reduce both VRAM and RAM usage:
|
||||
|
||||
```shell
|
||||
--backend cuda0 --params-backend disk
|
||||
```
|
||||
|
||||
This reloads parameters from the model file on demand and releases them after use. It has the lowest memory residency, but can be slower because weights must be read again. `disk` is never selected implicitly; set it explicitly when RAM usage matters more than reload cost.
|
||||
|
||||
Per-module assignments can target only the largest modules:
|
||||
|
||||
```shell
|
||||
--backend cuda0 --params-backend diffusion=disk,te=cpu,vae=cpu
|
||||
```
|
||||
|
||||
See [backend selection](./backend.md) for full syntax.
|
||||
|
||||
## Use quantization to reduce memory usage.
|
||||
|
||||
[quantization](./quantization_and_gguf.md)
|
||||
[quantization](./quantization_and_gguf.md)
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# How to Use
|
||||
|
||||
PiD is NVIDIA's Pixel Diffusion Decoder. It replaces the usual VAE decode or decode-then-upscale path with a pixel-space diffusion decoder conditioned on a
|
||||
source latent and text prompt.
|
||||
|
||||
In stable-diffusion.cpp, PiD currently runs as an image edit pipeline: provide a reference image with `-r`/`--ref-image`, encode that image with a matching VAE, then let the PiD diffusion model decode/upscale directly to RGB.
|
||||
|
||||
## Download weights
|
||||
|
||||
- Download PiD
|
||||
- safetensors: https://huggingface.co/Comfy-Org/PixelDiT/tree/main/diffusion_models
|
||||
- Download Gemma 2 2B
|
||||
- safetensors: https://huggingface.co/Comfy-Org/PixelDiT/tree/main/text_encoders
|
||||
- Download the VAE that matches the PiD checkpoint backbone
|
||||
- safetensors: https://huggingface.co/nvidia/PiD/tree/main/checkpoints
|
||||
- Flux / Z-Image PiD: use the Flux VAE and pass `--vae-format flux`
|
||||
- SD3 PiD: use the SD3 VAE and pass `--vae-format sd3`
|
||||
- Flux.2 PiD: use the Flux.2 VAE and pass `--vae-format flux2`
|
||||
|
||||
The official PiD model card should be checked before use. At the time of the initial PiD release, the official weights are under the NSCLv1 non-commercial license.
|
||||
|
||||
## Examples
|
||||
|
||||
```
|
||||
.\bin\Release\sd-cli.exe --diffusion-model ..\..\ComfyUI\models\diffusion_models\pid_flux1_512_to_2048_4step_bf16.safetensors --llm "..\..\ComfyUI\models\text_encoders\gemma_2_2b_it_elm_bf16.safetensors" --vae ..\..\ComfyUI\models\vae\ae.sft --vae-format flux --cfg-scale 1.0 -p "a lovely cat" -r ..\assets\ernie_image\turbo_example.png --diffusion-fa -v --steps 4 -H 2048 -W 2048 --rng cpu
|
||||
```
|
||||
|
||||
Before:
|
||||
|
||||
<img width="256" alt="ERNIE-Image Turbo example" src="../assets/ernie_image/turbo_example.png" />
|
||||
|
||||
After:
|
||||
<img width="1024" alt="PiD example" src="../assets/pid/example.png" />
|
||||
|
||||
## Notes
|
||||
|
||||
- `-r`/`--ref-image` is required. PiD uses the first reference image as the source latent condition.
|
||||
- `--vae-format` should match the VAE latent layout used by the PiD checkpoint. This is important when using standalone VAE files because the PiD diffusion
|
||||
checkpoint alone does not identify the VAE format.
|
||||
@@ -0,0 +1,196 @@
|
||||
# PuLID-Flux face-identity preservation
|
||||
|
||||
stable-diffusion.cpp supports the [PuLID-Flux](https://github.com/ToTheBeginning/PuLID)
|
||||
identity-injection technique on top of Flux.1 (schnell or dev) models.
|
||||
Given a single source portrait, PuLID-Flux produces new generations that
|
||||
preserve the source person's face across arbitrary scenes, poses, and
|
||||
prompts.
|
||||
|
||||
Unlike PhotoMaker (which extracts the identity inside the inference
|
||||
process from a directory of images), PuLID-Flux's identity extractor is
|
||||
a heavy stack (insightface ArcFace + EVA-CLIP-L + IDFormer encoder) that
|
||||
is impractical to port to C++/ggml. To keep this implementation small and
|
||||
cross-vendor, **stable-diffusion.cpp consumes a precomputed identity
|
||||
embedding** produced by an external Python tool that runs once per source
|
||||
portrait. Everything downstream of that one-shot extraction is C++ and
|
||||
runs on any backend (Vulkan, CUDA, Metal, ROCm, CPU).
|
||||
|
||||
## Architecture summary
|
||||
|
||||
The PuLID-Flux contribution to the Flux denoise loop is a stack of 20
|
||||
small cross-attention modules (`PerceiverAttentionCA`) inserted between
|
||||
the Flux transformer blocks:
|
||||
|
||||
- After every 2nd of the 19 double-stream blocks (10 hook points)
|
||||
- After every 4th of the 38 single-stream blocks (10 hook points)
|
||||
|
||||
Each cross-attention layer takes the current image tokens as query, the
|
||||
32-token / 2048-dim identity embedding as key+value, and adds its output
|
||||
(scaled by `id_weight`, typically 1.0) back to the image tokens.
|
||||
|
||||
## Required weights
|
||||
|
||||
Three files in addition to the standard Flux weight set:
|
||||
|
||||
1. **Flux base** (transformer + VAE + clip_l + t5xxl) -- exactly as
|
||||
[docs/flux.md](flux.md) describes.
|
||||
2. **PuLID weights** -- download from
|
||||
[guozinan/PuLID](https://huggingface.co/guozinan/PuLID):
|
||||
- `pulid_flux_v0.9.0.safetensors` or `pulid_flux_v0.9.1.safetensors`
|
||||
(recommended; this implementation is verified against v0.9.1)
|
||||
- **v1.1 (`pulid_v1.1.safetensors`) is NOT yet supported** -- it uses
|
||||
renamed keys (`id_adapter_attn_layers.*` instead of `pulid_ca.*`)
|
||||
and possibly different module structure. Future PR.
|
||||
3. **Identity embedding (.pulidembd)** -- produced by the precompute
|
||||
tool below.
|
||||
|
||||
## Precompute the identity embedding
|
||||
|
||||
The precompute tool runs the PyTorch identity-extraction stack on a
|
||||
single portrait image and writes the resulting `(32, 2048)` embedding
|
||||
to a `.pulidembd` binary file (about 131 KB). Run it once per source
|
||||
person; the same file is reused for any number of generations.
|
||||
|
||||
A reference Python script is provided alongside this docs file at
|
||||
[`script/pulid_extract_id.py`](../script/pulid_extract_id.py). It
|
||||
requires:
|
||||
- A working CUDA / CPU PyTorch stack
|
||||
- `insightface`, `facexlib`, `eva-clip`, `torchvision`, `opencv-python`,
|
||||
`huggingface_hub`, `gguf`
|
||||
- The PuLID weights file (same one stable-diffusion.cpp will load below)
|
||||
- The ToTheBeginning/PuLID repo's `pulid/` package (including
|
||||
`pulid/pipeline_flux.py`) and `eva_clip/` package on `PYTHONPATH`; `flux/`
|
||||
is not needed for embedding extraction
|
||||
|
||||
Run it as:
|
||||
|
||||
```
|
||||
python pulid_extract_id.py \
|
||||
--portrait /path/to/source-photo.jpg \
|
||||
--pulid-weights /path/to/pulid_flux_v0.9.1.safetensors \
|
||||
--out /path/to/source.pulidembd
|
||||
```
|
||||
|
||||
## Format (gguf)
|
||||
|
||||
The embedding is a standard **gguf** container holding a single tensor:
|
||||
|
||||
```
|
||||
tensor name : "pulid_id"
|
||||
shape : [token_dim, num_tokens] (ggml order; typically [2048, 32])
|
||||
type : F16 (also accepts F32 / BF16)
|
||||
metadata : general.architecture = "pulid", pulid.version = 1
|
||||
```
|
||||
|
||||
stable-diffusion.cpp loads it with the normal gguf reader
|
||||
(`gguf_init_from_file`) and converts to fp32 at load time -- no bespoke
|
||||
parser. Total file size for the typical (32, 2048, fp16) case is ~131 KB.
|
||||
|
||||
## Command-line usage
|
||||
|
||||
```
|
||||
.\bin\Release\sd-cli.exe \
|
||||
--diffusion-model models\flux1-schnell-Q4_K_S.gguf \
|
||||
--vae models\ae.safetensors \
|
||||
--clip_l models\clip_l.safetensors \
|
||||
--t5xxl models\t5xxl_fp16.safetensors \
|
||||
--pulid-weights models\pulid_flux_v0.9.1.safetensors \
|
||||
--pulid-id-embedding source.pulidembd \
|
||||
--pulid-id-weight 1.0 \
|
||||
-p "candid photograph of a young woman on a beach at sunset" \
|
||||
--cfg-scale 1.0 --sampling-method euler --steps 4 -W 512 -H 512 \
|
||||
--seed 42 --clip-on-cpu \
|
||||
-o out.png
|
||||
```
|
||||
|
||||
For Flux Dev (instead of Schnell), add `--guidance 3.5` and `--steps 20`.
|
||||
|
||||
## Flags
|
||||
|
||||
| Flag | Purpose |
|
||||
|----------------------------|-------------------------------------------------------------------|
|
||||
| `--pulid-weights <path>` | Path to `pulid_flux_v0.9.x.safetensors`. Loaded with the model. |
|
||||
| `--pulid-id-embedding <p>` | Path to a `.pulidembd` binary produced by the precompute tool. |
|
||||
| `--pulid-id-weight <f>` | Identity-injection strength. Typical 0.7-1.2; default 1.0. |
|
||||
|
||||
All three flags must be set together to activate PuLID. Setting only
|
||||
`--pulid-weights` (no embedding) loads the weights but disables injection
|
||||
at runtime. Setting `--pulid-id-weight 0` zeros out the contribution
|
||||
(useful for falsification testing: outputs should be byte-identical to
|
||||
a no-PuLID run with the same seed).
|
||||
|
||||
## Memory budget
|
||||
|
||||
At 512x512, 4 steps (Schnell), the 20 cross-attention layers add roughly
|
||||
10% to denoise time and almost nothing to peak VRAM. Tested on a 12 GB
|
||||
consumer card alongside Flux Schnell Q4 GGUF + CPU-offloaded clip_l and
|
||||
t5xxl + GPU-resident VAE.
|
||||
|
||||
At 1024x1024 with Flux Dev Q4 + 20 steps + PuLID, the VAE decode compute
|
||||
buffer doesn't fit on a 12 GB card even with `--vae-on-cpu`. Workaround:
|
||||
explicitly route VAE to the CPU backend instead of the offload flag:
|
||||
|
||||
```
|
||||
--backend "diffusion=vulkan0,vae=cpu"
|
||||
```
|
||||
|
||||
The `--vae-on-cpu` flag offloads VAE weights but leaves the compute graph
|
||||
on the default backend; this is existing stable-diffusion.cpp behavior,
|
||||
not a PuLID-specific issue. Documented here because anyone running PuLID
|
||||
at 1024 will hit it.
|
||||
|
||||
## Backend selection
|
||||
|
||||
The standard `--backend` flag works as documented. Common patterns:
|
||||
|
||||
```
|
||||
# AMD Vulkan
|
||||
--backend "diffusion=vulkan0,vae=cpu"
|
||||
|
||||
# NVIDIA Vulkan
|
||||
--backend "diffusion=vulkan1,vae=cpu"
|
||||
|
||||
# CUDA
|
||||
--backend "diffusion=cuda0,vae=cpu"
|
||||
```
|
||||
|
||||
The PuLID cross-attention layers run on the same backend as the main
|
||||
diffusion model. They have not yet been independently profiled on every
|
||||
backend; only Vulkan and CPU have been tested by the original contributor.
|
||||
|
||||
## Verification
|
||||
|
||||
A three-way SHA-256 check is the recommended sanity test when bringing up
|
||||
a new combination of model + backend + hardware:
|
||||
|
||||
| Run | Expected hash relation |
|
||||
|----------------------------------------------|------------------------------------|
|
||||
| A: no `--pulid-*` flags | baseline |
|
||||
| B: PuLID flags, `--pulid-id-weight 0.0` | **byte-identical to A** |
|
||||
| C: PuLID flags, `--pulid-id-weight 1.0` | **different from A,B**, preserves source identity |
|
||||
|
||||
If A and C differ but A and B differ too, the injection is allocating
|
||||
or computing something even at zero weight -- likely a bug.
|
||||
|
||||
## Limitations / not yet supported
|
||||
|
||||
- **`--skip-layers` (skip-layer-guidance / SLG) combined with PuLID** is not
|
||||
supported. The `pulid_ca` index advances per non-skipped block, so a
|
||||
skipped block silently misaligns the cross-attention weight assignment
|
||||
vs. the trained intervals. The reference PyTorch implementation does
|
||||
not have SLG either, so there is no well-defined behavior to emulate.
|
||||
Use either feature alone.
|
||||
- **PuLID v1.1 weights** (`pulid_v1.1.safetensors`, renamed key layout).
|
||||
- **Multiple ID images.** The reference PyTorch implementation can fuse
|
||||
several portraits into one embedding for stronger identity. This
|
||||
implementation accepts a single embedding produced from one or more
|
||||
images by the external precompute tool.
|
||||
- **Negative-prompt branch of CFG.** PuLID only injects on the positive
|
||||
conditioning path in the published reference, and the implementation
|
||||
here follows that. Flux's distilled guidance doesn't run a separate
|
||||
uncond branch in normal use, so this matters only for `--true-cfg`
|
||||
workflows that aren't standard for Flux.
|
||||
- **Backends other than Vulkan and CPU** are untested by the original
|
||||
contributor. The implementation is pure-ggml and should work on CUDA,
|
||||
ROCm, and Metal, but verification by users on those backends is
|
||||
welcomed.
|
||||
@@ -0,0 +1,220 @@
|
||||
# Building and Using the RPC Server with `stable-diffusion.cpp`
|
||||
|
||||
This guide covers how to build a version of [the RPC server from `llama.cpp`](https://github.com/ggml-org/llama.cpp/blob/master/tools/rpc/README.md) that is compatible with your version of `stable-diffusion.cpp` to manage multi-backends setups. RPC allows you to offload specific model components to a remote server.
|
||||
|
||||
> **Note on Model Location:** The model files (e.g., `.safetensors` or `.gguf`) remain on the **Client** machine. The client parses the file and transmits the necessary tensor data and computational graphs to the server. The server does not need to store the model files locally.
|
||||
|
||||
## 1. Building `stable-diffusion.cpp` with RPC client
|
||||
|
||||
First, you should build the client application from source. It requires `SD_RPC=ON` to include the RPC backend to your client.
|
||||
|
||||
```bash
|
||||
mkdir build
|
||||
cd build
|
||||
cmake .. \
|
||||
-DSD_RPC=ON \
|
||||
# Add other build flags here (e.g., -DSD_VULKAN=ON)
|
||||
cmake --build . --config Release -j $(nproc)
|
||||
```
|
||||
|
||||
> **Note:** Ensure you add the other flags you would normally use (e.g., `-DSD_VULKAN=ON`, `-DSD_CUDA=ON`, `-DSD_HIPBLAS=ON`, or `-DGGML_METAL=ON`), for more information about building `stable-diffusion.cpp` from source, please refer to the [build.md](build.md) documentation.
|
||||
|
||||
## 2. Ensure `llama.cpp` is at the correct commit
|
||||
|
||||
`stable-diffusion.cpp`'s RPC client is designed to work with a specific version of `llama.cpp` (compatible with the `ggml` submodule) to ensure API compatibility. The commit hash for `llama.cpp` is stored in `ggml/scripts/sync-llama.last`.
|
||||
|
||||
> **Start from Root:** Perform these steps from the root of your `stable-diffusion.cpp` directory.
|
||||
|
||||
1. Read the target commit hash from the submodule tracker:
|
||||
|
||||
```bash
|
||||
# Linux / WSL / MacOS
|
||||
HASH=$(cat ggml/scripts/sync-llama.last)
|
||||
|
||||
# Windows (PowerShell)
|
||||
$HASH = Get-Content -Path "ggml\scripts\sync-llama.last"
|
||||
```
|
||||
|
||||
2. Clone `llama.cpp` at the target commit .
|
||||
```bash
|
||||
git clone https://github.com/ggml-org/llama.cpp.git
|
||||
cd llama.cpp
|
||||
git checkout $HASH
|
||||
```
|
||||
To save on download time and storage, you can use a shallow clone to download only the target commit:
|
||||
```bash
|
||||
mkdir -p llama.cpp
|
||||
cd llama.cpp
|
||||
git init
|
||||
git remote add origin https://github.com/ggml-org/llama.cpp.git
|
||||
git fetch --depth 1 origin $HASH
|
||||
git checkout FETCH_HEAD
|
||||
```
|
||||
|
||||
## 3. Build `llama.cpp` (RPC Server)
|
||||
|
||||
The RPC server acts as the worker. You must explicitly enable the **backend** (the hardware interface, such as CUDA for Nvidia, Metal for Apple Silicon, or Vulkan) when building, otherwise the server will default to using only the CPU.
|
||||
|
||||
To find the correct flags for your system, refer to the official documentation for the [`llama.cpp`](https://github.com/ggml-org/llama.cpp/blob/master/docs/build.md) repository.
|
||||
|
||||
> **Crucial:** You must include the compiler flags required to satisfy the API compatibility with `stable-diffusion.cpp` (`-DGGML_MAX_NAME=128`). Without this flag, `GGML_MAX_NAME` will default to `64` for the server, and data transfers between the client and server will fail. Of course, `-DGGML_RPC` must also be enabled.
|
||||
>
|
||||
> I recommend disabling the `LLAMA_CURL` flag to avoid unnecessary dependencies, and disabling shared library builds to avoid potential conflicts.
|
||||
|
||||
> **Build Target:** We are specifically building the `rpc-server` target. This prevents the build system from compiling the entire `llama.cpp` suite (like `llama-server`), making the build significantly faster.
|
||||
|
||||
### Linux / WSL (Vulkan)
|
||||
|
||||
```bash
|
||||
mkdir build
|
||||
cd build
|
||||
cmake .. -DGGML_RPC=ON \
|
||||
-DGGML_VULKAN=ON \ # Ensure backend is enabled
|
||||
-DGGML_BUILD_SHARED_LIBS=OFF \
|
||||
-DLLAMA_CURL=OFF \
|
||||
-DCMAKE_C_FLAGS=-DGGML_MAX_NAME=128 \
|
||||
-DCMAKE_CXX_FLAGS=-DGGML_MAX_NAME=128
|
||||
cmake --build . --config Release --target rpc-server -j $(nproc)
|
||||
```
|
||||
|
||||
### macOS (Metal)
|
||||
|
||||
```bash
|
||||
mkdir build
|
||||
cd build
|
||||
cmake .. -DGGML_RPC=ON \
|
||||
-DGGML_METAL=ON \
|
||||
-DGGML_BUILD_SHARED_LIBS=OFF \
|
||||
-DLLAMA_CURL=OFF \
|
||||
-DCMAKE_C_FLAGS=-DGGML_MAX_NAME=128 \
|
||||
-DCMAKE_CXX_FLAGS=-DGGML_MAX_NAME=128
|
||||
cmake --build . --config Release --target rpc-server
|
||||
```
|
||||
|
||||
### Windows (Visual Studio 2022, Vulkan)
|
||||
|
||||
```powershell
|
||||
mkdir build
|
||||
cd build
|
||||
cmake .. -G "Visual Studio 17 2022" -A x64 `
|
||||
-DGGML_RPC=ON `
|
||||
-DGGML_VULKAN=ON `
|
||||
-DGGML_BUILD_SHARED_LIBS=OFF `
|
||||
-DLLAMA_CURL=OFF `
|
||||
-DCMAKE_C_FLAGS=-DGGML_MAX_NAME=128 `
|
||||
-DCMAKE_CXX_FLAGS=-DGGML_MAX_NAME=128
|
||||
cmake --build . --config Release --target rpc-server
|
||||
```
|
||||
|
||||
## 4. Usage
|
||||
|
||||
Once both applications are built, you can run the server and the client to manage your GPU allocation.
|
||||
|
||||
### Step A: Run the RPC Server
|
||||
|
||||
Start the server. It listens for connections on the default address (usually `localhost:50052`). If your server is on a different machine, ensure the server binds to the correct interface and your firewall allows the connection.
|
||||
|
||||
**On the Server :**
|
||||
If running on the same machine, you can use the default address:
|
||||
|
||||
```bash
|
||||
./rpc-server
|
||||
```
|
||||
|
||||
If you want to allow connections from other machines on the network:
|
||||
|
||||
```bash
|
||||
./rpc-server --host 0.0.0.0
|
||||
```
|
||||
|
||||
> **Security Warning:** The RPC server does not currently support authentication or encryption. **Only run the server on trusted local networks**. Never expose the RPC server directly to the open internet.
|
||||
|
||||
> **Drivers & Hardware:** Ensure the Server machine has the necessary drivers installed and functional (e.g., Nvidia Drivers for CUDA, Vulkan SDK, or Metal). If no devices are found, the server will simply fallback to CPU usage.
|
||||
|
||||
<!-- ### Step B: Check if the client is able to connect to the server and see the available devices
|
||||
|
||||
We're assuming the server is running on your local machine, and listening on the default port `50052`. If it's running on a different machine, you can replace `localhost` with the IP address of the server.
|
||||
|
||||
**On the Client:**
|
||||
|
||||
```bash
|
||||
./sd-cli --rpc-servers localhost:50052 --list-devices
|
||||
```
|
||||
|
||||
If the server is running and the client is able to connect, you should see `RPC0 localhost:50052` in the list of devices.
|
||||
|
||||
Example output:
|
||||
(Client built without GPU acceleration, two GPUs available on the server)
|
||||
|
||||
```
|
||||
List of available GGML devices:
|
||||
Name Description
|
||||
-------------------
|
||||
CPU AMD Ryzen 9 5900X 12-Core Processor
|
||||
RPC0 localhost:50052
|
||||
RPC1 localhost:50052
|
||||
``` -->
|
||||
|
||||
### Step B: Run with RPC device
|
||||
|
||||
If everything is working correctly, you can now run the client while offloading some or all of the work to the RPC server.
|
||||
|
||||
Example: Setting the main backend to the RPC0 device for doing all the work on the server.
|
||||
|
||||
```bash
|
||||
./sd-cli -m models/sd1.5.safetensors -p "A cat" --rpc-servers localhost:50052 --backend RPC0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Scaling: Multiple RPC Servers
|
||||
|
||||
You can connect the client to multiple RPC servers simultaneously to scale out your hardware usage.
|
||||
|
||||
Example: A main machine (192.168.1.10) with 3 GPUs, with one GPU running CUDA and the other two running Vulkan, and a second machine (192.168.1.11) only one GPU.
|
||||
|
||||
**On the first machine (Running two server instances):**
|
||||
|
||||
**Terminal 1 (CUDA):**
|
||||
|
||||
```bash
|
||||
# Linux / WSL
|
||||
export CUDA_VISIBLE_DEVICES=0
|
||||
cd ./build_cuda/bin/Release
|
||||
./rpc-server --host 0.0.0.0
|
||||
|
||||
# Windows PowerShell
|
||||
$env:CUDA_VISIBLE_DEVICES="0"
|
||||
cd .\build_cuda\bin\Release
|
||||
./rpc-server --host 0.0.0.0
|
||||
```
|
||||
|
||||
**Terminal 2 (Vulkan):**
|
||||
|
||||
```bash
|
||||
cd ./build_vulkan/bin/Release
|
||||
# ignore the first GPU (used by CUDA server)
|
||||
./rpc-server --host 0.0.0.0 --port 50053 -d Vulkan1,Vulkan2
|
||||
```
|
||||
|
||||
**On the second machine:**
|
||||
|
||||
```bash
|
||||
cd ./build/bin/Release
|
||||
./rpc-server --host 0.0.0.0
|
||||
```
|
||||
|
||||
**On the Client:**
|
||||
Pass multiple server addresses separated by commas.
|
||||
|
||||
```bash
|
||||
./sd-cli --rpc-servers 192.168.1.10:50052,192.168.1.10:50053,192.168.1.11:50052 [...]
|
||||
```
|
||||
|
||||
The client will map these servers to sequential device IDs (e.g., RPC0 from the first server, RPC2, RPC3 from the second, and RPC4 from the third). With this setup, you could for example use RPC0 for the main backend, RPC1 and RPC2 for the text encoders, and RPC3 for the VAE.
|
||||
|
||||
---
|
||||
|
||||
## 6. Performance Considerations
|
||||
|
||||
RPC performance is heavily dependent on network bandwidth, as large weights and activations must be transferred back and forth over the network, especially for large models, or when using high resolutions. For best results, ensure your network connection is stable and has sufficient bandwidth (>1Gbps recommended). This shoumd not be a concern if you are running the server and client on the same machine, as the data transfer will happen over the loopback interface.
|
||||
@@ -0,0 +1,50 @@
|
||||
# How to Use
|
||||
|
||||
SeFi-Image uses a Flux2-style dual-time transformer (semantic + texture streams), the standard Flux2 VAE, and Qwen3-VL as the LLM text encoder. Tech report: [arXiv:2606.22568](https://arxiv.org/abs/2606.22568).
|
||||
|
||||
## Download weights
|
||||
|
||||
The SeFi-Image family ships in three scales (1B / 2B / 5B) and three families (Base / RL / turbo), all gated on Hugging Face under https://huggingface.co/SeFi-Image.
|
||||
|
||||
- 1B and 2B variants pair with Qwen3-VL-2B-Instruct.
|
||||
- 5B variants pair with Qwen3-VL-4B-Instruct.
|
||||
- All variants use the standard Flux2 VAE (`flux2_ae.safetensors` from https://huggingface.co/black-forest-labs/FLUX.2-dev).
|
||||
|
||||
Convert the transformer and text encoder to sd.cpp safetensors:
|
||||
|
||||
```bash
|
||||
python3 script/convert_sefi.py <hf_repo_dir> <out_dir>/sefi_<scale>_<family>.safetensors
|
||||
python3 script/convert_qwen3_vl.py <hf_repo_dir>/Qwen3-VL-XB-Instruct <out_dir>/qwen3_vl_<X>b.safetensors
|
||||
```
|
||||
|
||||
## Variant defaults
|
||||
|
||||
| Family | timestep_shift_alpha | steps | cfg-scale |
|
||||
|---|---|---|---|
|
||||
| Base | 0.3 | 50 | 4.0 |
|
||||
| RL | 0.3 | 50 | 4.0 |
|
||||
| turbo | 1.0 | 4 | 1.0 |
|
||||
|
||||
The dispatcher picks `alpha` from the filename (`turbo` substring => 1.0, otherwise 0.3). Override via `--extra-sample-args sefi_alpha=<value>` or `sefi_delta_t=<value>`.
|
||||
|
||||
## Examples
|
||||
|
||||
### 1B / 2B turbo
|
||||
|
||||
```
|
||||
./build/bin/sd-cli --diffusion-model /path/to/sefi_1b_turbo.safetensors --vae /path/to/flux2_ae.safetensors --llm /path/to/qwen3_vl_2b.safetensors -p "a photograph of an orange tabby cat sitting on a couch" --cfg-scale 1.0 --steps 4 -W 1024 -H 1024 -s 42 --diffusion-fa --offload-to-cpu -o out.png
|
||||
```
|
||||
|
||||
### 1B / 2B base
|
||||
|
||||
```
|
||||
./build/bin/sd-cli --diffusion-model /path/to/sefi_1b_base.safetensors --vae /path/to/flux2_ae.safetensors --llm /path/to/qwen3_vl_2b.safetensors -p "a photograph of an orange tabby cat sitting on a couch" --cfg-scale 4.0 --steps 50 -W 1024 -H 1024 -s 42 --diffusion-fa --offload-to-cpu -o out.png
|
||||
```
|
||||
|
||||
### 5B (needs streaming on 12 GiB VRAM)
|
||||
|
||||
```
|
||||
./build/bin/sd-cli --diffusion-model /path/to/sefi_5b_turbo.safetensors --vae /path/to/flux2_ae.safetensors --llm /path/to/qwen3_vl_4b.safetensors -p "a photograph of an orange tabby cat sitting on a couch" --cfg-scale 1.0 --steps 4 -W 1024 -H 1024 -s 42 --diffusion-fa --max-vram 8 --stream-layers --offload-to-cpu -o out.png
|
||||
```
|
||||
|
||||
<img alt="SeFi-Image 5B turbo example" src="../assets/sefi_image/example.png" />
|
||||
@@ -7,6 +7,9 @@ You can run Z-Image with stable-diffusion.cpp on GPUs with 4GB of VRAM — or ev
|
||||
- Download Z-Image-Turbo
|
||||
- safetensors: https://huggingface.co/Comfy-Org/z_image_turbo/tree/main/split_files/diffusion_models
|
||||
- gguf: https://huggingface.co/leejet/Z-Image-Turbo-GGUF/tree/main
|
||||
- Download Z-Image
|
||||
- safetensors: https://huggingface.co/Comfy-Org/z_image/tree/main/split_files/diffusion_models
|
||||
- gguf: https://huggingface.co/unsloth/Z-Image-GGUF/tree/main
|
||||
- Download vae
|
||||
- safetensors: https://huggingface.co/black-forest-labs/FLUX.1-schnell/tree/main
|
||||
- Download Qwen3 4b
|
||||
@@ -15,12 +18,22 @@ You can run Z-Image with stable-diffusion.cpp on GPUs with 4GB of VRAM — or ev
|
||||
|
||||
## Examples
|
||||
|
||||
### Z-Image-Turbo
|
||||
|
||||
```
|
||||
.\bin\Release\sd-cli.exe --diffusion-model z_image_turbo-Q3_K.gguf --vae ..\..\ComfyUI\models\vae\ae.sft --llm ..\..\ComfyUI\models\text_encoders\Qwen3-4B-Instruct-2507-Q4_K_M.gguf -p "A cinematic, melancholic photograph of a solitary hooded figure walking through a sprawling, rain-slicked metropolis at night. The city lights are a chaotic blur of neon orange and cool blue, reflecting on the wet asphalt. The scene evokes a sense of being a single component in a vast machine. Superimposed over the image in a sleek, modern, slightly glitched font is the philosophical quote: 'THE CITY IS A CIRCUIT BOARD, AND I AM A BROKEN TRANSISTOR.' -- moody, atmospheric, profound, dark academic" --cfg-scale 1.0 -v --offload-to-cpu --diffusion-fa -H 1024 -W 512
|
||||
.\bin\Release\sd-cli.exe --diffusion-model z_image_turbo-Q3_K.gguf --vae ..\..\ComfyUI\models\vae\ae.sft --llm ..\..\ComfyUI\models\text_encoders\Qwen3-4B-Instruct-2507-Q4_K_M.gguf -p "A cinematic, melancholic photograph of a solitary hooded figure walking through a sprawling, rain-slicked metropolis at night. The city lights are a chaotic blur of neon orange and cool blue, reflecting on the wet asphalt. The scene evokes a sense of being a single component in a vast machine. Superimposed over the image in a sleek, modern, slightly glitched font is the philosophical quote: 'THE CITY IS A CIRCUIT BOARD, AND I AM A BROKEN TRANSISTOR.' -- moody, atmospheric, profound, dark academic" --cfg-scale 1.0 -v --offload-to-cpu --diffusion-fa -H 1024 -W 512 --steps 8
|
||||
```
|
||||
|
||||
<img width="256" alt="z-image example" src="../assets/z_image/q3_K.png" />
|
||||
|
||||
### Z-Image-Base
|
||||
|
||||
```
|
||||
.\bin\Release\sd-cli.exe --diffusion-model ..\..\ComfyUI\models\diffusion_models\z_image_bf16.safetensors --vae ..\..\ComfyUI\models\vae\ae.sft --llm ..\..\ComfyUI\models\text_encoders\qwen_3_4b.safetensors -p "A cinematic, melancholic photograph of a solitary hooded figure walking through a sprawling, rain-slicked metropolis at night. The city lights are a chaotic blur of neon orange and cool blue, reflecting on the wet asphalt. The scene evokes a sense of being a single component in a vast machine. Superimposed over the image in a sleek, modern, slightly glitched font is the philosophical quote: 'THE CITY IS A CIRCUIT BOARD, AND I AM A BROKEN TRANSISTOR.' -- moody, atmospheric, profound, dark academic" --cfg-scale 5.0 -v --offload-to-cpu --diffusion-fa -H 1024 -W 512
|
||||
```
|
||||
|
||||
<img width="256" alt="z-image example" src="../assets/z_image/base_bf16.png" />
|
||||
|
||||
## Comparison of Different Quantization Types
|
||||
|
||||
| bf16 | q8_0 | q6_K | q5_0 | q4_K | q4_0 | q3_K | q2_K|
|
||||
|
||||
@@ -1,368 +0,0 @@
|
||||
#ifndef __ESRGAN_HPP__
|
||||
#define __ESRGAN_HPP__
|
||||
|
||||
#include "ggml_extend.hpp"
|
||||
#include "model.h"
|
||||
|
||||
/*
|
||||
=================================== ESRGAN ===================================
|
||||
References:
|
||||
https://github.com/xinntao/Real-ESRGAN/blob/master/inference_realesrgan.py
|
||||
https://github.com/XPixelGroup/BasicSR/blob/v1.4.2/basicsr/archs/rrdbnet_arch.py
|
||||
|
||||
*/
|
||||
|
||||
class ResidualDenseBlock : public GGMLBlock {
|
||||
protected:
|
||||
int num_feat;
|
||||
int num_grow_ch;
|
||||
|
||||
public:
|
||||
ResidualDenseBlock(int num_feat = 64, int num_grow_ch = 32)
|
||||
: num_feat(num_feat), num_grow_ch(num_grow_ch) {
|
||||
blocks["conv1"] = std::shared_ptr<GGMLBlock>(new Conv2d(num_feat, num_grow_ch, {3, 3}, {1, 1}, {1, 1}));
|
||||
blocks["conv2"] = std::shared_ptr<GGMLBlock>(new Conv2d(num_feat + num_grow_ch, num_grow_ch, {3, 3}, {1, 1}, {1, 1}));
|
||||
blocks["conv3"] = std::shared_ptr<GGMLBlock>(new Conv2d(num_feat + 2 * num_grow_ch, num_grow_ch, {3, 3}, {1, 1}, {1, 1}));
|
||||
blocks["conv4"] = std::shared_ptr<GGMLBlock>(new Conv2d(num_feat + 3 * num_grow_ch, num_grow_ch, {3, 3}, {1, 1}, {1, 1}));
|
||||
blocks["conv5"] = std::shared_ptr<GGMLBlock>(new Conv2d(num_feat + 4 * num_grow_ch, num_feat, {3, 3}, {1, 1}, {1, 1}));
|
||||
}
|
||||
|
||||
struct ggml_tensor* lrelu(GGMLRunnerContext* ctx, struct ggml_tensor* x) {
|
||||
return ggml_leaky_relu(ctx->ggml_ctx, x, 0.2f, true);
|
||||
}
|
||||
|
||||
struct ggml_tensor* forward(GGMLRunnerContext* ctx, struct ggml_tensor* x) {
|
||||
// x: [n, num_feat, h, w]
|
||||
// return: [n, num_feat, h, w]
|
||||
|
||||
auto conv1 = std::dynamic_pointer_cast<Conv2d>(blocks["conv1"]);
|
||||
auto conv2 = std::dynamic_pointer_cast<Conv2d>(blocks["conv2"]);
|
||||
auto conv3 = std::dynamic_pointer_cast<Conv2d>(blocks["conv3"]);
|
||||
auto conv4 = std::dynamic_pointer_cast<Conv2d>(blocks["conv4"]);
|
||||
auto conv5 = std::dynamic_pointer_cast<Conv2d>(blocks["conv5"]);
|
||||
|
||||
auto x1 = lrelu(ctx, conv1->forward(ctx, x));
|
||||
auto x_cat = ggml_concat(ctx->ggml_ctx, x, x1, 2);
|
||||
auto x2 = lrelu(ctx, conv2->forward(ctx, x_cat));
|
||||
x_cat = ggml_concat(ctx->ggml_ctx, x_cat, x2, 2);
|
||||
auto x3 = lrelu(ctx, conv3->forward(ctx, x_cat));
|
||||
x_cat = ggml_concat(ctx->ggml_ctx, x_cat, x3, 2);
|
||||
auto x4 = lrelu(ctx, conv4->forward(ctx, x_cat));
|
||||
x_cat = ggml_concat(ctx->ggml_ctx, x_cat, x4, 2);
|
||||
auto x5 = conv5->forward(ctx, x_cat);
|
||||
|
||||
x5 = ggml_add(ctx->ggml_ctx, ggml_scale(ctx->ggml_ctx, x5, 0.2f), x);
|
||||
return x5;
|
||||
}
|
||||
};
|
||||
|
||||
class RRDB : public GGMLBlock {
|
||||
public:
|
||||
RRDB(int num_feat, int num_grow_ch = 32) {
|
||||
blocks["rdb1"] = std::shared_ptr<GGMLBlock>(new ResidualDenseBlock(num_feat, num_grow_ch));
|
||||
blocks["rdb2"] = std::shared_ptr<GGMLBlock>(new ResidualDenseBlock(num_feat, num_grow_ch));
|
||||
blocks["rdb3"] = std::shared_ptr<GGMLBlock>(new ResidualDenseBlock(num_feat, num_grow_ch));
|
||||
}
|
||||
|
||||
struct ggml_tensor* forward(GGMLRunnerContext* ctx, struct ggml_tensor* x) {
|
||||
// x: [n, num_feat, h, w]
|
||||
// return: [n, num_feat, h, w]
|
||||
|
||||
auto rdb1 = std::dynamic_pointer_cast<ResidualDenseBlock>(blocks["rdb1"]);
|
||||
auto rdb2 = std::dynamic_pointer_cast<ResidualDenseBlock>(blocks["rdb2"]);
|
||||
auto rdb3 = std::dynamic_pointer_cast<ResidualDenseBlock>(blocks["rdb3"]);
|
||||
|
||||
auto out = rdb1->forward(ctx, x);
|
||||
out = rdb2->forward(ctx, out);
|
||||
out = rdb3->forward(ctx, out);
|
||||
|
||||
out = ggml_add(ctx->ggml_ctx, ggml_scale(ctx->ggml_ctx, out, 0.2f), x);
|
||||
return out;
|
||||
}
|
||||
};
|
||||
|
||||
class RRDBNet : public GGMLBlock {
|
||||
protected:
|
||||
int scale = 4;
|
||||
int num_block = 23;
|
||||
int num_in_ch = 3;
|
||||
int num_out_ch = 3;
|
||||
int num_feat = 64;
|
||||
int num_grow_ch = 32;
|
||||
|
||||
public:
|
||||
RRDBNet(int scale, int num_block, int num_in_ch, int num_out_ch, int num_feat, int num_grow_ch)
|
||||
: scale(scale), num_block(num_block), num_in_ch(num_in_ch), num_out_ch(num_out_ch), num_feat(num_feat), num_grow_ch(num_grow_ch) {
|
||||
blocks["conv_first"] = std::shared_ptr<GGMLBlock>(new Conv2d(num_in_ch, num_feat, {3, 3}, {1, 1}, {1, 1}));
|
||||
for (int i = 0; i < num_block; i++) {
|
||||
std::string name = "body." + std::to_string(i);
|
||||
blocks[name] = std::shared_ptr<GGMLBlock>(new RRDB(num_feat, num_grow_ch));
|
||||
}
|
||||
blocks["conv_body"] = std::shared_ptr<GGMLBlock>(new Conv2d(num_feat, num_feat, {3, 3}, {1, 1}, {1, 1}));
|
||||
if (scale >= 2) {
|
||||
blocks["conv_up1"] = std::shared_ptr<GGMLBlock>(new Conv2d(num_feat, num_feat, {3, 3}, {1, 1}, {1, 1}));
|
||||
}
|
||||
if (scale == 4) {
|
||||
blocks["conv_up2"] = std::shared_ptr<GGMLBlock>(new Conv2d(num_feat, num_feat, {3, 3}, {1, 1}, {1, 1}));
|
||||
}
|
||||
blocks["conv_hr"] = std::shared_ptr<GGMLBlock>(new Conv2d(num_feat, num_feat, {3, 3}, {1, 1}, {1, 1}));
|
||||
blocks["conv_last"] = std::shared_ptr<GGMLBlock>(new Conv2d(num_feat, num_out_ch, {3, 3}, {1, 1}, {1, 1}));
|
||||
}
|
||||
|
||||
int get_scale() { return scale; }
|
||||
int get_num_block() { return num_block; }
|
||||
|
||||
struct ggml_tensor* lrelu(GGMLRunnerContext* ctx, struct ggml_tensor* x) {
|
||||
return ggml_leaky_relu(ctx->ggml_ctx, x, 0.2f, true);
|
||||
}
|
||||
|
||||
struct ggml_tensor* forward(GGMLRunnerContext* ctx, struct ggml_tensor* x) {
|
||||
// x: [n, num_in_ch, h, w]
|
||||
// return: [n, num_out_ch, h*scale, w*scale]
|
||||
auto conv_first = std::dynamic_pointer_cast<Conv2d>(blocks["conv_first"]);
|
||||
auto conv_body = std::dynamic_pointer_cast<Conv2d>(blocks["conv_body"]);
|
||||
auto conv_hr = std::dynamic_pointer_cast<Conv2d>(blocks["conv_hr"]);
|
||||
auto conv_last = std::dynamic_pointer_cast<Conv2d>(blocks["conv_last"]);
|
||||
|
||||
auto feat = conv_first->forward(ctx, x);
|
||||
auto body_feat = feat;
|
||||
for (int i = 0; i < num_block; i++) {
|
||||
std::string name = "body." + std::to_string(i);
|
||||
auto block = std::dynamic_pointer_cast<RRDB>(blocks[name]);
|
||||
|
||||
body_feat = block->forward(ctx, body_feat);
|
||||
}
|
||||
body_feat = conv_body->forward(ctx, body_feat);
|
||||
feat = ggml_add(ctx->ggml_ctx, feat, body_feat);
|
||||
// upsample
|
||||
if (scale >= 2) {
|
||||
auto conv_up1 = std::dynamic_pointer_cast<Conv2d>(blocks["conv_up1"]);
|
||||
feat = lrelu(ctx, conv_up1->forward(ctx, ggml_upscale(ctx->ggml_ctx, feat, 2, GGML_SCALE_MODE_NEAREST)));
|
||||
if (scale == 4) {
|
||||
auto conv_up2 = std::dynamic_pointer_cast<Conv2d>(blocks["conv_up2"]);
|
||||
feat = lrelu(ctx, conv_up2->forward(ctx, ggml_upscale(ctx->ggml_ctx, feat, 2, GGML_SCALE_MODE_NEAREST)));
|
||||
}
|
||||
}
|
||||
// for all scales
|
||||
auto out = conv_last->forward(ctx, lrelu(ctx, conv_hr->forward(ctx, feat)));
|
||||
return out;
|
||||
}
|
||||
};
|
||||
|
||||
struct ESRGAN : public GGMLRunner {
|
||||
std::unique_ptr<RRDBNet> rrdb_net;
|
||||
int scale = 4;
|
||||
int tile_size = 128; // avoid cuda OOM for 4gb VRAM
|
||||
|
||||
ESRGAN(ggml_backend_t backend,
|
||||
bool offload_params_to_cpu,
|
||||
int tile_size = 128,
|
||||
const String2TensorStorage& tensor_storage_map = {})
|
||||
: GGMLRunner(backend, offload_params_to_cpu) {
|
||||
this->tile_size = tile_size;
|
||||
}
|
||||
|
||||
std::string get_desc() override {
|
||||
return "esrgan";
|
||||
}
|
||||
|
||||
bool load_from_file(const std::string& file_path, int n_threads) {
|
||||
LOG_INFO("loading esrgan from '%s'", file_path.c_str());
|
||||
|
||||
ModelLoader model_loader;
|
||||
if (!model_loader.init_from_file_and_convert_name(file_path)) {
|
||||
LOG_ERROR("init esrgan model loader from file failed: '%s'", file_path.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get tensor names
|
||||
auto tensor_names = model_loader.get_tensor_names();
|
||||
|
||||
// Detect if it's ESRGAN format
|
||||
bool is_ESRGAN = std::find(tensor_names.begin(), tensor_names.end(), "model.0.weight") != tensor_names.end();
|
||||
|
||||
// Detect parameters from tensor names
|
||||
int detected_num_block = 0;
|
||||
if (is_ESRGAN) {
|
||||
for (const auto& name : tensor_names) {
|
||||
if (name.find("model.1.sub.") == 0) {
|
||||
size_t first_dot = name.find('.', 12);
|
||||
if (first_dot != std::string::npos) {
|
||||
size_t second_dot = name.find('.', first_dot + 1);
|
||||
if (second_dot != std::string::npos && name.substr(first_dot + 1, 3) == "RDB") {
|
||||
try {
|
||||
int idx = std::stoi(name.substr(12, first_dot - 12));
|
||||
detected_num_block = std::max(detected_num_block, idx + 1);
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Original format
|
||||
for (const auto& name : tensor_names) {
|
||||
if (name.find("body.") == 0) {
|
||||
size_t pos = name.find('.', 5);
|
||||
if (pos != std::string::npos) {
|
||||
try {
|
||||
int idx = std::stoi(name.substr(5, pos - 5));
|
||||
detected_num_block = std::max(detected_num_block, idx + 1);
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int detected_scale = 4; // default
|
||||
if (is_ESRGAN) {
|
||||
// For ESRGAN format, detect scale by highest model number
|
||||
int max_model_num = 0;
|
||||
for (const auto& name : tensor_names) {
|
||||
if (name.find("model.") == 0) {
|
||||
size_t dot_pos = name.find('.', 6);
|
||||
if (dot_pos != std::string::npos) {
|
||||
try {
|
||||
int num = std::stoi(name.substr(6, dot_pos - 6));
|
||||
max_model_num = std::max(max_model_num, num);
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (max_model_num <= 4) {
|
||||
detected_scale = 1;
|
||||
} else if (max_model_num <= 7) {
|
||||
detected_scale = 2;
|
||||
} else {
|
||||
detected_scale = 4;
|
||||
}
|
||||
} else {
|
||||
// Original format
|
||||
bool has_conv_up2 = std::any_of(tensor_names.begin(), tensor_names.end(), [](const std::string& name) {
|
||||
return name == "conv_up2.weight";
|
||||
});
|
||||
bool has_conv_up1 = std::any_of(tensor_names.begin(), tensor_names.end(), [](const std::string& name) {
|
||||
return name == "conv_up1.weight";
|
||||
});
|
||||
if (has_conv_up2) {
|
||||
detected_scale = 4;
|
||||
} else if (has_conv_up1) {
|
||||
detected_scale = 2;
|
||||
} else {
|
||||
detected_scale = 1;
|
||||
}
|
||||
}
|
||||
|
||||
int detected_num_in_ch = 3;
|
||||
int detected_num_out_ch = 3;
|
||||
int detected_num_feat = 64;
|
||||
int detected_num_grow_ch = 32;
|
||||
|
||||
// Create RRDBNet with detected parameters
|
||||
rrdb_net = std::make_unique<RRDBNet>(detected_scale, detected_num_block, detected_num_in_ch, detected_num_out_ch, detected_num_feat, detected_num_grow_ch);
|
||||
rrdb_net->init(params_ctx, {}, "");
|
||||
|
||||
alloc_params_buffer();
|
||||
std::map<std::string, ggml_tensor*> esrgan_tensors;
|
||||
rrdb_net->get_param_tensors(esrgan_tensors);
|
||||
|
||||
bool success;
|
||||
if (is_ESRGAN) {
|
||||
// Build name mapping for ESRGAN format
|
||||
std::map<std::string, std::string> expected_to_model;
|
||||
expected_to_model["conv_first.weight"] = "model.0.weight";
|
||||
expected_to_model["conv_first.bias"] = "model.0.bias";
|
||||
|
||||
for (int i = 0; i < detected_num_block; i++) {
|
||||
for (int j = 1; j <= 3; j++) {
|
||||
for (int k = 1; k <= 5; k++) {
|
||||
std::string expected_weight = "body." + std::to_string(i) + ".rdb" + std::to_string(j) + ".conv" + std::to_string(k) + ".weight";
|
||||
std::string model_weight = "model.1.sub." + std::to_string(i) + ".RDB" + std::to_string(j) + ".conv" + std::to_string(k) + ".0.weight";
|
||||
expected_to_model[expected_weight] = model_weight;
|
||||
|
||||
std::string expected_bias = "body." + std::to_string(i) + ".rdb" + std::to_string(j) + ".conv" + std::to_string(k) + ".bias";
|
||||
std::string model_bias = "model.1.sub." + std::to_string(i) + ".RDB" + std::to_string(j) + ".conv" + std::to_string(k) + ".0.bias";
|
||||
expected_to_model[expected_bias] = model_bias;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (detected_scale == 1) {
|
||||
expected_to_model["conv_body.weight"] = "model.1.sub." + std::to_string(detected_num_block) + ".weight";
|
||||
expected_to_model["conv_body.bias"] = "model.1.sub." + std::to_string(detected_num_block) + ".bias";
|
||||
expected_to_model["conv_hr.weight"] = "model.2.weight";
|
||||
expected_to_model["conv_hr.bias"] = "model.2.bias";
|
||||
expected_to_model["conv_last.weight"] = "model.4.weight";
|
||||
expected_to_model["conv_last.bias"] = "model.4.bias";
|
||||
} else {
|
||||
expected_to_model["conv_body.weight"] = "model.1.sub." + std::to_string(detected_num_block) + ".weight";
|
||||
expected_to_model["conv_body.bias"] = "model.1.sub." + std::to_string(detected_num_block) + ".bias";
|
||||
if (detected_scale >= 2) {
|
||||
expected_to_model["conv_up1.weight"] = "model.3.weight";
|
||||
expected_to_model["conv_up1.bias"] = "model.3.bias";
|
||||
}
|
||||
if (detected_scale == 4) {
|
||||
expected_to_model["conv_up2.weight"] = "model.6.weight";
|
||||
expected_to_model["conv_up2.bias"] = "model.6.bias";
|
||||
expected_to_model["conv_hr.weight"] = "model.8.weight";
|
||||
expected_to_model["conv_hr.bias"] = "model.8.bias";
|
||||
expected_to_model["conv_last.weight"] = "model.10.weight";
|
||||
expected_to_model["conv_last.bias"] = "model.10.bias";
|
||||
} else if (detected_scale == 2) {
|
||||
expected_to_model["conv_hr.weight"] = "model.5.weight";
|
||||
expected_to_model["conv_hr.bias"] = "model.5.bias";
|
||||
expected_to_model["conv_last.weight"] = "model.7.weight";
|
||||
expected_to_model["conv_last.bias"] = "model.7.bias";
|
||||
}
|
||||
}
|
||||
|
||||
std::map<std::string, ggml_tensor*> model_tensors;
|
||||
for (auto& p : esrgan_tensors) {
|
||||
auto it = expected_to_model.find(p.first);
|
||||
if (it != expected_to_model.end()) {
|
||||
model_tensors[it->second] = p.second;
|
||||
}
|
||||
}
|
||||
|
||||
success = model_loader.load_tensors(model_tensors, {}, n_threads);
|
||||
} else {
|
||||
success = model_loader.load_tensors(esrgan_tensors, {}, n_threads);
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
LOG_ERROR("load esrgan tensors from model loader failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
scale = rrdb_net->get_scale();
|
||||
LOG_INFO("esrgan model loaded with scale=%d, num_block=%d", scale, detected_num_block);
|
||||
return success;
|
||||
}
|
||||
|
||||
struct ggml_cgraph* build_graph(struct ggml_tensor* x) {
|
||||
if (!rrdb_net)
|
||||
return nullptr;
|
||||
constexpr int kGraphNodes = 1 << 16; // 65k
|
||||
struct ggml_cgraph* gf = new_graph_custom(kGraphNodes);
|
||||
x = to_backend(x);
|
||||
|
||||
auto runner_ctx = get_context();
|
||||
struct ggml_tensor* out = rrdb_net->forward(&runner_ctx, x);
|
||||
ggml_build_forward_expand(gf, out);
|
||||
return gf;
|
||||
}
|
||||
|
||||
bool compute(const int n_threads,
|
||||
struct ggml_tensor* x,
|
||||
ggml_tensor** output,
|
||||
ggml_context* output_ctx = nullptr) {
|
||||
auto get_graph = [&]() -> struct ggml_cgraph* {
|
||||
return build_graph(x);
|
||||
};
|
||||
return GGMLRunner::compute(get_graph, n_threads, false, output, output_ctx);
|
||||
}
|
||||
};
|
||||
|
||||
#endif // __ESRGAN_HPP__
|
||||
@@ -1,6 +1,27 @@
|
||||
set(TARGET sd-cli)
|
||||
|
||||
add_executable(${TARGET} main.cpp)
|
||||
add_executable(${TARGET}
|
||||
../common/common.cpp
|
||||
../common/log.cpp
|
||||
../common/media_io.cpp
|
||||
image_metadata.cpp
|
||||
main.cpp
|
||||
)
|
||||
if(APPLE)
|
||||
sd_set_macos_rpaths(${TARGET})
|
||||
endif()
|
||||
target_include_directories(${TARGET} PRIVATE
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/.."
|
||||
"${PROJECT_SOURCE_DIR}/src"
|
||||
)
|
||||
install(TARGETS ${TARGET} RUNTIME)
|
||||
target_link_libraries(${TARGET} PRIVATE stable-diffusion ${CMAKE_THREAD_LIBS_INIT})
|
||||
target_compile_features(${TARGET} PUBLIC c_std_11 cxx_std_17)
|
||||
target_link_libraries(${TARGET} PRIVATE stable-diffusion zip ${CMAKE_THREAD_LIBS_INIT})
|
||||
if(SD_WEBP)
|
||||
target_compile_definitions(${TARGET} PRIVATE SD_USE_WEBP)
|
||||
target_link_libraries(${TARGET} PRIVATE webp libwebpmux)
|
||||
endif()
|
||||
if(SD_WEBM)
|
||||
target_compile_definitions(${TARGET} PRIVATE SD_USE_WEBM)
|
||||
target_link_libraries(${TARGET} PRIVATE webm)
|
||||
endif()
|
||||
target_compile_features(${TARGET} PUBLIC c_std_11 cxx_std_17)
|
||||
|
||||
@@ -1,143 +1,16 @@
|
||||
# Run
|
||||
# Usage
|
||||
|
||||
For detailed command-line arguments, run:
|
||||
|
||||
```bash
|
||||
./bin/sd-cli -h
|
||||
```
|
||||
usage: ./bin/sd-cli [options]
|
||||
|
||||
CLI Options:
|
||||
-o, --output <string> path to write result image to. you can use printf-style %d format specifiers for image sequences (default: ./output.png) (eg. output_%03d.png)
|
||||
--output-begin-idx <int> starting index for output image sequence, must be non-negative (default 0 if specified %d in output path, 1 otherwise)
|
||||
--preview-path <string> path to write preview image to (default: ./preview.png)
|
||||
--preview-interval <int> interval in denoising steps between consecutive updates of the image preview file (default is 1, meaning updating at
|
||||
every step)
|
||||
--canny apply canny preprocessor (edge detection)
|
||||
--convert-name convert tensor name (for convert mode)
|
||||
-v, --verbose print extra info
|
||||
--color colors the logging tags according to level
|
||||
--taesd-preview-only prevents usage of taesd for decoding the final image. (for use with --preview tae)
|
||||
--preview-noisy enables previewing noisy inputs of the models rather than the denoised outputs
|
||||
-M, --mode run mode, one of [img_gen, vid_gen, upscale, convert], default: img_gen
|
||||
--preview preview method. must be one of the following [none, proj, tae, vae] (default is none)
|
||||
-h, --help show this help message and exit
|
||||
Metadata mode inspects PNG/JPEG container metadata without loading any model:
|
||||
|
||||
Context Options:
|
||||
-m, --model <string> path to full model
|
||||
--clip_l <string> path to the clip-l text encoder
|
||||
--clip_g <string> path to the clip-g text encoder
|
||||
--clip_vision <string> path to the clip-vision encoder
|
||||
--t5xxl <string> path to the t5xxl text encoder
|
||||
--llm <string> path to the llm text encoder. For example: (qwenvl2.5 for qwen-image, mistral-small3.2 for flux2, ...)
|
||||
--llm_vision <string> path to the llm vit
|
||||
--qwen2vl <string> alias of --llm. Deprecated.
|
||||
--qwen2vl_vision <string> alias of --llm_vision. Deprecated.
|
||||
--diffusion-model <string> path to the standalone diffusion model
|
||||
--high-noise-diffusion-model <string> path to the standalone high noise diffusion model
|
||||
--vae <string> path to standalone vae model
|
||||
--taesd <string> path to taesd. Using Tiny AutoEncoder for fast decoding (low quality)
|
||||
--tae <string> alias of --taesd
|
||||
--control-net <string> path to control net model
|
||||
--embd-dir <string> embeddings directory
|
||||
--lora-model-dir <string> lora model directory
|
||||
--tensor-type-rules <string> weight type per tensor pattern (example: "^vae\.=f16,model\.=q8_0")
|
||||
--photo-maker <string> path to PHOTOMAKER model
|
||||
--upscale-model <string> path to esrgan model.
|
||||
-t, --threads <int> number of threads to use during computation (default: -1). If threads <= 0, then threads will be set to the number of
|
||||
CPU physical cores
|
||||
--chroma-t5-mask-pad <int> t5 mask pad size of chroma
|
||||
--vae-tile-overlap <float> tile overlap for vae tiling, in fraction of tile size (default: 0.5)
|
||||
--flow-shift <float> shift value for Flow models like SD3.x or WAN (default: auto)
|
||||
--vae-tiling process vae in tiles to reduce memory usage
|
||||
--force-sdxl-vae-conv-scale force use of conv scale on sdxl vae
|
||||
--offload-to-cpu place the weights in RAM to save VRAM, and automatically load them into VRAM when needed
|
||||
--mmap whether to memory-map model
|
||||
--control-net-cpu keep controlnet in cpu (for low vram)
|
||||
--clip-on-cpu keep clip in cpu (for low vram)
|
||||
--vae-on-cpu keep vae in cpu (for low vram)
|
||||
--diffusion-fa use flash attention in the diffusion model
|
||||
--diffusion-conv-direct use ggml_conv2d_direct in the diffusion model
|
||||
--vae-conv-direct use ggml_conv2d_direct in the vae model
|
||||
--circular enable circular padding for convolutions
|
||||
--circularx enable circular RoPE wrapping on x-axis (width) only
|
||||
--circulary enable circular RoPE wrapping on y-axis (height) only
|
||||
--chroma-disable-dit-mask disable dit mask for chroma
|
||||
--chroma-enable-t5-mask enable t5 mask for chroma
|
||||
--type weight type (examples: f32, f16, q4_0, q4_1, q5_0, q5_1, q8_0, q2_K, q3_K, q4_K). If not specified, the default is the
|
||||
type of the weight file
|
||||
--rng RNG, one of [std_default, cuda, cpu], default: cuda(sd-webui), cpu(comfyui)
|
||||
--sampler-rng sampler RNG, one of [std_default, cuda, cpu]. If not specified, use --rng
|
||||
--prediction prediction type override, one of [eps, v, edm_v, sd3_flow, flux_flow, flux2_flow]
|
||||
--lora-apply-mode the way to apply LoRA, one of [auto, immediately, at_runtime], default is auto. In auto mode, if the model weights
|
||||
contain any quantized parameters, the at_runtime mode will be used; otherwise,
|
||||
immediately will be used.The immediately mode may have precision and
|
||||
compatibility issues with quantized parameters, but it usually offers faster inference
|
||||
speed and, in some cases, lower memory usage. The at_runtime mode, on the
|
||||
other hand, is exactly the opposite.
|
||||
--vae-tile-size tile size for vae tiling, format [X]x[Y] (default: 32x32)
|
||||
--vae-relative-tile-size relative tile size for vae tiling, format [X]x[Y], in fraction of image size if < 1, in number of tiles per dim if >=1
|
||||
(overrides --vae-tile-size)
|
||||
|
||||
Generation Options:
|
||||
-p, --prompt <string> the prompt to render
|
||||
-n, --negative-prompt <string> the negative prompt (default: "")
|
||||
-i, --init-img <string> path to the init image
|
||||
--end-img <string> path to the end image, required by flf2v
|
||||
--mask <string> path to the mask image
|
||||
--control-image <string> path to control image, control net
|
||||
--control-video <string> path to control video frames, It must be a directory path. The video frames inside should be stored as images in
|
||||
lexicographical (character) order. For example, if the control video path is
|
||||
`frames`, the directory contain images such as 00.png, 01.png, ... etc.
|
||||
--pm-id-images-dir <string> path to PHOTOMAKER input id images dir
|
||||
--pm-id-embed-path <string> path to PHOTOMAKER v2 id embed
|
||||
-H, --height <int> image height, in pixel space (default: 512)
|
||||
-W, --width <int> image width, in pixel space (default: 512)
|
||||
--steps <int> number of sample steps (default: 20)
|
||||
--high-noise-steps <int> (high noise) number of sample steps (default: -1 = auto)
|
||||
--clip-skip <int> ignore last layers of CLIP network; 1 ignores none, 2 ignores one layer (default: -1). <= 0 represents unspecified,
|
||||
will be 1 for SD1.x, 2 for SD2.x
|
||||
-b, --batch-count <int> batch count
|
||||
--video-frames <int> video frames (default: 1)
|
||||
--fps <int> fps (default: 24)
|
||||
--timestep-shift <int> shift timestep for NitroFusion models (default: 0). recommended N for NitroSD-Realism around 250 and 500 for
|
||||
NitroSD-Vibrant
|
||||
--upscale-repeats <int> Run the ESRGAN upscaler this many times (default: 1)
|
||||
--upscale-tile-size <int> tile size for ESRGAN upscaling (default: 128)
|
||||
--cfg-scale <float> unconditional guidance scale: (default: 7.0)
|
||||
--img-cfg-scale <float> image guidance scale for inpaint or instruct-pix2pix models: (default: same as --cfg-scale)
|
||||
--guidance <float> distilled guidance scale for models with guidance input (default: 3.5)
|
||||
--slg-scale <float> skip layer guidance (SLG) scale, only for DiT models: (default: 0). 0 means disabled, a value of 2.5 is nice for sd3.5
|
||||
medium
|
||||
--skip-layer-start <float> SLG enabling point (default: 0.01)
|
||||
--skip-layer-end <float> SLG disabling point (default: 0.2)
|
||||
--eta <float> eta in DDIM, only for DDIM and TCD (default: 0)
|
||||
--high-noise-cfg-scale <float> (high noise) unconditional guidance scale: (default: 7.0)
|
||||
--high-noise-img-cfg-scale <float> (high noise) image guidance scale for inpaint or instruct-pix2pix models (default: same as --cfg-scale)
|
||||
--high-noise-guidance <float> (high noise) distilled guidance scale for models with guidance input (default: 3.5)
|
||||
--high-noise-slg-scale <float> (high noise) skip layer guidance (SLG) scale, only for DiT models: (default: 0)
|
||||
--high-noise-skip-layer-start <float> (high noise) SLG enabling point (default: 0.01)
|
||||
--high-noise-skip-layer-end <float> (high noise) SLG disabling point (default: 0.2)
|
||||
--high-noise-eta <float> (high noise) eta in DDIM, only for DDIM and TCD (default: 0)
|
||||
--strength <float> strength for noising/unnoising (default: 0.75)
|
||||
--pm-style-strength <float>
|
||||
--control-strength <float> strength to apply Control Net (default: 0.9). 1.0 corresponds to full destruction of information in init image
|
||||
--moe-boundary <float> timestep boundary for Wan2.2 MoE model. (default: 0.875). Only enabled if `--high-noise-steps` is set to -1
|
||||
--vace-strength <float> wan vace strength
|
||||
--increase-ref-index automatically increase the indices of references images based on the order they are listed (starting with 1).
|
||||
--disable-auto-resize-ref-image disable auto resize of ref images
|
||||
-s, --seed RNG seed (default: 42, use random seed for < 0)
|
||||
--sampling-method sampling method, one of [euler, euler_a, heun, dpm2, dpm++2s_a, dpm++2m, dpm++2mv2, ipndm, ipndm_v, lcm, ddim_trailing,
|
||||
tcd] (default: euler for Flux/SD3/Wan, euler_a otherwise)
|
||||
--high-noise-sampling-method (high noise) sampling method, one of [euler, euler_a, heun, dpm2, dpm++2s_a, dpm++2m, dpm++2mv2, ipndm, ipndm_v, lcm,
|
||||
ddim_trailing, tcd] default: euler for Flux/SD3/Wan, euler_a otherwise
|
||||
--scheduler denoiser sigma scheduler, one of [discrete, karras, exponential, ays, gits, smoothstep, sgm_uniform, simple,
|
||||
kl_optimal, lcm], default: discrete
|
||||
--sigmas custom sigma values for the sampler, comma-separated (e.g., "14.61,7.8,3.5,0.0").
|
||||
--skip-layers layers to skip for SLG steps (default: [7,8,9])
|
||||
--high-noise-skip-layers (high noise) layers to skip for SLG steps (default: [7,8,9])
|
||||
-r, --ref-image reference image for Flux Kontext models (can be used multiple times)
|
||||
--cache-mode caching method: 'easycache' (DiT), 'ucache' (UNET), 'dbcache'/'taylorseer'/'cache-dit' (DiT block-level)
|
||||
--cache-option named cache params (key=value format, comma-separated). easycache/ucache:
|
||||
threshold=,start=,end=,decay=,relative=,reset=; dbcache/taylorseer/cache-dit: Fn=,Bn=,threshold=,warmup=. Examples:
|
||||
"threshold=0.25" or "threshold=1.5,reset=0"
|
||||
--cache-preset cache-dit preset: 'slow'/'s', 'medium'/'m', 'fast'/'f', 'ultra'/'u'
|
||||
--scm-mask SCM steps mask for cache-dit: comma-separated 0/1 (e.g., "1,1,1,0,0,1,0,0,1,0") - 1=compute, 0=can cache
|
||||
--scm-policy SCM policy: 'dynamic' (default) or 'static'
|
||||
```bash
|
||||
./bin/sd-cli -M metadata --image ./output.png
|
||||
./bin/sd-cli -M metadata --image ./output.jpg --metadata-format json
|
||||
./bin/sd-cli -M metadata --image ./output.png --metadata-raw
|
||||
./bin/sd-cli -M metadata --image ./output.png --metadata-all
|
||||
```
|
||||
|
||||
@@ -1,217 +0,0 @@
|
||||
#ifndef __AVI_WRITER_H__
|
||||
#define __AVI_WRITER_H__
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
#include "stable-diffusion.h"
|
||||
|
||||
#ifndef INCLUDE_STB_IMAGE_WRITE_H
|
||||
#include "stb_image_write.h"
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
uint32_t offset;
|
||||
uint32_t size;
|
||||
} avi_index_entry;
|
||||
|
||||
// Write 32-bit little-endian integer
|
||||
void write_u32_le(FILE* f, uint32_t val) {
|
||||
fwrite(&val, 4, 1, f);
|
||||
}
|
||||
|
||||
// Write 16-bit little-endian integer
|
||||
void write_u16_le(FILE* f, uint16_t val) {
|
||||
fwrite(&val, 2, 1, f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an MJPG AVI file from an array of sd_image_t images.
|
||||
* Images are encoded to JPEG using stb_image_write.
|
||||
*
|
||||
* @param filename Output AVI file name.
|
||||
* @param images Array of input images.
|
||||
* @param num_images Number of images in the array.
|
||||
* @param fps Frames per second for the video.
|
||||
* @param quality JPEG quality (0-100).
|
||||
* @return 0 on success, -1 on failure.
|
||||
*/
|
||||
int create_mjpg_avi_from_sd_images(const char* filename, sd_image_t* images, int num_images, int fps, int quality = 90) {
|
||||
if (num_images == 0) {
|
||||
fprintf(stderr, "Error: Image array is empty.\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
FILE* f = fopen(filename, "wb");
|
||||
if (!f) {
|
||||
perror("Error opening file for writing");
|
||||
return -1;
|
||||
}
|
||||
|
||||
uint32_t width = images[0].width;
|
||||
uint32_t height = images[0].height;
|
||||
uint32_t channels = images[0].channel;
|
||||
if (channels != 3 && channels != 4) {
|
||||
fprintf(stderr, "Error: Unsupported channel count: %u\n", channels);
|
||||
fclose(f);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// --- RIFF AVI Header ---
|
||||
fwrite("RIFF", 4, 1, f);
|
||||
long riff_size_pos = ftell(f);
|
||||
write_u32_le(f, 0); // Placeholder for file size
|
||||
fwrite("AVI ", 4, 1, f);
|
||||
|
||||
// 'hdrl' LIST (header list)
|
||||
fwrite("LIST", 4, 1, f);
|
||||
write_u32_le(f, 4 + 8 + 56 + 8 + 4 + 8 + 56 + 8 + 40);
|
||||
fwrite("hdrl", 4, 1, f);
|
||||
|
||||
// 'avih' chunk (AVI main header)
|
||||
fwrite("avih", 4, 1, f);
|
||||
write_u32_le(f, 56);
|
||||
write_u32_le(f, 1000000 / fps); // Microseconds per frame
|
||||
write_u32_le(f, 0); // Max bytes per second
|
||||
write_u32_le(f, 0); // Padding granularity
|
||||
write_u32_le(f, 0x110); // Flags (HASINDEX | ISINTERLEAVED)
|
||||
write_u32_le(f, num_images); // Total frames
|
||||
write_u32_le(f, 0); // Initial frames
|
||||
write_u32_le(f, 1); // Number of streams
|
||||
write_u32_le(f, width * height * 3); // Suggested buffer size
|
||||
write_u32_le(f, width);
|
||||
write_u32_le(f, height);
|
||||
write_u32_le(f, 0); // Reserved
|
||||
write_u32_le(f, 0); // Reserved
|
||||
write_u32_le(f, 0); // Reserved
|
||||
write_u32_le(f, 0); // Reserved
|
||||
|
||||
// 'strl' LIST (stream list)
|
||||
fwrite("LIST", 4, 1, f);
|
||||
write_u32_le(f, 4 + 8 + 56 + 8 + 40);
|
||||
fwrite("strl", 4, 1, f);
|
||||
|
||||
// 'strh' chunk (stream header)
|
||||
fwrite("strh", 4, 1, f);
|
||||
write_u32_le(f, 56);
|
||||
fwrite("vids", 4, 1, f); // Stream type: video
|
||||
fwrite("MJPG", 4, 1, f); // Codec: Motion JPEG
|
||||
write_u32_le(f, 0); // Flags
|
||||
write_u16_le(f, 0); // Priority
|
||||
write_u16_le(f, 0); // Language
|
||||
write_u32_le(f, 0); // Initial frames
|
||||
write_u32_le(f, 1); // Scale
|
||||
write_u32_le(f, fps); // Rate
|
||||
write_u32_le(f, 0); // Start
|
||||
write_u32_le(f, num_images); // Length
|
||||
write_u32_le(f, width * height * 3); // Suggested buffer size
|
||||
write_u32_le(f, (uint32_t)-1); // Quality
|
||||
write_u32_le(f, 0); // Sample size
|
||||
write_u16_le(f, 0); // rcFrame.left
|
||||
write_u16_le(f, 0); // rcFrame.top
|
||||
write_u16_le(f, 0); // rcFrame.right
|
||||
write_u16_le(f, 0); // rcFrame.bottom
|
||||
|
||||
// 'strf' chunk (stream format: BITMAPINFOHEADER)
|
||||
fwrite("strf", 4, 1, f);
|
||||
write_u32_le(f, 40);
|
||||
write_u32_le(f, 40); // biSize
|
||||
write_u32_le(f, width);
|
||||
write_u32_le(f, height);
|
||||
write_u16_le(f, 1); // biPlanes
|
||||
write_u16_le(f, 24); // biBitCount
|
||||
fwrite("MJPG", 4, 1, f); // biCompression (FOURCC)
|
||||
write_u32_le(f, width * height * 3); // biSizeImage
|
||||
write_u32_le(f, 0); // XPelsPerMeter
|
||||
write_u32_le(f, 0); // YPelsPerMeter
|
||||
write_u32_le(f, 0); // Colors used
|
||||
write_u32_le(f, 0); // Colors important
|
||||
|
||||
// 'movi' LIST (video frames)
|
||||
// long movi_list_pos = ftell(f);
|
||||
fwrite("LIST", 4, 1, f);
|
||||
long movi_size_pos = ftell(f);
|
||||
write_u32_le(f, 0); // Placeholder for movi size
|
||||
fwrite("movi", 4, 1, f);
|
||||
|
||||
avi_index_entry* index = (avi_index_entry*)malloc(sizeof(avi_index_entry) * num_images);
|
||||
if (!index) {
|
||||
fclose(f);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Encode and write each frame as JPEG
|
||||
struct {
|
||||
uint8_t* buf;
|
||||
size_t size;
|
||||
} jpeg_data;
|
||||
|
||||
for (int i = 0; i < num_images; i++) {
|
||||
jpeg_data.buf = nullptr;
|
||||
jpeg_data.size = 0;
|
||||
|
||||
// Callback function to collect JPEG data into memory
|
||||
auto write_to_buf = [](void* context, void* data, int size) {
|
||||
auto jd = (decltype(jpeg_data)*)context;
|
||||
jd->buf = (uint8_t*)realloc(jd->buf, jd->size + size);
|
||||
memcpy(jd->buf + jd->size, data, size);
|
||||
jd->size += size;
|
||||
};
|
||||
|
||||
// Encode to JPEG in memory
|
||||
stbi_write_jpg_to_func(
|
||||
write_to_buf,
|
||||
&jpeg_data,
|
||||
images[i].width,
|
||||
images[i].height,
|
||||
channels,
|
||||
images[i].data,
|
||||
quality);
|
||||
|
||||
// Write '00dc' chunk (video frame)
|
||||
fwrite("00dc", 4, 1, f);
|
||||
write_u32_le(f, (uint32_t)jpeg_data.size);
|
||||
index[i].offset = ftell(f) - 8;
|
||||
index[i].size = (uint32_t)jpeg_data.size;
|
||||
fwrite(jpeg_data.buf, 1, jpeg_data.size, f);
|
||||
|
||||
// Align to even byte size
|
||||
if (jpeg_data.size % 2)
|
||||
fputc(0, f);
|
||||
|
||||
free(jpeg_data.buf);
|
||||
}
|
||||
|
||||
// Finalize 'movi' size
|
||||
long cur_pos = ftell(f);
|
||||
long movi_size = cur_pos - movi_size_pos - 4;
|
||||
fseek(f, movi_size_pos, SEEK_SET);
|
||||
write_u32_le(f, movi_size);
|
||||
fseek(f, cur_pos, SEEK_SET);
|
||||
|
||||
// Write 'idx1' index
|
||||
fwrite("idx1", 4, 1, f);
|
||||
write_u32_le(f, num_images * 16);
|
||||
for (int i = 0; i < num_images; i++) {
|
||||
fwrite("00dc", 4, 1, f);
|
||||
write_u32_le(f, 0x10);
|
||||
write_u32_le(f, index[i].offset);
|
||||
write_u32_le(f, index[i].size);
|
||||
}
|
||||
|
||||
// Finalize RIFF size
|
||||
cur_pos = ftell(f);
|
||||
long file_size = cur_pos - riff_size_pos - 4;
|
||||
fseek(f, riff_size_pos, SEEK_SET);
|
||||
write_u32_le(f, file_size);
|
||||
fseek(f, cur_pos, SEEK_SET);
|
||||
|
||||
fclose(f);
|
||||
free(index);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
#endif // __AVI_WRITER_H__
|
||||
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include <iosfwd>
|
||||
#include <string>
|
||||
|
||||
enum class MetadataOutputFormat {
|
||||
TEXT,
|
||||
JSON,
|
||||
};
|
||||
|
||||
struct MetadataReadOptions {
|
||||
MetadataOutputFormat output_format = MetadataOutputFormat::TEXT;
|
||||
bool include_raw = false;
|
||||
bool brief = false;
|
||||
bool include_structural = false;
|
||||
};
|
||||
|
||||
bool print_image_metadata(const std::string& image_path,
|
||||
const MetadataReadOptions& options,
|
||||
std::ostream& out,
|
||||
std::string& error);
|
||||
@@ -1,6 +1,7 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
@@ -15,9 +16,12 @@
|
||||
// #include "preprocessing.hpp"
|
||||
#include "stable-diffusion.h"
|
||||
|
||||
#include "common/common.hpp"
|
||||
#include "common/common.h"
|
||||
#include "common/media_io.h"
|
||||
#include "common/resource_owners.hpp"
|
||||
#include "image_metadata.h"
|
||||
|
||||
#include "avi_writer.h"
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
const char* previews_str[] = {
|
||||
"none",
|
||||
@@ -32,6 +36,8 @@ struct SDCliParams {
|
||||
SDMode mode = IMG_GEN;
|
||||
std::string output_path = "output.png";
|
||||
int output_begin_idx = -1;
|
||||
std::string image_path;
|
||||
std::string metadata_format = "text";
|
||||
|
||||
bool verbose = false;
|
||||
bool canny_preprocess = false;
|
||||
@@ -44,6 +50,12 @@ struct SDCliParams {
|
||||
bool taesd_preview = false;
|
||||
bool preview_noisy = false;
|
||||
bool color = false;
|
||||
bool metadata_raw = false;
|
||||
bool metadata_brief = false;
|
||||
bool metadata_all = false;
|
||||
|
||||
std::string imatrix_out;
|
||||
std::vector<std::string> imatrix_in;
|
||||
|
||||
bool normal_exit = false;
|
||||
|
||||
@@ -53,12 +65,29 @@ struct SDCliParams {
|
||||
options.string_options = {
|
||||
{"-o",
|
||||
"--output",
|
||||
"path to write result image to. you can use printf-style %d format specifiers for image sequences (default: ./output.png) (eg. output_%03d.png)",
|
||||
"path to write result image to. you can use printf-style %d format specifiers for image sequences (default: ./output.png) (eg. output_%03d.png). Single-file video outputs support .avi, .webm, and animated .webp",
|
||||
0,
|
||||
&output_path},
|
||||
{"",
|
||||
"--image",
|
||||
"path to the image to inspect (for metadata mode)",
|
||||
0,
|
||||
&image_path},
|
||||
{"",
|
||||
"--metadata-format",
|
||||
"metadata output format, one of [text, json] (default: text)",
|
||||
0,
|
||||
&metadata_format},
|
||||
{"",
|
||||
"--preview-path",
|
||||
"path to write preview image to (default: ./preview.png)",
|
||||
"path to write preview image to (default: ./preview.png). Multi-frame previews support .avi, .webm, and animated .webp",
|
||||
0,
|
||||
&preview_path},
|
||||
{"",
|
||||
"--imat-out",
|
||||
"compute the imatrix for this run and save it to the provided path",
|
||||
0,
|
||||
&imatrix_out},
|
||||
};
|
||||
|
||||
options.int_options = {
|
||||
@@ -97,6 +126,18 @@ struct SDCliParams {
|
||||
"--preview-noisy",
|
||||
"enables previewing noisy inputs of the models rather than the denoised outputs",
|
||||
true, &preview_noisy},
|
||||
{"",
|
||||
"--metadata-raw",
|
||||
"include raw hex previews for unparsed metadata payloads",
|
||||
true, &metadata_raw},
|
||||
{"",
|
||||
"--metadata-brief",
|
||||
"truncate long metadata text values in text output",
|
||||
true, &metadata_brief},
|
||||
{"",
|
||||
"--metadata-all",
|
||||
"include structural/container entries such as IHDR, IDAT, and non-metadata JPEG segments",
|
||||
true, &metadata_all},
|
||||
|
||||
};
|
||||
|
||||
@@ -141,15 +182,24 @@ struct SDCliParams {
|
||||
return 1;
|
||||
};
|
||||
|
||||
auto on_help_arg = [&](int argc, const char** argv, int index) {
|
||||
auto on_help_arg = [&](int argc, const char** argv, int index, bool& valid) {
|
||||
normal_exit = true;
|
||||
valid = true;
|
||||
return -1;
|
||||
};
|
||||
|
||||
auto on_imatrix_in_arg = [&](int argc, const char** argv, int index) {
|
||||
if (++index >= argc) {
|
||||
return -1;
|
||||
}
|
||||
imatrix_in.push_back(argv[index]);
|
||||
return 1;
|
||||
};
|
||||
|
||||
options.manual_options = {
|
||||
{"-M",
|
||||
"--mode",
|
||||
"run mode, one of [img_gen, vid_gen, upscale, convert], default: img_gen",
|
||||
"run mode, one of [img_gen, vid_gen, upscale, convert, metadata], default: img_gen",
|
||||
on_mode_arg},
|
||||
{"",
|
||||
"--preview",
|
||||
@@ -159,17 +209,16 @@ struct SDCliParams {
|
||||
"--help",
|
||||
"show this help message and exit",
|
||||
on_help_arg},
|
||||
{"",
|
||||
"--imat-in",
|
||||
"load an imatrix file for quantization or continued collection; can be specified multiple times",
|
||||
on_imatrix_in_arg},
|
||||
};
|
||||
|
||||
return options;
|
||||
};
|
||||
|
||||
bool process_and_check() {
|
||||
if (output_path.length() == 0) {
|
||||
LOG_ERROR("error: the following arguments are required: output_path");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool resolve() {
|
||||
if (mode == CONVERT) {
|
||||
if (output_path == "output.png") {
|
||||
output_path = "output.gguf";
|
||||
@@ -178,11 +227,43 @@ struct SDCliParams {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool validate() {
|
||||
if (mode != METADATA) {
|
||||
if (output_path.length() == 0) {
|
||||
LOG_ERROR("error: the following arguments are required: output_path");
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (image_path.empty()) {
|
||||
LOG_ERROR("error: metadata mode needs an image path (--image)");
|
||||
return false;
|
||||
}
|
||||
if (metadata_format != "text" && metadata_format != "json") {
|
||||
LOG_ERROR("error: invalid metadata format %s, must be one of [text, json]",
|
||||
metadata_format.c_str());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool resolve_and_validate() {
|
||||
if (!resolve()) {
|
||||
return false;
|
||||
}
|
||||
if (!validate()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string to_string() const {
|
||||
std::ostringstream oss;
|
||||
oss << "SDCliParams {\n"
|
||||
<< " mode: " << modes_str[mode] << ",\n"
|
||||
<< " output_path: \"" << output_path << "\",\n"
|
||||
<< " image_path: \"" << image_path << "\",\n"
|
||||
<< " metadata_format: \"" << metadata_format << "\",\n"
|
||||
<< " verbose: " << (verbose ? "true" : "false") << ",\n"
|
||||
<< " color: " << (color ? "true" : "false") << ",\n"
|
||||
<< " canny_preprocess: " << (canny_preprocess ? "true" : "false") << ",\n"
|
||||
@@ -192,7 +273,11 @@ struct SDCliParams {
|
||||
<< " preview_path: \"" << preview_path << "\",\n"
|
||||
<< " preview_fps: " << preview_fps << ",\n"
|
||||
<< " taesd_preview: " << (taesd_preview ? "true" : "false") << ",\n"
|
||||
<< " preview_noisy: " << (preview_noisy ? "true" : "false") << "\n"
|
||||
<< " preview_noisy: " << (preview_noisy ? "true" : "false") << ",\n"
|
||||
<< " imatrix_out: \"" << imatrix_out << "\",\n"
|
||||
<< " metadata_raw: " << (metadata_raw ? "true" : "false") << ",\n"
|
||||
<< " metadata_brief: " << (metadata_brief ? "true" : "false") << ",\n"
|
||||
<< " metadata_all: " << (metadata_all ? "true" : "false") << "\n"
|
||||
<< "}";
|
||||
return oss.str();
|
||||
}
|
||||
@@ -217,78 +302,27 @@ void parse_args(int argc, const char** argv, SDCliParams& cli_params, SDContextP
|
||||
exit(cli_params.normal_exit ? 0 : 1);
|
||||
}
|
||||
|
||||
if (!cli_params.process_and_check() ||
|
||||
!ctx_params.process_and_check(cli_params.mode) ||
|
||||
!gen_params.process_and_check(cli_params.mode, ctx_params.lora_model_dir)) {
|
||||
bool valid = cli_params.resolve_and_validate();
|
||||
if (valid && cli_params.mode != METADATA) {
|
||||
valid = ctx_params.resolve_and_validate(cli_params.mode) &&
|
||||
gen_params.resolve_and_validate(cli_params.mode,
|
||||
ctx_params.lora_model_dir,
|
||||
ctx_params.hires_upscalers_dir);
|
||||
}
|
||||
|
||||
if (!valid) {
|
||||
print_usage(argc, argv, options_vec);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
std::string get_image_params(const SDCliParams& cli_params, const SDContextParams& ctx_params, const SDGenerationParams& gen_params, int64_t seed) {
|
||||
std::string parameter_string = gen_params.prompt_with_lora + "\n";
|
||||
if (gen_params.negative_prompt.size() != 0) {
|
||||
parameter_string += "Negative prompt: " + gen_params.negative_prompt + "\n";
|
||||
}
|
||||
parameter_string += "Steps: " + std::to_string(gen_params.sample_params.sample_steps) + ", ";
|
||||
parameter_string += "CFG scale: " + std::to_string(gen_params.sample_params.guidance.txt_cfg) + ", ";
|
||||
if (gen_params.sample_params.guidance.slg.scale != 0 && gen_params.skip_layers.size() != 0) {
|
||||
parameter_string += "SLG scale: " + std::to_string(gen_params.sample_params.guidance.txt_cfg) + ", ";
|
||||
parameter_string += "Skip layers: [";
|
||||
for (const auto& layer : gen_params.skip_layers) {
|
||||
parameter_string += std::to_string(layer) + ", ";
|
||||
}
|
||||
parameter_string += "], ";
|
||||
parameter_string += "Skip layer start: " + std::to_string(gen_params.sample_params.guidance.slg.layer_start) + ", ";
|
||||
parameter_string += "Skip layer end: " + std::to_string(gen_params.sample_params.guidance.slg.layer_end) + ", ";
|
||||
}
|
||||
parameter_string += "Guidance: " + std::to_string(gen_params.sample_params.guidance.distilled_guidance) + ", ";
|
||||
parameter_string += "Eta: " + std::to_string(gen_params.sample_params.eta) + ", ";
|
||||
parameter_string += "Seed: " + std::to_string(seed) + ", ";
|
||||
parameter_string += "Size: " + std::to_string(gen_params.width) + "x" + std::to_string(gen_params.height) + ", ";
|
||||
parameter_string += "Model: " + sd_basename(ctx_params.model_path) + ", ";
|
||||
parameter_string += "RNG: " + std::string(sd_rng_type_name(ctx_params.rng_type)) + ", ";
|
||||
if (ctx_params.sampler_rng_type != RNG_TYPE_COUNT) {
|
||||
parameter_string += "Sampler RNG: " + std::string(sd_rng_type_name(ctx_params.sampler_rng_type)) + ", ";
|
||||
}
|
||||
parameter_string += "Sampler: " + std::string(sd_sample_method_name(gen_params.sample_params.sample_method));
|
||||
if (!gen_params.custom_sigmas.empty()) {
|
||||
parameter_string += ", Custom Sigmas: [";
|
||||
for (size_t i = 0; i < gen_params.custom_sigmas.size(); ++i) {
|
||||
std::ostringstream oss;
|
||||
oss << std::fixed << std::setprecision(4) << gen_params.custom_sigmas[i];
|
||||
parameter_string += oss.str() + (i == gen_params.custom_sigmas.size() - 1 ? "" : ", ");
|
||||
}
|
||||
parameter_string += "]";
|
||||
} else if (gen_params.sample_params.scheduler != SCHEDULER_COUNT) { // Only show schedule if not using custom sigmas
|
||||
parameter_string += " " + std::string(sd_scheduler_name(gen_params.sample_params.scheduler));
|
||||
}
|
||||
parameter_string += ", ";
|
||||
for (const auto& te : {ctx_params.clip_l_path, ctx_params.clip_g_path, ctx_params.t5xxl_path, ctx_params.llm_path, ctx_params.llm_vision_path}) {
|
||||
if (!te.empty()) {
|
||||
parameter_string += "TE: " + sd_basename(te) + ", ";
|
||||
}
|
||||
}
|
||||
if (!ctx_params.diffusion_model_path.empty()) {
|
||||
parameter_string += "Unet: " + sd_basename(ctx_params.diffusion_model_path) + ", ";
|
||||
}
|
||||
if (!ctx_params.vae_path.empty()) {
|
||||
parameter_string += "VAE: " + sd_basename(ctx_params.vae_path) + ", ";
|
||||
}
|
||||
if (gen_params.clip_skip != -1) {
|
||||
parameter_string += "Clip skip: " + std::to_string(gen_params.clip_skip) + ", ";
|
||||
}
|
||||
parameter_string += "Version: stable-diffusion.cpp";
|
||||
return parameter_string;
|
||||
}
|
||||
|
||||
void sd_log_cb(enum sd_log_level_t level, const char* log, void* data) {
|
||||
SDCliParams* cli_params = (SDCliParams*)data;
|
||||
log_print(level, log, cli_params->verbose, cli_params->color);
|
||||
}
|
||||
|
||||
bool load_images_from_dir(const std::string dir,
|
||||
std::vector<sd_image_t>& images,
|
||||
std::vector<SDImageOwner>& images,
|
||||
int expected_width = 0,
|
||||
int expected_height = 0,
|
||||
int max_image_num = 0,
|
||||
@@ -315,7 +349,7 @@ bool load_images_from_dir(const std::string dir,
|
||||
std::string ext = entry.path().extension().string();
|
||||
std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
|
||||
|
||||
if (ext == ".jpg" || ext == ".jpeg" || ext == ".png" || ext == ".bmp") {
|
||||
if (ext == ".jpg" || ext == ".jpeg" || ext == ".png" || ext == ".bmp" || ext == ".webp") {
|
||||
LOG_DEBUG("load image %zu from '%s'", images.size(), path.c_str());
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
@@ -325,12 +359,12 @@ bool load_images_from_dir(const std::string dir,
|
||||
return false;
|
||||
}
|
||||
|
||||
images.push_back({(uint32_t)width,
|
||||
(uint32_t)height,
|
||||
3,
|
||||
image_buffer});
|
||||
images.emplace_back(sd_image_t{(uint32_t)width,
|
||||
(uint32_t)height,
|
||||
3,
|
||||
image_buffer});
|
||||
|
||||
if (max_image_num > 0 && images.size() >= max_image_num) {
|
||||
if (max_image_num > 0 && static_cast<int>(images.size()) >= max_image_num) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -345,9 +379,17 @@ void step_callback(int step, int frame_count, sd_image_t* image, bool is_noisy,
|
||||
// is_noisy is set to true if the preview corresponds to noisy latents, false if it's denoised latents
|
||||
// unused in this app, it will either be always noisy or always denoised here
|
||||
if (frame_count == 1) {
|
||||
stbi_write_png(cli_params->preview_path.c_str(), image->width, image->height, image->channel, image->data, 0);
|
||||
if (!write_image_to_file(cli_params->preview_path,
|
||||
image->data,
|
||||
image->width,
|
||||
image->height,
|
||||
image->channel)) {
|
||||
LOG_ERROR("save preview image to '%s' failed", cli_params->preview_path.c_str());
|
||||
}
|
||||
} else {
|
||||
create_mjpg_avi_from_sd_images(cli_params->preview_path.c_str(), image, frame_count, cli_params->preview_fps);
|
||||
if (create_video_from_sd_images(cli_params->preview_path.c_str(), image, frame_count, cli_params->preview_fps) != 0) {
|
||||
LOG_ERROR("save preview video to '%s' failed", cli_params->preview_path.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -370,11 +412,32 @@ std::string format_frame_idx(std::string pattern, int frame_idx) {
|
||||
return result;
|
||||
}
|
||||
|
||||
static fs::path get_video_audio_sidecar_path(const SDCliParams& cli_params) {
|
||||
fs::path out_path = cli_params.output_path;
|
||||
fs::path base_path = out_path;
|
||||
fs::path ext = out_path.has_extension() ? out_path.extension() : fs::path{};
|
||||
std::string ext_lower = ext.string();
|
||||
std::transform(ext_lower.begin(), ext_lower.end(), ext_lower.begin(), ::tolower);
|
||||
const EncodedImageFormat output_format = encoded_image_format_from_path(out_path.string());
|
||||
if (!ext.empty()) {
|
||||
if (output_format == EncodedImageFormat::JPEG ||
|
||||
output_format == EncodedImageFormat::PNG ||
|
||||
output_format == EncodedImageFormat::WEBP ||
|
||||
ext_lower == ".avi" ||
|
||||
ext_lower == ".webm") {
|
||||
base_path.replace_extension();
|
||||
}
|
||||
}
|
||||
base_path += ".wav";
|
||||
return base_path;
|
||||
}
|
||||
|
||||
bool save_results(const SDCliParams& cli_params,
|
||||
const SDContextParams& ctx_params,
|
||||
const SDGenerationParams& gen_params,
|
||||
sd_image_t* results,
|
||||
int num_results) {
|
||||
int num_results,
|
||||
const sd_audio_t* generated_audio = nullptr) {
|
||||
if (results == nullptr || num_results <= 0) {
|
||||
return false;
|
||||
}
|
||||
@@ -394,12 +457,19 @@ bool save_results(const SDCliParams& cli_params,
|
||||
|
||||
fs::path base_path = out_path;
|
||||
fs::path ext = out_path.has_extension() ? out_path.extension() : fs::path{};
|
||||
if (!ext.empty())
|
||||
base_path.replace_extension();
|
||||
|
||||
std::string ext_lower = ext.string();
|
||||
std::transform(ext_lower.begin(), ext_lower.end(), ext_lower.begin(), ::tolower);
|
||||
bool is_jpg = (ext_lower == ".jpg" || ext_lower == ".jpeg" || ext_lower == ".jpe");
|
||||
const EncodedImageFormat output_format = encoded_image_format_from_path(out_path.string());
|
||||
if (!ext.empty()) {
|
||||
if (output_format == EncodedImageFormat::JPEG ||
|
||||
output_format == EncodedImageFormat::PNG ||
|
||||
output_format == EncodedImageFormat::WEBP ||
|
||||
ext_lower == ".avi" ||
|
||||
ext_lower == ".webm") {
|
||||
base_path.replace_extension();
|
||||
}
|
||||
}
|
||||
|
||||
int output_begin_idx = cli_params.output_begin_idx;
|
||||
if (output_begin_idx < 0) {
|
||||
@@ -409,42 +479,74 @@ bool save_results(const SDCliParams& cli_params,
|
||||
auto write_image = [&](const fs::path& path, int idx) {
|
||||
const sd_image_t& img = results[idx];
|
||||
if (!img.data)
|
||||
return;
|
||||
return false;
|
||||
|
||||
std::string params = get_image_params(cli_params, ctx_params, gen_params, gen_params.seed + idx);
|
||||
int ok = 0;
|
||||
if (is_jpg) {
|
||||
ok = stbi_write_jpg(path.string().c_str(), img.width, img.height, img.channel, img.data, 90, params.c_str());
|
||||
} else {
|
||||
ok = stbi_write_png(path.string().c_str(), img.width, img.height, img.channel, img.data, 0, params.c_str());
|
||||
}
|
||||
int images_per_batch = gen_params.batch_count > 0 ? std::max(1, num_results / gen_params.batch_count) : 1;
|
||||
const int64_t metadata_seed = cli_params.mode == VID_GEN ? gen_params.seed : gen_params.seed + idx / images_per_batch;
|
||||
std::string params = gen_params.embed_image_metadata
|
||||
? get_image_params(ctx_params, gen_params, metadata_seed, cli_params.mode)
|
||||
: "";
|
||||
const bool ok = write_image_to_file(path.string(), img.data, img.width, img.height, img.channel, params, 90);
|
||||
LOG_INFO("save result image %d to '%s' (%s)", idx, path.string().c_str(), ok ? "success" : "failure");
|
||||
return ok;
|
||||
};
|
||||
|
||||
auto write_audio_sidecar = [&](const fs::path& wav_path) {
|
||||
if (generated_audio == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (write_wav_to_file(wav_path.string(),
|
||||
generated_audio->data,
|
||||
generated_audio->sample_count,
|
||||
generated_audio->channels,
|
||||
generated_audio->sample_rate)) {
|
||||
LOG_INFO("save result audio to '%s'", wav_path.string().c_str());
|
||||
} else {
|
||||
LOG_WARN("failed to save result audio to '%s'", wav_path.string().c_str());
|
||||
}
|
||||
};
|
||||
|
||||
int sucessful_reults = 0;
|
||||
|
||||
if (std::regex_search(cli_params.output_path, format_specifier_regex)) {
|
||||
if (!is_jpg && ext_lower != ".png")
|
||||
if (output_format == EncodedImageFormat::UNKNOWN)
|
||||
ext = ".png";
|
||||
fs::path pattern = base_path;
|
||||
pattern += ext;
|
||||
|
||||
for (int i = 0; i < num_results; ++i) {
|
||||
fs::path img_path = format_frame_idx(pattern.string(), output_begin_idx + i);
|
||||
write_image(img_path, i);
|
||||
if (write_image(img_path, i)) {
|
||||
sucessful_reults++;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
LOG_INFO("%d/%d images saved", sucessful_reults, num_results);
|
||||
return sucessful_reults != 0;
|
||||
}
|
||||
|
||||
if (cli_params.mode == VID_GEN && num_results > 1) {
|
||||
if (ext_lower != ".avi")
|
||||
if (ext_lower != ".avi" && ext_lower != ".webp" && ext_lower != ".webm")
|
||||
ext = ".avi";
|
||||
fs::path video_path = base_path;
|
||||
video_path += ext;
|
||||
create_mjpg_avi_from_sd_images(video_path.string().c_str(), results, num_results, gen_params.fps);
|
||||
LOG_INFO("save result MJPG AVI video to '%s'", video_path.string().c_str());
|
||||
return true;
|
||||
std::string final_ext_lower = ext.string();
|
||||
std::transform(final_ext_lower.begin(), final_ext_lower.end(), final_ext_lower.begin(), ::tolower);
|
||||
const bool mux_audio = generated_audio != nullptr && (final_ext_lower == ".avi" || final_ext_lower == ".webm");
|
||||
if (create_video_from_sd_images(video_path.string().c_str(), results, num_results, gen_params.fps, 90, mux_audio ? generated_audio : nullptr) == 0) {
|
||||
LOG_INFO("save result video to '%s'", video_path.string().c_str());
|
||||
if (generated_audio != nullptr && !mux_audio) {
|
||||
fs::path wav_path = video_path;
|
||||
wav_path.replace_extension(".wav");
|
||||
write_audio_sidecar(wav_path);
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
LOG_ERROR("Failed to save result video to '%s'", video_path.string().c_str());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_jpg && ext_lower != ".png")
|
||||
if (output_format == EncodedImageFormat::UNKNOWN)
|
||||
ext = ".png";
|
||||
|
||||
for (int i = 0; i < num_results; ++i) {
|
||||
@@ -453,10 +555,15 @@ bool save_results(const SDCliParams& cli_params,
|
||||
img_path += "_" + std::to_string(output_begin_idx + i);
|
||||
}
|
||||
img_path += ext;
|
||||
write_image(img_path, i);
|
||||
if (write_image(img_path, i)) {
|
||||
sucessful_reults++;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
LOG_INFO("%d/%d images saved", sucessful_reults, num_results);
|
||||
if (generated_audio != nullptr) {
|
||||
write_audio_sidecar(get_video_audio_sidecar_path(cli_params));
|
||||
}
|
||||
return sucessful_reults != 0;
|
||||
}
|
||||
|
||||
int main(int argc, const char* argv[]) {
|
||||
@@ -470,6 +577,27 @@ int main(int argc, const char* argv[]) {
|
||||
SDGenerationParams gen_params;
|
||||
|
||||
parse_args(argc, argv, cli_params, ctx_params, gen_params);
|
||||
sd_set_log_callback(sd_log_cb, (void*)&cli_params);
|
||||
log_verbose = cli_params.verbose;
|
||||
log_color = cli_params.color;
|
||||
|
||||
if (cli_params.mode == METADATA) {
|
||||
MetadataReadOptions options;
|
||||
options.output_format = cli_params.metadata_format == "json"
|
||||
? MetadataOutputFormat::JSON
|
||||
: MetadataOutputFormat::TEXT;
|
||||
options.include_raw = cli_params.metadata_raw;
|
||||
options.brief = cli_params.metadata_brief;
|
||||
options.include_structural = cli_params.metadata_all;
|
||||
|
||||
std::string error;
|
||||
if (!print_image_metadata(cli_params.image_path, options, std::cout, error)) {
|
||||
LOG_ERROR("%s", error.c_str());
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (gen_params.video_frames > 4) {
|
||||
size_t last_dot_pos = cli_params.preview_path.find_last_of(".");
|
||||
std::string base_path = cli_params.preview_path;
|
||||
@@ -487,9 +615,6 @@ int main(int argc, const char* argv[]) {
|
||||
if (cli_params.preview_method == PREVIEW_PROJ)
|
||||
cli_params.preview_fps /= 4;
|
||||
|
||||
sd_set_log_callback(sd_log_cb, (void*)&cli_params);
|
||||
log_verbose = cli_params.verbose;
|
||||
log_color = cli_params.color;
|
||||
sd_set_preview_callback(step_callback,
|
||||
cli_params.preview_method,
|
||||
cli_params.preview_interval,
|
||||
@@ -503,13 +628,32 @@ int main(int argc, const char* argv[]) {
|
||||
LOG_DEBUG("%s", ctx_params.to_string().c_str());
|
||||
LOG_DEBUG("%s", gen_params.to_string().c_str());
|
||||
|
||||
if (!cli_params.imatrix_out.empty()) {
|
||||
if (fs::exists(cli_params.imatrix_out) &&
|
||||
std::find(cli_params.imatrix_in.begin(), cli_params.imatrix_in.end(), cli_params.imatrix_out) == cli_params.imatrix_in.end()) {
|
||||
LOG_WARN("imatrix file '%s' already exists and will be overwritten", cli_params.imatrix_out.c_str());
|
||||
}
|
||||
enable_imatrix_collection();
|
||||
}
|
||||
|
||||
for (const auto& in_file : cli_params.imatrix_in) {
|
||||
LOG_INFO("loading imatrix from '%s'", in_file.c_str());
|
||||
if (!load_imatrix(in_file.c_str())) {
|
||||
LOG_WARN("failed to load imatrix from '%s'", in_file.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
if (cli_params.mode == CONVERT) {
|
||||
bool success = convert(ctx_params.model_path.c_str(),
|
||||
ctx_params.vae_path.c_str(),
|
||||
cli_params.output_path.c_str(),
|
||||
ctx_params.wtype,
|
||||
ctx_params.tensor_type_rules.c_str(),
|
||||
cli_params.convert_name);
|
||||
bool success = convert_with_components(ctx_params.model_path.c_str(),
|
||||
ctx_params.clip_l_path.c_str(),
|
||||
ctx_params.clip_g_path.c_str(),
|
||||
ctx_params.t5xxl_path.c_str(),
|
||||
ctx_params.diffusion_model_path.c_str(),
|
||||
ctx_params.vae_path.c_str(),
|
||||
cli_params.output_path.c_str(),
|
||||
ctx_params.wtype,
|
||||
ctx_params.tensor_type_rules.c_str(),
|
||||
cli_params.convert_name);
|
||||
if (!success) {
|
||||
LOG_ERROR("convert '%s'/'%s' to '%s' failed",
|
||||
ctx_params.model_path.c_str(),
|
||||
@@ -525,94 +669,81 @@ int main(int argc, const char* argv[]) {
|
||||
}
|
||||
}
|
||||
|
||||
bool vae_decode_only = true;
|
||||
sd_image_t init_image = {(uint32_t)gen_params.width, (uint32_t)gen_params.height, 3, nullptr};
|
||||
sd_image_t end_image = {(uint32_t)gen_params.width, (uint32_t)gen_params.height, 3, nullptr};
|
||||
sd_image_t control_image = {(uint32_t)gen_params.width, (uint32_t)gen_params.height, 3, nullptr};
|
||||
sd_image_t mask_image = {(uint32_t)gen_params.width, (uint32_t)gen_params.height, 1, nullptr};
|
||||
std::vector<sd_image_t> ref_images;
|
||||
std::vector<sd_image_t> pmid_images;
|
||||
std::vector<sd_image_t> control_frames;
|
||||
auto load_image_and_update_size = [&](const std::string& path,
|
||||
SDImageOwner& image,
|
||||
bool resize_image = true,
|
||||
int expected_channel = 3) -> bool {
|
||||
int expected_width = 0;
|
||||
int expected_height = 0;
|
||||
if (resize_image && gen_params.width_and_height_are_set()) {
|
||||
expected_width = gen_params.width;
|
||||
expected_height = gen_params.height;
|
||||
}
|
||||
|
||||
auto release_all_resources = [&]() {
|
||||
free(init_image.data);
|
||||
free(end_image.data);
|
||||
free(control_image.data);
|
||||
free(mask_image.data);
|
||||
for (auto image : ref_images) {
|
||||
free(image.data);
|
||||
image.data = nullptr;
|
||||
if (!load_sd_image_from_file(image.put(), path.c_str(), expected_width, expected_height, expected_channel)) {
|
||||
LOG_ERROR("load image from '%s' failed", path.c_str());
|
||||
return false;
|
||||
}
|
||||
ref_images.clear();
|
||||
for (auto image : pmid_images) {
|
||||
free(image.data);
|
||||
image.data = nullptr;
|
||||
}
|
||||
pmid_images.clear();
|
||||
for (auto image : control_frames) {
|
||||
free(image.data);
|
||||
image.data = nullptr;
|
||||
}
|
||||
control_frames.clear();
|
||||
|
||||
gen_params.set_width_and_height_if_unset(image.get().width, image.get().height);
|
||||
return true;
|
||||
};
|
||||
|
||||
if (gen_params.init_image_path.size() > 0) {
|
||||
vae_decode_only = false;
|
||||
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
init_image.data = load_image_from_file(gen_params.init_image_path.c_str(), width, height, gen_params.width, gen_params.height);
|
||||
if (init_image.data == nullptr) {
|
||||
LOG_ERROR("load image from '%s' failed", gen_params.init_image_path.c_str());
|
||||
release_all_resources();
|
||||
if (!load_image_and_update_size(gen_params.init_image_path, gen_params.init_image)) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (gen_params.end_image_path.size() > 0) {
|
||||
vae_decode_only = false;
|
||||
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
end_image.data = load_image_from_file(gen_params.end_image_path.c_str(), width, height, gen_params.width, gen_params.height);
|
||||
if (end_image.data == nullptr) {
|
||||
LOG_ERROR("load image from '%s' failed", gen_params.end_image_path.c_str());
|
||||
release_all_resources();
|
||||
if (!load_image_and_update_size(gen_params.end_image_path, gen_params.end_image)) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (gen_params.ref_image_paths.size() > 0) {
|
||||
gen_params.ref_images.clear();
|
||||
for (auto& path : gen_params.ref_image_paths) {
|
||||
SDImageOwner ref_image({0, 0, 3, nullptr});
|
||||
if (!load_image_and_update_size(path, ref_image, false)) {
|
||||
return 1;
|
||||
}
|
||||
gen_params.ref_images.push_back(std::move(ref_image));
|
||||
}
|
||||
}
|
||||
|
||||
if (gen_params.mask_image_path.size() > 0) {
|
||||
int c = 0;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
mask_image.data = load_image_from_file(gen_params.mask_image_path.c_str(), width, height, gen_params.width, gen_params.height, 1);
|
||||
if (mask_image.data == nullptr) {
|
||||
if (!load_sd_image_from_file(gen_params.mask_image.put(),
|
||||
gen_params.mask_image_path.c_str(),
|
||||
gen_params.get_resolved_width(),
|
||||
gen_params.get_resolved_height(),
|
||||
1)) {
|
||||
LOG_ERROR("load image from '%s' failed", gen_params.mask_image_path.c_str());
|
||||
release_all_resources();
|
||||
return 1;
|
||||
}
|
||||
} else {
|
||||
mask_image.data = (uint8_t*)malloc(gen_params.width * gen_params.height);
|
||||
memset(mask_image.data, 255, gen_params.width * gen_params.height);
|
||||
if (mask_image.data == nullptr) {
|
||||
sd_image_t generated_mask = {0, 0, 1, nullptr};
|
||||
generated_mask.data = (uint8_t*)malloc(gen_params.get_resolved_width() * gen_params.get_resolved_height());
|
||||
if (generated_mask.data == nullptr) {
|
||||
LOG_ERROR("malloc mask image failed");
|
||||
release_all_resources();
|
||||
return 1;
|
||||
}
|
||||
generated_mask.width = gen_params.get_resolved_width();
|
||||
generated_mask.height = gen_params.get_resolved_height();
|
||||
memset(generated_mask.data, 255, gen_params.get_resolved_width() * gen_params.get_resolved_height());
|
||||
gen_params.mask_image.reset(generated_mask);
|
||||
}
|
||||
|
||||
if (gen_params.control_image_path.size() > 0) {
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
control_image.data = load_image_from_file(gen_params.control_image_path.c_str(), width, height, gen_params.width, gen_params.height);
|
||||
if (control_image.data == nullptr) {
|
||||
if (!load_sd_image_from_file(gen_params.control_image.put(),
|
||||
gen_params.control_image_path.c_str(),
|
||||
gen_params.get_resolved_width(),
|
||||
gen_params.get_resolved_height())) {
|
||||
LOG_ERROR("load image from '%s' failed", gen_params.control_image_path.c_str());
|
||||
release_all_resources();
|
||||
return 1;
|
||||
}
|
||||
if (cli_params.canny_preprocess) { // apply preprocessor
|
||||
preprocess_canny(control_image,
|
||||
preprocess_canny(gen_params.control_image.get(),
|
||||
0.08f,
|
||||
0.08f,
|
||||
0.8f,
|
||||
@@ -621,165 +752,91 @@ int main(int argc, const char* argv[]) {
|
||||
}
|
||||
}
|
||||
|
||||
if (gen_params.ref_image_paths.size() > 0) {
|
||||
vae_decode_only = false;
|
||||
for (auto& path : gen_params.ref_image_paths) {
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
uint8_t* image_buffer = load_image_from_file(path.c_str(), width, height);
|
||||
if (image_buffer == nullptr) {
|
||||
LOG_ERROR("load image from '%s' failed", path.c_str());
|
||||
release_all_resources();
|
||||
return 1;
|
||||
}
|
||||
ref_images.push_back({(uint32_t)width,
|
||||
(uint32_t)height,
|
||||
3,
|
||||
image_buffer});
|
||||
}
|
||||
}
|
||||
|
||||
if (!gen_params.control_video_path.empty()) {
|
||||
gen_params.control_frames.clear();
|
||||
if (!load_images_from_dir(gen_params.control_video_path,
|
||||
control_frames,
|
||||
gen_params.width,
|
||||
gen_params.height,
|
||||
gen_params.control_frames,
|
||||
gen_params.get_resolved_width(),
|
||||
gen_params.get_resolved_height(),
|
||||
gen_params.video_frames,
|
||||
cli_params.verbose)) {
|
||||
release_all_resources();
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (!gen_params.pm_id_images_dir.empty()) {
|
||||
gen_params.pm_id_images.clear();
|
||||
if (!load_images_from_dir(gen_params.pm_id_images_dir,
|
||||
pmid_images,
|
||||
gen_params.pm_id_images,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
cli_params.verbose)) {
|
||||
release_all_resources();
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (cli_params.mode == VID_GEN) {
|
||||
vae_decode_only = false;
|
||||
}
|
||||
sd_ctx_params_t sd_ctx_params = ctx_params.to_sd_ctx_params_t(cli_params.taesd_preview);
|
||||
|
||||
sd_ctx_params_t sd_ctx_params = ctx_params.to_sd_ctx_params_t(vae_decode_only, true, cli_params.taesd_preview);
|
||||
|
||||
sd_image_t* results = nullptr;
|
||||
int num_results = 0;
|
||||
SDImageVec results;
|
||||
int num_results = 0;
|
||||
sd_audio_t* generated_audio = nullptr;
|
||||
|
||||
if (cli_params.mode == UPSCALE) {
|
||||
num_results = 1;
|
||||
results = (sd_image_t*)calloc(num_results, sizeof(sd_image_t));
|
||||
if (results == nullptr) {
|
||||
LOG_INFO("failed to allocate results array");
|
||||
release_all_resources();
|
||||
return 1;
|
||||
}
|
||||
|
||||
results[0] = init_image;
|
||||
init_image.data = nullptr;
|
||||
results.push_back(gen_params.init_image.release());
|
||||
} else {
|
||||
sd_ctx_t* sd_ctx = new_sd_ctx(&sd_ctx_params);
|
||||
SDCtxPtr sd_ctx(new_sd_ctx(&sd_ctx_params));
|
||||
|
||||
if (sd_ctx == nullptr) {
|
||||
LOG_INFO("new_sd_ctx_t failed");
|
||||
release_all_resources();
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (gen_params.sample_params.sample_method == SAMPLE_METHOD_COUNT) {
|
||||
gen_params.sample_params.sample_method = sd_get_default_sample_method(sd_ctx);
|
||||
gen_params.sample_params.sample_method = sd_get_default_sample_method(sd_ctx.get());
|
||||
}
|
||||
|
||||
if (gen_params.high_noise_sample_params.sample_method == SAMPLE_METHOD_COUNT) {
|
||||
gen_params.high_noise_sample_params.sample_method = sd_get_default_sample_method(sd_ctx);
|
||||
gen_params.high_noise_sample_params.sample_method = sd_get_default_sample_method(sd_ctx.get());
|
||||
}
|
||||
|
||||
if (gen_params.sample_params.scheduler == SCHEDULER_COUNT) {
|
||||
gen_params.sample_params.scheduler = sd_get_default_scheduler(sd_ctx, gen_params.sample_params.sample_method);
|
||||
gen_params.sample_params.scheduler = sd_get_default_scheduler(sd_ctx.get(), gen_params.sample_params.sample_method);
|
||||
}
|
||||
|
||||
if (cli_params.mode == IMG_GEN) {
|
||||
sd_img_gen_params_t img_gen_params = {
|
||||
gen_params.lora_vec.data(),
|
||||
static_cast<uint32_t>(gen_params.lora_vec.size()),
|
||||
gen_params.prompt.c_str(),
|
||||
gen_params.negative_prompt.c_str(),
|
||||
gen_params.clip_skip,
|
||||
init_image,
|
||||
ref_images.data(),
|
||||
(int)ref_images.size(),
|
||||
gen_params.auto_resize_ref_image,
|
||||
gen_params.increase_ref_index,
|
||||
mask_image,
|
||||
gen_params.width,
|
||||
gen_params.height,
|
||||
gen_params.sample_params,
|
||||
gen_params.strength,
|
||||
gen_params.seed,
|
||||
gen_params.batch_count,
|
||||
control_image,
|
||||
gen_params.control_strength,
|
||||
{
|
||||
pmid_images.data(),
|
||||
(int)pmid_images.size(),
|
||||
gen_params.pm_id_embed_path.c_str(),
|
||||
gen_params.pm_style_strength,
|
||||
}, // pm_params
|
||||
ctx_params.vae_tiling_params,
|
||||
gen_params.cache_params,
|
||||
};
|
||||
sd_img_gen_params_t img_gen_params = gen_params.to_sd_img_gen_params_t();
|
||||
|
||||
results = generate_image(sd_ctx, &img_gen_params);
|
||||
num_results = gen_params.batch_count;
|
||||
sd_image_t* generated_images = nullptr;
|
||||
if (!generate_image(sd_ctx.get(), &img_gen_params, &generated_images, &num_results)) {
|
||||
generated_images = nullptr;
|
||||
num_results = 0;
|
||||
}
|
||||
results.adopt(generated_images, num_results);
|
||||
} else if (cli_params.mode == VID_GEN) {
|
||||
sd_vid_gen_params_t vid_gen_params = {
|
||||
gen_params.lora_vec.data(),
|
||||
static_cast<uint32_t>(gen_params.lora_vec.size()),
|
||||
gen_params.prompt.c_str(),
|
||||
gen_params.negative_prompt.c_str(),
|
||||
gen_params.clip_skip,
|
||||
init_image,
|
||||
end_image,
|
||||
control_frames.data(),
|
||||
(int)control_frames.size(),
|
||||
gen_params.width,
|
||||
gen_params.height,
|
||||
gen_params.sample_params,
|
||||
gen_params.high_noise_sample_params,
|
||||
gen_params.moe_boundary,
|
||||
gen_params.strength,
|
||||
gen_params.seed,
|
||||
gen_params.video_frames,
|
||||
gen_params.vace_strength,
|
||||
ctx_params.vae_tiling_params,
|
||||
gen_params.cache_params,
|
||||
};
|
||||
|
||||
results = generate_video(sd_ctx, &vid_gen_params, &num_results);
|
||||
sd_vid_gen_params_t vid_gen_params = gen_params.to_sd_vid_gen_params_t();
|
||||
sd_image_t* generated_video = nullptr;
|
||||
if (!generate_video(sd_ctx.get(), &vid_gen_params, &generated_video, &num_results, &generated_audio)) {
|
||||
generated_video = nullptr;
|
||||
}
|
||||
results.adopt(generated_video, num_results);
|
||||
}
|
||||
|
||||
if (results == nullptr) {
|
||||
if (!results) {
|
||||
LOG_ERROR("generate failed");
|
||||
free_sd_ctx(sd_ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
free_sd_ctx(sd_ctx);
|
||||
}
|
||||
|
||||
int upscale_factor = 4; // unused for RealESRGAN_x4plus_anime_6B.pth
|
||||
if (ctx_params.esrgan_path.size() > 0 && gen_params.upscale_repeats > 0) {
|
||||
upscaler_ctx_t* upscaler_ctx = new_upscaler_ctx(ctx_params.esrgan_path.c_str(),
|
||||
ctx_params.offload_params_to_cpu,
|
||||
ctx_params.diffusion_conv_direct,
|
||||
ctx_params.n_threads,
|
||||
gen_params.upscale_tile_size);
|
||||
UpscalerCtxPtr upscaler_ctx(new_upscaler_ctx(ctx_params.esrgan_path.c_str(),
|
||||
ctx_params.diffusion_conv_direct,
|
||||
ctx_params.n_threads,
|
||||
gen_params.upscale_tile_size,
|
||||
sd_ctx_params.backend,
|
||||
sd_ctx_params.params_backend));
|
||||
|
||||
if (upscaler_ctx == nullptr) {
|
||||
LOG_ERROR("new_upscaler_ctx failed");
|
||||
@@ -788,32 +845,42 @@ int main(int argc, const char* argv[]) {
|
||||
if (results[i].data == nullptr) {
|
||||
continue;
|
||||
}
|
||||
sd_image_t current_image = results[i];
|
||||
SDImageOwner current_image(results[i]);
|
||||
results[i] = {0, 0, 0, nullptr};
|
||||
for (int u = 0; u < gen_params.upscale_repeats; ++u) {
|
||||
sd_image_t upscaled_image = upscale(upscaler_ctx, current_image, upscale_factor);
|
||||
if (upscaled_image.data == nullptr) {
|
||||
sd_image_t* upscaled_images = nullptr;
|
||||
int upscaled_count = 0;
|
||||
bool upscale_ok = upscale(upscaler_ctx.get(),
|
||||
current_image.get(),
|
||||
upscale_factor,
|
||||
&upscaled_images,
|
||||
&upscaled_count);
|
||||
if (!upscale_ok || upscaled_count <= 0 || upscaled_images[0].data == nullptr) {
|
||||
free_sd_images(upscaled_images, upscaled_count);
|
||||
LOG_ERROR("upscale failed");
|
||||
break;
|
||||
}
|
||||
free(current_image.data);
|
||||
current_image = upscaled_image;
|
||||
sd_image_t upscaled_image = upscaled_images[0];
|
||||
upscaled_images[0] = {0, 0, 0, nullptr};
|
||||
free_sd_images(upscaled_images, upscaled_count);
|
||||
current_image.reset(upscaled_image);
|
||||
}
|
||||
results[i] = current_image; // Set the final upscaled image as the result
|
||||
results[i] = current_image.release(); // Set the final upscaled image as the result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!save_results(cli_params, ctx_params, gen_params, results, num_results)) {
|
||||
if (!save_results(cli_params, ctx_params, gen_params, results.data(), num_results, generated_audio)) {
|
||||
free_sd_audio(generated_audio);
|
||||
return 1;
|
||||
}
|
||||
|
||||
for (int i = 0; i < num_results; i++) {
|
||||
free(results[i].data);
|
||||
results[i].data = nullptr;
|
||||
if (!cli_params.imatrix_out.empty()) {
|
||||
LOG_INFO("saving imatrix to '%s'", cli_params.imatrix_out.c_str());
|
||||
save_imatrix(cli_params.imatrix_out.c_str());
|
||||
}
|
||||
free(results);
|
||||
|
||||
release_all_resources();
|
||||
free_sd_audio(generated_audio);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,317 @@
|
||||
#ifndef __EXAMPLES_COMMON_COMMON_H__
|
||||
#define __EXAMPLES_COMMON_COMMON_H__
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "log.h"
|
||||
#include "resource_owners.hpp"
|
||||
#include "stable-diffusion.h"
|
||||
|
||||
#define SAFE_STR(s) ((s) ? (s) : "")
|
||||
#define BOOL_STR(b) ((b) ? "true" : "false")
|
||||
|
||||
extern const char* const modes_str[];
|
||||
#define SD_ALL_MODES_STR "img_gen, vid_gen, convert, upscale, metadata"
|
||||
|
||||
enum SDMode {
|
||||
IMG_GEN,
|
||||
VID_GEN,
|
||||
CONVERT,
|
||||
UPSCALE,
|
||||
METADATA,
|
||||
MODE_COUNT
|
||||
};
|
||||
|
||||
struct StringOption {
|
||||
std::string short_name;
|
||||
std::string long_name;
|
||||
std::string desc;
|
||||
int concat;
|
||||
std::string* target;
|
||||
};
|
||||
|
||||
struct IntOption {
|
||||
std::string short_name;
|
||||
std::string long_name;
|
||||
std::string desc;
|
||||
int* target;
|
||||
};
|
||||
|
||||
struct FloatOption {
|
||||
std::string short_name;
|
||||
std::string long_name;
|
||||
std::string desc;
|
||||
float* target;
|
||||
};
|
||||
|
||||
struct BoolOption {
|
||||
std::string short_name;
|
||||
std::string long_name;
|
||||
std::string desc;
|
||||
bool keep_true;
|
||||
bool* target;
|
||||
};
|
||||
|
||||
struct ManualFunction {
|
||||
std::function<int(int, const char**, int, bool&)> _func;
|
||||
|
||||
ManualFunction() = default;
|
||||
|
||||
ManualFunction(std::function<int(int argc, const char** argv, int index, bool& valid)> func)
|
||||
: _func(std::move(func)) {
|
||||
}
|
||||
|
||||
template <typename F>
|
||||
ManualFunction(F func)
|
||||
: _func(make_function(func)) {
|
||||
}
|
||||
|
||||
int operator()(int argc, const char** argv, int index, bool& valid) const {
|
||||
return _func(argc, argv, index, valid);
|
||||
}
|
||||
|
||||
private:
|
||||
template <typename F>
|
||||
static std::function<int(int, const char**, int, bool&)> make_function(F func) {
|
||||
if constexpr (std::is_invocable_v<F, int, const char**, int, bool&>) {
|
||||
return func;
|
||||
} else {
|
||||
return [func](int argc, const char** argv, int index, bool&) {
|
||||
return func(argc, argv, index);
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
struct ManualOption {
|
||||
std::string short_name;
|
||||
std::string long_name;
|
||||
std::string desc;
|
||||
ManualFunction cb;
|
||||
};
|
||||
|
||||
struct ArgOptions {
|
||||
std::vector<StringOption> string_options;
|
||||
std::vector<IntOption> int_options;
|
||||
std::vector<FloatOption> float_options;
|
||||
std::vector<BoolOption> bool_options;
|
||||
std::vector<ManualOption> manual_options;
|
||||
|
||||
static std::string wrap_text(const std::string& text, size_t width, size_t indent);
|
||||
void print() const;
|
||||
};
|
||||
|
||||
bool parse_options(int argc, const char** argv, const std::vector<ArgOptions>& options_list);
|
||||
bool decode_base64_image(const std::string& encoded_input,
|
||||
int target_channels,
|
||||
int expected_width,
|
||||
int expected_height,
|
||||
SDImageOwner& out_image);
|
||||
|
||||
struct SDContextParams {
|
||||
int n_threads = -1;
|
||||
std::string model_path;
|
||||
std::string clip_l_path;
|
||||
std::string clip_g_path;
|
||||
std::string clip_vision_path;
|
||||
std::string t5xxl_path;
|
||||
std::string llm_path;
|
||||
std::string llm_vision_path;
|
||||
std::string diffusion_model_path;
|
||||
std::string high_noise_diffusion_model_path;
|
||||
std::string uncond_diffusion_model_path;
|
||||
std::string embeddings_connectors_path;
|
||||
std::string vae_path;
|
||||
std::string vae_format = "auto";
|
||||
std::string audio_vae_path;
|
||||
std::string taesd_path;
|
||||
std::string esrgan_path;
|
||||
std::string control_net_path;
|
||||
std::string embedding_dir;
|
||||
std::string photo_maker_path;
|
||||
std::string pulid_weights_path;
|
||||
sd_type_t wtype = SD_TYPE_COUNT;
|
||||
std::string tensor_type_rules;
|
||||
std::string lora_model_dir = ".";
|
||||
std::string hires_upscalers_dir;
|
||||
|
||||
std::map<std::string, std::string> embedding_map;
|
||||
std::vector<sd_embedding_t> embedding_vec;
|
||||
|
||||
rng_type_t rng_type = CUDA_RNG;
|
||||
rng_type_t sampler_rng_type = RNG_TYPE_COUNT;
|
||||
bool offload_params_to_cpu = false;
|
||||
std::string max_vram = "0";
|
||||
bool stream_layers = false;
|
||||
bool eager_load = false;
|
||||
std::string backend;
|
||||
std::string params_backend;
|
||||
std::string split_mode;
|
||||
bool auto_fit = false;
|
||||
std::string rpc_servers;
|
||||
std::string effective_backend;
|
||||
std::string effective_params_backend;
|
||||
bool enable_mmap = false;
|
||||
bool control_net_cpu = false;
|
||||
bool clip_on_cpu = false;
|
||||
bool vae_on_cpu = false;
|
||||
bool flash_attn = false;
|
||||
bool diffusion_flash_attn = false;
|
||||
bool diffusion_conv_direct = false;
|
||||
bool vae_conv_direct = false;
|
||||
|
||||
bool circular = false;
|
||||
bool circular_x = false;
|
||||
bool circular_y = false;
|
||||
|
||||
bool chroma_use_dit_mask = true;
|
||||
bool chroma_use_t5_mask = false;
|
||||
int chroma_t5_mask_pad = 1;
|
||||
|
||||
bool qwen_image_zero_cond_t = false;
|
||||
|
||||
prediction_t prediction = PREDICTION_COUNT;
|
||||
lora_apply_mode_t lora_apply_mode = LORA_APPLY_AUTO;
|
||||
|
||||
bool force_sdxl_vae_conv_scale = false;
|
||||
|
||||
float flow_shift = INFINITY;
|
||||
ArgOptions get_options();
|
||||
void build_embedding_map();
|
||||
void prepare_backend_assignments();
|
||||
bool resolve(SDMode mode);
|
||||
bool validate(SDMode mode);
|
||||
bool resolve_and_validate(SDMode mode);
|
||||
std::string to_string() const;
|
||||
sd_ctx_params_t to_sd_ctx_params_t(bool taesd_preview);
|
||||
};
|
||||
|
||||
struct SDGenerationParams {
|
||||
// User-facing input fields.
|
||||
std::string prompt;
|
||||
std::string negative_prompt;
|
||||
int clip_skip = -1; // <= 0 represents unspecified
|
||||
int width = -1;
|
||||
int height = -1;
|
||||
int batch_count = 1;
|
||||
int qwen_image_layers = 3;
|
||||
int64_t seed = 42;
|
||||
float strength = 0.75f;
|
||||
float control_strength = 0.9f;
|
||||
bool auto_resize_ref_image = true;
|
||||
bool increase_ref_index = false;
|
||||
bool embed_image_metadata = true;
|
||||
|
||||
std::string init_image_path;
|
||||
std::string end_image_path;
|
||||
std::string mask_image_path;
|
||||
std::string control_image_path;
|
||||
std::vector<std::string> ref_image_paths;
|
||||
std::string control_video_path;
|
||||
|
||||
sd_sample_params_t sample_params;
|
||||
sd_sample_params_t high_noise_sample_params;
|
||||
std::string extra_sample_args;
|
||||
std::string high_noise_extra_sample_args;
|
||||
std::vector<int> skip_layers = {7, 8, 9};
|
||||
std::vector<int> high_noise_skip_layers = {7, 8, 9};
|
||||
|
||||
std::vector<float> custom_sigmas;
|
||||
|
||||
std::string cache_mode;
|
||||
std::string cache_option;
|
||||
std::string scm_mask;
|
||||
bool scm_policy_dynamic = true;
|
||||
sd_cache_params_t cache_params{};
|
||||
|
||||
float moe_boundary = 0.875f;
|
||||
int video_frames = 1;
|
||||
int fps = 16;
|
||||
float vace_strength = 1.f;
|
||||
sd_tiling_params_t vae_tiling_params = {false, false, 0, 0, 0.5f, 0.0f, 0.0f, nullptr};
|
||||
std::string extra_tiling_args;
|
||||
|
||||
std::string pm_id_images_dir;
|
||||
std::string pm_id_embed_path;
|
||||
float pm_style_strength = 20.f;
|
||||
|
||||
std::string pulid_id_embedding_path;
|
||||
float pulid_id_weight = 1.0f;
|
||||
|
||||
int upscale_repeats = 1;
|
||||
int upscale_tile_size = 128;
|
||||
|
||||
bool hires_enabled = false;
|
||||
std::string hires_upscaler = "Latent";
|
||||
std::string hires_upscaler_model_path;
|
||||
float hires_scale = 2.f;
|
||||
int hires_width = 0;
|
||||
int hires_height = 0;
|
||||
int hires_steps = 0;
|
||||
float hires_denoising_strength = 0.7f;
|
||||
int hires_upscale_tile_size = 128;
|
||||
std::vector<float> hires_custom_sigmas;
|
||||
|
||||
std::map<std::string, float> lora_map;
|
||||
std::map<std::string, float> high_noise_lora_map;
|
||||
|
||||
// Derived and normalized fields.
|
||||
std::string prompt_with_lora; // for metadata record only
|
||||
std::vector<sd_lora_t> lora_vec;
|
||||
sd_hires_upscaler_t resolved_hires_upscaler;
|
||||
|
||||
// Owned execution payload.
|
||||
SDImageOwner init_image;
|
||||
SDImageOwner end_image;
|
||||
std::vector<SDImageOwner> ref_images;
|
||||
SDImageOwner mask_image;
|
||||
SDImageOwner control_image;
|
||||
std::vector<SDImageOwner> pm_id_images;
|
||||
std::vector<SDImageOwner> control_frames;
|
||||
|
||||
// Backing storage for sd_img_gen_params_t view fields.
|
||||
std::vector<sd_image_t> ref_image_views;
|
||||
std::vector<sd_image_t> pm_id_image_views;
|
||||
std::vector<sd_image_t> control_frame_views;
|
||||
|
||||
SDGenerationParams();
|
||||
SDGenerationParams(const SDGenerationParams& other) = default;
|
||||
SDGenerationParams& operator=(const SDGenerationParams& other) = default;
|
||||
SDGenerationParams(SDGenerationParams&& other) noexcept = default;
|
||||
SDGenerationParams& operator=(SDGenerationParams&& other) noexcept = default;
|
||||
ArgOptions get_options();
|
||||
bool from_json_str(const std::string& json_str,
|
||||
const std::function<std::string(const std::string&)>& lora_path_resolver = {});
|
||||
bool initialize_cache_params();
|
||||
void extract_and_remove_lora(const std::string& lora_model_dir);
|
||||
bool width_and_height_are_set() const;
|
||||
void set_width_and_height_if_unset(int w, int h);
|
||||
int get_resolved_width() const;
|
||||
int get_resolved_height() const;
|
||||
bool resolve(const std::string& lora_model_dir, const std::string& hires_upscalers_dir, bool strict = false);
|
||||
bool validate(SDMode mode);
|
||||
bool resolve_and_validate(SDMode mode,
|
||||
const std::string& lora_model_dir,
|
||||
const std::string& hires_upscalers_dir,
|
||||
bool strict = false);
|
||||
sd_img_gen_params_t to_sd_img_gen_params_t();
|
||||
sd_vid_gen_params_t to_sd_vid_gen_params_t();
|
||||
std::string to_string() const;
|
||||
};
|
||||
|
||||
std::string version_string();
|
||||
std::string build_sdcpp_image_metadata_json(const SDContextParams& ctx_params,
|
||||
const SDGenerationParams& gen_params,
|
||||
int64_t seed,
|
||||
SDMode mode = IMG_GEN);
|
||||
std::string get_image_params(const SDContextParams& ctx_params,
|
||||
const SDGenerationParams& gen_params,
|
||||
int64_t seed,
|
||||
SDMode mode = IMG_GEN);
|
||||
|
||||
#endif // __EXAMPLES_COMMON_COMMON_H__
|
||||
@@ -0,0 +1,115 @@
|
||||
#include "log.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
bool log_verbose = false;
|
||||
bool log_color = false;
|
||||
|
||||
std::string sd_basename(const std::string& path) {
|
||||
size_t pos = path.find_last_of('/');
|
||||
if (pos != std::string::npos) {
|
||||
return path.substr(pos + 1);
|
||||
}
|
||||
pos = path.find_last_of('\\');
|
||||
if (pos != std::string::npos) {
|
||||
return path.substr(pos + 1);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
void print_utf8(FILE* stream, const char* utf8) {
|
||||
if (!utf8) {
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
HANDLE h = (stream == stderr)
|
||||
? GetStdHandle(STD_ERROR_HANDLE)
|
||||
: GetStdHandle(STD_OUTPUT_HANDLE);
|
||||
|
||||
DWORD mode;
|
||||
BOOL is_console = GetConsoleMode(h, &mode);
|
||||
|
||||
if (is_console) {
|
||||
int wlen = MultiByteToWideChar(CP_UTF8, 0, utf8, -1, NULL, 0);
|
||||
if (wlen <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<wchar_t> wbuf(static_cast<size_t>(wlen));
|
||||
|
||||
MultiByteToWideChar(CP_UTF8, 0, utf8, -1, wbuf.data(), wlen);
|
||||
|
||||
DWORD written;
|
||||
WriteConsoleW(h, wbuf.data(), wlen - 1, &written, NULL);
|
||||
} else {
|
||||
DWORD written;
|
||||
WriteFile(h, utf8, (DWORD)strlen(utf8), &written, NULL);
|
||||
}
|
||||
#else
|
||||
fputs(utf8, stream);
|
||||
#endif
|
||||
}
|
||||
|
||||
void log_print(enum sd_log_level_t level, const char* log, bool verbose, bool color) {
|
||||
int tag_color;
|
||||
const char* level_str;
|
||||
FILE* out_stream = (level == SD_LOG_ERROR) ? stderr : stdout;
|
||||
|
||||
if (!log || (!verbose && level <= SD_LOG_DEBUG)) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (level) {
|
||||
case SD_LOG_DEBUG:
|
||||
tag_color = 37;
|
||||
level_str = "DEBUG";
|
||||
break;
|
||||
case SD_LOG_INFO:
|
||||
tag_color = 34;
|
||||
level_str = "INFO";
|
||||
break;
|
||||
case SD_LOG_WARN:
|
||||
tag_color = 35;
|
||||
level_str = "WARN";
|
||||
break;
|
||||
case SD_LOG_ERROR:
|
||||
tag_color = 31;
|
||||
level_str = "ERROR";
|
||||
break;
|
||||
default:
|
||||
tag_color = 33;
|
||||
level_str = "?????";
|
||||
break;
|
||||
}
|
||||
|
||||
if (color) {
|
||||
fprintf(out_stream, "\033[%d;1m[%-5s]\033[0m ", tag_color, level_str);
|
||||
} else {
|
||||
fprintf(out_stream, "[%-5s] ", level_str);
|
||||
}
|
||||
print_utf8(out_stream, log);
|
||||
fflush(out_stream);
|
||||
}
|
||||
|
||||
void example_log_printf(sd_log_level_t level, const char* file, int line, const char* format, ...) {
|
||||
constexpr size_t LOG_BUFFER_SIZE = 4096;
|
||||
|
||||
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_verbose, log_color);
|
||||
|
||||
va_end(args);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
#ifndef __EXAMPLE_LOG_H__
|
||||
#define __EXAMPLE_LOG_H__
|
||||
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#endif // _WIN32
|
||||
|
||||
#include "stable-diffusion.h"
|
||||
|
||||
extern bool log_verbose;
|
||||
extern bool log_color;
|
||||
|
||||
std::string sd_basename(const std::string& path);
|
||||
void print_utf8(FILE* stream, const char* utf8);
|
||||
void log_print(sd_log_level_t level, const char* log, bool verbose, bool color);
|
||||
void example_log_printf(sd_log_level_t level, const char* file, int line, const char* format, ...);
|
||||
|
||||
#define LOG_DEBUG(format, ...) example_log_printf(SD_LOG_DEBUG, __FILE__, __LINE__, format, ##__VA_ARGS__)
|
||||
#define LOG_INFO(format, ...) example_log_printf(SD_LOG_INFO, __FILE__, __LINE__, format, ##__VA_ARGS__)
|
||||
#define LOG_WARN(format, ...) example_log_printf(SD_LOG_WARN, __FILE__, __LINE__, format, ##__VA_ARGS__)
|
||||
#define LOG_ERROR(format, ...) example_log_printf(SD_LOG_ERROR, __FILE__, __LINE__, format, ##__VA_ARGS__)
|
||||
|
||||
#endif // __EXAMPLE_LOG_H__
|
||||
@@ -0,0 +1,113 @@
|
||||
#ifndef __MEDIA_IO_H__
|
||||
#define __MEDIA_IO_H__
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "stable-diffusion.h"
|
||||
|
||||
enum class EncodedImageFormat {
|
||||
JPEG,
|
||||
PNG,
|
||||
WEBP,
|
||||
UNKNOWN,
|
||||
};
|
||||
|
||||
EncodedImageFormat encoded_image_format_from_path(const std::string& path);
|
||||
|
||||
std::vector<uint8_t> encode_image_to_vector(EncodedImageFormat format,
|
||||
const uint8_t* image,
|
||||
int width,
|
||||
int height,
|
||||
int channels,
|
||||
const std::string& parameters = "",
|
||||
int quality = 90);
|
||||
|
||||
bool write_image_to_file(const std::string& path,
|
||||
const uint8_t* image,
|
||||
int width,
|
||||
int height,
|
||||
int channels,
|
||||
const std::string& parameters = "",
|
||||
int quality = 90);
|
||||
|
||||
uint8_t* load_image_from_file(const char* image_path,
|
||||
int& width,
|
||||
int& height,
|
||||
int expected_width = 0,
|
||||
int expected_height = 0,
|
||||
int expected_channel = 3);
|
||||
|
||||
bool load_sd_image_from_file(sd_image_t* image,
|
||||
const char* image_path,
|
||||
int expected_width = 0,
|
||||
int expected_height = 0,
|
||||
int expected_channel = 3);
|
||||
|
||||
uint8_t* load_image_from_memory(const char* image_bytes,
|
||||
int len,
|
||||
int& width,
|
||||
int& height,
|
||||
int expected_width = 0,
|
||||
int expected_height = 0,
|
||||
int expected_channel = 3);
|
||||
|
||||
int create_mjpg_avi_from_sd_images(const char* filename,
|
||||
sd_image_t* images,
|
||||
int num_images,
|
||||
int fps,
|
||||
int quality = 90,
|
||||
const sd_audio_t* audio = nullptr);
|
||||
std::vector<uint8_t> create_mjpg_avi_from_sd_images_to_vector(sd_image_t* images,
|
||||
int num_images,
|
||||
int fps,
|
||||
int quality = 90,
|
||||
const sd_audio_t* audio = nullptr);
|
||||
|
||||
#ifdef SD_USE_WEBP
|
||||
int create_animated_webp_from_sd_images(const char* filename,
|
||||
sd_image_t* images,
|
||||
int num_images,
|
||||
int fps,
|
||||
int quality = 90);
|
||||
std::vector<uint8_t> create_animated_webp_from_sd_images_to_vector(sd_image_t* images,
|
||||
int num_images,
|
||||
int fps,
|
||||
int quality = 90);
|
||||
#endif
|
||||
|
||||
#ifdef SD_USE_WEBM
|
||||
int create_webm_from_sd_images(const char* filename,
|
||||
sd_image_t* images,
|
||||
int num_images,
|
||||
int fps,
|
||||
int quality = 90,
|
||||
const sd_audio_t* audio = nullptr);
|
||||
std::vector<uint8_t> create_webm_from_sd_images_to_vector(sd_image_t* images,
|
||||
int num_images,
|
||||
int fps,
|
||||
int quality = 90,
|
||||
const sd_audio_t* audio = nullptr);
|
||||
#endif
|
||||
|
||||
int create_video_from_sd_images(const char* filename,
|
||||
sd_image_t* images,
|
||||
int num_images,
|
||||
int fps,
|
||||
int quality = 90,
|
||||
const sd_audio_t* audio = nullptr);
|
||||
std::vector<uint8_t> create_video_from_sd_images_to_vector(const std::string& output_format,
|
||||
sd_image_t* images,
|
||||
int num_images,
|
||||
int fps,
|
||||
int quality = 90,
|
||||
const sd_audio_t* audio = nullptr);
|
||||
|
||||
bool write_wav_to_file(const std::string& path,
|
||||
const float* interleaved_samples,
|
||||
uint64_t sample_count,
|
||||
uint32_t channels,
|
||||
uint32_t sample_rate);
|
||||
|
||||
#endif // __MEDIA_IO_H__
|
||||
@@ -0,0 +1,236 @@
|
||||
#ifndef __EXAMPLE_RESOURCE_OWNERS_H__
|
||||
#define __EXAMPLE_RESOURCE_OWNERS_H__
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "stable-diffusion.h"
|
||||
|
||||
struct FreeDeleter {
|
||||
void operator()(void* ptr) const {
|
||||
free(ptr);
|
||||
}
|
||||
};
|
||||
|
||||
struct FileCloser {
|
||||
void operator()(FILE* file) const {
|
||||
if (file != nullptr) {
|
||||
fclose(file);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
struct SDCtxDeleter {
|
||||
void operator()(sd_ctx_t* ctx) const {
|
||||
if (ctx != nullptr) {
|
||||
free_sd_ctx(ctx);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
struct UpscalerCtxDeleter {
|
||||
void operator()(upscaler_ctx_t* ctx) const {
|
||||
if (ctx != nullptr) {
|
||||
free_upscaler_ctx(ctx);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
using FreeUniquePtr = std::unique_ptr<T, FreeDeleter>;
|
||||
|
||||
using FilePtr = std::unique_ptr<FILE, FileCloser>;
|
||||
using SDCtxPtr = std::unique_ptr<sd_ctx_t, SDCtxDeleter>;
|
||||
using UpscalerCtxPtr = std::unique_ptr<upscaler_ctx_t, UpscalerCtxDeleter>;
|
||||
|
||||
class SDImageOwner {
|
||||
private:
|
||||
static sd_image_t copy_image(const sd_image_t& image) {
|
||||
if (image.data == nullptr) {
|
||||
return {image.width, image.height, image.channel, nullptr};
|
||||
}
|
||||
|
||||
const size_t byte_count = static_cast<size_t>(image.width) * image.height * image.channel;
|
||||
uint8_t* raw_copy = static_cast<uint8_t*>(malloc(byte_count));
|
||||
if (raw_copy == nullptr) {
|
||||
return {0, 0, 0, nullptr};
|
||||
}
|
||||
|
||||
std::memcpy(raw_copy, image.data, byte_count);
|
||||
return {image.width, image.height, image.channel, raw_copy};
|
||||
}
|
||||
|
||||
sd_image_t image_ = {0, 0, 0, nullptr};
|
||||
|
||||
public:
|
||||
SDImageOwner() = default;
|
||||
explicit SDImageOwner(sd_image_t image)
|
||||
: image_(image) {
|
||||
}
|
||||
|
||||
SDImageOwner(const SDImageOwner& other)
|
||||
: image_(copy_image(other.image_)) {
|
||||
}
|
||||
|
||||
SDImageOwner& operator=(const SDImageOwner& other) {
|
||||
if (this != &other) {
|
||||
reset(copy_image(other.image_));
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
SDImageOwner(SDImageOwner&& other) noexcept
|
||||
: image_(other.release()) {
|
||||
}
|
||||
|
||||
SDImageOwner& operator=(SDImageOwner&& other) noexcept {
|
||||
if (this != &other) {
|
||||
reset();
|
||||
image_ = other.release();
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
~SDImageOwner() {
|
||||
reset();
|
||||
}
|
||||
|
||||
sd_image_t* put() {
|
||||
if (image_.data != nullptr) {
|
||||
free(image_.data);
|
||||
image_.data = nullptr;
|
||||
}
|
||||
image_.width = 0;
|
||||
image_.height = 0;
|
||||
image_.channel = 0;
|
||||
return &image_;
|
||||
}
|
||||
|
||||
sd_image_t& get() {
|
||||
return image_;
|
||||
}
|
||||
|
||||
const sd_image_t& get() const {
|
||||
return image_;
|
||||
}
|
||||
|
||||
sd_image_t release() {
|
||||
sd_image_t image = image_;
|
||||
image_ = {0, 0, 0, nullptr};
|
||||
return image;
|
||||
}
|
||||
|
||||
void reset(sd_image_t image = {0, 0, 0, nullptr}) {
|
||||
if (image_.data != nullptr) {
|
||||
free(image_.data);
|
||||
}
|
||||
image_ = image;
|
||||
}
|
||||
};
|
||||
|
||||
class SDImageVec {
|
||||
private:
|
||||
std::vector<sd_image_t> images_;
|
||||
|
||||
public:
|
||||
SDImageVec() = default;
|
||||
|
||||
SDImageVec(const SDImageVec&) = delete;
|
||||
SDImageVec& operator=(const SDImageVec&) = delete;
|
||||
|
||||
SDImageVec(SDImageVec&& other) noexcept
|
||||
: images_(std::move(other.images_)) {
|
||||
}
|
||||
|
||||
SDImageVec& operator=(SDImageVec&& other) noexcept {
|
||||
if (this != &other) {
|
||||
clear();
|
||||
images_ = std::move(other.images_);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
~SDImageVec() {
|
||||
clear();
|
||||
}
|
||||
|
||||
void push_back(sd_image_t image) {
|
||||
images_.push_back(image);
|
||||
}
|
||||
|
||||
void push_back(SDImageOwner&& image) {
|
||||
images_.push_back(image.release());
|
||||
}
|
||||
|
||||
void reserve(size_t count) {
|
||||
images_.reserve(count);
|
||||
}
|
||||
|
||||
void adopt(sd_image_t* images, int count) {
|
||||
clear();
|
||||
if (images == nullptr || count <= 0) {
|
||||
free(images);
|
||||
return;
|
||||
}
|
||||
|
||||
images_.reserve(static_cast<size_t>(count));
|
||||
for (int i = 0; i < count; ++i) {
|
||||
images_.push_back(images[i]);
|
||||
}
|
||||
free(images);
|
||||
}
|
||||
|
||||
size_t size() const {
|
||||
return images_.size();
|
||||
}
|
||||
|
||||
bool empty() const {
|
||||
return images_.empty();
|
||||
}
|
||||
|
||||
int count() const {
|
||||
return static_cast<int>(images_.size());
|
||||
}
|
||||
|
||||
explicit operator bool() const {
|
||||
return !images_.empty();
|
||||
}
|
||||
|
||||
sd_image_t* data() {
|
||||
return images_.data();
|
||||
}
|
||||
|
||||
const sd_image_t* data() const {
|
||||
return images_.data();
|
||||
}
|
||||
|
||||
sd_image_t& operator[](size_t index) {
|
||||
return images_[index];
|
||||
}
|
||||
|
||||
const sd_image_t& operator[](size_t index) const {
|
||||
return images_[index];
|
||||
}
|
||||
|
||||
std::vector<sd_image_t>& raw() {
|
||||
return images_;
|
||||
}
|
||||
|
||||
const std::vector<sd_image_t>& raw() const {
|
||||
return images_;
|
||||
}
|
||||
|
||||
void clear() {
|
||||
for (sd_image_t& image : images_) {
|
||||
free(image.data);
|
||||
image.data = nullptr;
|
||||
}
|
||||
images_.clear();
|
||||
}
|
||||
};
|
||||
|
||||
#endif // __EXAMPLE_RESOURCE_OWNERS_H__
|
||||
@@ -1,6 +1,107 @@
|
||||
set(TARGET sd-server)
|
||||
|
||||
add_executable(${TARGET} main.cpp)
|
||||
option(SD_SERVER_BUILD_FRONTEND "Build server frontend with pnpm" ON)
|
||||
|
||||
set(FRONTEND_DIR "${CMAKE_CURRENT_SOURCE_DIR}/frontend")
|
||||
set(GENERATED_HTML_HEADER "${FRONTEND_DIR}/dist/gen_index_html.h")
|
||||
|
||||
set(HAVE_FRONTEND_BUILD OFF)
|
||||
|
||||
if(SD_SERVER_BUILD_FRONTEND AND EXISTS "${FRONTEND_DIR}")
|
||||
if(WIN32)
|
||||
find_program(PNPM_EXECUTABLE NAMES pnpm.cmd pnpm)
|
||||
else()
|
||||
find_program(PNPM_EXECUTABLE NAMES pnpm)
|
||||
endif()
|
||||
|
||||
if(PNPM_EXECUTABLE)
|
||||
message(STATUS "Frontend dir found: ${FRONTEND_DIR}")
|
||||
message(STATUS "pnpm found: ${PNPM_EXECUTABLE}")
|
||||
|
||||
set(HAVE_FRONTEND_BUILD ON)
|
||||
|
||||
add_custom_target(${TARGET}_frontend_install
|
||||
COMMAND "${PNPM_EXECUTABLE}" -C "${FRONTEND_DIR}" install
|
||||
WORKING_DIRECTORY "${FRONTEND_DIR}"
|
||||
COMMENT "Installing frontend dependencies"
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
add_custom_target(${TARGET}_frontend_build
|
||||
COMMAND "${PNPM_EXECUTABLE}" -C "${FRONTEND_DIR}" run build
|
||||
WORKING_DIRECTORY "${FRONTEND_DIR}"
|
||||
COMMENT "Building frontend"
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
add_custom_target(${TARGET}_frontend_header
|
||||
COMMAND "${PNPM_EXECUTABLE}" -C "${FRONTEND_DIR}" run build:header
|
||||
WORKING_DIRECTORY "${FRONTEND_DIR}"
|
||||
COMMENT "Generating gen_index_html.h"
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
add_dependencies(${TARGET}_frontend_build ${TARGET}_frontend_install)
|
||||
add_dependencies(${TARGET}_frontend_header ${TARGET}_frontend_build)
|
||||
|
||||
add_custom_target(${TARGET}_frontend
|
||||
DEPENDS ${TARGET}_frontend_header
|
||||
)
|
||||
|
||||
set_source_files_properties("${GENERATED_HTML_HEADER}" PROPERTIES GENERATED TRUE)
|
||||
else()
|
||||
if(EXISTS "${GENERATED_HTML_HEADER}")
|
||||
message(STATUS "pnpm not found; using pre-built frontend header detected at ${GENERATED_HTML_HEADER}")
|
||||
set(HAVE_FRONTEND_BUILD ON)
|
||||
add_custom_target(${TARGET}_frontend)
|
||||
else()
|
||||
message(WARNING "pnpm not found; frontend build disabled.")
|
||||
endif()
|
||||
endif()
|
||||
else()
|
||||
message(STATUS "Frontend disabled or directory not found: ${FRONTEND_DIR}")
|
||||
endif()
|
||||
|
||||
add_executable(${TARGET}
|
||||
../common/common.cpp
|
||||
../common/log.cpp
|
||||
../common/media_io.cpp
|
||||
main.cpp
|
||||
runtime.cpp
|
||||
async_jobs.cpp
|
||||
routes_index.cpp
|
||||
routes_openai.cpp
|
||||
routes_sdapi.cpp
|
||||
routes_sdcpp.cpp
|
||||
)
|
||||
if(APPLE)
|
||||
sd_set_macos_rpaths(${TARGET})
|
||||
endif()
|
||||
|
||||
if(HAVE_FRONTEND_BUILD)
|
||||
add_dependencies(${TARGET} ${TARGET}_frontend)
|
||||
target_sources(${TARGET} PRIVATE "${GENERATED_HTML_HEADER}")
|
||||
target_include_directories(${TARGET} PRIVATE "${FRONTEND_DIR}/dist")
|
||||
target_compile_definitions(${TARGET} PRIVATE HAVE_INDEX_HTML)
|
||||
message(STATUS "HAVE_INDEX_HTML enabled")
|
||||
else()
|
||||
message(STATUS "HAVE_INDEX_HTML disabled")
|
||||
endif()
|
||||
|
||||
install(TARGETS ${TARGET} RUNTIME)
|
||||
target_link_libraries(${TARGET} PRIVATE stable-diffusion ${CMAKE_THREAD_LIBS_INIT})
|
||||
target_compile_features(${TARGET} PUBLIC c_std_11 cxx_std_17)
|
||||
if(SD_WEBP)
|
||||
target_compile_definitions(${TARGET} PRIVATE SD_USE_WEBP)
|
||||
target_link_libraries(${TARGET} PRIVATE webp libwebpmux)
|
||||
endif()
|
||||
if(SD_WEBM)
|
||||
target_compile_definitions(${TARGET} PRIVATE SD_USE_WEBM)
|
||||
target_link_libraries(${TARGET} PRIVATE webm)
|
||||
endif()
|
||||
|
||||
# due to httplib; it contains a pragma for MSVC, but other things need explicit flags
|
||||
if(WIN32 AND NOT MSVC)
|
||||
target_link_libraries(${TARGET} PRIVATE ws2_32)
|
||||
endif()
|
||||
|
||||
target_compile_features(${TARGET} PUBLIC c_std_11 cxx_std_17)
|
||||
|
||||
@@ -1,135 +1,126 @@
|
||||
# Run
|
||||
# Example
|
||||
|
||||
The following example starts `sd-server` with a standalone diffusion model, VAE, and LLM text encoder:
|
||||
|
||||
```
|
||||
usage: ./bin/sd-server [options]
|
||||
|
||||
Svr Options:
|
||||
-l, --listen-ip <string> server listen ip (default: 127.0.0.1)
|
||||
--listen-port <int> server listen port (default: 1234)
|
||||
--serve-html-path <string> path to HTML file to serve at root (optional)
|
||||
-v, --verbose print extra info
|
||||
--color colors the logging tags according to level
|
||||
-h, --help show this help message and exit
|
||||
|
||||
Context Options:
|
||||
-m, --model <string> path to full model
|
||||
--clip_l <string> path to the clip-l text encoder
|
||||
--clip_g <string> path to the clip-g text encoder
|
||||
--clip_vision <string> path to the clip-vision encoder
|
||||
--t5xxl <string> path to the t5xxl text encoder
|
||||
--llm <string> path to the llm text encoder. For example: (qwenvl2.5 for qwen-image, mistral-small3.2 for flux2, ...)
|
||||
--llm_vision <string> path to the llm vit
|
||||
--qwen2vl <string> alias of --llm. Deprecated.
|
||||
--qwen2vl_vision <string> alias of --llm_vision. Deprecated.
|
||||
--diffusion-model <string> path to the standalone diffusion model
|
||||
--high-noise-diffusion-model <string> path to the standalone high noise diffusion model
|
||||
--vae <string> path to standalone vae model
|
||||
--taesd <string> path to taesd. Using Tiny AutoEncoder for fast decoding (low quality)
|
||||
--tae <string> alias of --taesd
|
||||
--control-net <string> path to control net model
|
||||
--embd-dir <string> embeddings directory
|
||||
--lora-model-dir <string> lora model directory
|
||||
--tensor-type-rules <string> weight type per tensor pattern (example: "^vae\.=f16,model\.=q8_0")
|
||||
--photo-maker <string> path to PHOTOMAKER model
|
||||
--upscale-model <string> path to esrgan model.
|
||||
-t, --threads <int> number of threads to use during computation (default: -1). If threads <= 0, then threads will be set to the number of
|
||||
CPU physical cores
|
||||
--chroma-t5-mask-pad <int> t5 mask pad size of chroma
|
||||
--vae-tile-overlap <float> tile overlap for vae tiling, in fraction of tile size (default: 0.5)
|
||||
--flow-shift <float> shift value for Flow models like SD3.x or WAN (default: auto)
|
||||
--vae-tiling process vae in tiles to reduce memory usage
|
||||
--force-sdxl-vae-conv-scale force use of conv scale on sdxl vae
|
||||
--offload-to-cpu place the weights in RAM to save VRAM, and automatically load them into VRAM when needed
|
||||
--control-net-cpu keep controlnet in cpu (for low vram)
|
||||
--clip-on-cpu keep clip in cpu (for low vram)
|
||||
--vae-on-cpu keep vae in cpu (for low vram)
|
||||
--mmap whether to memory-map model
|
||||
--diffusion-fa use flash attention in the diffusion model
|
||||
--diffusion-conv-direct use ggml_conv2d_direct in the diffusion model
|
||||
--vae-conv-direct use ggml_conv2d_direct in the vae model
|
||||
--circular enable circular padding for convolutions
|
||||
--circularx enable circular RoPE wrapping on x-axis (width) only
|
||||
--circulary enable circular RoPE wrapping on y-axis (height) only
|
||||
--chroma-disable-dit-mask disable dit mask for chroma
|
||||
--chroma-enable-t5-mask enable t5 mask for chroma
|
||||
--type weight type (examples: f32, f16, q4_0, q4_1, q5_0, q5_1, q8_0, q2_K, q3_K, q4_K). If not specified, the default is the
|
||||
type of the weight file
|
||||
--rng RNG, one of [std_default, cuda, cpu], default: cuda(sd-webui), cpu(comfyui)
|
||||
--sampler-rng sampler RNG, one of [std_default, cuda, cpu]. If not specified, use --rng
|
||||
--prediction prediction type override, one of [eps, v, edm_v, sd3_flow, flux_flow, flux2_flow]
|
||||
--lora-apply-mode the way to apply LoRA, one of [auto, immediately, at_runtime], default is auto. In auto mode, if the model weights
|
||||
contain any quantized parameters, the at_runtime mode will be used; otherwise,
|
||||
immediately will be used.The immediately mode may have precision and
|
||||
compatibility issues with quantized parameters, but it usually offers faster inference
|
||||
speed and, in some cases, lower memory usage. The at_runtime mode, on the
|
||||
other hand, is exactly the opposite.
|
||||
--vae-tile-size tile size for vae tiling, format [X]x[Y] (default: 32x32)
|
||||
--vae-relative-tile-size relative tile size for vae tiling, format [X]x[Y], in fraction of image size if < 1, in number of tiles per dim if >=1
|
||||
(overrides --vae-tile-size)
|
||||
|
||||
Default Generation Options:
|
||||
-p, --prompt <string> the prompt to render
|
||||
-n, --negative-prompt <string> the negative prompt (default: "")
|
||||
-i, --init-img <string> path to the init image
|
||||
--end-img <string> path to the end image, required by flf2v
|
||||
--mask <string> path to the mask image
|
||||
--control-image <string> path to control image, control net
|
||||
--control-video <string> path to control video frames, It must be a directory path. The video frames inside should be stored as images in
|
||||
lexicographical (character) order. For example, if the control video path is
|
||||
`frames`, the directory contain images such as 00.png, 01.png, ... etc.
|
||||
--pm-id-images-dir <string> path to PHOTOMAKER input id images dir
|
||||
--pm-id-embed-path <string> path to PHOTOMAKER v2 id embed
|
||||
-H, --height <int> image height, in pixel space (default: 512)
|
||||
-W, --width <int> image width, in pixel space (default: 512)
|
||||
--steps <int> number of sample steps (default: 20)
|
||||
--high-noise-steps <int> (high noise) number of sample steps (default: -1 = auto)
|
||||
--clip-skip <int> ignore last layers of CLIP network; 1 ignores none, 2 ignores one layer (default: -1). <= 0 represents unspecified,
|
||||
will be 1 for SD1.x, 2 for SD2.x
|
||||
-b, --batch-count <int> batch count
|
||||
--video-frames <int> video frames (default: 1)
|
||||
--fps <int> fps (default: 24)
|
||||
--timestep-shift <int> shift timestep for NitroFusion models (default: 0). recommended N for NitroSD-Realism around 250 and 500 for
|
||||
NitroSD-Vibrant
|
||||
--upscale-repeats <int> Run the ESRGAN upscaler this many times (default: 1)
|
||||
--upscale-tile-size <int> tile size for ESRGAN upscaling (default: 128)
|
||||
--cfg-scale <float> unconditional guidance scale: (default: 7.0)
|
||||
--img-cfg-scale <float> image guidance scale for inpaint or instruct-pix2pix models: (default: same as --cfg-scale)
|
||||
--guidance <float> distilled guidance scale for models with guidance input (default: 3.5)
|
||||
--slg-scale <float> skip layer guidance (SLG) scale, only for DiT models: (default: 0). 0 means disabled, a value of 2.5 is nice for sd3.5
|
||||
medium
|
||||
--skip-layer-start <float> SLG enabling point (default: 0.01)
|
||||
--skip-layer-end <float> SLG disabling point (default: 0.2)
|
||||
--eta <float> eta in DDIM, only for DDIM and TCD (default: 0)
|
||||
--high-noise-cfg-scale <float> (high noise) unconditional guidance scale: (default: 7.0)
|
||||
--high-noise-img-cfg-scale <float> (high noise) image guidance scale for inpaint or instruct-pix2pix models (default: same as --cfg-scale)
|
||||
--high-noise-guidance <float> (high noise) distilled guidance scale for models with guidance input (default: 3.5)
|
||||
--high-noise-slg-scale <float> (high noise) skip layer guidance (SLG) scale, only for DiT models: (default: 0)
|
||||
--high-noise-skip-layer-start <float> (high noise) SLG enabling point (default: 0.01)
|
||||
--high-noise-skip-layer-end <float> (high noise) SLG disabling point (default: 0.2)
|
||||
--high-noise-eta <float> (high noise) eta in DDIM, only for DDIM and TCD (default: 0)
|
||||
--strength <float> strength for noising/unnoising (default: 0.75)
|
||||
--pm-style-strength <float>
|
||||
--control-strength <float> strength to apply Control Net (default: 0.9). 1.0 corresponds to full destruction of information in init image
|
||||
--moe-boundary <float> timestep boundary for Wan2.2 MoE model. (default: 0.875). Only enabled if `--high-noise-steps` is set to -1
|
||||
--vace-strength <float> wan vace strength
|
||||
--increase-ref-index automatically increase the indices of references images based on the order they are listed (starting with 1).
|
||||
--disable-auto-resize-ref-image disable auto resize of ref images
|
||||
-s, --seed RNG seed (default: 42, use random seed for < 0)
|
||||
--sampling-method sampling method, one of [euler, euler_a, heun, dpm2, dpm++2s_a, dpm++2m, dpm++2mv2, ipndm, ipndm_v, lcm, ddim_trailing,
|
||||
tcd] (default: euler for Flux/SD3/Wan, euler_a otherwise)
|
||||
--high-noise-sampling-method (high noise) sampling method, one of [euler, euler_a, heun, dpm2, dpm++2s_a, dpm++2m, dpm++2mv2, ipndm, ipndm_v, lcm,
|
||||
ddim_trailing, tcd] default: euler for Flux/SD3/Wan, euler_a otherwise
|
||||
--scheduler denoiser sigma scheduler, one of [discrete, karras, exponential, ays, gits, smoothstep, sgm_uniform, simple,
|
||||
kl_optimal, lcm], default: discrete
|
||||
--sigmas custom sigma values for the sampler, comma-separated (e.g., "14.61,7.8,3.5,0.0").
|
||||
--skip-layers layers to skip for SLG steps (default: [7,8,9])
|
||||
--high-noise-skip-layers (high noise) layers to skip for SLG steps (default: [7,8,9])
|
||||
-r, --ref-image reference image for Flux Kontext models (can be used multiple times)
|
||||
--cache-mode caching method: 'easycache' (DiT), 'ucache' (UNET), 'dbcache'/'taylorseer'/'cache-dit' (DiT block-level)
|
||||
--cache-option named cache params (key=value format, comma-separated). easycache/ucache:
|
||||
threshold=,start=,end=,decay=,relative=,reset=; dbcache/taylorseer/cache-dit: Fn=,Bn=,threshold=,warmup=. Examples:
|
||||
"threshold=0.25" or "threshold=1.5,reset=0"
|
||||
--cache-preset cache-dit preset: 'slow'/'s', 'medium'/'m', 'fast'/'f', 'ultra'/'u'
|
||||
--scm-mask SCM steps mask for cache-dit: comma-separated 0/1 (e.g., "1,1,1,0,0,1,0,0,1,0") - 1=compute, 0=can cache
|
||||
--scm-policy SCM policy: 'dynamic' (default) or 'static'
|
||||
.\bin\Release\sd-server.exe --diffusion-model ..\models\diffusion_models\z_image_turbo_bf16.safetensors --vae ..\models\vae\ae.sft --llm ..\models\text_encoders\qwen_3_4b.safetensors --diffusion-fa --offload-to-cpu -v --cfg-scale 1.0
|
||||
```
|
||||
|
||||
What this example does:
|
||||
|
||||
* `--diffusion-model` selects the standalone diffusion model
|
||||
* `--vae` selects the VAE decoder
|
||||
* `--llm` selects the text encoder / language model used by this pipeline
|
||||
* `--diffusion-fa` enables flash attention in the diffusion model
|
||||
* `--offload-to-cpu` reduces VRAM pressure by keeping weights in RAM when possible
|
||||
* `-v` enables verbose logging
|
||||
* `--cfg-scale 1.0` sets the default CFG scale for generation
|
||||
|
||||
After the server starts successfully:
|
||||
|
||||
* the web UI is available at `http://127.0.0.1:1234/`
|
||||
* the native async API is available under `/sdcpp/v1/...`
|
||||
* the compatibility APIs are available under `/v1/...` and `/sdapi/v1/...`
|
||||
|
||||
If you want to use a different host or port, pass:
|
||||
|
||||
```bash
|
||||
--listen-ip <ip> --listen-port <port>
|
||||
```
|
||||
|
||||
# Frontend
|
||||
|
||||
## Build with Frontend
|
||||
|
||||
The server can optionally build the web frontend and embed it into the binary as `gen_index_html.h`.
|
||||
|
||||
### Requirements
|
||||
|
||||
Install the following tools:
|
||||
|
||||
* **Node.js** ≥ 20
|
||||
https://nodejs.org/
|
||||
|
||||
* **pnpm** ≥ 10
|
||||
Install via npm:
|
||||
|
||||
```bash
|
||||
npm install -g pnpm
|
||||
```
|
||||
|
||||
Verify installation:
|
||||
|
||||
```bash
|
||||
node -v
|
||||
pnpm -v
|
||||
```
|
||||
|
||||
### Install frontend dependencies
|
||||
|
||||
Go to the frontend directory and install dependencies:
|
||||
|
||||
```bash
|
||||
cd examples/server/frontend
|
||||
pnpm install
|
||||
```
|
||||
|
||||
### Build the server with CMake
|
||||
|
||||
Enable the frontend build option when configuring CMake:
|
||||
|
||||
```bash
|
||||
cmake -B build -DSD_SERVER_BUILD_FRONTEND=ON
|
||||
cmake --build build --config Release
|
||||
```
|
||||
|
||||
If `pnpm` is available, the build system will automatically run:
|
||||
|
||||
```
|
||||
pnpm run build
|
||||
pnpm run build:header
|
||||
```
|
||||
|
||||
and embed the generated frontend into the server binary.
|
||||
|
||||
## Frontend Repository
|
||||
|
||||
The web frontend is maintained in a **separate repository**, https://github.com/leejet/sdcpp-webui.
|
||||
|
||||
If you want to modify the UI or frontend logic, please submit pull requests to the **frontend repository**.
|
||||
|
||||
This repository (`stable-diffusion.cpp`) only vendors the frontend periodically. Changes from the frontend repo are synchronized:
|
||||
|
||||
* approximately **every 1–2 weeks**, or
|
||||
* when there are **major frontend updates**
|
||||
|
||||
Because of this, frontend changes will **not appear here immediately** after being merged upstream.
|
||||
|
||||
## Using an external frontend
|
||||
|
||||
By default, the server uses the **embedded frontend** generated during the build (`gen_index_html.h`).
|
||||
|
||||
You can also serve a custom frontend file instead of the embedded one by using:
|
||||
|
||||
```bash
|
||||
--serve-html-path <path-to-index.html>
|
||||
```
|
||||
|
||||
For example:
|
||||
|
||||
```bash
|
||||
sd-server --serve-html-path ./index.html
|
||||
```
|
||||
|
||||
In this case, the server will load and serve the specified `index.html` file instead of the embedded frontend. This is useful when:
|
||||
|
||||
* developing or testing frontend changes
|
||||
* using a custom UI
|
||||
* avoiding rebuilding the binary after frontend modifications
|
||||
|
||||
# Usage
|
||||
|
||||
For detailed command-line arguments, run:
|
||||
|
||||
```bash
|
||||
./bin/sd-server -h
|
||||
```
|
||||
|
||||
@@ -0,0 +1,364 @@
|
||||
// Extracted from main.cpp during server refactor.
|
||||
|
||||
#include "async_jobs.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
|
||||
#include "common/log.h"
|
||||
#include "common/media_io.h"
|
||||
#include "common/resource_owners.hpp"
|
||||
|
||||
const char* async_job_kind_name(AsyncJobKind kind) {
|
||||
switch (kind) {
|
||||
case AsyncJobKind::ImgGen:
|
||||
return "img_gen";
|
||||
case AsyncJobKind::VidGen:
|
||||
return "vid_gen";
|
||||
default:
|
||||
return "img_gen";
|
||||
}
|
||||
}
|
||||
|
||||
const char* async_job_status_name(AsyncJobStatus status) {
|
||||
switch (status) {
|
||||
case AsyncJobStatus::Queued:
|
||||
return "queued";
|
||||
case AsyncJobStatus::Generating:
|
||||
return "generating";
|
||||
case AsyncJobStatus::Completed:
|
||||
return "completed";
|
||||
case AsyncJobStatus::Failed:
|
||||
return "failed";
|
||||
case AsyncJobStatus::Cancelled:
|
||||
return "cancelled";
|
||||
default:
|
||||
return "failed";
|
||||
}
|
||||
}
|
||||
|
||||
void purge_expired_jobs(AsyncJobManager& manager) {
|
||||
const int64_t now = unix_timestamp_now();
|
||||
|
||||
for (auto it = manager.expired_jobs.begin(); it != manager.expired_jobs.end();) {
|
||||
if (it->second <= now) {
|
||||
it = manager.expired_jobs.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
for (auto it = manager.jobs.begin(); it != manager.jobs.end();) {
|
||||
const auto& job = it->second;
|
||||
if (job->completed_at == 0) {
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
|
||||
int64_t ttl_seconds = job->status == AsyncJobStatus::Completed
|
||||
? manager.completed_ttl_seconds
|
||||
: manager.failed_ttl_seconds;
|
||||
if (now - job->completed_at >= ttl_seconds) {
|
||||
manager.expired_jobs[job->id] = now + std::max<int64_t>(ttl_seconds, 60);
|
||||
it = manager.jobs.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
size_t count_pending_jobs(const AsyncJobManager& manager) {
|
||||
size_t pending = 0;
|
||||
for (const auto& entry : manager.jobs) {
|
||||
if (entry.second->status == AsyncJobStatus::Queued ||
|
||||
entry.second->status == AsyncJobStatus::Generating) {
|
||||
++pending;
|
||||
}
|
||||
}
|
||||
return pending;
|
||||
}
|
||||
|
||||
std::string make_async_job_id(AsyncJobManager& manager) {
|
||||
std::ostringstream oss;
|
||||
oss << "job_" << std::hex << unix_timestamp_now() << "_" << std::setw(8)
|
||||
<< std::setfill('0') << manager.next_id++;
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
bool cancel_queued_job(AsyncJobManager& manager, AsyncGenerationJob& job) {
|
||||
auto new_end = std::remove(manager.queue.begin(), manager.queue.end(), job.id);
|
||||
if (new_end == manager.queue.end()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
manager.queue.erase(new_end, manager.queue.end());
|
||||
job.status = AsyncJobStatus::Cancelled;
|
||||
job.completed_at = unix_timestamp_now();
|
||||
job.result_images_b64.clear();
|
||||
job.result_media_b64.clear();
|
||||
job.result_media_mime_type.clear();
|
||||
job.result_frame_count = 0;
|
||||
job.result_fps = 0;
|
||||
job.error_code = "cancelled";
|
||||
job.error_message = "job cancelled by client";
|
||||
return true;
|
||||
}
|
||||
|
||||
json make_async_job_json(const AsyncJobManager& manager, const AsyncGenerationJob& job) {
|
||||
json result;
|
||||
result["id"] = job.id;
|
||||
result["kind"] = async_job_kind_name(job.kind);
|
||||
result["status"] = async_job_status_name(job.status);
|
||||
result["created"] = job.created_at;
|
||||
result["started"] = job.started_at == 0 ? json(nullptr) : json(job.started_at);
|
||||
result["completed"] = job.completed_at == 0 ? json(nullptr) : json(job.completed_at);
|
||||
result["queue_position"] = 0;
|
||||
|
||||
if (job.status == AsyncJobStatus::Queued) {
|
||||
size_t position = 1;
|
||||
for (const auto& queued_id : manager.queue) {
|
||||
if (queued_id == job.id) {
|
||||
result["queue_position"] = position;
|
||||
break;
|
||||
}
|
||||
++position;
|
||||
}
|
||||
}
|
||||
|
||||
if (job.status == AsyncJobStatus::Completed) {
|
||||
if (job.kind == AsyncJobKind::VidGen) {
|
||||
result["result"] = {
|
||||
{"output_format", job.vid_gen.output_format},
|
||||
{"mime_type", job.result_media_mime_type},
|
||||
{"fps", job.result_fps},
|
||||
{"frame_count", job.result_frame_count},
|
||||
{"b64_json", job.result_media_b64},
|
||||
};
|
||||
} else {
|
||||
json images = json::array();
|
||||
for (size_t i = 0; i < job.result_images_b64.size(); ++i) {
|
||||
images.push_back({{"index", i}, {"b64_json", job.result_images_b64[i]}});
|
||||
}
|
||||
result["result"] = {
|
||||
{"output_format", job.img_gen.output_format},
|
||||
{"images", images},
|
||||
};
|
||||
}
|
||||
result["error"] = nullptr;
|
||||
} else if (job.status == AsyncJobStatus::Failed ||
|
||||
job.status == AsyncJobStatus::Cancelled) {
|
||||
result["result"] = nullptr;
|
||||
result["error"] = {
|
||||
{"code",
|
||||
job.error_code.empty()
|
||||
? (job.status == AsyncJobStatus::Cancelled ? "cancelled" : "generation_failed")
|
||||
: job.error_code},
|
||||
{"message", job.error_message},
|
||||
};
|
||||
} else {
|
||||
result["result"] = nullptr;
|
||||
result["error"] = nullptr;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool execute_img_gen_job(ServerRuntime& runtime,
|
||||
AsyncGenerationJob& job,
|
||||
std::vector<std::string>& output_images,
|
||||
std::string& error_message) {
|
||||
sd_img_gen_params_t params = job.img_gen.to_sd_img_gen_params_t();
|
||||
|
||||
SDImageVec results;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(*runtime.sd_ctx_mutex);
|
||||
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;
|
||||
}
|
||||
results.adopt(raw_results, num_results);
|
||||
}
|
||||
|
||||
const int num_results = results.count();
|
||||
if (num_results <= 0) {
|
||||
error_message = "generate_image returned no results";
|
||||
return false;
|
||||
}
|
||||
|
||||
EncodedImageFormat encoded_format = EncodedImageFormat::PNG;
|
||||
if (job.img_gen.output_format == "jpeg") {
|
||||
encoded_format = EncodedImageFormat::JPEG;
|
||||
} else if (job.img_gen.output_format == "webp") {
|
||||
encoded_format = EncodedImageFormat::WEBP;
|
||||
}
|
||||
|
||||
int batch_count = job.img_gen.gen_params.batch_count;
|
||||
int images_per_batch = batch_count > 0 ? std::max(1, num_results / batch_count) : 1;
|
||||
for (int i = 0; i < num_results; ++i) {
|
||||
if (results[i].data == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::string metadata = job.img_gen.gen_params.embed_image_metadata
|
||||
? get_image_params(*runtime.ctx_params,
|
||||
job.img_gen.gen_params,
|
||||
job.img_gen.gen_params.seed + i / images_per_batch)
|
||||
: "";
|
||||
auto image_bytes = encode_image_to_vector(encoded_format,
|
||||
results[i].data,
|
||||
results[i].width,
|
||||
results[i].height,
|
||||
results[i].channel,
|
||||
metadata,
|
||||
job.img_gen.output_compression);
|
||||
if (image_bytes.empty()) {
|
||||
continue;
|
||||
}
|
||||
output_images.push_back(base64_encode(image_bytes));
|
||||
}
|
||||
|
||||
if (output_images.empty()) {
|
||||
error_message = "generate_image returned empty encoded outputs";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool execute_vid_gen_job(ServerRuntime& runtime,
|
||||
AsyncGenerationJob& job,
|
||||
std::string& output_media_b64,
|
||||
std::string& output_media_mime_type,
|
||||
int& output_frame_count,
|
||||
int& output_fps,
|
||||
std::string& error_message) {
|
||||
sd_vid_gen_params_t params = job.vid_gen.to_sd_vid_gen_params_t();
|
||||
|
||||
SDImageVec results;
|
||||
int num_results = 0;
|
||||
sd_audio_t* generated_audio = nullptr;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(*runtime.sd_ctx_mutex);
|
||||
sd_image_t* raw_results = nullptr;
|
||||
if (!generate_video(runtime.sd_ctx, ¶ms, &raw_results, &num_results, &generated_audio)) {
|
||||
raw_results = nullptr;
|
||||
}
|
||||
results.adopt(raw_results, num_results);
|
||||
}
|
||||
|
||||
num_results = results.count();
|
||||
if (num_results <= 0) {
|
||||
free_sd_audio(generated_audio);
|
||||
error_message = "generate_video returned no results";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> video_bytes = create_video_from_sd_images_to_vector(job.vid_gen.output_format,
|
||||
results.data(),
|
||||
num_results,
|
||||
job.vid_gen.gen_params.fps,
|
||||
job.vid_gen.output_compression,
|
||||
generated_audio);
|
||||
free_sd_audio(generated_audio);
|
||||
if (video_bytes.empty()) {
|
||||
error_message = "failed to encode generated video container";
|
||||
return false;
|
||||
}
|
||||
|
||||
output_media_b64 = base64_encode(video_bytes);
|
||||
output_media_mime_type = video_mime_type(job.vid_gen.output_format);
|
||||
output_frame_count = num_results;
|
||||
output_fps = job.vid_gen.gen_params.fps;
|
||||
return true;
|
||||
}
|
||||
|
||||
void async_job_worker(ServerRuntime& runtime) {
|
||||
AsyncJobManager& manager = *runtime.async_job_manager;
|
||||
|
||||
while (true) {
|
||||
std::shared_ptr<AsyncGenerationJob> job;
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(manager.mutex);
|
||||
manager.cv.wait(lock, [&]() { return manager.stop || !manager.queue.empty(); });
|
||||
|
||||
if (manager.stop && manager.queue.empty()) {
|
||||
break;
|
||||
}
|
||||
|
||||
purge_expired_jobs(manager);
|
||||
if (manager.queue.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::string job_id = manager.queue.front();
|
||||
manager.queue.pop_front();
|
||||
|
||||
auto it = manager.jobs.find(job_id);
|
||||
if (it == manager.jobs.end()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
job = it->second;
|
||||
job->status = AsyncJobStatus::Generating;
|
||||
job->started_at = unix_timestamp_now();
|
||||
}
|
||||
|
||||
std::vector<std::string> output_images;
|
||||
std::string output_media_b64;
|
||||
std::string output_media_mime_type;
|
||||
int output_frame_count = 0;
|
||||
int output_fps = 0;
|
||||
std::string error_message;
|
||||
bool ok = false;
|
||||
|
||||
if (job->kind == AsyncJobKind::ImgGen) {
|
||||
ok = execute_img_gen_job(runtime, *job, output_images, error_message);
|
||||
} else if (job->kind == AsyncJobKind::VidGen) {
|
||||
ok = execute_vid_gen_job(runtime,
|
||||
*job,
|
||||
output_media_b64,
|
||||
output_media_mime_type,
|
||||
output_frame_count,
|
||||
output_fps,
|
||||
error_message);
|
||||
} else {
|
||||
error_message = "unsupported job kind";
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(manager.mutex);
|
||||
auto it = manager.jobs.find(job->id);
|
||||
if (it == manager.jobs.end()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
job->completed_at = unix_timestamp_now();
|
||||
if (ok) {
|
||||
job->status = AsyncJobStatus::Completed;
|
||||
job->result_images_b64 = std::move(output_images);
|
||||
job->result_media_b64 = std::move(output_media_b64);
|
||||
job->result_media_mime_type = std::move(output_media_mime_type);
|
||||
job->result_frame_count = output_frame_count;
|
||||
job->result_fps = output_fps;
|
||||
job->error_code.clear();
|
||||
job->error_message.clear();
|
||||
} else {
|
||||
job->status = AsyncJobStatus::Failed;
|
||||
job->error_code = "generation_failed";
|
||||
job->error_message = error_message.empty() ? "unknown generation error" : error_message;
|
||||
job->result_images_b64.clear();
|
||||
job->result_media_b64.clear();
|
||||
job->result_media_mime_type.clear();
|
||||
job->result_frame_count = 0;
|
||||
job->result_fps = 0;
|
||||
}
|
||||
|
||||
purge_expired_jobs(manager);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
#pragma once
|
||||
|
||||
#include <condition_variable>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
|
||||
#include "runtime.h"
|
||||
|
||||
enum class AsyncJobKind {
|
||||
ImgGen,
|
||||
VidGen,
|
||||
};
|
||||
|
||||
enum class AsyncJobStatus {
|
||||
Queued,
|
||||
Generating,
|
||||
Completed,
|
||||
Failed,
|
||||
Cancelled,
|
||||
};
|
||||
|
||||
const char* async_job_kind_name(AsyncJobKind kind);
|
||||
const char* async_job_status_name(AsyncJobStatus status);
|
||||
|
||||
struct AsyncGenerationJob {
|
||||
std::string id;
|
||||
AsyncJobKind kind = AsyncJobKind::ImgGen;
|
||||
AsyncJobStatus status = AsyncJobStatus::Queued;
|
||||
int64_t created_at = unix_timestamp_now();
|
||||
int64_t started_at = 0;
|
||||
int64_t completed_at = 0;
|
||||
ImgGenJobRequest img_gen;
|
||||
VidGenJobRequest vid_gen;
|
||||
std::vector<std::string> result_images_b64;
|
||||
std::string result_media_b64;
|
||||
std::string result_media_mime_type;
|
||||
int result_frame_count = 0;
|
||||
int result_fps = 0;
|
||||
std::string error_code;
|
||||
std::string error_message;
|
||||
};
|
||||
|
||||
struct AsyncJobManager {
|
||||
std::mutex mutex;
|
||||
std::condition_variable cv;
|
||||
std::unordered_map<std::string, std::shared_ptr<AsyncGenerationJob>> jobs;
|
||||
std::unordered_map<std::string, int64_t> expired_jobs;
|
||||
std::deque<std::string> queue;
|
||||
uint64_t next_id = 0;
|
||||
bool stop = false;
|
||||
size_t max_pending_jobs = 64;
|
||||
int64_t completed_ttl_seconds = 600;
|
||||
int64_t failed_ttl_seconds = 600;
|
||||
};
|
||||
|
||||
void purge_expired_jobs(AsyncJobManager& manager);
|
||||
size_t count_pending_jobs(const AsyncJobManager& manager);
|
||||
std::string make_async_job_id(AsyncJobManager& manager);
|
||||
bool cancel_queued_job(AsyncJobManager& manager, AsyncGenerationJob& job);
|
||||
json make_async_job_json(const AsyncJobManager& manager, const AsyncGenerationJob& job);
|
||||
bool execute_img_gen_job(ServerRuntime& runtime,
|
||||
AsyncGenerationJob& job,
|
||||
std::vector<std::string>& output_images,
|
||||
std::string& error_message);
|
||||
bool execute_vid_gen_job(ServerRuntime& runtime,
|
||||
AsyncGenerationJob& job,
|
||||
std::string& output_media_b64,
|
||||
std::string& output_media_mime_type,
|
||||
int& output_frame_count,
|
||||
int& output_fps,
|
||||
std::string& error_message);
|
||||
void async_job_worker(ServerRuntime& runtime);
|
||||
@@ -1,191 +1,25 @@
|
||||
// main.cpp
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <mutex>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "httplib.h"
|
||||
#include "stable-diffusion.h"
|
||||
|
||||
#include "common/common.hpp"
|
||||
#include "async_jobs.h"
|
||||
#include "common/common.h"
|
||||
#include "common/resource_owners.hpp"
|
||||
#include "routes.h"
|
||||
#include "runtime.h"
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
// ----------------------- helpers -----------------------
|
||||
static const std::string base64_chars =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
"abcdefghijklmnopqrstuvwxyz"
|
||||
"0123456789+/";
|
||||
|
||||
std::string base64_encode(const std::vector<uint8_t>& bytes) {
|
||||
std::string ret;
|
||||
int val = 0, valb = -6;
|
||||
for (uint8_t c : bytes) {
|
||||
val = (val << 8) + c;
|
||||
valb += 8;
|
||||
while (valb >= 0) {
|
||||
ret.push_back(base64_chars[(val >> valb) & 0x3F]);
|
||||
valb -= 6;
|
||||
}
|
||||
}
|
||||
if (valb > -6)
|
||||
ret.push_back(base64_chars[((val << 8) >> (valb + 8)) & 0x3F]);
|
||||
while (ret.size() % 4)
|
||||
ret.push_back('=');
|
||||
return ret;
|
||||
}
|
||||
|
||||
inline bool is_base64(unsigned char c) {
|
||||
return (isalnum(c) || (c == '+') || (c == '/'));
|
||||
}
|
||||
|
||||
std::vector<uint8_t> base64_decode(const std::string& encoded_string) {
|
||||
int in_len = static_cast<int>(encoded_string.size());
|
||||
int i = 0;
|
||||
int j = 0;
|
||||
int in_ = 0;
|
||||
uint8_t char_array_4[4], char_array_3[3];
|
||||
std::vector<uint8_t> ret;
|
||||
|
||||
while (in_len-- && (encoded_string[in_] != '=') && is_base64(encoded_string[in_])) {
|
||||
char_array_4[i++] = encoded_string[in_];
|
||||
in_++;
|
||||
if (i == 4) {
|
||||
for (i = 0; i < 4; i++)
|
||||
char_array_4[i] = static_cast<uint8_t>(base64_chars.find(char_array_4[i]));
|
||||
|
||||
char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4);
|
||||
char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
|
||||
char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3];
|
||||
|
||||
for (i = 0; i < 3; i++)
|
||||
ret.push_back(char_array_3[i]);
|
||||
i = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (i) {
|
||||
for (j = i; j < 4; j++)
|
||||
char_array_4[j] = 0;
|
||||
|
||||
for (j = 0; j < 4; j++)
|
||||
char_array_4[j] = static_cast<uint8_t>(base64_chars.find(char_array_4[j]));
|
||||
|
||||
char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4);
|
||||
char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
|
||||
char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3];
|
||||
|
||||
for (j = 0; j < i - 1; j++)
|
||||
ret.push_back(char_array_3[j]);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::string iso_timestamp_now() {
|
||||
using namespace std::chrono;
|
||||
auto now = system_clock::now();
|
||||
std::time_t t = system_clock::to_time_t(now);
|
||||
std::tm tm{};
|
||||
#ifdef _MSC_VER
|
||||
gmtime_s(&tm, &t);
|
||||
#else
|
||||
gmtime_r(&t, &tm);
|
||||
#ifdef HAVE_INDEX_HTML
|
||||
#include "frontend/dist/gen_index_html.h"
|
||||
#endif
|
||||
std::ostringstream oss;
|
||||
oss << std::put_time(&tm, "%Y-%m-%dT%H:%M:%SZ");
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
struct SDSvrParams {
|
||||
std::string listen_ip = "127.0.0.1";
|
||||
int listen_port = 1234;
|
||||
std::string serve_html_path;
|
||||
bool normal_exit = false;
|
||||
bool verbose = false;
|
||||
bool color = false;
|
||||
|
||||
ArgOptions get_options() {
|
||||
ArgOptions options;
|
||||
|
||||
options.string_options = {
|
||||
{"-l",
|
||||
"--listen-ip",
|
||||
"server listen ip (default: 127.0.0.1)",
|
||||
&listen_ip},
|
||||
{"",
|
||||
"--serve-html-path",
|
||||
"path to HTML file to serve at root (optional)",
|
||||
&serve_html_path}};
|
||||
|
||||
options.int_options = {
|
||||
{"",
|
||||
"--listen-port",
|
||||
"server listen port (default: 1234)",
|
||||
&listen_port},
|
||||
};
|
||||
|
||||
options.bool_options = {
|
||||
{"-v",
|
||||
"--verbose",
|
||||
"print extra info",
|
||||
true, &verbose},
|
||||
{"",
|
||||
"--color",
|
||||
"colors the logging tags according to level",
|
||||
true, &color},
|
||||
};
|
||||
|
||||
auto on_help_arg = [&](int argc, const char** argv, int index) {
|
||||
normal_exit = true;
|
||||
return -1;
|
||||
};
|
||||
|
||||
options.manual_options = {
|
||||
{"-h",
|
||||
"--help",
|
||||
"show this help message and exit",
|
||||
on_help_arg},
|
||||
};
|
||||
return options;
|
||||
};
|
||||
|
||||
bool process_and_check() {
|
||||
if (listen_ip.empty()) {
|
||||
LOG_ERROR("error: the following arguments are required: listen_ip");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (listen_port < 0 || listen_port > 65535) {
|
||||
LOG_ERROR("error: listen_port should be in the range [0, 65535]");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!serve_html_path.empty() && !fs::exists(serve_html_path)) {
|
||||
LOG_ERROR("error: serve_html_path file does not exist: %s", serve_html_path.c_str());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string to_string() const {
|
||||
std::ostringstream oss;
|
||||
oss << "SDSvrParams {\n"
|
||||
<< " listen_ip: " << listen_ip << ",\n"
|
||||
<< " listen_port: \"" << listen_port << "\",\n"
|
||||
<< " serve_html_path: \"" << serve_html_path << "\",\n"
|
||||
<< "}";
|
||||
return oss.str();
|
||||
}
|
||||
};
|
||||
|
||||
void print_usage(int argc, const char* argv[], const std::vector<ArgOptions>& options_list) {
|
||||
static void print_usage(const char* argv0, const std::vector<ArgOptions>& options_list) {
|
||||
std::cout << version_string() << "\n";
|
||||
std::cout << "Usage: " << argv[0] << " [options]\n\n";
|
||||
std::cout << "Usage: " << argv0 << " [options]\n\n";
|
||||
std::cout << "Svr Options:\n";
|
||||
options_list[0].print();
|
||||
std::cout << "\nContext Options:\n";
|
||||
@@ -194,20 +28,30 @@ void print_usage(int argc, const char* argv[], const std::vector<ArgOptions>& op
|
||||
options_list[2].print();
|
||||
}
|
||||
|
||||
void parse_args(int argc, const char** argv, SDSvrParams& svr_params, SDContextParams& ctx_params, SDGenerationParams& default_gen_params) {
|
||||
std::vector<ArgOptions> options_vec = {svr_params.get_options(), ctx_params.get_options(), default_gen_params.get_options()};
|
||||
static void parse_args(int argc,
|
||||
const char** argv,
|
||||
SDSvrParams& svr_params,
|
||||
SDContextParams& ctx_params,
|
||||
SDGenerationParams& default_gen_params) {
|
||||
std::vector<ArgOptions> options_vec = {
|
||||
svr_params.get_options(),
|
||||
ctx_params.get_options(),
|
||||
default_gen_params.get_options(),
|
||||
};
|
||||
|
||||
if (!parse_options(argc, argv, options_vec)) {
|
||||
print_usage(argc, argv, options_vec);
|
||||
print_usage(argv[0], options_vec);
|
||||
exit(svr_params.normal_exit ? 0 : 1);
|
||||
}
|
||||
|
||||
const bool random_seed_requested = default_gen_params.seed < 0;
|
||||
|
||||
if (!svr_params.process_and_check() ||
|
||||
!ctx_params.process_and_check(IMG_GEN) ||
|
||||
!default_gen_params.process_and_check(IMG_GEN, ctx_params.lora_model_dir)) {
|
||||
print_usage(argc, argv, options_vec);
|
||||
if (!svr_params.resolve_and_validate() ||
|
||||
!ctx_params.resolve_and_validate(IMG_GEN) ||
|
||||
!default_gen_params.resolve_and_validate(IMG_GEN,
|
||||
ctx_params.lora_model_dir,
|
||||
ctx_params.hires_upscalers_dir)) {
|
||||
print_usage(argv[0], options_vec);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
@@ -216,63 +60,6 @@ void parse_args(int argc, const char** argv, SDSvrParams& svr_params, SDContextP
|
||||
}
|
||||
}
|
||||
|
||||
std::string extract_and_remove_sd_cpp_extra_args(std::string& text) {
|
||||
std::regex re("<sd_cpp_extra_args>(.*?)</sd_cpp_extra_args>");
|
||||
std::smatch match;
|
||||
|
||||
std::string extracted;
|
||||
if (std::regex_search(text, match, re)) {
|
||||
extracted = match[1].str();
|
||||
text = std::regex_replace(text, re, "");
|
||||
}
|
||||
return extracted;
|
||||
}
|
||||
|
||||
enum class ImageFormat { JPEG,
|
||||
PNG };
|
||||
|
||||
std::vector<uint8_t> write_image_to_vector(
|
||||
ImageFormat format,
|
||||
const uint8_t* image,
|
||||
int width,
|
||||
int height,
|
||||
int channels,
|
||||
int quality = 90) {
|
||||
std::vector<uint8_t> buffer;
|
||||
|
||||
auto write_func = [&buffer](void* context, void* data, int size) {
|
||||
uint8_t* src = reinterpret_cast<uint8_t*>(data);
|
||||
buffer.insert(buffer.end(), src, src + size);
|
||||
};
|
||||
|
||||
struct ContextWrapper {
|
||||
decltype(write_func)& func;
|
||||
} ctx{write_func};
|
||||
|
||||
auto c_func = [](void* context, void* data, int size) {
|
||||
auto* wrapper = reinterpret_cast<ContextWrapper*>(context);
|
||||
wrapper->func(context, data, size);
|
||||
};
|
||||
|
||||
int result = 0;
|
||||
switch (format) {
|
||||
case ImageFormat::JPEG:
|
||||
result = stbi_write_jpg_to_func(c_func, &ctx, width, height, channels, image, quality);
|
||||
break;
|
||||
case ImageFormat::PNG:
|
||||
result = stbi_write_png_to_func(c_func, &ctx, width, height, channels, image, width * channels);
|
||||
break;
|
||||
default:
|
||||
throw std::runtime_error("invalid image format");
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
throw std::runtime_error("write imgage to mem failed");
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
void sd_log_cb(enum sd_log_level_t level, const char* log, void* data) {
|
||||
SDSvrParams* svr_params = (SDSvrParams*)data;
|
||||
log_print(level, log, svr_params->verbose, svr_params->color);
|
||||
@@ -298,8 +85,8 @@ int main(int argc, const char** argv) {
|
||||
LOG_DEBUG("%s", ctx_params.to_string().c_str());
|
||||
LOG_DEBUG("%s", default_gen_params.to_string().c_str());
|
||||
|
||||
sd_ctx_params_t sd_ctx_params = ctx_params.to_sd_ctx_params_t(false, false, false);
|
||||
sd_ctx_t* sd_ctx = new_sd_ctx(&sd_ctx_params);
|
||||
sd_ctx_params_t sd_ctx_params = ctx_params.to_sd_ctx_params_t(false);
|
||||
SDCtxPtr sd_ctx(new_sd_ctx(&sd_ctx_params));
|
||||
|
||||
if (sd_ctx == nullptr) {
|
||||
LOG_ERROR("new_sd_ctx_t failed");
|
||||
@@ -308,6 +95,26 @@ int main(int argc, const char** argv) {
|
||||
|
||||
std::mutex sd_ctx_mutex;
|
||||
|
||||
std::vector<LoraEntry> lora_cache;
|
||||
std::mutex lora_mutex;
|
||||
std::vector<UpscalerEntry> upscaler_cache;
|
||||
std::mutex upscaler_mutex;
|
||||
AsyncJobManager async_job_manager;
|
||||
ServerRuntime runtime = {
|
||||
sd_ctx.get(),
|
||||
&sd_ctx_mutex,
|
||||
&svr_params,
|
||||
&ctx_params,
|
||||
&default_gen_params,
|
||||
&lora_cache,
|
||||
&lora_mutex,
|
||||
&upscaler_cache,
|
||||
&upscaler_mutex,
|
||||
&async_job_manager,
|
||||
};
|
||||
|
||||
std::thread async_worker(async_job_worker, std::ref(runtime));
|
||||
|
||||
httplib::Server svr;
|
||||
|
||||
svr.set_pre_routing_handler([](const httplib::Request& req, httplib::Response& res) {
|
||||
@@ -327,409 +134,25 @@ int main(int argc, const char** argv) {
|
||||
return httplib::Server::HandlerResponse::Unhandled;
|
||||
});
|
||||
|
||||
// health
|
||||
svr.Get("/", [&](const httplib::Request&, httplib::Response& res) {
|
||||
if (!svr_params.serve_html_path.empty()) {
|
||||
std::ifstream file(svr_params.serve_html_path);
|
||||
if (file) {
|
||||
std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
|
||||
res.set_content(content, "text/html");
|
||||
} else {
|
||||
res.status = 500;
|
||||
res.set_content("Error: Unable to read HTML file", "text/plain");
|
||||
}
|
||||
} else {
|
||||
res.set_content("Stable Diffusion Server is running", "text/plain");
|
||||
}
|
||||
});
|
||||
std::string index_html;
|
||||
#ifdef HAVE_INDEX_HTML
|
||||
index_html.assign(reinterpret_cast<const char*>(index_html_bytes), index_html_size);
|
||||
#else
|
||||
index_html = "Stable Diffusion Server is running";
|
||||
#endif
|
||||
register_index_endpoints(svr, svr_params, index_html);
|
||||
register_openai_api_endpoints(svr, runtime);
|
||||
register_sdapi_endpoints(svr, runtime);
|
||||
register_sdcpp_api_endpoints(svr, runtime);
|
||||
|
||||
// models endpoint (minimal)
|
||||
svr.Get("/v1/models", [&](const httplib::Request&, httplib::Response& res) {
|
||||
json r;
|
||||
r["data"] = json::array();
|
||||
r["data"].push_back({{"id", "sd-cpp-local"}, {"object", "model"}, {"owned_by", "local"}});
|
||||
res.set_content(r.dump(), "application/json");
|
||||
});
|
||||
|
||||
// core endpoint: /v1/images/generations
|
||||
svr.Post("/v1/images/generations", [&](const httplib::Request& req, httplib::Response& res) {
|
||||
try {
|
||||
if (req.body.empty()) {
|
||||
res.status = 400;
|
||||
res.set_content(R"({"error":"empty body"})", "application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
json j = json::parse(req.body);
|
||||
std::string prompt = j.value("prompt", "");
|
||||
int n = std::max(1, j.value("n", 1));
|
||||
std::string size = j.value("size", "");
|
||||
std::string output_format = j.value("output_format", "png");
|
||||
int output_compression = j.value("output_compression", 100);
|
||||
int width = 512;
|
||||
int height = 512;
|
||||
if (!size.empty()) {
|
||||
auto pos = size.find('x');
|
||||
if (pos != std::string::npos) {
|
||||
try {
|
||||
width = std::stoi(size.substr(0, pos));
|
||||
height = std::stoi(size.substr(pos + 1));
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (prompt.empty()) {
|
||||
res.status = 400;
|
||||
res.set_content(R"({"error":"prompt required"})", "application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
std::string sd_cpp_extra_args_str = extract_and_remove_sd_cpp_extra_args(prompt);
|
||||
|
||||
if (output_format != "png" && output_format != "jpeg") {
|
||||
res.status = 400;
|
||||
res.set_content(R"({"error":"invalid output_format, must be one of [png, jpeg]"})", "application/json");
|
||||
return;
|
||||
}
|
||||
if (n <= 0)
|
||||
n = 1;
|
||||
if (n > 8)
|
||||
n = 8; // safety
|
||||
if (output_compression > 100) {
|
||||
output_compression = 100;
|
||||
}
|
||||
if (output_compression < 0) {
|
||||
output_compression = 0;
|
||||
}
|
||||
|
||||
json out;
|
||||
out["created"] = iso_timestamp_now();
|
||||
out["data"] = json::array();
|
||||
out["output_format"] = output_format;
|
||||
|
||||
SDGenerationParams gen_params = default_gen_params;
|
||||
gen_params.prompt = prompt;
|
||||
gen_params.width = width;
|
||||
gen_params.height = height;
|
||||
gen_params.batch_count = n;
|
||||
|
||||
if (!sd_cpp_extra_args_str.empty() && !gen_params.from_json_str(sd_cpp_extra_args_str)) {
|
||||
res.status = 400;
|
||||
res.set_content(R"({"error":"invalid sd_cpp_extra_args"})", "application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!gen_params.process_and_check(IMG_GEN, "")) {
|
||||
res.status = 400;
|
||||
res.set_content(R"({"error":"invalid params"})", "application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_DEBUG("%s\n", gen_params.to_string().c_str());
|
||||
|
||||
sd_image_t init_image = {(uint32_t)gen_params.width, (uint32_t)gen_params.height, 3, nullptr};
|
||||
sd_image_t control_image = {(uint32_t)gen_params.width, (uint32_t)gen_params.height, 3, nullptr};
|
||||
sd_image_t mask_image = {(uint32_t)gen_params.width, (uint32_t)gen_params.height, 1, nullptr};
|
||||
std::vector<sd_image_t> pmid_images;
|
||||
|
||||
sd_img_gen_params_t img_gen_params = {
|
||||
gen_params.lora_vec.data(),
|
||||
static_cast<uint32_t>(gen_params.lora_vec.size()),
|
||||
gen_params.prompt.c_str(),
|
||||
gen_params.negative_prompt.c_str(),
|
||||
gen_params.clip_skip,
|
||||
init_image,
|
||||
nullptr,
|
||||
0,
|
||||
gen_params.auto_resize_ref_image,
|
||||
gen_params.increase_ref_index,
|
||||
mask_image,
|
||||
gen_params.width,
|
||||
gen_params.height,
|
||||
gen_params.sample_params,
|
||||
gen_params.strength,
|
||||
gen_params.seed,
|
||||
gen_params.batch_count,
|
||||
control_image,
|
||||
gen_params.control_strength,
|
||||
{
|
||||
pmid_images.data(),
|
||||
(int)pmid_images.size(),
|
||||
gen_params.pm_id_embed_path.c_str(),
|
||||
gen_params.pm_style_strength,
|
||||
}, // pm_params
|
||||
ctx_params.vae_tiling_params,
|
||||
gen_params.cache_params,
|
||||
};
|
||||
|
||||
sd_image_t* results = nullptr;
|
||||
int num_results = 0;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(sd_ctx_mutex);
|
||||
results = generate_image(sd_ctx, &img_gen_params);
|
||||
num_results = gen_params.batch_count;
|
||||
}
|
||||
|
||||
for (int i = 0; i < num_results; i++) {
|
||||
if (results[i].data == nullptr) {
|
||||
continue;
|
||||
}
|
||||
auto image_bytes = write_image_to_vector(output_format == "jpeg" ? ImageFormat::JPEG : ImageFormat::PNG,
|
||||
results[i].data,
|
||||
results[i].width,
|
||||
results[i].height,
|
||||
results[i].channel,
|
||||
output_compression);
|
||||
if (image_bytes.empty()) {
|
||||
LOG_ERROR("write image to mem failed");
|
||||
continue;
|
||||
}
|
||||
|
||||
// base64 encode
|
||||
std::string b64 = base64_encode(image_bytes);
|
||||
json item;
|
||||
item["b64_json"] = b64;
|
||||
out["data"].push_back(item);
|
||||
}
|
||||
|
||||
res.set_content(out.dump(), "application/json");
|
||||
res.status = 200;
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
res.status = 500;
|
||||
json err;
|
||||
err["error"] = "server_error";
|
||||
err["message"] = e.what();
|
||||
res.set_content(err.dump(), "application/json");
|
||||
}
|
||||
});
|
||||
|
||||
svr.Post("/v1/images/edits", [&](const httplib::Request& req, httplib::Response& res) {
|
||||
try {
|
||||
if (!req.is_multipart_form_data()) {
|
||||
res.status = 400;
|
||||
res.set_content(R"({"error":"Content-Type must be multipart/form-data"})", "application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
std::string prompt = req.form.get_field("prompt");
|
||||
if (prompt.empty()) {
|
||||
res.status = 400;
|
||||
res.set_content(R"({"error":"prompt required"})", "application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
std::string sd_cpp_extra_args_str = extract_and_remove_sd_cpp_extra_args(prompt);
|
||||
|
||||
size_t image_count = req.form.get_file_count("image[]");
|
||||
if (image_count == 0) {
|
||||
res.status = 400;
|
||||
res.set_content(R"({"error":"at least one image[] required"})", "application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<std::vector<uint8_t>> images_bytes;
|
||||
for (size_t i = 0; i < image_count; i++) {
|
||||
auto file = req.form.get_file("image[]", i);
|
||||
images_bytes.emplace_back(file.content.begin(), file.content.end());
|
||||
}
|
||||
|
||||
std::vector<uint8_t> mask_bytes;
|
||||
if (req.form.has_file("mask")) {
|
||||
auto file = req.form.get_file("mask");
|
||||
mask_bytes.assign(file.content.begin(), file.content.end());
|
||||
}
|
||||
|
||||
int n = 1;
|
||||
if (req.form.has_field("n")) {
|
||||
try {
|
||||
n = std::stoi(req.form.get_field("n"));
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
n = std::clamp(n, 1, 8);
|
||||
|
||||
std::string size = req.form.get_field("size");
|
||||
int width = 512, height = 512;
|
||||
if (!size.empty()) {
|
||||
auto pos = size.find('x');
|
||||
if (pos != std::string::npos) {
|
||||
try {
|
||||
width = std::stoi(size.substr(0, pos));
|
||||
height = std::stoi(size.substr(pos + 1));
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::string output_format = "png";
|
||||
if (req.form.has_field("output_format"))
|
||||
output_format = req.form.get_field("output_format");
|
||||
if (output_format != "png" && output_format != "jpeg") {
|
||||
res.status = 400;
|
||||
res.set_content(R"({"error":"invalid output_format, must be one of [png, jpeg]"})", "application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
std::string output_compression_str = req.form.get_field("output_compression");
|
||||
int output_compression = 100;
|
||||
try {
|
||||
output_compression = std::stoi(output_compression_str);
|
||||
} catch (...) {
|
||||
}
|
||||
if (output_compression > 100) {
|
||||
output_compression = 100;
|
||||
}
|
||||
if (output_compression < 0) {
|
||||
output_compression = 0;
|
||||
}
|
||||
|
||||
SDGenerationParams gen_params = default_gen_params;
|
||||
gen_params.prompt = prompt;
|
||||
gen_params.width = width;
|
||||
gen_params.height = height;
|
||||
gen_params.batch_count = n;
|
||||
|
||||
if (!sd_cpp_extra_args_str.empty() && !gen_params.from_json_str(sd_cpp_extra_args_str)) {
|
||||
res.status = 400;
|
||||
res.set_content(R"({"error":"invalid sd_cpp_extra_args"})", "application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!gen_params.process_and_check(IMG_GEN, "")) {
|
||||
res.status = 400;
|
||||
res.set_content(R"({"error":"invalid params"})", "application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_DEBUG("%s\n", gen_params.to_string().c_str());
|
||||
|
||||
sd_image_t init_image = {(uint32_t)gen_params.width, (uint32_t)gen_params.height, 3, nullptr};
|
||||
sd_image_t control_image = {(uint32_t)gen_params.width, (uint32_t)gen_params.height, 3, nullptr};
|
||||
std::vector<sd_image_t> pmid_images;
|
||||
|
||||
std::vector<sd_image_t> ref_images;
|
||||
ref_images.reserve(images_bytes.size());
|
||||
for (auto& bytes : images_bytes) {
|
||||
int img_w = width;
|
||||
int img_h = height;
|
||||
uint8_t* raw_pixels = load_image_from_memory(
|
||||
reinterpret_cast<const char*>(bytes.data()),
|
||||
static_cast<int>(bytes.size()),
|
||||
img_w, img_h,
|
||||
width, height, 3);
|
||||
|
||||
if (!raw_pixels) {
|
||||
continue;
|
||||
}
|
||||
|
||||
sd_image_t img{(uint32_t)img_w, (uint32_t)img_h, 3, raw_pixels};
|
||||
ref_images.push_back(img);
|
||||
}
|
||||
|
||||
sd_image_t mask_image = {0};
|
||||
if (!mask_bytes.empty()) {
|
||||
int mask_w = width;
|
||||
int mask_h = height;
|
||||
uint8_t* mask_raw = load_image_from_memory(
|
||||
reinterpret_cast<const char*>(mask_bytes.data()),
|
||||
static_cast<int>(mask_bytes.size()),
|
||||
mask_w, mask_h,
|
||||
width, height, 1);
|
||||
mask_image = {(uint32_t)mask_w, (uint32_t)mask_h, 1, mask_raw};
|
||||
} else {
|
||||
mask_image.width = width;
|
||||
mask_image.height = height;
|
||||
mask_image.channel = 1;
|
||||
mask_image.data = nullptr;
|
||||
}
|
||||
|
||||
sd_img_gen_params_t img_gen_params = {
|
||||
gen_params.lora_vec.data(),
|
||||
static_cast<uint32_t>(gen_params.lora_vec.size()),
|
||||
gen_params.prompt.c_str(),
|
||||
gen_params.negative_prompt.c_str(),
|
||||
gen_params.clip_skip,
|
||||
init_image,
|
||||
ref_images.data(),
|
||||
(int)ref_images.size(),
|
||||
gen_params.auto_resize_ref_image,
|
||||
gen_params.increase_ref_index,
|
||||
mask_image,
|
||||
gen_params.width,
|
||||
gen_params.height,
|
||||
gen_params.sample_params,
|
||||
gen_params.strength,
|
||||
gen_params.seed,
|
||||
gen_params.batch_count,
|
||||
control_image,
|
||||
gen_params.control_strength,
|
||||
{
|
||||
pmid_images.data(),
|
||||
(int)pmid_images.size(),
|
||||
gen_params.pm_id_embed_path.c_str(),
|
||||
gen_params.pm_style_strength,
|
||||
}, // pm_params
|
||||
ctx_params.vae_tiling_params,
|
||||
gen_params.cache_params,
|
||||
};
|
||||
|
||||
sd_image_t* results = nullptr;
|
||||
int num_results = 0;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(sd_ctx_mutex);
|
||||
results = generate_image(sd_ctx, &img_gen_params);
|
||||
num_results = gen_params.batch_count;
|
||||
}
|
||||
|
||||
json out;
|
||||
out["created"] = iso_timestamp_now();
|
||||
out["data"] = json::array();
|
||||
out["output_format"] = output_format;
|
||||
|
||||
for (int i = 0; i < num_results; i++) {
|
||||
if (results[i].data == nullptr)
|
||||
continue;
|
||||
auto image_bytes = write_image_to_vector(output_format == "jpeg" ? ImageFormat::JPEG : ImageFormat::PNG,
|
||||
results[i].data,
|
||||
results[i].width,
|
||||
results[i].height,
|
||||
results[i].channel,
|
||||
output_compression);
|
||||
std::string b64 = base64_encode(image_bytes);
|
||||
json item;
|
||||
item["b64_json"] = b64;
|
||||
out["data"].push_back(item);
|
||||
}
|
||||
|
||||
res.set_content(out.dump(), "application/json");
|
||||
res.status = 200;
|
||||
|
||||
if (init_image.data) {
|
||||
stbi_image_free(init_image.data);
|
||||
}
|
||||
if (mask_image.data) {
|
||||
stbi_image_free(mask_image.data);
|
||||
}
|
||||
for (auto ref_image : ref_images) {
|
||||
stbi_image_free(ref_image.data);
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
res.status = 500;
|
||||
json err;
|
||||
err["error"] = "server_error";
|
||||
err["message"] = e.what();
|
||||
res.set_content(err.dump(), "application/json");
|
||||
}
|
||||
});
|
||||
|
||||
LOG_INFO("listening on: %s:%d\n", svr_params.listen_ip.c_str(), svr_params.listen_port);
|
||||
LOG_INFO("listening on: http://%s:%d\n", svr_params.listen_ip.c_str(), svr_params.listen_port);
|
||||
svr.listen(svr_params.listen_ip, svr_params.listen_port);
|
||||
|
||||
// cleanup
|
||||
free_sd_ctx(sd_ctx);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(async_job_manager.mutex);
|
||||
async_job_manager.stop = true;
|
||||
}
|
||||
async_job_manager.cv.notify_all();
|
||||
async_worker.join();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "httplib.h"
|
||||
#include "runtime.h"
|
||||
|
||||
void register_index_endpoints(httplib::Server& svr, const SDSvrParams& svr_params, const std::string& index_html);
|
||||
void register_openai_api_endpoints(httplib::Server& svr, ServerRuntime& rt);
|
||||
void register_sdapi_endpoints(httplib::Server& svr, ServerRuntime& rt);
|
||||
void register_sdcpp_api_endpoints(httplib::Server& svr, ServerRuntime& rt);
|
||||
@@ -0,0 +1,22 @@
|
||||
#include "routes.h"
|
||||
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
|
||||
void register_index_endpoints(httplib::Server& svr, const SDSvrParams& svr_params, const std::string& index_html) {
|
||||
const std::string serve_html_path = svr_params.serve_html_path;
|
||||
svr.Get("/", [serve_html_path, index_html](const httplib::Request&, httplib::Response& res) {
|
||||
if (!serve_html_path.empty()) {
|
||||
std::ifstream file(serve_html_path);
|
||||
if (file) {
|
||||
std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
|
||||
res.set_content(content, "text/html");
|
||||
} else {
|
||||
res.status = 500;
|
||||
res.set_content("Error: Unable to read HTML file", "text/plain");
|
||||
}
|
||||
} else {
|
||||
res.set_content(index_html, "text/html");
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,395 @@
|
||||
#include "routes.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <ctime>
|
||||
#include <regex>
|
||||
|
||||
#include "common/common.h"
|
||||
#include "common/media_io.h"
|
||||
#include "common/resource_owners.hpp"
|
||||
|
||||
static std::string extract_and_remove_sd_cpp_extra_args(std::string& text) {
|
||||
std::regex re("<sd_cpp_extra_args>(.*?)</sd_cpp_extra_args>");
|
||||
std::smatch match;
|
||||
|
||||
std::string extracted;
|
||||
if (std::regex_search(text, match, re)) {
|
||||
extracted = match[1].str();
|
||||
text = std::regex_replace(text, re, "");
|
||||
}
|
||||
return extracted;
|
||||
}
|
||||
|
||||
static bool build_openai_generation_request(const httplib::Request& req,
|
||||
ServerRuntime& runtime,
|
||||
ImgGenJobRequest& request,
|
||||
std::string& error_message) {
|
||||
if (req.body.empty()) {
|
||||
error_message = "empty body";
|
||||
return false;
|
||||
}
|
||||
|
||||
json j = json::parse(req.body);
|
||||
std::string prompt = j.value("prompt", "");
|
||||
int n = std::max(1, j.value("n", 1));
|
||||
std::string size = j.value("size", "");
|
||||
std::string output_format = j.value("output_format", "png");
|
||||
int output_compression = j.value("output_compression", 100);
|
||||
int width = runtime.default_gen_params->width > 0 ? runtime.default_gen_params->width : 512;
|
||||
int height = runtime.default_gen_params->width > 0 ? runtime.default_gen_params->height : 512;
|
||||
if (!size.empty()) {
|
||||
auto pos = size.find('x');
|
||||
if (pos != std::string::npos) {
|
||||
try {
|
||||
width = std::stoi(size.substr(0, pos));
|
||||
height = std::stoi(size.substr(pos + 1));
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (prompt.empty()) {
|
||||
error_message = "prompt required";
|
||||
return false;
|
||||
}
|
||||
|
||||
request.gen_params = *runtime.default_gen_params;
|
||||
if (!assign_output_options(request, output_format, output_compression, true, error_message)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
request.gen_params.prompt = prompt;
|
||||
request.gen_params.width = width;
|
||||
request.gen_params.height = height;
|
||||
request.gen_params.batch_count = n;
|
||||
|
||||
std::string sd_cpp_extra_args_str = extract_and_remove_sd_cpp_extra_args(request.gen_params.prompt);
|
||||
if (!sd_cpp_extra_args_str.empty() && !request.gen_params.from_json_str(sd_cpp_extra_args_str)) {
|
||||
error_message = "invalid sd_cpp_extra_args";
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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 params";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool build_openai_edit_request(const httplib::Request& req,
|
||||
ServerRuntime& runtime,
|
||||
ImgGenJobRequest& request,
|
||||
std::string& error_message) {
|
||||
if (!req.is_multipart_form_data()) {
|
||||
error_message = "Content-Type must be multipart/form-data";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string prompt = req.form.get_field("prompt");
|
||||
if (prompt.empty()) {
|
||||
error_message = "prompt required";
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t image_count = req.form.get_file_count("image[]");
|
||||
bool has_legacy_image = req.form.has_file("image");
|
||||
if (image_count == 0 && !has_legacy_image) {
|
||||
error_message = "at least one image[] required";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<std::vector<uint8_t>> images_bytes;
|
||||
for (size_t i = 0; i < image_count; ++i) {
|
||||
auto file = req.form.get_file("image[]", i);
|
||||
images_bytes.emplace_back(file.content.begin(), file.content.end());
|
||||
}
|
||||
if (image_count == 0 && has_legacy_image) {
|
||||
auto file = req.form.get_file("image");
|
||||
images_bytes.emplace_back(file.content.begin(), file.content.end());
|
||||
}
|
||||
|
||||
std::vector<uint8_t> mask_bytes;
|
||||
if (req.form.has_file("mask")) {
|
||||
auto file = req.form.get_file("mask");
|
||||
mask_bytes.assign(file.content.begin(), file.content.end());
|
||||
}
|
||||
|
||||
int n = 1;
|
||||
if (req.form.has_field("n")) {
|
||||
try {
|
||||
n = std::stoi(req.form.get_field("n"));
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
|
||||
std::string size = req.form.get_field("size");
|
||||
int width = -1;
|
||||
int height = -1;
|
||||
if (!size.empty()) {
|
||||
auto pos = size.find('x');
|
||||
if (pos != std::string::npos) {
|
||||
try {
|
||||
width = std::stoi(size.substr(0, pos));
|
||||
height = std::stoi(size.substr(pos + 1));
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::string output_format = req.form.has_field("output_format")
|
||||
? req.form.get_field("output_format")
|
||||
: "png";
|
||||
|
||||
int output_compression = 100;
|
||||
try {
|
||||
output_compression = std::stoi(req.form.get_field("output_compression"));
|
||||
} catch (...) {
|
||||
}
|
||||
|
||||
request.gen_params = *runtime.default_gen_params;
|
||||
if (!assign_output_options(request, output_format, output_compression, false, error_message)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
request.gen_params.prompt = prompt;
|
||||
request.gen_params.width = width;
|
||||
request.gen_params.height = height;
|
||||
request.gen_params.batch_count = n;
|
||||
|
||||
for (auto& bytes : images_bytes) {
|
||||
int img_w = 0;
|
||||
int img_h = 0;
|
||||
uint8_t* raw_pixels = load_image_from_memory(
|
||||
reinterpret_cast<const char*>(bytes.data()),
|
||||
static_cast<int>(bytes.size()),
|
||||
img_w, img_h,
|
||||
width, height, 3);
|
||||
if (raw_pixels == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
SDImageOwner image_owner({(uint32_t)img_w, (uint32_t)img_h, 3, raw_pixels});
|
||||
request.gen_params.set_width_and_height_if_unset(image_owner.get().width, image_owner.get().height);
|
||||
request.gen_params.ref_images.push_back(std::move(image_owner));
|
||||
}
|
||||
|
||||
if (!request.gen_params.ref_images.empty()) {
|
||||
request.gen_params.init_image = request.gen_params.ref_images.front();
|
||||
}
|
||||
|
||||
if (!mask_bytes.empty()) {
|
||||
int expected_width = 0;
|
||||
int expected_height = 0;
|
||||
if (request.gen_params.width_and_height_are_set()) {
|
||||
expected_width = request.gen_params.width;
|
||||
expected_height = request.gen_params.height;
|
||||
}
|
||||
int mask_w = 0;
|
||||
int mask_h = 0;
|
||||
|
||||
uint8_t* mask_raw = load_image_from_memory(
|
||||
reinterpret_cast<const char*>(mask_bytes.data()),
|
||||
static_cast<int>(mask_bytes.size()),
|
||||
mask_w, mask_h,
|
||||
expected_width, expected_height, 1);
|
||||
request.gen_params.mask_image.reset({(uint32_t)mask_w, (uint32_t)mask_h, 1, mask_raw});
|
||||
const sd_image_t& mask_image = request.gen_params.mask_image.get();
|
||||
request.gen_params.set_width_and_height_if_unset(mask_image.width, mask_image.height);
|
||||
} else {
|
||||
request.gen_params.mask_image.reset({
|
||||
(uint32_t)request.gen_params.get_resolved_width(),
|
||||
(uint32_t)request.gen_params.get_resolved_height(),
|
||||
1,
|
||||
nullptr,
|
||||
});
|
||||
}
|
||||
|
||||
std::string sd_cpp_extra_args_str = extract_and_remove_sd_cpp_extra_args(request.gen_params.prompt);
|
||||
if (!sd_cpp_extra_args_str.empty() && !request.gen_params.from_json_str(sd_cpp_extra_args_str)) {
|
||||
error_message = "invalid sd_cpp_extra_args";
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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 params";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool execute_sync_img_gen_request(ServerRuntime& runtime,
|
||||
ImgGenJobRequest& request,
|
||||
SDImageVec& results,
|
||||
std::string& error_message) {
|
||||
sd_img_gen_params_t img_gen_params = request.to_sd_img_gen_params_t();
|
||||
int num_results = 0;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(*runtime.sd_ctx_mutex);
|
||||
sd_image_t* raw_results = nullptr;
|
||||
if (!generate_image(runtime.sd_ctx, &img_gen_params, &raw_results, &num_results)) {
|
||||
raw_results = nullptr;
|
||||
num_results = 0;
|
||||
}
|
||||
results.adopt(raw_results, num_results);
|
||||
}
|
||||
|
||||
if (results.empty()) {
|
||||
error_message = "generate_image returned no results";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void register_openai_api_endpoints(httplib::Server& svr, ServerRuntime& rt) {
|
||||
ServerRuntime* runtime = &rt;
|
||||
|
||||
svr.Get("/v1/models", [runtime](const httplib::Request&, httplib::Response& res) {
|
||||
json r;
|
||||
r["data"] = json::array();
|
||||
r["data"].push_back({{"id", "sd-cpp-local"}, {"object", "model"}, {"owned_by", "local"}});
|
||||
res.set_content(r.dump(), "application/json");
|
||||
});
|
||||
|
||||
svr.Post("/v1/images/generations", [runtime](const httplib::Request& req, httplib::Response& res) {
|
||||
try {
|
||||
if (!runtime_supports_generation_mode(*runtime, IMG_GEN)) {
|
||||
res.status = 400;
|
||||
res.set_content(json({{"error", unsupported_generation_mode_error(IMG_GEN)}}).dump(), "application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
ImgGenJobRequest request;
|
||||
std::string error_message;
|
||||
if (!build_openai_generation_request(req, *runtime, request, error_message)) {
|
||||
res.status = 400;
|
||||
res.set_content(json({{"error", error_message}}).dump(), "application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_DEBUG("%s\n", request.gen_params.to_string().c_str());
|
||||
|
||||
SDImageVec results;
|
||||
if (!execute_sync_img_gen_request(*runtime, request, results, error_message)) {
|
||||
res.status = 500;
|
||||
res.set_content(json({{"error", error_message}}).dump(), "application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
json out;
|
||||
out["created"] = static_cast<long long>(std::time(nullptr));
|
||||
out["data"] = json::array();
|
||||
out["output_format"] = request.output_format;
|
||||
|
||||
int result_count = results.count();
|
||||
int images_per_batch = request.gen_params.batch_count > 0 ? std::max(1, result_count / request.gen_params.batch_count) : 1;
|
||||
for (int i = 0; i < result_count; ++i) {
|
||||
if (results[i].data == nullptr) {
|
||||
continue;
|
||||
}
|
||||
std::string params = request.gen_params.embed_image_metadata
|
||||
? get_image_params(*runtime->ctx_params,
|
||||
request.gen_params,
|
||||
request.gen_params.seed + i / images_per_batch)
|
||||
: "";
|
||||
auto image_bytes = encode_image_to_vector(request.output_format == "jpeg"
|
||||
? EncodedImageFormat::JPEG
|
||||
: request.output_format == "webp"
|
||||
? EncodedImageFormat::WEBP
|
||||
: EncodedImageFormat::PNG,
|
||||
results[i].data,
|
||||
results[i].width,
|
||||
results[i].height,
|
||||
results[i].channel,
|
||||
params,
|
||||
request.output_compression);
|
||||
if (image_bytes.empty()) {
|
||||
LOG_ERROR("write image to mem failed");
|
||||
continue;
|
||||
}
|
||||
|
||||
json item;
|
||||
item["b64_json"] = base64_encode(image_bytes);
|
||||
out["data"].push_back(item);
|
||||
}
|
||||
|
||||
res.set_content(out.dump(), "application/json");
|
||||
res.status = 200;
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
res.status = 500;
|
||||
json err;
|
||||
err["error"] = "server_error";
|
||||
err["message"] = e.what();
|
||||
res.set_content(err.dump(), "application/json");
|
||||
}
|
||||
});
|
||||
|
||||
svr.Post("/v1/images/edits", [runtime](const httplib::Request& req, httplib::Response& res) {
|
||||
try {
|
||||
if (!runtime_supports_generation_mode(*runtime, IMG_GEN)) {
|
||||
res.status = 400;
|
||||
res.set_content(json({{"error", unsupported_generation_mode_error(IMG_GEN)}}).dump(), "application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
ImgGenJobRequest request;
|
||||
std::string error_message;
|
||||
if (!build_openai_edit_request(req, *runtime, request, error_message)) {
|
||||
res.status = 400;
|
||||
res.set_content(json({{"error", error_message}}).dump(), "application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_DEBUG("%s\n", request.gen_params.to_string().c_str());
|
||||
|
||||
SDImageVec results;
|
||||
if (!execute_sync_img_gen_request(*runtime, request, results, error_message)) {
|
||||
res.status = 500;
|
||||
res.set_content(json({{"error", error_message}}).dump(), "application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
json out;
|
||||
out["created"] = static_cast<long long>(std::time(nullptr));
|
||||
out["data"] = json::array();
|
||||
out["output_format"] = request.output_format;
|
||||
|
||||
int result_count = results.count();
|
||||
int images_per_batch = request.gen_params.batch_count > 0 ? std::max(1, result_count / request.gen_params.batch_count) : 1;
|
||||
for (int i = 0; i < result_count; ++i) {
|
||||
if (results[i].data == nullptr) {
|
||||
continue;
|
||||
}
|
||||
std::string params = request.gen_params.embed_image_metadata
|
||||
? get_image_params(*runtime->ctx_params,
|
||||
request.gen_params,
|
||||
request.gen_params.seed + i / images_per_batch)
|
||||
: "";
|
||||
auto image_bytes = encode_image_to_vector(request.output_format == "jpeg" ? EncodedImageFormat::JPEG : EncodedImageFormat::PNG,
|
||||
results[i].data,
|
||||
results[i].width,
|
||||
results[i].height,
|
||||
results[i].channel,
|
||||
params,
|
||||
request.output_compression);
|
||||
json item;
|
||||
item["b64_json"] = base64_encode(image_bytes);
|
||||
out["data"].push_back(item);
|
||||
}
|
||||
|
||||
res.set_content(out.dump(), "application/json");
|
||||
res.status = 200;
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
res.status = 500;
|
||||
json err;
|
||||
err["error"] = "server_error";
|
||||
err["message"] = e.what();
|
||||
res.set_content(err.dump(), "application/json");
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,480 @@
|
||||
#include "routes.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
#include <regex>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "common/common.h"
|
||||
#include "common/media_io.h"
|
||||
#include "common/resource_owners.hpp"
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
static std::string extract_and_remove_sd_cpp_extra_args(std::string& text) {
|
||||
std::regex re("<sd_cpp_extra_args>(.*?)</sd_cpp_extra_args>");
|
||||
std::smatch match;
|
||||
|
||||
std::string extracted;
|
||||
if (std::regex_search(text, match, re)) {
|
||||
extracted = match[1].str();
|
||||
text = std::regex_replace(text, re, "");
|
||||
}
|
||||
return extracted;
|
||||
}
|
||||
|
||||
static fs::path resolve_display_model_path(const ServerRuntime& runtime) {
|
||||
const auto& ctx = *runtime.ctx_params;
|
||||
if (!ctx.model_path.empty()) {
|
||||
return fs::path(ctx.model_path);
|
||||
}
|
||||
if (!ctx.diffusion_model_path.empty()) {
|
||||
return fs::path(ctx.diffusion_model_path);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
static std::string lower_ascii(std::string value) {
|
||||
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) {
|
||||
return static_cast<char>(std::tolower(c));
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
static enum sample_method_t get_sdapi_sample_method(std::string name) {
|
||||
enum sample_method_t result = str_to_sample_method(name.c_str());
|
||||
if (result != SAMPLE_METHOD_COUNT) {
|
||||
return result;
|
||||
}
|
||||
|
||||
name = lower_ascii(name);
|
||||
static const std::unordered_map<std::string_view, sample_method_t> hardcoded{
|
||||
{"euler a", EULER_A_SAMPLE_METHOD},
|
||||
{"k_euler_a", EULER_A_SAMPLE_METHOD},
|
||||
{"euler", EULER_SAMPLE_METHOD},
|
||||
{"k_euler", EULER_SAMPLE_METHOD},
|
||||
{"heun", HEUN_SAMPLE_METHOD},
|
||||
{"k_heun", HEUN_SAMPLE_METHOD},
|
||||
{"dpm2", DPM2_SAMPLE_METHOD},
|
||||
{"k_dpm_2", DPM2_SAMPLE_METHOD},
|
||||
{"lcm", LCM_SAMPLE_METHOD},
|
||||
{"ddim", DDIM_TRAILING_SAMPLE_METHOD},
|
||||
{"dpm++ 2m", DPMPP2M_SAMPLE_METHOD},
|
||||
{"k_dpmpp_2m", DPMPP2M_SAMPLE_METHOD},
|
||||
{"res multistep", RES_MULTISTEP_SAMPLE_METHOD},
|
||||
{"k_res_multistep", RES_MULTISTEP_SAMPLE_METHOD},
|
||||
{"res 2s", RES_2S_SAMPLE_METHOD},
|
||||
{"k_res_2s", RES_2S_SAMPLE_METHOD},
|
||||
{"euler_cfg_pp", EULER_CFG_PP_SAMPLE_METHOD},
|
||||
{"k_euler_cfg_pp", EULER_CFG_PP_SAMPLE_METHOD},
|
||||
{"euler_a_cfg_pp", EULER_CFG_PP_SAMPLE_METHOD},
|
||||
{"k_euler_a_cfg_pp", EULER_CFG_PP_SAMPLE_METHOD},
|
||||
};
|
||||
auto it = hardcoded.find(name);
|
||||
return it != hardcoded.end() ? it->second : SAMPLE_METHOD_COUNT;
|
||||
}
|
||||
|
||||
static void assign_solid_mask(SDImageOwner& mask_owner, int width, int height) {
|
||||
const size_t pixel_count = static_cast<size_t>(width) * static_cast<size_t>(height);
|
||||
uint8_t* raw_mask = static_cast<uint8_t*>(malloc(pixel_count));
|
||||
if (raw_mask == nullptr) {
|
||||
mask_owner.reset({0, 0, 1, nullptr});
|
||||
return;
|
||||
}
|
||||
std::memset(raw_mask, 255, pixel_count);
|
||||
mask_owner.reset({(uint32_t)width, (uint32_t)height, 1, raw_mask});
|
||||
}
|
||||
|
||||
static bool build_sdapi_img_gen_request(const json& j,
|
||||
ServerRuntime& runtime,
|
||||
bool img2img,
|
||||
ImgGenJobRequest& request,
|
||||
std::string& error_message) {
|
||||
std::string prompt = j.value("prompt", "");
|
||||
std::string negative_prompt = j.value("negative_prompt", "");
|
||||
int width = j.value("width", 512);
|
||||
int height = j.value("height", 512);
|
||||
int steps = j.value("steps", runtime.default_gen_params->sample_params.sample_steps);
|
||||
float cfg_scale = j.value("cfg_scale", runtime.default_gen_params->sample_params.guidance.txt_cfg);
|
||||
int64_t seed = j.value("seed", -1);
|
||||
int batch_size = j.value("batch_size", 1);
|
||||
int clip_skip = j.value("clip_skip", -1);
|
||||
std::string sampler_name = j.value("sampler_name", "");
|
||||
std::string scheduler_name = j.value("scheduler", "");
|
||||
|
||||
if (width <= 0 || height <= 0) {
|
||||
error_message = "width and height must be positive";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (prompt.empty()) {
|
||||
error_message = "prompt required";
|
||||
return false;
|
||||
}
|
||||
|
||||
request.gen_params = *runtime.default_gen_params;
|
||||
|
||||
request.gen_params.prompt = prompt;
|
||||
request.gen_params.negative_prompt = negative_prompt;
|
||||
request.gen_params.seed = seed;
|
||||
request.gen_params.sample_params.sample_steps = steps;
|
||||
request.gen_params.batch_count = batch_size;
|
||||
request.gen_params.sample_params.guidance.txt_cfg = cfg_scale;
|
||||
request.gen_params.width = j.value("width", -1);
|
||||
request.gen_params.height = j.value("height", -1);
|
||||
|
||||
if (!img2img && j.value("enable_hr", false)) {
|
||||
request.gen_params.hires_enabled = true;
|
||||
request.gen_params.hires_scale = j.value("hr_scale", request.gen_params.hires_scale);
|
||||
request.gen_params.hires_width = j.value("hr_resize_x", request.gen_params.hires_width);
|
||||
request.gen_params.hires_height = j.value("hr_resize_y", request.gen_params.hires_height);
|
||||
request.gen_params.hires_steps = j.value("hr_steps", request.gen_params.hires_steps);
|
||||
request.gen_params.hires_denoising_strength =
|
||||
j.value("denoising_strength", request.gen_params.hires_denoising_strength);
|
||||
|
||||
request.gen_params.hires_upscaler = j.value("hr_upscaler", request.gen_params.hires_upscaler);
|
||||
}
|
||||
|
||||
std::string sd_cpp_extra_args_str = extract_and_remove_sd_cpp_extra_args(request.gen_params.prompt);
|
||||
if (!sd_cpp_extra_args_str.empty() && !request.gen_params.from_json_str(sd_cpp_extra_args_str)) {
|
||||
error_message = "invalid sd_cpp_extra_args";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (clip_skip > 0) {
|
||||
request.gen_params.clip_skip = clip_skip;
|
||||
}
|
||||
|
||||
enum sample_method_t sample_method = get_sdapi_sample_method(sampler_name);
|
||||
if (sample_method != SAMPLE_METHOD_COUNT) {
|
||||
request.gen_params.sample_params.sample_method = sample_method;
|
||||
}
|
||||
|
||||
enum scheduler_t scheduler = str_to_scheduler(scheduler_name.c_str());
|
||||
if (scheduler != SCHEDULER_COUNT) {
|
||||
request.gen_params.sample_params.scheduler = scheduler;
|
||||
}
|
||||
|
||||
if (j.contains("lora") && j["lora"].is_array()) {
|
||||
request.gen_params.lora_map.clear();
|
||||
request.gen_params.high_noise_lora_map.clear();
|
||||
|
||||
for (const auto& item : j["lora"]) {
|
||||
if (!item.is_object()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string path = item.value("path", "");
|
||||
float multiplier = item.value("multiplier", 1.0f);
|
||||
bool is_high_noise = item.value("is_high_noise", false);
|
||||
|
||||
if (path.empty()) {
|
||||
error_message = "lora.path required";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string fullpath = get_lora_full_path(runtime, path);
|
||||
if (fullpath.empty()) {
|
||||
error_message = "invalid lora path: " + path;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (is_high_noise) {
|
||||
request.gen_params.high_noise_lora_map[fullpath] += multiplier;
|
||||
} else {
|
||||
request.gen_params.lora_map[fullpath] += multiplier;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (img2img) {
|
||||
const int expected_width = request.gen_params.width_and_height_are_set() ? request.gen_params.width : 0;
|
||||
const int expected_height = request.gen_params.width_and_height_are_set() ? request.gen_params.height : 0;
|
||||
|
||||
if (j.contains("init_images") && j["init_images"].is_array() && !j["init_images"].empty()) {
|
||||
if (decode_base64_image(j["init_images"][0].get<std::string>(),
|
||||
3,
|
||||
expected_width,
|
||||
expected_height,
|
||||
request.gen_params.init_image)) {
|
||||
const sd_image_t& image = request.gen_params.init_image.get();
|
||||
request.gen_params.set_width_and_height_if_unset(image.width, image.height);
|
||||
}
|
||||
}
|
||||
|
||||
if (j.contains("mask") && j["mask"].is_string()) {
|
||||
if (decode_base64_image(j["mask"].get<std::string>(),
|
||||
1,
|
||||
expected_width,
|
||||
expected_height,
|
||||
request.gen_params.mask_image)) {
|
||||
const sd_image_t& image = request.gen_params.mask_image.get();
|
||||
request.gen_params.set_width_and_height_if_unset(image.width, image.height);
|
||||
}
|
||||
sd_image_t& mask_image = request.gen_params.mask_image.get();
|
||||
bool inpainting_mask_invert = j.value("inpainting_mask_invert", 0) != 0;
|
||||
if (inpainting_mask_invert && mask_image.data != nullptr) {
|
||||
for (uint32_t i = 0; i < mask_image.width * mask_image.height; ++i) {
|
||||
mask_image.data[i] = 255 - mask_image.data[i];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const int resolved_width = request.gen_params.get_resolved_width();
|
||||
const int resolved_height = request.gen_params.get_resolved_height();
|
||||
assign_solid_mask(request.gen_params.mask_image, resolved_width, resolved_height);
|
||||
}
|
||||
|
||||
float denoising_strength = j.value("denoising_strength", -1.f);
|
||||
if (denoising_strength >= 0.f) {
|
||||
request.gen_params.strength = std::min(denoising_strength, 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
if (j.contains("extra_images") && j["extra_images"].is_array()) {
|
||||
for (const auto& extra_image : j["extra_images"]) {
|
||||
if (!extra_image.is_string()) {
|
||||
continue;
|
||||
}
|
||||
SDImageOwner image_owner;
|
||||
if (decode_base64_image(extra_image.get<std::string>(),
|
||||
3,
|
||||
request.gen_params.width_and_height_are_set() ? request.gen_params.width : 0,
|
||||
request.gen_params.width_and_height_are_set() ? request.gen_params.height : 0,
|
||||
image_owner)) {
|
||||
const sd_image_t& image = image_owner.get();
|
||||
request.gen_params.set_width_and_height_if_unset(image.width, image.height);
|
||||
request.gen_params.ref_images.push_back(std::move(image_owner));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 params";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void register_sdapi_endpoints(httplib::Server& svr, ServerRuntime& rt) {
|
||||
ServerRuntime* runtime = &rt;
|
||||
|
||||
auto sdapi_any2img = [runtime](const httplib::Request& req, httplib::Response& res, bool img2img) {
|
||||
try {
|
||||
if (req.body.empty()) {
|
||||
res.status = 400;
|
||||
res.set_content(R"({"error":"empty body"})", "application/json");
|
||||
return;
|
||||
}
|
||||
if (!runtime_supports_generation_mode(*runtime, IMG_GEN)) {
|
||||
res.status = 400;
|
||||
res.set_content(json({{"error", unsupported_generation_mode_error(IMG_GEN)}}).dump(), "application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
json j = json::parse(req.body);
|
||||
ImgGenJobRequest request;
|
||||
std::string error_message;
|
||||
if (!build_sdapi_img_gen_request(j, *runtime, img2img, request, error_message)) {
|
||||
res.status = 400;
|
||||
res.set_content(json({{"error", error_message}}).dump(), "application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_DEBUG("%s\n", request.gen_params.to_string().c_str());
|
||||
|
||||
sd_img_gen_params_t img_gen_params = request.to_sd_img_gen_params_t();
|
||||
SDImageVec results;
|
||||
int num_results = 0;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(*runtime->sd_ctx_mutex);
|
||||
sd_image_t* raw_results = nullptr;
|
||||
if (!generate_image(runtime->sd_ctx, &img_gen_params, &raw_results, &num_results)) {
|
||||
raw_results = nullptr;
|
||||
num_results = 0;
|
||||
}
|
||||
results.adopt(raw_results, num_results);
|
||||
}
|
||||
|
||||
if (results.empty()) {
|
||||
res.status = 500;
|
||||
res.set_content(R"({"error":"generate_image returned no results"})", "application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
json out;
|
||||
out["images"] = json::array();
|
||||
out["parameters"] = j;
|
||||
out["info"] = "";
|
||||
|
||||
int images_per_batch = request.gen_params.batch_count > 0 ? std::max(1, num_results / request.gen_params.batch_count) : 1;
|
||||
for (int i = 0; i < num_results; ++i) {
|
||||
if (results[i].data == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string params = request.gen_params.embed_image_metadata
|
||||
? get_image_params(*runtime->ctx_params,
|
||||
request.gen_params,
|
||||
request.gen_params.seed + i / images_per_batch)
|
||||
: "";
|
||||
auto image_bytes = encode_image_to_vector(EncodedImageFormat::PNG,
|
||||
results[i].data,
|
||||
results[i].width,
|
||||
results[i].height,
|
||||
results[i].channel,
|
||||
params);
|
||||
|
||||
if (image_bytes.empty()) {
|
||||
LOG_ERROR("write image to mem failed");
|
||||
continue;
|
||||
}
|
||||
|
||||
out["images"].push_back(base64_encode(image_bytes));
|
||||
}
|
||||
|
||||
res.set_content(out.dump(), "application/json");
|
||||
res.status = 200;
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
res.status = 500;
|
||||
json err;
|
||||
err["error"] = "server_error";
|
||||
err["message"] = e.what();
|
||||
res.set_content(err.dump(), "application/json");
|
||||
}
|
||||
};
|
||||
|
||||
svr.Post("/sdapi/v1/txt2img", [sdapi_any2img](const httplib::Request& req, httplib::Response& res) {
|
||||
sdapi_any2img(req, res, false);
|
||||
});
|
||||
|
||||
svr.Post("/sdapi/v1/img2img", [sdapi_any2img](const httplib::Request& req, httplib::Response& res) {
|
||||
sdapi_any2img(req, res, true);
|
||||
});
|
||||
|
||||
svr.Get("/sdapi/v1/loras", [runtime](const httplib::Request&, httplib::Response& res) {
|
||||
refresh_lora_cache(*runtime);
|
||||
|
||||
json result = json::array();
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(*runtime->lora_mutex);
|
||||
for (const auto& e : *runtime->lora_cache) {
|
||||
json item;
|
||||
item["name"] = e.name;
|
||||
item["path"] = e.path;
|
||||
result.push_back(item);
|
||||
}
|
||||
}
|
||||
|
||||
res.set_content(result.dump(), "application/json");
|
||||
});
|
||||
|
||||
svr.Get("/sdapi/v1/upscalers", [runtime](const httplib::Request&, httplib::Response& res) {
|
||||
refresh_upscaler_cache(*runtime);
|
||||
|
||||
auto make_builtin = [](const char* name) {
|
||||
json item;
|
||||
item["name"] = name;
|
||||
item["model_name"] = nullptr;
|
||||
item["model_path"] = nullptr;
|
||||
item["model_url"] = nullptr;
|
||||
item["scale"] = 4;
|
||||
return item;
|
||||
};
|
||||
|
||||
json result = json::array();
|
||||
result.push_back(make_builtin("None"));
|
||||
result.push_back(make_builtin("Lanczos"));
|
||||
result.push_back(make_builtin("Nearest"));
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(*runtime->upscaler_mutex);
|
||||
for (const auto& e : *runtime->upscaler_cache) {
|
||||
json item;
|
||||
item["name"] = e.name;
|
||||
item["model_name"] = e.model_name;
|
||||
item["model_path"] = e.fullpath;
|
||||
item["model_url"] = nullptr;
|
||||
item["scale"] = e.scale;
|
||||
result.push_back(item);
|
||||
}
|
||||
}
|
||||
|
||||
res.set_content(result.dump(), "application/json");
|
||||
});
|
||||
|
||||
svr.Get("/sdapi/v1/latent-upscale-modes", [](const httplib::Request&, httplib::Response& res) {
|
||||
json result = json::array({
|
||||
{{"name", "Latent"}},
|
||||
{{"name", "Latent (nearest)"}},
|
||||
{{"name", "Latent (nearest-exact)"}},
|
||||
{{"name", "Latent (antialiased)"}},
|
||||
{{"name", "Latent (bicubic)"}},
|
||||
{{"name", "Latent (bicubic antialiased)"}},
|
||||
});
|
||||
res.set_content(result.dump(), "application/json");
|
||||
});
|
||||
|
||||
svr.Get("/sdapi/v1/samplers", [runtime](const httplib::Request&, httplib::Response& res) {
|
||||
std::vector<std::string> sampler_names;
|
||||
sampler_names.push_back("default");
|
||||
for (int i = 0; i < SAMPLE_METHOD_COUNT; i++) {
|
||||
sampler_names.push_back(sd_sample_method_name((sample_method_t)i));
|
||||
}
|
||||
json r = json::array();
|
||||
for (auto name : sampler_names) {
|
||||
json entry;
|
||||
entry["name"] = name;
|
||||
entry["aliases"] = json::array({name});
|
||||
entry["options"] = json::object();
|
||||
r.push_back(entry);
|
||||
}
|
||||
res.set_content(r.dump(), "application/json");
|
||||
});
|
||||
|
||||
svr.Get("/sdapi/v1/schedulers", [runtime](const httplib::Request&, httplib::Response& res) {
|
||||
std::vector<std::string> scheduler_names;
|
||||
scheduler_names.push_back("default");
|
||||
for (int i = 0; i < SCHEDULER_COUNT; i++) {
|
||||
scheduler_names.push_back(sd_scheduler_name((scheduler_t)i));
|
||||
if (i == DISCRETE_SCHEDULER) {
|
||||
scheduler_names.push_back("normal");
|
||||
}
|
||||
}
|
||||
json r = json::array();
|
||||
for (auto name : scheduler_names) {
|
||||
json entry;
|
||||
entry["name"] = name;
|
||||
entry["label"] = name;
|
||||
r.push_back(entry);
|
||||
}
|
||||
res.set_content(r.dump(), "application/json");
|
||||
});
|
||||
|
||||
svr.Get("/sdapi/v1/sd-models", [runtime](const httplib::Request&, httplib::Response& res) {
|
||||
fs::path model_path = resolve_display_model_path(*runtime);
|
||||
json entry;
|
||||
entry["title"] = model_path.stem();
|
||||
entry["model_name"] = model_path.stem();
|
||||
entry["filename"] = model_path.filename();
|
||||
entry["hash"] = "8888888888";
|
||||
entry["sha256"] = "8888888888888888888888888888888888888888888888888888888888888888";
|
||||
entry["config"] = nullptr;
|
||||
json r = json::array();
|
||||
r.push_back(entry);
|
||||
res.set_content(r.dump(), "application/json");
|
||||
});
|
||||
|
||||
svr.Get("/sdapi/v1/options", [runtime](const httplib::Request&, httplib::Response& res) {
|
||||
fs::path model_path = resolve_display_model_path(*runtime);
|
||||
json r;
|
||||
r["samples_format"] = "png";
|
||||
r["sd_model_checkpoint"] = model_path.stem();
|
||||
res.set_content(r.dump(), "application/json");
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,599 @@
|
||||
#include "routes.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <filesystem>
|
||||
|
||||
#include "async_jobs.h"
|
||||
#include "common/common.h"
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
static bool parse_cache_mode(const std::string& mode_str, sd_cache_mode_t& mode_out) {
|
||||
if (mode_str == "disabled") {
|
||||
mode_out = SD_CACHE_DISABLED;
|
||||
return true;
|
||||
}
|
||||
if (mode_str == "easycache") {
|
||||
mode_out = SD_CACHE_EASYCACHE;
|
||||
return true;
|
||||
}
|
||||
if (mode_str == "ucache") {
|
||||
mode_out = SD_CACHE_UCACHE;
|
||||
return true;
|
||||
}
|
||||
if (mode_str == "dbcache") {
|
||||
mode_out = SD_CACHE_DBCACHE;
|
||||
return true;
|
||||
}
|
||||
if (mode_str == "taylorseer") {
|
||||
mode_out = SD_CACHE_TAYLORSEER;
|
||||
return true;
|
||||
}
|
||||
if (mode_str == "cache-dit") {
|
||||
mode_out = SD_CACHE_CACHE_DIT;
|
||||
return true;
|
||||
}
|
||||
if (mode_str == "spectrum") {
|
||||
mode_out = SD_CACHE_SPECTRUM;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static json finite_number_or_null(float value) {
|
||||
return std::isfinite(value) ? json(value) : json(nullptr);
|
||||
}
|
||||
|
||||
static const char* capability_scheduler_name(enum scheduler_t scheduler) {
|
||||
return scheduler < SCHEDULER_COUNT ? sd_scheduler_name(scheduler) : "default";
|
||||
}
|
||||
|
||||
static const char* capability_sample_method_name(enum sample_method_t sample_method) {
|
||||
return sample_method < SAMPLE_METHOD_COUNT ? sd_sample_method_name(sample_method) : "default";
|
||||
}
|
||||
|
||||
static json make_vae_tiling_json(const sd_tiling_params_t& params) {
|
||||
return {
|
||||
{"enabled", params.enabled},
|
||||
{"temporal_tiling", params.temporal_tiling},
|
||||
{"tile_size_x", params.tile_size_x},
|
||||
{"tile_size_y", params.tile_size_y},
|
||||
{"target_overlap", params.target_overlap},
|
||||
{"rel_size_x", params.rel_size_x},
|
||||
{"rel_size_y", params.rel_size_y},
|
||||
{"extra_tiling_args", params.extra_tiling_args ? params.extra_tiling_args : ""},
|
||||
};
|
||||
}
|
||||
|
||||
static fs::path resolve_display_model_path(const ServerRuntime& runtime) {
|
||||
const auto& ctx = *runtime.ctx_params;
|
||||
if (!ctx.model_path.empty()) {
|
||||
return fs::path(ctx.model_path);
|
||||
}
|
||||
if (!ctx.diffusion_model_path.empty()) {
|
||||
return fs::path(ctx.diffusion_model_path);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
static json make_sample_params_json(const sd_sample_params_t& sample_params, const std::vector<int>& skip_layers) {
|
||||
const auto& guidance = sample_params.guidance;
|
||||
return {
|
||||
{"scheduler", capability_scheduler_name(sample_params.scheduler)},
|
||||
{"sample_method", capability_sample_method_name(sample_params.sample_method)},
|
||||
{"sample_steps", sample_params.sample_steps},
|
||||
{"eta", finite_number_or_null(sample_params.eta)},
|
||||
{"shifted_timestep", sample_params.shifted_timestep},
|
||||
{"flow_shift", finite_number_or_null(sample_params.flow_shift)},
|
||||
{"guidance",
|
||||
{
|
||||
{"txt_cfg", guidance.txt_cfg},
|
||||
{"img_cfg", finite_number_or_null(guidance.img_cfg)},
|
||||
{"distilled_guidance", guidance.distilled_guidance},
|
||||
{"slg",
|
||||
{
|
||||
{"layers", skip_layers},
|
||||
{"layer_start", guidance.slg.layer_start},
|
||||
{"layer_end", guidance.slg.layer_end},
|
||||
{"scale", guidance.slg.scale},
|
||||
}},
|
||||
}},
|
||||
};
|
||||
}
|
||||
|
||||
static json make_hires_json(const SDGenerationParams& defaults) {
|
||||
return {
|
||||
{"enabled", defaults.hires_enabled},
|
||||
{"upscaler", defaults.hires_upscaler},
|
||||
{"scale", defaults.hires_scale},
|
||||
{"target_width", defaults.hires_width},
|
||||
{"target_height", defaults.hires_height},
|
||||
{"steps", defaults.hires_steps},
|
||||
{"denoising_strength", defaults.hires_denoising_strength},
|
||||
{"custom_sigmas", defaults.hires_custom_sigmas},
|
||||
{"upscale_tile_size", defaults.hires_upscale_tile_size},
|
||||
};
|
||||
}
|
||||
|
||||
static json make_img_gen_defaults_json(const SDGenerationParams& defaults, const std::string& output_format) {
|
||||
return {
|
||||
{"prompt", defaults.prompt},
|
||||
{"negative_prompt", defaults.negative_prompt},
|
||||
{"clip_skip", defaults.clip_skip},
|
||||
{"width", defaults.width > 0 ? defaults.width : 512},
|
||||
{"height", defaults.height > 0 ? defaults.height : 512},
|
||||
{"strength", defaults.strength},
|
||||
{"seed", defaults.seed},
|
||||
{"batch_count", defaults.batch_count},
|
||||
{"qwen_image_layers", defaults.qwen_image_layers},
|
||||
{"auto_resize_ref_image", defaults.auto_resize_ref_image},
|
||||
{"increase_ref_index", defaults.increase_ref_index},
|
||||
{"control_strength", defaults.control_strength},
|
||||
{"sample_params", make_sample_params_json(defaults.sample_params, defaults.skip_layers)},
|
||||
{"hires", make_hires_json(defaults)},
|
||||
{"vae_tiling_params", make_vae_tiling_json(defaults.vae_tiling_params)},
|
||||
{"cache_mode", defaults.cache_mode},
|
||||
{"cache_option", defaults.cache_option},
|
||||
{"scm_mask", defaults.scm_mask},
|
||||
{"scm_policy_dynamic", defaults.scm_policy_dynamic},
|
||||
{"output_format", output_format},
|
||||
{"output_compression", 100},
|
||||
};
|
||||
}
|
||||
|
||||
static json make_vid_gen_defaults_json(const SDGenerationParams& defaults, const std::string& output_format) {
|
||||
return {
|
||||
{"prompt", defaults.prompt},
|
||||
{"negative_prompt", defaults.negative_prompt},
|
||||
{"clip_skip", defaults.clip_skip},
|
||||
{"width", defaults.width > 0 ? defaults.width : 512},
|
||||
{"height", defaults.height > 0 ? defaults.height : 512},
|
||||
{"strength", defaults.strength},
|
||||
{"seed", defaults.seed},
|
||||
{"video_frames", defaults.video_frames},
|
||||
{"fps", defaults.fps},
|
||||
{"moe_boundary", defaults.moe_boundary},
|
||||
{"vace_strength", defaults.vace_strength},
|
||||
{"sample_params", make_sample_params_json(defaults.sample_params, defaults.skip_layers)},
|
||||
{"high_noise_sample_params", make_sample_params_json(defaults.high_noise_sample_params, defaults.high_noise_skip_layers)},
|
||||
{"hires", make_hires_json(defaults)},
|
||||
{"vae_tiling_params", make_vae_tiling_json(defaults.vae_tiling_params)},
|
||||
{"cache_mode", defaults.cache_mode},
|
||||
{"cache_option", defaults.cache_option},
|
||||
{"scm_mask", defaults.scm_mask},
|
||||
{"scm_policy_dynamic", defaults.scm_policy_dynamic},
|
||||
{"output_format", output_format},
|
||||
{"output_compression", 100},
|
||||
};
|
||||
}
|
||||
|
||||
static json make_img_gen_features_json() {
|
||||
return {
|
||||
{"init_image", true},
|
||||
{"mask_image", true},
|
||||
{"control_image", true},
|
||||
{"ref_images", true},
|
||||
{"lora", true},
|
||||
{"vae_tiling", true},
|
||||
{"hires", true},
|
||||
{"cache", true},
|
||||
{"cancel_queued", true},
|
||||
{"cancel_generating", false},
|
||||
};
|
||||
}
|
||||
|
||||
static json make_vid_gen_features_json() {
|
||||
return {
|
||||
{"init_image", true},
|
||||
{"end_image", true},
|
||||
{"control_frames", true},
|
||||
{"high_noise_sample_params", true},
|
||||
{"lora", true},
|
||||
{"vae_tiling", true},
|
||||
{"cache", true},
|
||||
{"cancel_queued", true},
|
||||
{"cancel_generating", false},
|
||||
};
|
||||
}
|
||||
|
||||
static json make_capabilities_json(ServerRuntime& runtime) {
|
||||
refresh_lora_cache(runtime);
|
||||
refresh_upscaler_cache(runtime);
|
||||
|
||||
AsyncJobManager& manager = *runtime.async_job_manager;
|
||||
const auto& defaults = *runtime.default_gen_params;
|
||||
const fs::path model_path = resolve_display_model_path(runtime);
|
||||
const bool supports_img = runtime_supports_generation_mode(runtime, IMG_GEN);
|
||||
const bool supports_vid = runtime_supports_generation_mode(runtime, VID_GEN);
|
||||
json samplers = json::array();
|
||||
json schedulers = json::array();
|
||||
json image_output_formats = supported_img_output_formats();
|
||||
json video_output_formats = supported_vid_output_formats();
|
||||
json available_loras = json::array();
|
||||
json available_upscalers = json::array();
|
||||
json supported_modes = json::array();
|
||||
|
||||
for (int i = 0; i < SAMPLE_METHOD_COUNT; ++i) {
|
||||
samplers.push_back(sd_sample_method_name((sample_method_t)i));
|
||||
}
|
||||
|
||||
for (int i = 0; i < SCHEDULER_COUNT; ++i) {
|
||||
schedulers.push_back(sd_scheduler_name((scheduler_t)i));
|
||||
if (i == DISCRETE_SCHEDULER) {
|
||||
schedulers.push_back("normal");
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(*runtime.lora_mutex);
|
||||
for (const auto& entry : *runtime.lora_cache) {
|
||||
available_loras.push_back({
|
||||
{"name", entry.name},
|
||||
{"path", entry.path},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
available_upscalers.push_back({
|
||||
{"name", "None"},
|
||||
});
|
||||
available_upscalers.push_back({
|
||||
{"name", "Lanczos"},
|
||||
});
|
||||
available_upscalers.push_back({
|
||||
{"name", "Nearest"},
|
||||
});
|
||||
available_upscalers.push_back({
|
||||
{"name", "Latent"},
|
||||
});
|
||||
available_upscalers.push_back({
|
||||
{"name", "Latent (nearest)"},
|
||||
});
|
||||
available_upscalers.push_back({
|
||||
{"name", "Latent (nearest-exact)"},
|
||||
});
|
||||
available_upscalers.push_back({
|
||||
{"name", "Latent (antialiased)"},
|
||||
});
|
||||
available_upscalers.push_back({
|
||||
{"name", "Latent (bicubic)"},
|
||||
});
|
||||
available_upscalers.push_back({
|
||||
{"name", "Latent (bicubic antialiased)"},
|
||||
});
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(*runtime.upscaler_mutex);
|
||||
for (const auto& entry : *runtime.upscaler_cache) {
|
||||
available_upscalers.push_back({
|
||||
{"name", entry.name},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (supports_img) {
|
||||
supported_modes.push_back("img_gen");
|
||||
}
|
||||
if (supports_vid) {
|
||||
supported_modes.push_back("vid_gen");
|
||||
}
|
||||
|
||||
std::string default_img_output_format = "png";
|
||||
std::string default_vid_output_format = "avi";
|
||||
if (!image_output_formats.empty()) {
|
||||
default_img_output_format = image_output_formats[0].get<std::string>();
|
||||
}
|
||||
if (!video_output_formats.empty()) {
|
||||
default_vid_output_format = video_output_formats[0].get<std::string>();
|
||||
}
|
||||
|
||||
json defaults_by_mode = json::object();
|
||||
json output_formats_by_mode = json::object();
|
||||
json features_by_mode = json::object();
|
||||
if (supports_img) {
|
||||
defaults_by_mode["img_gen"] = make_img_gen_defaults_json(defaults, default_img_output_format);
|
||||
output_formats_by_mode["img_gen"] = image_output_formats;
|
||||
features_by_mode["img_gen"] = make_img_gen_features_json();
|
||||
}
|
||||
if (supports_vid) {
|
||||
defaults_by_mode["vid_gen"] = make_vid_gen_defaults_json(defaults, default_vid_output_format);
|
||||
output_formats_by_mode["vid_gen"] = video_output_formats;
|
||||
features_by_mode["vid_gen"] = make_vid_gen_features_json();
|
||||
}
|
||||
|
||||
json top_level_defaults = json::object();
|
||||
json top_level_output_formats = json::array();
|
||||
json top_level_features = {
|
||||
{"cancel_queued", true},
|
||||
{"cancel_generating", false},
|
||||
};
|
||||
std::string current_mode = "";
|
||||
if (supports_img) {
|
||||
current_mode = "img_gen";
|
||||
top_level_defaults = defaults_by_mode["img_gen"];
|
||||
top_level_output_formats = output_formats_by_mode["img_gen"];
|
||||
top_level_features = features_by_mode["img_gen"];
|
||||
} else if (supports_vid) {
|
||||
current_mode = "vid_gen";
|
||||
top_level_defaults = defaults_by_mode["vid_gen"];
|
||||
top_level_output_formats = output_formats_by_mode["vid_gen"];
|
||||
top_level_features = features_by_mode["vid_gen"];
|
||||
}
|
||||
|
||||
json result;
|
||||
result["model"] = {
|
||||
{"name", model_path.filename().u8string()},
|
||||
{"stem", model_path.stem().u8string()},
|
||||
{"path", model_path.u8string()},
|
||||
};
|
||||
result["current_mode"] = current_mode;
|
||||
result["supported_modes"] = supported_modes;
|
||||
result["defaults"] = top_level_defaults;
|
||||
result["defaults_by_mode"] = defaults_by_mode;
|
||||
result["limits"] = {
|
||||
{"min_width", 64},
|
||||
{"max_width", 4096},
|
||||
{"min_height", 64},
|
||||
{"max_height", 4096},
|
||||
{"max_batch_count", 8},
|
||||
{"max_queue_size", manager.max_pending_jobs},
|
||||
};
|
||||
result["samplers"] = samplers;
|
||||
result["schedulers"] = schedulers;
|
||||
result["output_formats"] = top_level_output_formats;
|
||||
result["output_formats_by_mode"] = output_formats_by_mode;
|
||||
result["features"] = top_level_features;
|
||||
result["features_by_mode"] = features_by_mode;
|
||||
result["loras"] = available_loras;
|
||||
result["upscalers"] = available_upscalers;
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool parse_img_gen_request(const json& body,
|
||||
ServerRuntime& runtime,
|
||||
ImgGenJobRequest& request,
|
||||
std::string& error_message) {
|
||||
request.gen_params = *runtime.default_gen_params;
|
||||
|
||||
refresh_lora_cache(runtime);
|
||||
if (!request.gen_params.from_json_str(body.dump(), [&](const std::string& path) {
|
||||
return get_lora_full_path(runtime, path);
|
||||
})) {
|
||||
error_message = "invalid generation parameters";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string output_format = body.value("output_format", "png");
|
||||
int output_compression = body.value("output_compression", 100);
|
||||
if (!assign_output_options(request, output_format, output_compression, true, error_message)) {
|
||||
return false;
|
||||
}
|
||||
// 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";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool parse_vid_gen_request(const json& body,
|
||||
ServerRuntime& runtime,
|
||||
VidGenJobRequest& request,
|
||||
std::string& error_message) {
|
||||
request.gen_params = *runtime.default_gen_params;
|
||||
|
||||
refresh_lora_cache(runtime);
|
||||
if (!request.gen_params.from_json_str(body.dump(), [&](const std::string& path) {
|
||||
return get_lora_full_path(runtime, path);
|
||||
})) {
|
||||
error_message = "invalid generation parameters";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string output_format = body.value("output_format", "webm");
|
||||
int output_compression = body.value("output_compression", 100);
|
||||
if (!assign_output_options(request, output_format, output_compression, error_message)) {
|
||||
return false;
|
||||
}
|
||||
// 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";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void register_sdcpp_api_endpoints(httplib::Server& svr, ServerRuntime& rt) {
|
||||
ServerRuntime* runtime = &rt;
|
||||
|
||||
svr.Get("/sdcpp/v1/capabilities", [runtime](const httplib::Request&, httplib::Response& res) {
|
||||
res.status = 200;
|
||||
res.set_content(make_capabilities_json(*runtime).dump(), "application/json");
|
||||
});
|
||||
|
||||
svr.Post("/sdcpp/v1/img_gen", [runtime](const httplib::Request& req, httplib::Response& res) {
|
||||
try {
|
||||
if (req.body.empty()) {
|
||||
res.status = 400;
|
||||
res.set_content(R"({"error":"empty body"})", "application/json");
|
||||
return;
|
||||
}
|
||||
if (!runtime_supports_generation_mode(*runtime, IMG_GEN)) {
|
||||
res.status = 400;
|
||||
res.set_content(json({{"error", unsupported_generation_mode_error(IMG_GEN)}}).dump(), "application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
json body = json::parse(req.body);
|
||||
ImgGenJobRequest request;
|
||||
std::string error_message;
|
||||
if (!parse_img_gen_request(body, *runtime, request, error_message)) {
|
||||
res.status = 400;
|
||||
res.set_content(json({{"error", error_message}}).dump(), "application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncJobManager& manager = *runtime->async_job_manager;
|
||||
std::shared_ptr<AsyncGenerationJob> job = std::make_shared<AsyncGenerationJob>();
|
||||
job->kind = AsyncJobKind::ImgGen;
|
||||
job->status = AsyncJobStatus::Queued;
|
||||
job->created_at = unix_timestamp_now();
|
||||
job->img_gen = std::move(request);
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(manager.mutex);
|
||||
purge_expired_jobs(manager);
|
||||
if (count_pending_jobs(manager) >= manager.max_pending_jobs) {
|
||||
res.status = 429;
|
||||
res.set_content(R"({"error":"job queue is full"})", "application/json");
|
||||
return;
|
||||
}
|
||||
job->id = make_async_job_id(manager);
|
||||
manager.jobs[job->id] = job;
|
||||
manager.queue.push_back(job->id);
|
||||
}
|
||||
|
||||
manager.cv.notify_one();
|
||||
|
||||
json out;
|
||||
out["id"] = job->id;
|
||||
out["kind"] = async_job_kind_name(job->kind);
|
||||
out["status"] = async_job_status_name(job->status);
|
||||
out["created"] = job->created_at;
|
||||
out["poll_url"] = "/sdcpp/v1/jobs/" + job->id;
|
||||
|
||||
res.status = 202;
|
||||
res.set_content(out.dump(), "application/json");
|
||||
} catch (const json::parse_error& e) {
|
||||
res.status = 400;
|
||||
res.set_content(json({{"error", "invalid json"}, {"message", e.what()}}).dump(), "application/json");
|
||||
} catch (const std::exception& e) {
|
||||
res.status = 500;
|
||||
res.set_content(json({{"error", "server_error"}, {"message", e.what()}}).dump(), "application/json");
|
||||
}
|
||||
});
|
||||
|
||||
svr.Post("/sdcpp/v1/vid_gen", [runtime](const httplib::Request& req, httplib::Response& res) {
|
||||
try {
|
||||
if (req.body.empty()) {
|
||||
res.status = 400;
|
||||
res.set_content(R"({"error":"empty body"})", "application/json");
|
||||
return;
|
||||
}
|
||||
if (!runtime_supports_generation_mode(*runtime, VID_GEN)) {
|
||||
res.status = 400;
|
||||
res.set_content(json({{"error", unsupported_generation_mode_error(VID_GEN)}}).dump(), "application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
json body = json::parse(req.body);
|
||||
VidGenJobRequest request;
|
||||
std::string error_message;
|
||||
if (!parse_vid_gen_request(body, *runtime, request, error_message)) {
|
||||
res.status = 400;
|
||||
res.set_content(json({{"error", error_message}}).dump(), "application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncJobManager& manager = *runtime->async_job_manager;
|
||||
std::shared_ptr<AsyncGenerationJob> job = std::make_shared<AsyncGenerationJob>();
|
||||
job->kind = AsyncJobKind::VidGen;
|
||||
job->status = AsyncJobStatus::Queued;
|
||||
job->created_at = unix_timestamp_now();
|
||||
job->vid_gen = std::move(request);
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(manager.mutex);
|
||||
purge_expired_jobs(manager);
|
||||
if (count_pending_jobs(manager) >= manager.max_pending_jobs) {
|
||||
res.status = 429;
|
||||
res.set_content(R"({"error":"job queue is full"})", "application/json");
|
||||
return;
|
||||
}
|
||||
job->id = make_async_job_id(manager);
|
||||
manager.jobs[job->id] = job;
|
||||
manager.queue.push_back(job->id);
|
||||
}
|
||||
|
||||
manager.cv.notify_one();
|
||||
|
||||
json out;
|
||||
out["id"] = job->id;
|
||||
out["kind"] = async_job_kind_name(job->kind);
|
||||
out["status"] = async_job_status_name(job->status);
|
||||
out["created"] = job->created_at;
|
||||
out["poll_url"] = "/sdcpp/v1/jobs/" + job->id;
|
||||
|
||||
res.status = 202;
|
||||
res.set_content(out.dump(), "application/json");
|
||||
} catch (const json::parse_error& e) {
|
||||
res.status = 400;
|
||||
res.set_content(json({{"error", "invalid json"}, {"message", e.what()}}).dump(), "application/json");
|
||||
} catch (const std::exception& e) {
|
||||
res.status = 500;
|
||||
res.set_content(json({{"error", "server_error"}, {"message", e.what()}}).dump(), "application/json");
|
||||
}
|
||||
});
|
||||
|
||||
svr.Get(R"(/sdcpp/v1/jobs/([A-Za-z0-9_\-]+))", [runtime](const httplib::Request& req, httplib::Response& res) {
|
||||
AsyncJobManager& manager = *runtime->async_job_manager;
|
||||
std::lock_guard<std::mutex> lock(manager.mutex);
|
||||
purge_expired_jobs(manager);
|
||||
|
||||
std::string job_id = req.matches[1];
|
||||
auto it = manager.jobs.find(job_id);
|
||||
if (it == manager.jobs.end()) {
|
||||
if (manager.expired_jobs.find(job_id) != manager.expired_jobs.end()) {
|
||||
res.status = 410;
|
||||
res.set_content(R"({"error":"job expired"})", "application/json");
|
||||
} else {
|
||||
res.status = 404;
|
||||
res.set_content(R"({"error":"job not found"})", "application/json");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
res.status = 200;
|
||||
res.set_content(make_async_job_json(manager, *it->second).dump(), "application/json");
|
||||
});
|
||||
|
||||
svr.Post(R"(/sdcpp/v1/jobs/([A-Za-z0-9_\-]+)/cancel)", [runtime](const httplib::Request& req, httplib::Response& res) {
|
||||
AsyncJobManager& manager = *runtime->async_job_manager;
|
||||
std::lock_guard<std::mutex> lock(manager.mutex);
|
||||
purge_expired_jobs(manager);
|
||||
|
||||
std::string job_id = req.matches[1];
|
||||
auto it = manager.jobs.find(job_id);
|
||||
if (it == manager.jobs.end()) {
|
||||
if (manager.expired_jobs.find(job_id) != manager.expired_jobs.end()) {
|
||||
res.status = 410;
|
||||
res.set_content(R"({"error":"job expired"})", "application/json");
|
||||
} else {
|
||||
res.status = 404;
|
||||
res.set_content(R"({"error":"job not found"})", "application/json");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
auto& job = *it->second;
|
||||
if (job.status == AsyncJobStatus::Queued) {
|
||||
if (!cancel_queued_job(manager, job)) {
|
||||
res.status = 409;
|
||||
res.set_content(R"({"error":"job queue state changed before cancellation"})", "application/json");
|
||||
return;
|
||||
}
|
||||
res.status = 200;
|
||||
res.set_content(make_async_job_json(manager, job).dump(), "application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
if (job.status == AsyncJobStatus::Generating) {
|
||||
res.status = 409;
|
||||
res.set_content(R"({"error":"job is currently generating and cannot be interrupted yet"})", "application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
res.status = 200;
|
||||
res.set_content(make_async_job_json(manager, job).dump(), "application/json");
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
#include "runtime.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <mutex>
|
||||
#include <regex>
|
||||
#include <sstream>
|
||||
|
||||
#include "common/common.h"
|
||||
#include "common/log.h"
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
static std::string lower_ascii(std::string value) {
|
||||
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) {
|
||||
return static_cast<char>(std::tolower(c));
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
static bool is_supported_model_ext(const fs::path& p) {
|
||||
auto ext = lower_ascii(p.extension().string());
|
||||
return ext == ".gguf" || ext == ".pt" || ext == ".pth" || ext == ".safetensors";
|
||||
}
|
||||
|
||||
static const std::string k_base64_chars =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
"abcdefghijklmnopqrstuvwxyz"
|
||||
"0123456789+/";
|
||||
|
||||
std::string base64_encode(const std::vector<uint8_t>& bytes) {
|
||||
std::string ret;
|
||||
int val = 0;
|
||||
int valb = -6;
|
||||
for (uint8_t c : bytes) {
|
||||
val = (val << 8) + c;
|
||||
valb += 8;
|
||||
while (valb >= 0) {
|
||||
ret.push_back(k_base64_chars[(val >> valb) & 0x3F]);
|
||||
valb -= 6;
|
||||
}
|
||||
}
|
||||
if (valb > -6) {
|
||||
ret.push_back(k_base64_chars[((val << 8) >> (valb + 8)) & 0x3F]);
|
||||
}
|
||||
while (ret.size() % 4) {
|
||||
ret.push_back('=');
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::string normalize_output_format(std::string output_format) {
|
||||
std::transform(output_format.begin(), output_format.end(), output_format.begin(),
|
||||
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
return output_format;
|
||||
}
|
||||
|
||||
std::vector<std::string> supported_img_output_formats(bool allow_webp) {
|
||||
std::vector<std::string> formats = {"png", "jpeg"};
|
||||
#ifdef SD_USE_WEBP
|
||||
if (allow_webp) {
|
||||
formats.push_back("webp");
|
||||
}
|
||||
#else
|
||||
(void)allow_webp;
|
||||
#endif
|
||||
return formats;
|
||||
}
|
||||
|
||||
std::vector<std::string> supported_vid_output_formats() {
|
||||
std::vector<std::string> formats;
|
||||
#ifdef SD_USE_WEBM
|
||||
formats.push_back("webm");
|
||||
#endif
|
||||
#ifdef SD_USE_WEBP
|
||||
formats.push_back("webp");
|
||||
#endif
|
||||
formats.push_back("avi");
|
||||
return formats;
|
||||
}
|
||||
|
||||
static std::string valid_vid_output_formats_message() {
|
||||
const std::vector<std::string> formats = supported_vid_output_formats();
|
||||
|
||||
std::string message = "invalid output_format, must be one of [";
|
||||
for (size_t i = 0; i < formats.size(); ++i) {
|
||||
if (i > 0) {
|
||||
message += ", ";
|
||||
}
|
||||
message += formats[i];
|
||||
}
|
||||
message += "]";
|
||||
return message;
|
||||
}
|
||||
|
||||
bool assign_output_options(ImgGenJobRequest& request,
|
||||
std::string output_format,
|
||||
int output_compression,
|
||||
bool allow_webp,
|
||||
std::string& error_message) {
|
||||
request.output_format = normalize_output_format(std::move(output_format));
|
||||
request.output_compression = std::clamp(output_compression, 0, 100);
|
||||
|
||||
const std::vector<std::string> valid_formats = supported_img_output_formats(allow_webp);
|
||||
const bool valid_format = std::find(valid_formats.begin(),
|
||||
valid_formats.end(),
|
||||
request.output_format) != valid_formats.end();
|
||||
if (!valid_format) {
|
||||
error_message = "invalid output_format, must be one of [";
|
||||
for (size_t i = 0; i < valid_formats.size(); ++i) {
|
||||
if (i > 0) {
|
||||
error_message += ", ";
|
||||
}
|
||||
error_message += valid_formats[i];
|
||||
}
|
||||
error_message += "]";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool assign_output_options(VidGenJobRequest& request,
|
||||
std::string output_format,
|
||||
int output_compression,
|
||||
std::string& error_message) {
|
||||
request.output_format = normalize_output_format(std::move(output_format));
|
||||
request.output_compression = std::clamp(output_compression, 0, 100);
|
||||
|
||||
if (request.output_format == "avi") {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (request.output_format == "webm") {
|
||||
#ifdef SD_USE_WEBM
|
||||
return true;
|
||||
#else
|
||||
error_message = valid_vid_output_formats_message();
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
if (request.output_format == "webp") {
|
||||
#ifdef SD_USE_WEBP
|
||||
return true;
|
||||
#else
|
||||
error_message = valid_vid_output_formats_message();
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
error_message = valid_vid_output_formats_message();
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string video_mime_type(const std::string& output_format) {
|
||||
if (output_format == "webm") {
|
||||
return "video/webm";
|
||||
}
|
||||
if (output_format == "webp") {
|
||||
return "image/webp";
|
||||
}
|
||||
return "video/x-msvideo";
|
||||
}
|
||||
|
||||
bool runtime_supports_generation_mode(const ServerRuntime& runtime, SDMode mode) {
|
||||
if (mode == VID_GEN) {
|
||||
return sd_ctx_supports_video_generation(runtime.sd_ctx);
|
||||
}
|
||||
if (mode == IMG_GEN) {
|
||||
return sd_ctx_supports_image_generation(runtime.sd_ctx);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string unsupported_generation_mode_error(SDMode mode) {
|
||||
if (mode == VID_GEN) {
|
||||
return "loaded model does not support vid_gen";
|
||||
}
|
||||
if (mode == IMG_GEN) {
|
||||
return "loaded model does not support img_gen";
|
||||
}
|
||||
return "loaded model does not support requested mode";
|
||||
}
|
||||
|
||||
ArgOptions SDSvrParams::get_options() {
|
||||
ArgOptions options;
|
||||
|
||||
options.string_options = {
|
||||
{"-l", "--listen-ip", "server listen ip (default: 127.0.0.1)", 0, &listen_ip},
|
||||
{"", "--serve-html-path", "path to HTML file to serve at root (optional)", 0, &serve_html_path},
|
||||
};
|
||||
|
||||
options.int_options = {
|
||||
{"", "--listen-port", "server listen port (default: 1234)", &listen_port},
|
||||
};
|
||||
|
||||
options.bool_options = {
|
||||
{"-v", "--verbose", "print extra info", true, &verbose},
|
||||
{"", "--color", "colors the logging tags according to level", true, &color},
|
||||
};
|
||||
|
||||
auto on_help_arg = [&](int, const char**, int, bool& valid) {
|
||||
normal_exit = true;
|
||||
valid = true;
|
||||
return -1;
|
||||
};
|
||||
|
||||
options.manual_options = {
|
||||
{"-h", "--help", "show this help message and exit", on_help_arg},
|
||||
};
|
||||
return options;
|
||||
}
|
||||
|
||||
bool SDSvrParams::validate() {
|
||||
if (listen_ip.empty()) {
|
||||
LOG_ERROR("error: the following arguments are required: listen_ip");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (listen_port < 0 || listen_port > 65535) {
|
||||
LOG_ERROR("error: listen_port should be in the range [0, 65535]");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!serve_html_path.empty() && !fs::exists(serve_html_path)) {
|
||||
LOG_ERROR("error: serve_html_path file does not exist: %s", serve_html_path.c_str());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SDSvrParams::resolve_and_validate() {
|
||||
if (!validate()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string SDSvrParams::to_string() const {
|
||||
std::ostringstream oss;
|
||||
oss << "SDSvrParams {\n"
|
||||
<< " listen_ip: " << listen_ip << ",\n"
|
||||
<< " listen_port: \"" << listen_port << "\",\n"
|
||||
<< " serve_html_path: \"" << serve_html_path << "\",\n"
|
||||
<< "}";
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
void refresh_lora_cache(ServerRuntime& rt) {
|
||||
std::vector<LoraEntry> new_cache;
|
||||
|
||||
fs::path lora_dir = rt.ctx_params->lora_model_dir;
|
||||
if (fs::exists(lora_dir) && fs::is_directory(lora_dir)) {
|
||||
for (auto& entry : fs::recursive_directory_iterator(lora_dir, fs::directory_options::skip_permission_denied)) {
|
||||
if (!entry.is_regular_file()) {
|
||||
continue;
|
||||
}
|
||||
const fs::path& p = entry.path();
|
||||
if (!is_supported_model_ext(p)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
LoraEntry lora_entry;
|
||||
lora_entry.name = p.stem().u8string();
|
||||
lora_entry.fullpath = p.u8string();
|
||||
std::string rel = p.lexically_relative(lora_dir).u8string();
|
||||
std::replace(rel.begin(), rel.end(), '\\', '/');
|
||||
lora_entry.path = rel;
|
||||
|
||||
new_cache.push_back(std::move(lora_entry));
|
||||
}
|
||||
}
|
||||
|
||||
std::sort(new_cache.begin(), new_cache.end(), [](const LoraEntry& a, const LoraEntry& b) {
|
||||
return a.path < b.path;
|
||||
});
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(*rt.lora_mutex);
|
||||
*rt.lora_cache = std::move(new_cache);
|
||||
}
|
||||
}
|
||||
|
||||
std::string get_lora_full_path(ServerRuntime& rt, const std::string& path) {
|
||||
std::lock_guard<std::mutex> lock(*rt.lora_mutex);
|
||||
auto it = std::find_if(rt.lora_cache->begin(), rt.lora_cache->end(),
|
||||
[&](const LoraEntry& entry) { return entry.path == path; });
|
||||
return it != rt.lora_cache->end() ? it->fullpath : "";
|
||||
}
|
||||
|
||||
void refresh_upscaler_cache(ServerRuntime& rt) {
|
||||
std::vector<UpscalerEntry> new_cache;
|
||||
|
||||
fs::path upscaler_dir = rt.ctx_params->hires_upscalers_dir;
|
||||
if (fs::exists(upscaler_dir) && fs::is_directory(upscaler_dir)) {
|
||||
for (auto& entry : fs::directory_iterator(upscaler_dir)) {
|
||||
if (!entry.is_regular_file()) {
|
||||
continue;
|
||||
}
|
||||
const fs::path& p = entry.path();
|
||||
if (!is_supported_model_ext(p)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
UpscalerEntry upscaler_entry;
|
||||
upscaler_entry.name = p.stem().u8string();
|
||||
upscaler_entry.fullpath = fs::absolute(p).lexically_normal().u8string();
|
||||
upscaler_entry.model_name = "ESRGAN_4x";
|
||||
upscaler_entry.path = p.filename().u8string();
|
||||
|
||||
new_cache.push_back(std::move(upscaler_entry));
|
||||
}
|
||||
}
|
||||
|
||||
std::sort(new_cache.begin(), new_cache.end(), [](const UpscalerEntry& a, const UpscalerEntry& b) {
|
||||
return a.name < b.name;
|
||||
});
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(*rt.upscaler_mutex);
|
||||
*rt.upscaler_cache = std::move(new_cache);
|
||||
}
|
||||
}
|
||||
|
||||
int64_t unix_timestamp_now() {
|
||||
return std::chrono::duration_cast<std::chrono::seconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch())
|
||||
.count();
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <json.hpp>
|
||||
#include "common/common.h"
|
||||
#include "common/resource_owners.hpp"
|
||||
#include "stable-diffusion.h"
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
struct ArgOptions;
|
||||
struct SDContextParams;
|
||||
struct AsyncJobManager;
|
||||
|
||||
struct SDSvrParams {
|
||||
std::string listen_ip = "127.0.0.1";
|
||||
int listen_port = 1234;
|
||||
std::string serve_html_path;
|
||||
bool normal_exit = false;
|
||||
bool verbose = false;
|
||||
bool color = false;
|
||||
|
||||
ArgOptions get_options();
|
||||
bool validate();
|
||||
bool resolve_and_validate();
|
||||
std::string to_string() const;
|
||||
};
|
||||
|
||||
struct LoraEntry {
|
||||
std::string name;
|
||||
std::string path;
|
||||
std::string fullpath;
|
||||
};
|
||||
|
||||
struct UpscalerEntry {
|
||||
std::string name;
|
||||
std::string path;
|
||||
std::string fullpath;
|
||||
std::string model_name;
|
||||
int scale = 4;
|
||||
};
|
||||
|
||||
struct ServerRuntime {
|
||||
sd_ctx_t* sd_ctx;
|
||||
std::mutex* sd_ctx_mutex;
|
||||
const SDSvrParams* svr_params;
|
||||
const SDContextParams* ctx_params;
|
||||
const SDGenerationParams* default_gen_params;
|
||||
std::vector<LoraEntry>* lora_cache;
|
||||
std::mutex* lora_mutex;
|
||||
std::vector<UpscalerEntry>* upscaler_cache;
|
||||
std::mutex* upscaler_mutex;
|
||||
AsyncJobManager* async_job_manager;
|
||||
};
|
||||
|
||||
struct ImgGenJobRequest {
|
||||
SDGenerationParams gen_params;
|
||||
std::string output_format = "png";
|
||||
int output_compression = 100;
|
||||
|
||||
sd_img_gen_params_t to_sd_img_gen_params_t() {
|
||||
return gen_params.to_sd_img_gen_params_t();
|
||||
}
|
||||
};
|
||||
|
||||
struct VidGenJobRequest {
|
||||
SDGenerationParams gen_params;
|
||||
std::string output_format = "webm";
|
||||
int output_compression = 100;
|
||||
|
||||
sd_vid_gen_params_t to_sd_vid_gen_params_t() {
|
||||
return gen_params.to_sd_vid_gen_params_t();
|
||||
}
|
||||
};
|
||||
|
||||
std::string base64_encode(const std::vector<uint8_t>& bytes);
|
||||
std::string normalize_output_format(std::string output_format);
|
||||
std::vector<std::string> supported_img_output_formats(bool allow_webp = true);
|
||||
std::vector<std::string> supported_vid_output_formats();
|
||||
bool assign_output_options(ImgGenJobRequest& request,
|
||||
std::string output_format,
|
||||
int output_compression,
|
||||
bool allow_webp,
|
||||
std::string& error_message);
|
||||
bool assign_output_options(VidGenJobRequest& request,
|
||||
std::string output_format,
|
||||
int output_compression,
|
||||
std::string& error_message);
|
||||
std::string video_mime_type(const std::string& output_format);
|
||||
bool runtime_supports_generation_mode(const ServerRuntime& runtime, SDMode mode);
|
||||
std::string unsupported_generation_mode_error(SDMode mode);
|
||||
void refresh_lora_cache(ServerRuntime& rt);
|
||||
std::string get_lora_full_path(ServerRuntime& rt, const std::string& path);
|
||||
void refresh_upscaler_cache(ServerRuntime& rt);
|
||||
int64_t unix_timestamp_now();
|
||||
@@ -0,0 +1,54 @@
|
||||
$patterns = @(
|
||||
"src/*.cpp"
|
||||
"src/*.h"
|
||||
"src/*.hpp"
|
||||
"src/conditioning/*.cpp"
|
||||
"src/conditioning/*.h"
|
||||
"src/conditioning/*.hpp"
|
||||
"src/core/*.cpp"
|
||||
"src/core/*.h"
|
||||
"src/core/*.hpp"
|
||||
"src/extensions/*.cpp"
|
||||
"src/extensions/*.h"
|
||||
"src/extensions/*.hpp"
|
||||
"src/runtime/*.cpp"
|
||||
"src/runtime/*.h"
|
||||
"src/runtime/*.hpp"
|
||||
"src/model/*/*.cpp"
|
||||
"src/model/*/*.h"
|
||||
"src/model/*/*.hpp"
|
||||
"src/tokenizers/*.h"
|
||||
"src/tokenizers/*.cpp"
|
||||
"src/tokenizers/vocab/*.h"
|
||||
"src/tokenizers/vocab/*.cpp"
|
||||
"src/model_io/*.h"
|
||||
"src/model_io/*.cpp"
|
||||
"examples/cli/*.cpp"
|
||||
"examples/cli/*.h"
|
||||
"examples/server/*.cpp"
|
||||
"examples/common/*.hpp"
|
||||
"examples/common/*.h"
|
||||
"examples/common/*.cpp"
|
||||
)
|
||||
|
||||
$root = (Get-Location).Path
|
||||
|
||||
foreach ($pattern in $patterns) {
|
||||
$files = Get-ChildItem -Path $pattern -File -ErrorAction SilentlyContinue | Sort-Object FullName
|
||||
|
||||
foreach ($file in $files) {
|
||||
$relativePath = $file.FullName.Substring($root.Length).TrimStart('\', '/') -replace '\\', '/'
|
||||
|
||||
if ($relativePath -like "vocab*") {
|
||||
continue
|
||||
}
|
||||
|
||||
Write-Host "formatting '$relativePath'"
|
||||
|
||||
# if ($relativePath -ne "stable-diffusion.h") {
|
||||
# clang-tidy -fix -p build_linux/ "$relativePath"
|
||||
# }
|
||||
|
||||
& clang-format -style=file -i $relativePath
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,17 @@
|
||||
for f in *.cpp *.h *.hpp examples/cli/*.cpp examples/common/*.hpp examples/cli/*.h examples/server/*.cpp; do
|
||||
for f in src/*.cpp src/*.h src/*.hpp \
|
||||
src/conditioning/*.cpp src/conditioning/*.h src/conditioning/*.hpp \
|
||||
src/core/*.cpp src/core/*.h src/core/*.hpp \
|
||||
src/extensions/*.cpp src/extensions/*.h src/extensions/*.hpp \
|
||||
src/runtime/*.cpp src/runtime/*.h src/runtime/*.hpp \
|
||||
src/model/*/*.cpp src/model/*/*.h src/model/*/*.hpp \
|
||||
src/tokenizers/*.h src/tokenizers/*.cpp src/tokenizers/vocab/*.h src/tokenizers/vocab/*.cpp \
|
||||
src/model_io/*.h src/model_io/*.cpp examples/cli/*.cpp examples/cli/*.h examples/server/*.cpp \
|
||||
examples/common/*.hpp examples/common/*.h examples/common/*.cpp; do
|
||||
[[ -e "$f" ]] || continue
|
||||
[[ "$f" == vocab* ]] && continue
|
||||
echo "formatting '$f'"
|
||||
# if [ "$f" != "stable-diffusion.h" ]; then
|
||||
# clang-tidy -fix -p build_linux/ "$f"
|
||||
# fi
|
||||
clang-format -style=file -i "$f"
|
||||
done
|
||||
done
|
||||
|
||||