From 51187d5362878647468087453052a20c3bfc5157 Mon Sep 17 00:00:00 2001 From: Wagner Bruna Date: Mon, 16 Mar 2026 23:09:55 -0300 Subject: [PATCH] sd: support changing preloaded LoRA multipliers (#2041) * sd: remove C++ support for enforcing fixed LoRA multipliers The logic at the Python level is enough. * sd: support changing preloaded LoRA multipliers We keep the same rules as before: - Any LoRA with multiplier 0 can be changed - If all LoRAs have multiplier != 0, they are fixed and optimized but tweak the corner case of LoRAs specified more than once to allow adjusting the multiplier if the same LoRA is also specified with a zero multiplier, as if they were two different LoRAs. So the following keeps working as before: - --sdlora /loras/lcm.gguf --sdloramult 1 : fixed as 1 - --sdlora /loras/lcm.gguf --sdloramult 0 : dynamic, default 0 - --sdlora /loras/ : dynamic, default 0 - --sdlora /loras/lcm.gguf /loras/lcm.gguf --sdloramult 1 1 : fixed as 2 But now we have: - --sdlora /loras/lcm.gguf /loras/lcm.gguf --sdloramult 1 0 : dynamic, default 1 - --sdlora /loras/lcm.gguf /loras/ --sdloramult 1 : dynamic, default 1 --- koboldcpp.py | 35 +++++++++++++++++------------- otherarch/sdcpp/sdtype_adapter.cpp | 13 ++--------- tests/test_koboldcpp.py | 28 +++++++++++------------- 3 files changed, 35 insertions(+), 41 deletions(-) diff --git a/koboldcpp.py b/koboldcpp.py index c02969be0..00e81196a 100755 --- a/koboldcpp.py +++ b/koboldcpp.py @@ -89,8 +89,8 @@ ttsmodelpath = "" #if empty, not initialized embeddingsmodelpath = "" #if empty, not initialized musicllmmodelpath = "" #if empty, not initialized musicdiffusionmodelpath = "" #if empty, not initialized -imglora_preload = [] -imglora_bypath = {} +imglora_preload = [] # all preloaded LoRAs +imglora_bypath = {} # len(imglora_bypath) == 0 <==> static loras imglora_name2path = {} imglora_cached = True imglora_initial_fixed = True @@ -2070,8 +2070,7 @@ def sd_load_model(model_filename,vae_filename,t5xxl_filename,clip1_filename,clip if imglora_bypath: lora_dynamic = 1 << 3 # accept changes at runtime lora_cache = 1 << 4 if imglora_cached else 0 # cache the preloaded LoRAs - lora_fixed = 1 << 5 if imglora_initial_fixed else 0 # do not allow changes to the non-zero preloaded LoRAs - lora_apply_mode = lora_dynamic | lora_cache | lora_fixed + lora_apply_mode = lora_dynamic | lora_cache inputs.lora_apply_mode = lora_apply_mode inputs.img_hard_limit = args.sdclamped @@ -2253,7 +2252,7 @@ def mk_sdapi_lora_list(imglora_bypath): return [ {'name': info['name'], 'path': info['path']} for info in imglora_bypath.values() - if info['multiplier'] == 0.0 # both preloaded and scanned + if not info.get('fixed') ] def extract_loras_from_prompt(prompt): @@ -8854,8 +8853,12 @@ def mk_lora_info(imgloras, multipliers, mock_filesystem=False): if not mock_filesystem: lora_fullpath = os.path.abspath(lora_fullpath) # dedup paths (e.g. preloaded and on directory) - if lora_fullpath in lora_fullmap: - lora_fullmap[lora_fullpath]["multiplier"] += multiplier + info = lora_fullmap.get(lora_fullpath) + if info: + info["multiplier"] += multiplier + if multiplier == 0.0 and 'fixed' in info: + # allow changes if we see this lora again with weight 0 + del info['fixed'] continue lora_name, lora_ext = os.path.splitext(lora_file) # ensure unique names @@ -8867,23 +8870,25 @@ def mk_lora_info(imgloras, multipliers, mock_filesystem=False): unique_lora_names.add(lora_uname) lora_upath = lora_uname + lora_ext lora_entry = { - 'fullpath': lora_fullpath, - 'name': lora_uname, - 'path': lora_upath, - 'multiplier': multiplier, - 'preloaded': preloaded, + 'fullpath': lora_fullpath, # where it is on disk + 'name': lora_uname, # 'name' in api field and + 'path': lora_upath, # 'path' in api field (relative), + extension + 'multiplier': multiplier, # preload multiplier } + if preloaded: + lora_entry['preloaded'] = preloaded + if multiplier != 0.0 and imglora_initial_fixed: + lora_entry['fixed'] = True lora_fullmap[lora_fullpath] = lora_entry # build the runtime tables preloaded_table = [] lora_path_map = {} lora_name_map = {} for lora_entry in lora_fullmap.values(): - # only map LoRAs that can be changed - if not imglora_initial_fixed or lora_entry["multiplier"] == 0.0: + if not lora_entry.get("fixed"): # only map LoRAs that can be changed lora_path_map[lora_entry["path"]] = lora_entry lora_name_map[lora_entry["name"]] = lora_entry["path"] - if lora_entry["preloaded"]: + if lora_entry.get("preloaded"): preloaded_table.append(lora_entry) return preloaded_table, lora_path_map, lora_name_map diff --git a/otherarch/sdcpp/sdtype_adapter.cpp b/otherarch/sdcpp/sdtype_adapter.cpp index ea9884950..71058104c 100644 --- a/otherarch/sdcpp/sdtype_adapter.cpp +++ b/otherarch/sdcpp/sdtype_adapter.cpp @@ -137,7 +137,6 @@ struct SDParams { LoraMap lora_map; bool lora_dynamic = false; - bool lora_fixed = false; std::string cache_mode; std::string cache_options; @@ -288,17 +287,14 @@ bool sdtype_load_model(const sd_load_model_inputs inputs) { int lora_apply_mode = LORA_APPLY_AT_RUNTIME; bool lora_dynamic = false; bool lora_cache = false; - bool lora_fixed = false; if(inputs.lora_apply_mode >= 0 && inputs.lora_apply_mode <= 2) { lora_apply_mode = inputs.lora_apply_mode; } else { // bit 3: LoRAs can be changed dynamically // bit 4: cache the initial LoRA list in VRAM - // bit 5: do not allow multiplier changes for the initial LoRAs lora_dynamic = !!(inputs.lora_apply_mode & (1<<3)); lora_cache = lora_dynamic && !!(inputs.lora_apply_mode & (1<<4)); - lora_fixed = lora_dynamic && !!(inputs.lora_apply_mode & (1<<5)); } if(lora_map.items.size() > 0) @@ -311,8 +307,7 @@ bool sdtype_load_model(const sd_load_model_inputs inputs) { printf("With LoRAs in apply mode %s%s%s:\n", lora_apply_mode_name, lora_dynamic_name, lora_cache_name); for(auto lora: lora_map.items) { - const char * lora_fixed_name = lora_fixed && lora.second != 0.f ? " (fixed)" : ""; - printf(" %s at %f power%s\n", lora.first.c_str(), lora.second, lora_fixed_name); + printf(" %s at %f power\n", lora.first.c_str(), lora.second); } } @@ -402,7 +397,6 @@ bool sdtype_load_model(const sd_load_model_inputs inputs) { sd_params->stacked_id_embeddings_path = photomaker_filename; sd_params->lora_map = lora_map; sd_params->lora_dynamic = lora_dynamic; - sd_params->lora_fixed = lora_fixed; //if t5 is set, and model is a gguf, load it as a diffusion model path bool endswithgguf = (sd_params->model_path.rfind(".gguf") == sd_params->model_path.size() - 5); if((sd_params->t5xxl_path!="" || sd_params->clip_l_path!="" || sd_params->clip_g_path!="") && endswithgguf) @@ -1222,12 +1216,9 @@ sd_generation_outputs sdtype_generate(const sd_generation_inputs inputs) LoraMap lora_map = sd_params->lora_map; if (sd_params->lora_dynamic) { for (int i = 0; i < inputs.lora_len; i++) { - // check if it was initially fixed std::string path = inputs.lora_filenames[i]; float preloaded_mult = sd_params->lora_map.get_mult(path); - if (!sd_params->lora_fixed || preloaded_mult == 0.f) { - lora_map.add_lora(path, inputs.lora_multipliers[i]); - } + lora_map.add_lora(path, inputs.lora_multipliers[i]); } } diff --git a/tests/test_koboldcpp.py b/tests/test_koboldcpp.py index b0eb4eb01..373ac950a 100644 --- a/tests/test_koboldcpp.py +++ b/tests/test_koboldcpp.py @@ -59,7 +59,7 @@ def mk_lora_info(imgloras, multipliers): fake filesystem access fake filesystem access >>> pre - [{'fullpath': '/x/lora1.safetensors', 'name': 'lora1', 'path': 'lora1.safetensors', 'multiplier': 1.0, 'preloaded': True}, {'fullpath': '/y/lora2.gguf', 'name': 'lora2', 'path': 'lora2.gguf', 'multiplier': 1.0, 'preloaded': True}] + [{'fullpath': '/x/lora1.safetensors', 'name': 'lora1', 'path': 'lora1.safetensors', 'multiplier': 1.0, 'preloaded': True, 'fixed': True}, {'fullpath': '/y/lora2.gguf', 'name': 'lora2', 'path': 'lora2.gguf', 'multiplier': 1.0, 'preloaded': True, 'fixed': True}] >>> path {} >>> name @@ -79,7 +79,7 @@ def mk_lora_info(imgloras, multipliers): fake filesystem access fake filesystem access >>> pre - [{'fullpath': '/x/lora1.safetensors', 'name': 'lora1', 'path': 'lora1.safetensors', 'multiplier': 0.3, 'preloaded': True}, {'fullpath': '/y/lora1.safetensors', 'name': 'lora1_2', 'path': 'lora1_2.safetensors', 'multiplier': 0.3, 'preloaded': True}] + [{'fullpath': '/x/lora1.safetensors', 'name': 'lora1', 'path': 'lora1.safetensors', 'multiplier': 0.3, 'preloaded': True, 'fixed': True}, {'fullpath': '/y/lora1.safetensors', 'name': 'lora1_2', 'path': 'lora1_2.safetensors', 'multiplier': 0.3, 'preloaded': True, 'fixed': True}] >>> path {} @@ -95,14 +95,12 @@ def mk_lora_info(imgloras, multipliers): ... 'fullpath': '/lora/dir/lora1_makebelieve.gguf', ... 'name': 'lora1_makebelieve', ... 'path': 'lora1_makebelieve.gguf', - ... 'multiplier': 0.0, - ... 'preloaded': False}, + ... 'multiplier': 0.0}, ... 'lora2/makebelieve.gguf': { ... 'fullpath': '/lora/dir/lora2/makebelieve.gguf', ... 'name': 'lora2/makebelieve', ... 'path': 'lora2/makebelieve.gguf', - ... 'multiplier': 0.0, - ... 'preloaded': False}} + ... 'multiplier': 0.0}} >>> path == expected True >>> name @@ -185,21 +183,21 @@ def mk_sdapi_lora_list(imglora_bypath): ... 'lora_a.safetensors': {'name': 'lora_a', 'path': 'lora_a.safetensors', 'multiplier': 0.0}, ... 'lora_b.gguf' : {'name': 'lora_b', 'path': 'lora_b.gguf', 'multiplier': 0.0}, ... 'lora_c.safetensors': {'name': 'lora_c', 'path': 'lora_c.safetensors', 'multiplier': 1.0}, + ... 'lora_d.safetensors': {'name': 'lora_d', 'path': 'lora_d.safetensors', 'multiplier': 1.0, 'fixed': True}, ... 'chars/waifu.gguf' : {'name': 'chars/waifu', 'path': 'chars/waifu.gguf', 'multiplier': 0.0} ... } - >>> mk_sdapi_lora_list(imglora_bypath) - [{'name': 'lora_a', 'path': 'lora_a.safetensors'}, {'name': 'lora_b', 'path': 'lora_b.gguf'}, {'name': 'chars/waifu', 'path': 'chars/waifu.gguf'}] + >>> expected = [ + ... {'name': 'lora_a', 'path': 'lora_a.safetensors'}, + ... {'name': 'lora_b', 'path': 'lora_b.gguf'}, + ... {'name': 'lora_c', 'path': 'lora_c.safetensors'}, + ... {'name': 'chars/waifu', 'path': 'chars/waifu.gguf'} + ... ] + >>> mk_sdapi_lora_list(imglora_bypath) == expected + True >>> empty_data = {} >>> mk_sdapi_lora_list(empty_data) [] - - >>> mixed_data = { - ... 'k1': {'name': 'X', 'path': 'p1', 'multiplier': 0.5}, - ... 'k2': {'name': 'Y', 'path': 'p2', 'multiplier': 0.0} - ... } - >>> mk_sdapi_lora_list(mixed_data) - [{'name': 'Y', 'path': 'p2'}] ''' return koboldcpp.mk_sdapi_lora_list(imglora_bypath)