Skip to content

Commit abfb0ff

Browse files
authored
feat(stablediffusion-ggml): add lora support (#7542)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
1 parent 2bd6faa commit abfb0ff

1 file changed

Lines changed: 319 additions & 3 deletions

File tree

backend/go/stablediffusion-ggml/gosd.cpp

Lines changed: 319 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
#include <time.h>
99
#include <string>
1010
#include <vector>
11+
#include <map>
1112
#include <filesystem>
13+
#include <algorithm>
1214
#include "gosd.h"
1315

1416
#define STB_IMAGE_IMPLEMENTATION
@@ -23,6 +25,7 @@
2325
#define STB_IMAGE_RESIZE_STATIC
2426
#include "stb_image_resize.h"
2527
#include <stdlib.h>
28+
#include <regex>
2629

2730
// Names of the sampler method, same order as enum sample_method in stable-diffusion.h
2831
const char* sample_method_str[] = {
@@ -133,6 +136,13 @@ static std::vector<sd_embedding_t> embedding_vec;
133136
// Storage for embedding strings (needs to persist as long as embedding_vec references them)
134137
static std::vector<std::string> embedding_strings;
135138

139+
// Storage for LoRAs (needs to persist for the lifetime of generation params)
140+
static std::vector<sd_lora_t> lora_vec;
141+
// Storage for LoRA strings (needs to persist as long as lora_vec references them)
142+
static std::vector<std::string> lora_strings;
143+
// Storage for lora_dir path
144+
static std::string lora_dir_path;
145+
136146
// Build embeddings vector from directory, similar to upstream CLI
137147
static void build_embedding_vec(const char* embedding_dir) {
138148
embedding_vec.clear();
@@ -186,6 +196,229 @@ static void build_embedding_vec(const char* embedding_dir) {
186196
fprintf(stderr, "Loaded %zu embeddings from %s\n", embedding_vec.size(), embedding_dir);
187197
}
188198

199+
// Discover LoRA files in directory and build a map of name -> path
200+
static std::map<std::string, std::string> discover_lora_files(const char* lora_dir) {
201+
std::map<std::string, std::string> lora_map;
202+
203+
if (!lora_dir || strlen(lora_dir) == 0) {
204+
fprintf(stderr, "LoRA directory not specified\n");
205+
return lora_map;
206+
}
207+
208+
if (!std::filesystem::exists(lora_dir) || !std::filesystem::is_directory(lora_dir)) {
209+
fprintf(stderr, "LoRA directory does not exist or is not a directory: %s\n", lora_dir);
210+
return lora_map;
211+
}
212+
213+
static const std::vector<std::string> valid_ext = {".safetensors", ".ckpt", ".pt", ".gguf"};
214+
215+
fprintf(stderr, "Discovering LoRA files in: %s\n", lora_dir);
216+
217+
for (const auto& entry : std::filesystem::directory_iterator(lora_dir)) {
218+
if (!entry.is_regular_file()) {
219+
continue;
220+
}
221+
222+
auto path = entry.path();
223+
std::string ext = path.extension().string();
224+
225+
bool valid = false;
226+
for (const auto& e : valid_ext) {
227+
if (ext == e) {
228+
valid = true;
229+
break;
230+
}
231+
}
232+
if (!valid) {
233+
continue;
234+
}
235+
236+
std::string name = path.stem().string(); // stem() already removes extension
237+
std::string full_path = path.string();
238+
239+
// Store the name (without extension) -> full path mapping
240+
// This allows users to specify just the name in <lora:name:strength>
241+
lora_map[name] = full_path;
242+
243+
fprintf(stderr, "Found LoRA file: %s -> %s\n", name.c_str(), full_path.c_str());
244+
}
245+
246+
fprintf(stderr, "Discovered %zu LoRA files in %s\n", lora_map.size(), lora_dir);
247+
return lora_map;
248+
}
249+
250+
// Helper function to check if a path is absolute (matches upstream)
251+
static bool is_absolute_path(const std::string& p) {
252+
#ifdef _WIN32
253+
// Windows: C:/path or C:\path
254+
return p.size() > 1 && std::isalpha(static_cast<unsigned char>(p[0])) && p[1] == ':';
255+
#else
256+
// Unix: /path
257+
return !p.empty() && p[0] == '/';
258+
#endif
259+
}
260+
261+
// Parse LoRAs from prompt string (e.g., "<lora:name:1.0>" or "<lora:name>")
262+
// Returns a vector of LoRA info and the cleaned prompt with LoRA tags removed
263+
// Matches upstream implementation more closely
264+
static std::pair<std::vector<sd_lora_t>, std::string> parse_loras_from_prompt(const std::string& prompt, const char* lora_dir) {
265+
std::vector<sd_lora_t> loras;
266+
std::string cleaned_prompt = prompt;
267+
268+
if (!lora_dir || strlen(lora_dir) == 0) {
269+
fprintf(stderr, "LoRA directory not set, cannot parse LoRAs from prompt\n");
270+
return {loras, cleaned_prompt};
271+
}
272+
273+
// Discover LoRA files for name-based lookup
274+
std::map<std::string, std::string> discovered_lora_map = discover_lora_files(lora_dir);
275+
276+
// Map to accumulate multipliers for the same LoRA (matches upstream)
277+
std::map<std::string, float> lora_map;
278+
std::map<std::string, float> high_noise_lora_map;
279+
280+
static const std::regex re(R"(<lora:([^:>]+):([^>]+)>)");
281+
static const std::vector<std::string> valid_ext = {".pt", ".safetensors", ".gguf"};
282+
std::smatch m;
283+
284+
std::string tmp = prompt;
285+
286+
fprintf(stderr, "Parsing LoRAs from prompt: %s\n", prompt.c_str());
287+
288+
while (std::regex_search(tmp, m, re)) {
289+
std::string raw_path = m[1].str();
290+
const std::string raw_mul = m[2].str();
291+
292+
float mul = 0.f;
293+
try {
294+
mul = std::stof(raw_mul);
295+
} catch (...) {
296+
tmp = m.suffix().str();
297+
cleaned_prompt = std::regex_replace(cleaned_prompt, re, "", std::regex_constants::format_first_only);
298+
fprintf(stderr, "Invalid LoRA multiplier '%s', skipping\n", raw_mul.c_str());
299+
continue;
300+
}
301+
302+
bool is_high_noise = false;
303+
static const std::string prefix = "|high_noise|";
304+
if (raw_path.rfind(prefix, 0) == 0) {
305+
raw_path.erase(0, prefix.size());
306+
is_high_noise = true;
307+
}
308+
309+
std::filesystem::path final_path;
310+
if (is_absolute_path(raw_path)) {
311+
final_path = raw_path;
312+
} else {
313+
// Try name-based lookup first
314+
auto it = discovered_lora_map.find(raw_path);
315+
if (it != discovered_lora_map.end()) {
316+
final_path = it->second;
317+
} else {
318+
// Try case-insensitive lookup
319+
bool found = false;
320+
for (const auto& pair : discovered_lora_map) {
321+
std::string lower_name = raw_path;
322+
std::string lower_key = pair.first;
323+
std::transform(lower_name.begin(), lower_name.end(), lower_name.begin(), ::tolower);
324+
std::transform(lower_key.begin(), lower_key.end(), lower_key.begin(), ::tolower);
325+
if (lower_name == lower_key) {
326+
final_path = pair.second;
327+
found = true;
328+
break;
329+
}
330+
}
331+
if (!found) {
332+
// Try as relative path in lora_dir
333+
final_path = std::filesystem::path(lora_dir) / raw_path;
334+
}
335+
}
336+
}
337+
338+
// Try adding extensions if file doesn't exist
339+
if (!std::filesystem::exists(final_path)) {
340+
bool found = false;
341+
for (const auto& ext : valid_ext) {
342+
std::filesystem::path try_path = final_path;
343+
try_path += ext;
344+
if (std::filesystem::exists(try_path)) {
345+
final_path = try_path;
346+
found = true;
347+
break;
348+
}
349+
}
350+
if (!found) {
351+
fprintf(stderr, "WARNING: LoRA file not found: %s\n", final_path.lexically_normal().string().c_str());
352+
tmp = m.suffix().str();
353+
cleaned_prompt = std::regex_replace(cleaned_prompt, re, "", std::regex_constants::format_first_only);
354+
continue;
355+
}
356+
}
357+
358+
// Normalize path (matches upstream)
359+
const std::string key = final_path.lexically_normal().string();
360+
361+
// Accumulate multiplier if same LoRA appears multiple times (matches upstream)
362+
if (is_high_noise) {
363+
high_noise_lora_map[key] += mul;
364+
} else {
365+
lora_map[key] += mul;
366+
}
367+
368+
fprintf(stderr, "Parsed LoRA: path='%s', multiplier=%.2f, is_high_noise=%s\n",
369+
key.c_str(), mul, is_high_noise ? "true" : "false");
370+
371+
cleaned_prompt = std::regex_replace(cleaned_prompt, re, "", std::regex_constants::format_first_only);
372+
tmp = m.suffix().str();
373+
}
374+
375+
// Build final LoRA vector from accumulated maps (matches upstream)
376+
// Store all path strings first to ensure they persist
377+
for (const auto& kv : lora_map) {
378+
lora_strings.push_back(kv.first);
379+
}
380+
for (const auto& kv : high_noise_lora_map) {
381+
lora_strings.push_back(kv.first);
382+
}
383+
384+
// Now build the LoRA vector with pointers to the stored strings
385+
size_t string_idx = 0;
386+
for (const auto& kv : lora_map) {
387+
sd_lora_t item;
388+
item.is_high_noise = false;
389+
item.path = lora_strings[string_idx].c_str();
390+
item.multiplier = kv.second;
391+
loras.push_back(item);
392+
string_idx++;
393+
}
394+
395+
for (const auto& kv : high_noise_lora_map) {
396+
sd_lora_t item;
397+
item.is_high_noise = true;
398+
item.path = lora_strings[string_idx].c_str();
399+
item.multiplier = kv.second;
400+
loras.push_back(item);
401+
string_idx++;
402+
}
403+
404+
// Clean up extra spaces
405+
std::regex space_regex(R"(\s+)");
406+
cleaned_prompt = std::regex_replace(cleaned_prompt, space_regex, " ");
407+
// Trim leading/trailing spaces
408+
size_t first = cleaned_prompt.find_first_not_of(" \t");
409+
if (first != std::string::npos) {
410+
cleaned_prompt.erase(0, first);
411+
}
412+
size_t last = cleaned_prompt.find_last_not_of(" \t");
413+
if (last != std::string::npos) {
414+
cleaned_prompt.erase(last + 1);
415+
}
416+
417+
fprintf(stderr, "Parsed %zu LoRA(s) from prompt. Cleaned prompt: %s\n", loras.size(), cleaned_prompt.c_str());
418+
419+
return {loras, cleaned_prompt};
420+
}
421+
189422
// Copied from the upstream CLI
190423
static void sd_log_cb(enum sd_log_level_t level, const char* log, void* data) {
191424
//SDParams* params = (SDParams*)data;
@@ -304,11 +537,17 @@ int load_model(const char *model, char *model_path, char* options[], int threads
304537
std::filesystem::path lora_path(optval);
305538
std::filesystem::path full_lora_path = model_path_str / lora_path;
306539
lora_dir = strdup(full_lora_path.string().c_str());
307-
fprintf(stderr, "Lora dir resolved to: %s\n", lora_dir);
540+
lora_dir_path = full_lora_path.string();
541+
fprintf(stderr, "LoRA dir resolved to: %s\n", lora_dir);
308542
} else {
309543
lora_dir = strdup(optval);
544+
lora_dir_path = std::string(optval);
310545
fprintf(stderr, "No model path provided, using lora dir as-is: %s\n", lora_dir);
311546
}
547+
// Discover LoRAs immediately when directory is set
548+
if (lora_dir && strlen(lora_dir) > 0) {
549+
discover_lora_files(lora_dir);
550+
}
312551
}
313552

314553
// New parsing
@@ -450,6 +689,14 @@ int load_model(const char *model, char *model_path, char* options[], int threads
450689
ctx_params.taesd_path = taesd_path;
451690
ctx_params.control_net_path = control_net_path;
452691
ctx_params.lora_model_dir = lora_dir;
692+
if (lora_dir && strlen(lora_dir) > 0) {
693+
lora_dir_path = std::string(lora_dir);
694+
fprintf(stderr, "LoRA model directory set to: %s\n", lora_dir);
695+
// Discover LoRAs at load time for logging
696+
discover_lora_files(lora_dir);
697+
} else {
698+
fprintf(stderr, "WARNING: LoRA model directory not set. LoRAs in prompts will not be loaded.\n");
699+
}
453700
// Set embeddings array and count
454701
ctx_params.embeddings = embedding_vec.empty() ? NULL : embedding_vec.data();
455702
ctx_params.embedding_count = static_cast<uint32_t>(embedding_vec.size());
@@ -546,9 +793,63 @@ sd_img_gen_params_t* sd_img_gen_params_new(void) {
546793
return params;
547794
}
548795

796+
// Storage for cleaned prompt strings (needs to persist)
797+
static std::string cleaned_prompt_storage;
798+
static std::string cleaned_negative_prompt_storage;
799+
549800
void sd_img_gen_params_set_prompts(sd_img_gen_params_t *params, const char *prompt, const char *negative_prompt) {
550-
params->prompt = prompt;
551-
params->negative_prompt = negative_prompt;
801+
// Clear previous LoRA data
802+
lora_vec.clear();
803+
lora_strings.clear();
804+
805+
// Parse LoRAs from prompt
806+
std::string prompt_str = prompt ? prompt : "";
807+
std::string negative_prompt_str = negative_prompt ? negative_prompt : "";
808+
809+
// Get lora_dir from ctx_params if available, otherwise use stored path
810+
const char* lora_dir_to_use = ctx_params.lora_model_dir;
811+
if (!lora_dir_to_use || strlen(lora_dir_to_use) == 0) {
812+
lora_dir_to_use = lora_dir_path.empty() ? nullptr : lora_dir_path.c_str();
813+
}
814+
815+
auto [loras, cleaned_prompt] = parse_loras_from_prompt(prompt_str, lora_dir_to_use);
816+
lora_vec = loras;
817+
cleaned_prompt_storage = cleaned_prompt;
818+
819+
// Also check negative prompt for LoRAs (though this is less common)
820+
auto [neg_loras, cleaned_negative] = parse_loras_from_prompt(negative_prompt_str, lora_dir_to_use);
821+
// Merge negative prompt LoRAs (though typically not used)
822+
if (!neg_loras.empty()) {
823+
fprintf(stderr, "Note: Found %zu LoRAs in negative prompt (may not be supported)\n", neg_loras.size());
824+
}
825+
cleaned_negative_prompt_storage = cleaned_negative;
826+
827+
// Set the cleaned prompts
828+
params->prompt = cleaned_prompt_storage.c_str();
829+
params->negative_prompt = cleaned_negative_prompt_storage.c_str();
830+
831+
// Set LoRAs in params
832+
params->loras = lora_vec.empty() ? nullptr : lora_vec.data();
833+
params->lora_count = static_cast<uint32_t>(lora_vec.size());
834+
835+
fprintf(stderr, "Set prompts with %zu LoRAs. Original prompt: %s\n", lora_vec.size(), prompt ? prompt : "(null)");
836+
fprintf(stderr, "Cleaned prompt: %s\n", cleaned_prompt_storage.c_str());
837+
838+
// Debug: Verify LoRAs are set correctly
839+
if (params->loras && params->lora_count > 0) {
840+
fprintf(stderr, "DEBUG: LoRAs set in params structure:\n");
841+
for (uint32_t i = 0; i < params->lora_count; i++) {
842+
fprintf(stderr, " params->loras[%u]: path='%s' (ptr=%p), multiplier=%.2f, is_high_noise=%s\n",
843+
i,
844+
params->loras[i].path ? params->loras[i].path : "(null)",
845+
(void*)params->loras[i].path,
846+
params->loras[i].multiplier,
847+
params->loras[i].is_high_noise ? "true" : "false");
848+
}
849+
} else {
850+
fprintf(stderr, "DEBUG: No LoRAs set in params structure (loras=%p, lora_count=%u)\n",
851+
(void*)params->loras, params->lora_count);
852+
}
552853
}
553854

554855
void sd_img_gen_params_set_dimensions(sd_img_gen_params_t *params, int width, int height) {
@@ -740,6 +1041,20 @@ int gen_image(sd_img_gen_params_t *p, int steps, char *dst, float cfg_scale, cha
7401041
}
7411042
}
7421043

1044+
// Log LoRA information
1045+
if (p->loras && p->lora_count > 0) {
1046+
fprintf(stderr, "Using %u LoRA(s) in generation:\n", p->lora_count);
1047+
for (uint32_t i = 0; i < p->lora_count; i++) {
1048+
fprintf(stderr, " LoRA[%u]: path='%s', multiplier=%.2f, is_high_noise=%s\n",
1049+
i,
1050+
p->loras[i].path ? p->loras[i].path : "(null)",
1051+
p->loras[i].multiplier,
1052+
p->loras[i].is_high_noise ? "true" : "false");
1053+
}
1054+
} else {
1055+
fprintf(stderr, "No LoRAs specified for this generation\n");
1056+
}
1057+
7431058
fprintf(stderr, "Generating image with params: \nctx\n---\n%s\ngen\n---\n%s\n",
7441059
sd_ctx_params_to_str(&ctx_params),
7451060
sd_img_gen_params_to_str(p));
@@ -802,3 +1117,4 @@ int unload() {
8021117
free_sd_ctx(sd_c);
8031118
return 0;
8041119
}
1120+

0 commit comments

Comments
 (0)