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
This commit is contained in:
Wagner Bruna 2026-03-16 23:09:55 -03:00 committed by GitHub
parent 0c66ed863d
commit 51187d5362
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 35 additions and 41 deletions

View file

@ -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 <lora:name:multiplier>
'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

View file

@ -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]);
}
}

View file

@ -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)