From 455b2920a56b396722c21c193159bed5d713ec1f Mon Sep 17 00:00:00 2001 From: Richard Palethorpe Date: Thu, 7 Aug 2025 13:19:18 +0100 Subject: [PATCH 1/2] Try converting SD to purego --- .../go/stablediffusion-ggml/CMakeLists.txt | 15 +++ backend/go/stablediffusion-ggml/Makefile | 93 +++---------------- backend/go/stablediffusion-ggml/gosd.cpp | 82 ++++++++-------- backend/go/stablediffusion-ggml/gosd.go | 90 +++++------------- backend/go/stablediffusion-ggml/gosd.h | 6 +- backend/go/stablediffusion-ggml/main.go | 20 ++++ go.mod | 1 + go.sum | 5 + 8 files changed, 119 insertions(+), 193 deletions(-) create mode 100644 backend/go/stablediffusion-ggml/CMakeLists.txt diff --git a/backend/go/stablediffusion-ggml/CMakeLists.txt b/backend/go/stablediffusion-ggml/CMakeLists.txt new file mode 100644 index 000000000000..fd7a1214a811 --- /dev/null +++ b/backend/go/stablediffusion-ggml/CMakeLists.txt @@ -0,0 +1,15 @@ +cmake_minimum_required(VERSION 3.12) +project(gosd LANGUAGES C CXX) + +add_subdirectory(./sources/stablediffusion-ggml.cpp) + +add_library(gosd MODULE gosd.cpp) +target_link_libraries(gosd PRIVATE stable-diffusion ggml zip stdc++fs) + +target_include_directories(gosd PUBLIC + stable-diffusion.cpp + stable-diffusion.cpp/thirdparty +) + +set_property(TARGET gosd PROPERTY CXX_STANDARD 17) +set_target_properties(gosd PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) diff --git a/backend/go/stablediffusion-ggml/Makefile b/backend/go/stablediffusion-ggml/Makefile index 831da0bff378..d4983430f5d3 100644 --- a/backend/go/stablediffusion-ggml/Makefile +++ b/backend/go/stablediffusion-ggml/Makefile @@ -1,28 +1,15 @@ -INCLUDE_PATH := $(abspath ./) -LIBRARY_PATH := $(abspath ./) - -AR?=ar CMAKE_ARGS?= BUILD_TYPE?= NATIVE?=false -CUDA_LIBPATH?=/usr/local/cuda/lib64/ -ONEAPI_VARS?=/opt/intel/oneapi/setvars.sh -# keep standard at C11 and C++11 -CXXFLAGS = -I. -I$(INCLUDE_PATH)/sources/stablediffusion-ggml.cpp/thirdparty -I$(INCLUDE_PATH)/sources/stablediffusion-ggml.cpp/ggml/include -I$(INCLUDE_PATH)/sources/stablediffusion-ggml.cpp -O3 -DNDEBUG -std=c++17 -fPIC GOCMD?=go -CGO_LDFLAGS?= -# Avoid parent make file overwriting CGO_LDFLAGS which is needed for hipblas -CGO_LDFLAGS_SYCL= GO_TAGS?= -LD_FLAGS?= # stablediffusion.cpp (ggml) STABLEDIFFUSION_GGML_REPO?=https://github.com/leejet/stable-diffusion.cpp STABLEDIFFUSION_GGML_VERSION?=5900ef6605c6fbf7934239f795c13c97bc993853 -# Disable Shared libs as we are linking on static gRPC and we can't mix shared and static -CMAKE_ARGS+=-DBUILD_SHARED_LIBS=OFF -DGGML_MAX_NAME=128 -DSD_USE_SYSTEM_GGML=OFF +CMAKE_ARGS+=-DGGML_MAX_NAME=128 ifeq ($(NATIVE),false) CMAKE_ARGS+=-DGGML_NATIVE=OFF @@ -31,7 +18,6 @@ endif # If build type is cublas, then we set -DGGML_CUDA=ON to CMAKE_ARGS automatically ifeq ($(BUILD_TYPE),cublas) CMAKE_ARGS+=-DSD_CUDA=ON -DGGML_CUDA=ON - CGO_LDFLAGS+=-lcublas -lcudart -L$(CUDA_LIBPATH) -L$(CUDA_LIBPATH)/stubs/ -lcuda # If build type is openblas then we set -DGGML_BLAS=ON -DGGML_BLAS_VENDOR=OpenBLAS # to CMAKE_ARGS automatically else ifeq ($(BUILD_TYPE),openblas) @@ -46,14 +32,12 @@ else ifeq ($(BUILD_TYPE),hipblas) # But if it's OSX without metal, disable it here else ifeq ($(BUILD_TYPE),vulkan) CMAKE_ARGS+=-DSD_VULKAN=ON -DGGML_VULKAN=ON - CGO_LDFLAGS+=-lvulkan else ifeq ($(OS),Darwin) ifneq ($(BUILD_TYPE),metal) CMAKE_ARGS+=-DSD_METAL=OFF -DGGML_METAL=OFF else CMAKE_ARGS+=-DSD_METAL=ON -DGGML_METAL=ON CMAKE_ARGS+=-DGGML_METAL_EMBED_LIBRARY=ON - TARGET+=--target ggml-metal endif endif @@ -63,12 +47,6 @@ ifeq ($(BUILD_TYPE),sycl_f16) -DCMAKE_CXX_COMPILER=icpx \ -DSD_SYCL=ON \ -DGGML_SYCL_F16=ON - export CC=icx - export CXX=icpx - CGO_LDFLAGS_SYCL += -fsycl -L${DNNLROOT}/lib -ldnnl ${MKLROOT}/lib/intel64/libmkl_sycl.a -fiopenmp -fopenmp-targets=spir64 -lOpenCL - CGO_LDFLAGS_SYCL += $(shell pkg-config --libs mkl-static-lp64-gomp) - CGO_CXXFLAGS += -fiopenmp -fopenmp-targets=spir64 - CGO_CXXFLAGS += $(shell pkg-config --cflags mkl-static-lp64-gomp ) endif ifeq ($(BUILD_TYPE),sycl_f32) @@ -76,73 +54,24 @@ ifeq ($(BUILD_TYPE),sycl_f32) -DCMAKE_C_COMPILER=icx \ -DCMAKE_CXX_COMPILER=icpx \ -DSD_SYCL=ON - export CC=icx - export CXX=icpx - CGO_LDFLAGS_SYCL += -fsycl -L${DNNLROOT}/lib -ldnnl ${MKLROOT}/lib/intel64/libmkl_sycl.a -fiopenmp -fopenmp-targets=spir64 -lOpenCL - CGO_LDFLAGS_SYCL += $(shell pkg-config --libs mkl-static-lp64-gomp) - CGO_CXXFLAGS += -fiopenmp -fopenmp-targets=spir64 - CGO_CXXFLAGS += $(shell pkg-config --cflags mkl-static-lp64-gomp ) endif -# warnings -# CXXFLAGS += -Wall -Wextra -Wpedantic -Wcast-qual -Wno-unused-function - -# Find all .a archives in ARCHIVE_DIR -# (ggml can have different backends cpu, cuda, etc., each backend generates a .a archive) -GGML_ARCHIVE_DIR := build/ggml/src/ -ALL_ARCHIVES := $(shell find $(GGML_ARCHIVE_DIR) -type f -name '*.a') -ALL_OBJS := $(shell find $(GGML_ARCHIVE_DIR) -type f -name '*.o') - -# Name of the single merged library -COMBINED_LIB := libggmlall.a - -# Instead of using the archives generated by GGML, use the object files directly to avoid overwriting objects with the same base name -$(COMBINED_LIB): $(ALL_ARCHIVES) - @echo "Merging all .o into $(COMBINED_LIB): $(ALL_OBJS)" - rm -f $@ - ar -qc $@ $(ALL_OBJS) - # Ensure we have a proper index - ranlib $@ - -build/libstable-diffusion.a: - @echo "Building SD with $(BUILD_TYPE) build type and $(CMAKE_ARGS)" -ifneq (,$(findstring sycl,$(BUILD_TYPE))) - +bash -c "source $(ONEAPI_VARS); \ - mkdir -p build && \ - cd build && \ - cmake $(CMAKE_ARGS) ../sources/stablediffusion-ggml.cpp && \ - cmake --build . --config Release" -else - mkdir -p build && \ - cd build && \ - cmake $(CMAKE_ARGS) ../sources/stablediffusion-ggml.cpp && \ - cmake --build . --config Release -endif - $(MAKE) $(COMBINED_LIB) - -gosd.o: -ifneq (,$(findstring sycl,$(BUILD_TYPE))) - +bash -c "source $(ONEAPI_VARS); \ - $(CXX) $(CXXFLAGS) gosd.cpp -o gosd.o -c" -else - $(CXX) $(CXXFLAGS) gosd.cpp -o gosd.o -c -endif - -## stablediffusion (ggml) sources/stablediffusion-ggml.cpp: git clone --recursive $(STABLEDIFFUSION_GGML_REPO) sources/stablediffusion-ggml.cpp && \ cd sources/stablediffusion-ggml.cpp && \ git checkout $(STABLEDIFFUSION_GGML_VERSION) && \ git submodule update --init --recursive --depth 1 --single-branch -libsd.a: sources/stablediffusion-ggml.cpp build/libstable-diffusion.a gosd.o - cp $(INCLUDE_PATH)/build/libstable-diffusion.a ./libsd.a - $(AR) rcs libsd.a gosd.o +libgosd.so: sources/stablediffusion-ggml.cpp CMakeLists.txt gosd.cpp gosd.h + mkdir -p build && \ + cd build && \ + cmake .. $(CMAKE_ARGS) && \ + cmake --build . --config Release -j$(JOBS) && \ + cd .. && \ + mv build/libgosd.so ./ -stablediffusion-ggml: libsd.a - CGO_LDFLAGS="$(CGO_LDFLAGS) $(CGO_LDFLAGS_SYCL)" C_INCLUDE_PATH="$(INCLUDE_PATH)" LIBRARY_PATH="$(LIBRARY_PATH)" \ - CC="$(CC)" CXX="$(CXX)" CGO_CXXFLAGS="$(CGO_CXXFLAGS)" \ - $(GOCMD) build -ldflags "$(LD_FLAGS)" -tags "$(GO_TAGS)" -o stablediffusion-ggml ./ +stablediffusion-ggml: main.go gosd.go libgosd.so + CGO_ENABLED=0 $(GOCMD) build -tags "$(GO_TAGS)" -o stablediffusion-ggml ./ package: bash package.sh @@ -150,4 +79,4 @@ package: build: stablediffusion-ggml package clean: - rm -rf gosd.o libsd.a build $(COMBINED_LIB) + rm -rf libgosd.o build stablediffusion-ggml diff --git a/backend/go/stablediffusion-ggml/gosd.cpp b/backend/go/stablediffusion-ggml/gosd.cpp index b8db574747e4..d125f97ce611 100644 --- a/backend/go/stablediffusion-ggml/gosd.cpp +++ b/backend/go/stablediffusion-ggml/gosd.cpp @@ -1,3 +1,4 @@ +#include #define GGML_MAX_NAME 128 #include @@ -57,7 +58,7 @@ sd_ctx_t* sd_c; sample_method_t sample_method; // Copied from the upstream CLI -void sd_log_cb(enum sd_log_level_t level, const char* log, void* data) { +static void sd_log_cb(enum sd_log_level_t level, const char* log, void* data) { //SDParams* params = (SDParams*)data; const char* level_str; @@ -88,24 +89,24 @@ void sd_log_cb(enum sd_log_level_t level, const char* log, void* data) { fflush(stderr); } -int load_model(char *model, char *model_path, char* options[], int threads, int diff) { +int load_model(const char *model, char *model_path, char* options[], int threads, int diff) { fprintf (stderr, "Loading model!\n"); sd_set_log_callback(sd_log_cb, NULL); - char *stableDiffusionModel = ""; + const char *stableDiffusionModel = ""; if (diff == 1 ) { stableDiffusionModel = model; model = ""; } // decode options. Options are in form optname:optvale, or if booleans only optname. - char *clip_l_path = ""; - char *clip_g_path = ""; - char *t5xxl_path = ""; - char *vae_path = ""; - char *scheduler = ""; - char *sampler = ""; + const char *clip_l_path = ""; + const char *clip_g_path = ""; + const char *t5xxl_path = ""; + const char *vae_path = ""; + const char *scheduler = ""; + const char *sampler = ""; char *lora_dir = model_path; bool lora_dir_allocated = false; @@ -113,8 +114,8 @@ int load_model(char *model, char *model_path, char* options[], int threads, int // If options is not NULL, parse options for (int i = 0; options[i] != NULL; i++) { - char *optname = strtok(options[i], ":"); - char *optval = strtok(NULL, ":"); + const char *optname = strtok(options[i], ":"); + const char *optval = strtok(NULL, ":"); if (optval == NULL) { optval = "true"; } @@ -147,7 +148,8 @@ int load_model(char *model, char *model_path, char* options[], int threads, int lora_dir_allocated = true; fprintf(stderr, "Lora dir resolved to: %s\n", lora_dir); } else { - lora_dir = optval; + lora_dir = strdup(optval); + lora_dir_allocated = true; fprintf(stderr, "No model path provided, using lora dir as-is: %s\n", lora_dir); } } @@ -226,7 +228,7 @@ int load_model(char *model, char *model_path, char* options[], int threads, int return 0; } -int gen_image(char *text, char *negativeText, int width, int height, int steps, int seed , char *dst, float cfg_scale, char *src_image, float strength, char *mask_image, char **ref_images, int ref_images_count) { +int gen_image(char *text, char *negativeText, int width, int height, int steps, int64_t seed, char *dst, float cfg_scale, char *src_image, float strength, char *mask_image, char **ref_images, int ref_images_count) { sd_image_t* results; @@ -252,14 +254,14 @@ int gen_image(char *text, char *negativeText, int width, int height, int steps, // Handle input image for img2img bool has_input_image = (src_image != NULL && strlen(src_image) > 0); bool has_mask_image = (mask_image != NULL && strlen(mask_image) > 0); - + uint8_t* input_image_buffer = NULL; uint8_t* mask_image_buffer = NULL; std::vector default_mask_image_vec; - + if (has_input_image) { fprintf(stderr, "Loading input image: %s\n", src_image); - + int c = 0; int img_width = 0; int img_height = 0; @@ -273,29 +275,29 @@ int gen_image(char *text, char *negativeText, int width, int height, int steps, free(input_image_buffer); return 1; } - + // Resize input image if dimensions don't match if (img_width != width || img_height != height) { fprintf(stderr, "Resizing input image from %dx%d to %dx%d\n", img_width, img_height, width, height); - + uint8_t* resized_image_buffer = (uint8_t*)malloc(height * width * 3); if (resized_image_buffer == NULL) { fprintf(stderr, "Failed to allocate memory for resized image\n"); free(input_image_buffer); return 1; } - + stbir_resize(input_image_buffer, img_width, img_height, 0, resized_image_buffer, width, height, 0, STBIR_TYPE_UINT8, 3, STBIR_ALPHA_CHANNEL_NONE, 0, STBIR_EDGE_CLAMP, STBIR_EDGE_CLAMP, STBIR_FILTER_BOX, STBIR_FILTER_BOX, STBIR_COLORSPACE_SRGB, nullptr); - + free(input_image_buffer); input_image_buffer = resized_image_buffer; } - + p.init_image = {(uint32_t)width, (uint32_t)height, 3, input_image_buffer}; p.strength = strength; fprintf(stderr, "Using img2img with strength: %.2f\n", strength); @@ -304,11 +306,11 @@ int gen_image(char *text, char *negativeText, int width, int height, int steps, p.init_image = {(uint32_t)width, (uint32_t)height, 3, NULL}; p.strength = 0.0f; } - + // Handle mask image for inpainting if (has_mask_image) { fprintf(stderr, "Loading mask image: %s\n", mask_image); - + int c = 0; int mask_width = 0; int mask_height = 0; @@ -318,11 +320,11 @@ int gen_image(char *text, char *negativeText, int width, int height, int steps, if (input_image_buffer) free(input_image_buffer); return 1; } - + // Resize mask if dimensions don't match if (mask_width != width || mask_height != height) { fprintf(stderr, "Resizing mask image from %dx%d to %dx%d\n", mask_width, mask_height, width, height); - + uint8_t* resized_mask_buffer = (uint8_t*)malloc(height * width); if (resized_mask_buffer == NULL) { fprintf(stderr, "Failed to allocate memory for resized mask\n"); @@ -330,18 +332,18 @@ int gen_image(char *text, char *negativeText, int width, int height, int steps, if (input_image_buffer) free(input_image_buffer); return 1; } - + stbir_resize(mask_image_buffer, mask_width, mask_height, 0, resized_mask_buffer, width, height, 0, STBIR_TYPE_UINT8, 1, STBIR_ALPHA_CHANNEL_NONE, 0, STBIR_EDGE_CLAMP, STBIR_EDGE_CLAMP, STBIR_FILTER_BOX, STBIR_FILTER_BOX, STBIR_COLORSPACE_SRGB, nullptr); - + free(mask_image_buffer); mask_image_buffer = resized_mask_buffer; } - + p.mask_image = {(uint32_t)width, (uint32_t)height, 1, mask_image_buffer}; fprintf(stderr, "Using inpainting with mask\n"); } else { @@ -353,17 +355,17 @@ int gen_image(char *text, char *negativeText, int width, int height, int steps, // Handle reference images std::vector ref_images_vec; std::vector ref_image_buffers; - + if (ref_images_count > 0 && ref_images != NULL) { fprintf(stderr, "Loading %d reference images\n", ref_images_count); - + for (int i = 0; i < ref_images_count; i++) { if (ref_images[i] == NULL || strlen(ref_images[i]) == 0) { continue; } - + fprintf(stderr, "Loading reference image %d: %s\n", i + 1, ref_images[i]); - + int c = 0; int ref_width = 0; int ref_height = 0; @@ -377,33 +379,33 @@ int gen_image(char *text, char *negativeText, int width, int height, int steps, free(ref_image_buffer); continue; } - + // Resize reference image if dimensions don't match if (ref_width != width || ref_height != height) { fprintf(stderr, "Resizing reference image from %dx%d to %dx%d\n", ref_width, ref_height, width, height); - + uint8_t* resized_ref_buffer = (uint8_t*)malloc(height * width * 3); if (resized_ref_buffer == NULL) { fprintf(stderr, "Failed to allocate memory for resized reference image\n"); free(ref_image_buffer); continue; } - + stbir_resize(ref_image_buffer, ref_width, ref_height, 0, resized_ref_buffer, width, height, 0, STBIR_TYPE_UINT8, 3, STBIR_ALPHA_CHANNEL_NONE, 0, STBIR_EDGE_CLAMP, STBIR_EDGE_CLAMP, STBIR_FILTER_BOX, STBIR_FILTER_BOX, STBIR_COLORSPACE_SRGB, nullptr); - + free(ref_image_buffer); ref_image_buffer = resized_ref_buffer; } - + ref_image_buffers.push_back(ref_image_buffer); ref_images_vec.push_back({(uint32_t)width, (uint32_t)height, 3, ref_image_buffer}); } - + if (!ref_images_vec.empty()) { p.ref_images = ref_images_vec.data(); p.ref_images_count = ref_images_vec.size(); @@ -454,12 +456,12 @@ int gen_image(char *text, char *negativeText, int width, int height, int steps, for (auto buffer : ref_image_buffers) { if (buffer) free(buffer); } - fprintf (stderr, "gen_image is done", dst); + fprintf (stderr, "gen_image is done: %s", dst); return 0; } int unload() { free_sd_ctx(sd_c); + return 0; } - diff --git a/backend/go/stablediffusion-ggml/gosd.go b/backend/go/stablediffusion-ggml/gosd.go index 221ba4294e99..8c6738e67023 100644 --- a/backend/go/stablediffusion-ggml/gosd.go +++ b/backend/go/stablediffusion-ggml/gosd.go @@ -1,11 +1,5 @@ package main -// #cgo CXXFLAGS: -I${SRCDIR}/sources/stablediffusion-ggml.cpp/thirdparty -I${SRCDIR}/sources/stablediffusion-ggml.cpp -I${SRCDIR}/sources/stablediffusion-ggml.cpp/ggml/include -// #cgo LDFLAGS: -L${SRCDIR}/ -lsd -lstdc++ -lm -lggmlall -lgomp -// #include -// #include -import "C" - import ( "fmt" "os" @@ -25,25 +19,19 @@ type SDGGML struct { cfgScale float32 } +var ( + LoadModel func(model, model_apth string, options []string, threads int32, diff int) int + GenImage func(text, negativeText string, width, height, steps int, seed int64, dst string, cfgScale float32, srcImage string, strength float32, maskImage string, refImages []string, refImagesCount int) int +) + func (sd *SDGGML) Load(opts *pb.ModelOptions) error { sd.threads = int(opts.Threads) modelPath := opts.ModelPath - modelFile := C.CString(opts.ModelFile) - defer C.free(unsafe.Pointer(modelFile)) - - modelPathC := C.CString(modelPath) - defer C.free(unsafe.Pointer(modelPathC)) - - var options **C.char - // prepare the options array to pass to C - - size := C.size_t(unsafe.Sizeof((*C.char)(nil))) - length := C.size_t(len(opts.Options)) - options = (**C.char)(C.malloc((length + 1) * size)) - view := (*[1 << 30]*C.char)(unsafe.Pointer(options))[0 : len(opts.Options)+1 : len(opts.Options)+1] + modelFile := opts.ModelFile + modelPathC := modelPath var diffusionModel int @@ -68,14 +56,12 @@ func (sd *SDGGML) Load(opts *pb.ModelOptions) error { fmt.Fprintf(os.Stderr, "Options: %+v\n", oo) - for i, x := range oo { - view[i] = C.CString(x) - } - view[len(oo)] = nil + options := make([]string, len(oo), len(oo) + 1) + *(*uintptr)(unsafe.Add(unsafe.Pointer(&options), uintptr(len(oo)))) = 0 sd.cfgScale = opts.CFGScale - ret := C.load_model(modelFile, modelPathC, options, C.int(opts.Threads), C.int(diffusionModel)) + ret := LoadModel(modelFile, modelPathC, options, opts.Threads, diffusionModel) if ret != 0 { return fmt.Errorf("could not load model") } @@ -84,65 +70,33 @@ func (sd *SDGGML) Load(opts *pb.ModelOptions) error { } func (sd *SDGGML) GenerateImage(opts *pb.GenerateImageRequest) error { - t := C.CString(opts.PositivePrompt) - defer C.free(unsafe.Pointer(t)) + t := opts.PositivePrompt + dst := opts.Dst + negative := opts.NegativePrompt + srcImage := opts.Src - dst := C.CString(opts.Dst) - defer C.free(unsafe.Pointer(dst)) - - negative := C.CString(opts.NegativePrompt) - defer C.free(unsafe.Pointer(negative)) - - // Handle source image path - var srcImage *C.char - if opts.Src != "" { - srcImage = C.CString(opts.Src) - defer C.free(unsafe.Pointer(srcImage)) - } - - // Handle mask image path - var maskImage *C.char + var maskImage string if opts.EnableParameters != "" { - // Parse EnableParameters for mask path if provided - // This is a simple approach - in a real implementation you might want to parse JSON if strings.Contains(opts.EnableParameters, "mask:") { parts := strings.Split(opts.EnableParameters, "mask:") if len(parts) > 1 { maskPath := strings.TrimSpace(parts[1]) if maskPath != "" { - maskImage = C.CString(maskPath) - defer C.free(unsafe.Pointer(maskImage)) + maskImage = maskPath } } } } - // Handle reference images - var refImages **C.char - var refImagesCount C.int - if len(opts.RefImages) > 0 { - refImagesCount = C.int(len(opts.RefImages)) - // Allocate array of C strings - size := C.size_t(unsafe.Sizeof((*C.char)(nil))) - refImages = (**C.char)(C.malloc((C.size_t(len(opts.RefImages)) + 1) * size)) - view := (*[1 << 30]*C.char)(unsafe.Pointer(refImages))[0 : len(opts.RefImages)+1 : len(opts.RefImages)+1] - - for i, refImagePath := range opts.RefImages { - view[i] = C.CString(refImagePath) - defer C.free(unsafe.Pointer(view[i])) - } - view[len(opts.RefImages)] = nil - } + refImagesCount := len(opts.RefImages) + refImages := make([]string, refImagesCount, refImagesCount + 1) + copy(refImages, opts.RefImages) + *(*uintptr)(unsafe.Add(unsafe.Pointer(&refImages), refImagesCount)) = 0 // Default strength for img2img (0.75 is a good default) - strength := C.float(0.75) - if opts.Src != "" { - // If we have a source image, use img2img mode - // You could also parse strength from EnableParameters if needed - strength = C.float(0.75) - } + strength := float32(0.75) - ret := C.gen_image(t, negative, C.int(opts.Width), C.int(opts.Height), C.int(opts.Step), C.int(opts.Seed), dst, C.float(sd.cfgScale), srcImage, strength, maskImage, refImages, refImagesCount) + ret := GenImage(t, negative, int(opts.Width), int(opts.Height), int(opts.Step), int64(opts.Seed), dst, sd.cfgScale, srcImage, strength, maskImage, refImages, refImagesCount) if ret != 0 { return fmt.Errorf("inference failed") } diff --git a/backend/go/stablediffusion-ggml/gosd.h b/backend/go/stablediffusion-ggml/gosd.h index 45db2e4509f9..9ce94869b168 100644 --- a/backend/go/stablediffusion-ggml/gosd.h +++ b/backend/go/stablediffusion-ggml/gosd.h @@ -1,8 +1,8 @@ #ifdef __cplusplus extern "C" { #endif -int load_model(char *model, char *model_path, char* options[], int threads, int diffusionModel); -int gen_image(char *text, char *negativeText, int width, int height, int steps, int seed, char *dst, float cfg_scale, char *src_image, float strength, char *mask_image, char **ref_images, int ref_images_count); +int load_model(const char *model, char *model_path, char* options[], int threads, int diffusionModel); +int gen_image(char *text, char *negativeText, int width, int height, int steps, int64_t seed, char *dst, float cfg_scale, char *src_image, float strength, char *mask_image, char **ref_images, int ref_images_count); #ifdef __cplusplus } -#endif \ No newline at end of file +#endif diff --git a/backend/go/stablediffusion-ggml/main.go b/backend/go/stablediffusion-ggml/main.go index acee74fac0d4..b112ace4889b 100644 --- a/backend/go/stablediffusion-ggml/main.go +++ b/backend/go/stablediffusion-ggml/main.go @@ -3,7 +3,10 @@ package main // Note: this is started internally by LocalAI and a server is allocated for each model import ( "flag" + "fmt" + "runtime" + "github.com/ebitengine/purego" grpc "github.com/mudler/LocalAI/pkg/grpc" ) @@ -11,7 +14,24 @@ var ( addr = flag.String("addr", "localhost:50051", "the address to connect to") ) +func getLibrary() string { + switch runtime.GOOS { + case "linux": + return "./libgosd.so" + default: + panic(fmt.Errorf("GOOS=%s is not supported", runtime.GOOS)) + } +} + func main() { + gosd, err := purego.Dlopen(getLibrary(), purego.RTLD_NOW|purego.RTLD_GLOBAL) + if err != nil { + panic(err) + } + + purego.RegisterLibFunc(&LoadModel, gosd, "load_model") + purego.RegisterLibFunc(&GenImage, gosd, "gen_image") + flag.Parse() if err := grpc.StartServer(*addr, &SDGGML{}); err != nil { diff --git a/go.mod b/go.mod index 6da6aefa8efb..55d5ffaddff7 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/chasefleming/elem-go v0.26.0 github.com/containerd/containerd v1.7.19 github.com/dave-gray101/v2keyauth v0.0.0-20240624150259-c45d584d25e2 + github.com/ebitengine/purego v0.8.4 github.com/fsnotify/fsnotify v1.7.0 github.com/ggerganov/whisper.cpp/bindings/go v0.0.0-20240626202019-c118733a29ad github.com/go-audio/wav v1.1.0 diff --git a/go.sum b/go.sum index 1007529b0f6a..bba156725e54 100644 --- a/go.sum +++ b/go.sum @@ -126,6 +126,11 @@ github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdf github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= +github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/elastic/gosigar v0.12.0/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs= +github.com/elastic/gosigar v0.14.3 h1:xwkKwPia+hSfg9GqrCUKYdId102m9qTJIIr7egmK/uo= +github.com/elastic/gosigar v0.14.3/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= From eca2651fefea40348e9fff65ecca925156c9dddb Mon Sep 17 00:00:00 2001 From: Richard Palethorpe Date: Mon, 11 Aug 2025 12:26:16 +0100 Subject: [PATCH 2/2] chore(build): Use Purego with stablediffusion backend Signed-off-by: Richard Palethorpe --- .../go/stablediffusion-ggml/CMakeLists.txt | 3 +- backend/go/stablediffusion-ggml/gosd.cpp | 4 +-- backend/go/stablediffusion-ggml/gosd.go | 34 ++++++++++++++++--- backend/go/stablediffusion-ggml/package.sh | 5 +-- 4 files changed, 36 insertions(+), 10 deletions(-) diff --git a/backend/go/stablediffusion-ggml/CMakeLists.txt b/backend/go/stablediffusion-ggml/CMakeLists.txt index fd7a1214a811..f80aa2702b76 100644 --- a/backend/go/stablediffusion-ggml/CMakeLists.txt +++ b/backend/go/stablediffusion-ggml/CMakeLists.txt @@ -1,10 +1,11 @@ cmake_minimum_required(VERSION 3.12) project(gosd LANGUAGES C CXX) +set(CMAKE_POSITION_INDEPENDENT_CODE ON) add_subdirectory(./sources/stablediffusion-ggml.cpp) add_library(gosd MODULE gosd.cpp) -target_link_libraries(gosd PRIVATE stable-diffusion ggml zip stdc++fs) +target_link_libraries(gosd PRIVATE stable-diffusion ggml stdc++fs) target_include_directories(gosd PUBLIC stable-diffusion.cpp diff --git a/backend/go/stablediffusion-ggml/gosd.cpp b/backend/go/stablediffusion-ggml/gosd.cpp index d125f97ce611..7e5efb6ecfe5 100644 --- a/backend/go/stablediffusion-ggml/gosd.cpp +++ b/backend/go/stablediffusion-ggml/gosd.cpp @@ -90,7 +90,7 @@ static void sd_log_cb(enum sd_log_level_t level, const char* log, void* data) { } int load_model(const char *model, char *model_path, char* options[], int threads, int diff) { - fprintf (stderr, "Loading model!\n"); + fprintf (stderr, "Loading model: %p=%s\n", model, model); sd_set_log_callback(sd_log_cb, NULL); @@ -110,7 +110,7 @@ int load_model(const char *model, char *model_path, char* options[], int threads char *lora_dir = model_path; bool lora_dir_allocated = false; - fprintf(stderr, "parsing options\n"); + fprintf(stderr, "parsing options: %p\n", options); // If options is not NULL, parse options for (int i = 0; options[i] != NULL; i++) { diff --git a/backend/go/stablediffusion-ggml/gosd.go b/backend/go/stablediffusion-ggml/gosd.go index 8c6738e67023..6a1d7535cdf3 100644 --- a/backend/go/stablediffusion-ggml/gosd.go +++ b/backend/go/stablediffusion-ggml/gosd.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "runtime" "strings" "unsafe" @@ -20,10 +21,25 @@ type SDGGML struct { } var ( - LoadModel func(model, model_apth string, options []string, threads int32, diff int) int - GenImage func(text, negativeText string, width, height, steps int, seed int64, dst string, cfgScale float32, srcImage string, strength float32, maskImage string, refImages []string, refImagesCount int) int + LoadModel func(model, model_apth string, options []uintptr, threads int32, diff int) int + GenImage func(text, negativeText string, width, height, steps int, seed int64, dst string, cfgScale float32, srcImage string, strength float32, maskImage string, refImages []string, refImagesCount int) int ) +// Copied from Purego internal/strings +// TODO: We should upstream sending []string +func hasSuffix(s, suffix string) bool { + return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix +} + +func CString(name string) *byte { + if hasSuffix(name, "\x00") { + return &(*(*[]byte)(unsafe.Pointer(&name)))[0] + } + b := make([]byte, len(name)+1) + copy(b, name) + return &b[0] +} + func (sd *SDGGML) Load(opts *pb.ModelOptions) error { sd.threads = int(opts.Threads) @@ -56,8 +72,14 @@ func (sd *SDGGML) Load(opts *pb.ModelOptions) error { fmt.Fprintf(os.Stderr, "Options: %+v\n", oo) - options := make([]string, len(oo), len(oo) + 1) - *(*uintptr)(unsafe.Add(unsafe.Pointer(&options), uintptr(len(oo)))) = 0 + // At the time of writing Purego doesn't recurse into slices and convert Go strings to pointers so we need to do that + var keepAlive []any + options := make([]uintptr, len(oo), len(oo)+1) + for i, op := range oo { + bytep := CString(op) + options[i] = uintptr(unsafe.Pointer(bytep)) + keepAlive = append(keepAlive, bytep) + } sd.cfgScale = opts.CFGScale @@ -66,6 +88,8 @@ func (sd *SDGGML) Load(opts *pb.ModelOptions) error { return fmt.Errorf("could not load model") } + runtime.KeepAlive(keepAlive) + return nil } @@ -89,7 +113,7 @@ func (sd *SDGGML) GenerateImage(opts *pb.GenerateImageRequest) error { } refImagesCount := len(opts.RefImages) - refImages := make([]string, refImagesCount, refImagesCount + 1) + refImages := make([]string, refImagesCount, refImagesCount+1) copy(refImages, opts.RefImages) *(*uintptr)(unsafe.Add(unsafe.Pointer(&refImages), refImagesCount)) = 0 diff --git a/backend/go/stablediffusion-ggml/package.sh b/backend/go/stablediffusion-ggml/package.sh index d87f85bf7154..6e56dcd95a8d 100755 --- a/backend/go/stablediffusion-ggml/package.sh +++ b/backend/go/stablediffusion-ggml/package.sh @@ -10,6 +10,7 @@ CURDIR=$(dirname "$(realpath $0)") # Create lib directory mkdir -p $CURDIR/package/lib +cp -avrf $CURDIR/libgosd.so $CURDIR/package/ cp -avrf $CURDIR/stablediffusion-ggml $CURDIR/package/ cp -rfv $CURDIR/run.sh $CURDIR/package/ @@ -47,6 +48,6 @@ else exit 1 fi -echo "Packaging completed successfully" +echo "Packaging completed successfully" ls -liah $CURDIR/package/ -ls -liah $CURDIR/package/lib/ \ No newline at end of file +ls -liah $CURDIR/package/lib/