Vlad Iliescu commited on
Commit
b990348
·
1 Parent(s): 4511070

better lora

Browse files
Files changed (2) hide show
  1. lora_utils.py +411 -59
  2. tests/test_lora_utils.py +72 -0
lora_utils.py CHANGED
@@ -10,36 +10,38 @@ HF_TOKEN = os.environ.get("HF_TOKEN") or os.environ.get("hf")
10
  def _parse_hf_lora_url(url: str):
11
  parsed = urlparse(url)
12
  if "huggingface.co" not in parsed.netloc:
13
- return None, None
14
 
15
  path_parts = [part for part in parsed.path.split("/") if part]
16
  if len(path_parts) < 2:
17
- return None, None
18
 
19
  repo_id = f"{path_parts[0]}/{path_parts[1]}"
20
  weight_parts = path_parts[2:]
 
21
  if len(weight_parts) >= 2 and weight_parts[0] in {"blob", "resolve"}:
 
22
  weight_parts = weight_parts[2:]
23
  weight_name = "/".join(weight_parts) if weight_parts else None
24
  if not weight_name or not weight_name.endswith(".safetensors"):
25
- return repo_id, None
26
- return repo_id, weight_name
27
 
28
 
29
  def _split_lora_spec(spec: str):
30
  if not spec:
31
- return None, None
32
 
33
  spec = spec.strip()
34
  if not spec:
35
- return None, None
36
 
37
  if spec.startswith("http://") or spec.startswith("https://"):
38
  return _parse_hf_lora_url(spec)
39
  if ":" in spec:
40
  repo_id, weight_name = spec.split(":", 1)
41
- return repo_id.strip(), weight_name.strip()
42
- return spec, None
43
 
44
 
45
  def _split_adapter_line_scale(line: str):
@@ -67,7 +69,7 @@ def parse_adapter_specs(spec_text: str, global_scale: float):
67
  continue
68
 
69
  spec, inline_scale = _split_adapter_line_scale(line)
70
- repo_id, weight_name = _split_lora_spec(spec)
71
  if not repo_id or not weight_name:
72
  raise ValueError(
73
  "Please provide LoRA entries as "
@@ -75,7 +77,7 @@ def parse_adapter_specs(spec_text: str, global_scale: float):
75
  f"Invalid line {line_number}: {raw_line!r}"
76
  )
77
 
78
- adapter_key = (repo_id, weight_name)
79
  if adapter_key in seen_keys:
80
  raise ValueError(
81
  f"Duplicate LoRA entry for '{repo_id}:{weight_name}' on line {line_number}."
@@ -87,6 +89,7 @@ def parse_adapter_specs(spec_text: str, global_scale: float):
87
  "key": adapter_key,
88
  "repo_id": repo_id,
89
  "weight_name": weight_name,
 
90
  "adapter_name": adapter_runtime_name(adapter_key),
91
  "inline_scale": inline_scale,
92
  "global_scale": global_scale,
@@ -98,20 +101,26 @@ def parse_adapter_specs(spec_text: str, global_scale: float):
98
 
99
 
100
  def adapter_runtime_name(adapter_key):
101
- digest = hashlib.sha1(f"{adapter_key[0]}:{adapter_key[1]}".encode("utf-8")).hexdigest()[:12]
 
102
  return f"{ADAPTER_NAME_PREFIX}_{digest}"
103
 
104
 
105
- def _iter_adapter_hosts(pipe):
106
  seen = set()
107
- for host in (
108
- pipe,
109
- getattr(pipe, "transformer", None),
110
- getattr(pipe, "unconditional_transformer", None),
111
  ):
112
  if host is None or id(host) in seen:
113
  continue
114
  seen.add(id(host))
 
 
 
 
 
115
  yield host
116
 
117
 
@@ -135,12 +144,14 @@ def _sorted_lora_entries(entries):
135
  return sorted(entries, key=lambda entry: entry["adapter_name"])
136
 
137
 
138
- def _download_lora_weight(repo_id: str, weight_name: str, token=HF_TOKEN):
139
  from huggingface_hub import hf_hub_download
140
 
141
  kwargs = {}
142
  if token:
143
  kwargs["token"] = token
 
 
144
  return hf_hub_download(repo_id, filename=weight_name, **kwargs)
145
 
146
 
@@ -169,21 +180,275 @@ def _ensure_pipeline_lora_prefix(state_dict):
169
  return state_dict
170
 
171
 
172
- def safe_unload_lora_adapters(pipe):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
  deleted = False
174
- for host in _iter_adapter_hosts(pipe):
175
- if not hasattr(host, "delete_adapters"):
176
  continue
177
  try:
178
- adapter_names = sorted(_flatten_adapter_names(host.get_list_adapters()))
 
179
  except Exception:
180
- adapter_names = []
181
- for adapter_name in adapter_names:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  try:
183
- host.delete_adapters(adapter_name)
184
- deleted = True
 
 
 
 
 
 
185
  except Exception:
186
  pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
  if deleted:
188
  return
189
 
@@ -210,6 +475,7 @@ def safe_unload_lora_adapters(pipe):
210
  host.disable_lora()
211
  except Exception:
212
  pass
 
213
 
214
 
215
  def _set_adapters_on_host(host, adapter_names, adapter_weights):
@@ -247,6 +513,28 @@ def apply_lora_adapters(pipe, lora_entries):
247
  adapter_weights = [entry["scale"] for entry in sorted_entries]
248
 
249
  activated = False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
  for host in _iter_adapter_hosts(pipe):
251
  if _set_adapters_on_host(host, adapter_names, adapter_weights):
252
  activated = True
@@ -261,54 +549,115 @@ def apply_lora_adapters(pipe, lora_entries):
261
  raise ValueError("This runtime does not support activating multiple LoRA adapters.")
262
 
263
 
264
- def load_lora_adapter(pipe, entry, token=HF_TOKEN):
265
- load_kwargs = {
 
 
 
 
 
 
 
 
266
  "weight_name": entry["weight_name"],
267
  "adapter_name": entry["adapter_name"],
268
  }
 
 
269
  if token:
270
- load_kwargs["token"] = token
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
271
 
 
 
272
  native_error = None
273
  if hasattr(pipe, "load_lora_weights"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
  try:
275
- pipe.load_lora_weights(entry["repo_id"], **load_kwargs)
 
 
 
 
 
276
  return
277
- except TypeError as exc:
278
- native_error = exc
279
- if token:
280
- load_kwargs.pop("token", None)
281
- try:
282
- pipe.load_lora_weights(entry["repo_id"], **load_kwargs)
283
- return
284
- except Exception as retry_exc:
285
- native_error = retry_exc
286
  except Exception as exc:
 
 
 
 
 
 
 
287
  native_error = exc
288
 
289
- local_path = _download_lora_weight(entry["repo_id"], entry["weight_name"], token=token)
290
- state_dict = _load_adapter_state_dict(local_path)
 
 
 
 
 
291
 
292
- loaded = False
293
- for host in _iter_adapter_hosts(pipe):
294
- if not hasattr(host, "load_lora_adapter"):
295
- continue
296
  try:
297
- host.load_lora_adapter(dict(state_dict), adapter_name=entry["adapter_name"], prefix=None)
298
- loaded = True
299
- continue
300
- except TypeError:
301
- try:
302
- host.load_lora_adapter(dict(state_dict), adapter_name=entry["adapter_name"])
303
- loaded = True
304
- continue
305
- except Exception as exc:
306
- native_error = native_error or exc
307
  except Exception as exc:
308
- native_error = native_error or exc
309
-
310
- if loaded:
311
- return
312
 
313
  if hasattr(pipe, "load_lora_weights"):
314
  try:
@@ -320,7 +669,10 @@ def load_lora_adapter(pipe, entry, token=HF_TOKEN):
320
  raise ValueError(f"{native_error}; fallback failed with {exc}") from exc
321
  raise
322
 
323
- raise ValueError("This pipeline does not expose a LoRA loader on itself or its transformers.")
 
 
 
324
 
325
 
326
  def ensure_loras_loaded(pipe, spec_text: str, global_scale: float, active_by_key: dict, token=HF_TOKEN):
 
10
  def _parse_hf_lora_url(url: str):
11
  parsed = urlparse(url)
12
  if "huggingface.co" not in parsed.netloc:
13
+ return None, None, None
14
 
15
  path_parts = [part for part in parsed.path.split("/") if part]
16
  if len(path_parts) < 2:
17
+ return None, None, None
18
 
19
  repo_id = f"{path_parts[0]}/{path_parts[1]}"
20
  weight_parts = path_parts[2:]
21
+ revision = None
22
  if len(weight_parts) >= 2 and weight_parts[0] in {"blob", "resolve"}:
23
+ revision = weight_parts[1]
24
  weight_parts = weight_parts[2:]
25
  weight_name = "/".join(weight_parts) if weight_parts else None
26
  if not weight_name or not weight_name.endswith(".safetensors"):
27
+ return repo_id, None, revision
28
+ return repo_id, weight_name, revision
29
 
30
 
31
  def _split_lora_spec(spec: str):
32
  if not spec:
33
+ return None, None, None
34
 
35
  spec = spec.strip()
36
  if not spec:
37
+ return None, None, None
38
 
39
  if spec.startswith("http://") or spec.startswith("https://"):
40
  return _parse_hf_lora_url(spec)
41
  if ":" in spec:
42
  repo_id, weight_name = spec.split(":", 1)
43
+ return repo_id.strip(), weight_name.strip(), None
44
+ return spec, None, None
45
 
46
 
47
  def _split_adapter_line_scale(line: str):
 
69
  continue
70
 
71
  spec, inline_scale = _split_adapter_line_scale(line)
72
+ repo_id, weight_name, revision = _split_lora_spec(spec)
73
  if not repo_id or not weight_name:
74
  raise ValueError(
75
  "Please provide LoRA entries as "
 
77
  f"Invalid line {line_number}: {raw_line!r}"
78
  )
79
 
80
+ adapter_key = (repo_id, weight_name, revision)
81
  if adapter_key in seen_keys:
82
  raise ValueError(
83
  f"Duplicate LoRA entry for '{repo_id}:{weight_name}' on line {line_number}."
 
89
  "key": adapter_key,
90
  "repo_id": repo_id,
91
  "weight_name": weight_name,
92
+ "revision": revision,
93
  "adapter_name": adapter_runtime_name(adapter_key),
94
  "inline_scale": inline_scale,
95
  "global_scale": global_scale,
 
101
 
102
 
103
  def adapter_runtime_name(adapter_key):
104
+ key_parts = [part for part in adapter_key if part is not None]
105
+ digest = hashlib.sha1(":".join(str(part) for part in key_parts).encode("utf-8")).hexdigest()[:12]
106
  return f"{ADAPTER_NAME_PREFIX}_{digest}"
107
 
108
 
109
+ def _iter_named_adapter_hosts(pipe):
110
  seen = set()
111
+ for host_name, host in (
112
+ (None, pipe),
113
+ ("transformer", getattr(pipe, "transformer", None)),
114
+ ("unconditional_transformer", getattr(pipe, "unconditional_transformer", None)),
115
  ):
116
  if host is None or id(host) in seen:
117
  continue
118
  seen.add(id(host))
119
+ yield host_name, host
120
+
121
+
122
+ def _iter_adapter_hosts(pipe):
123
+ for _, host in _iter_named_adapter_hosts(pipe):
124
  yield host
125
 
126
 
 
144
  return sorted(entries, key=lambda entry: entry["adapter_name"])
145
 
146
 
147
+ def _download_lora_weight(repo_id: str, weight_name: str, revision=None, token=HF_TOKEN):
148
  from huggingface_hub import hf_hub_download
149
 
150
  kwargs = {}
151
  if token:
152
  kwargs["token"] = token
153
+ if revision:
154
+ kwargs["revision"] = revision
155
  return hf_hub_download(repo_id, filename=weight_name, **kwargs)
156
 
157
 
 
180
  return state_dict
181
 
182
 
183
+ def _has_lora_tensors(state_dict):
184
+ return any(".lora_A." in key or ".lora_B." in key for key in state_dict.keys())
185
+
186
+
187
+ def _strip_state_dict_prefix(state_dict, prefix):
188
+ if not prefix:
189
+ return state_dict
190
+ return {
191
+ key[len(prefix) :] if key.startswith(prefix) else key: value
192
+ for key, value in state_dict.items()
193
+ }
194
+
195
+
196
+ def _strip_known_peft_prefixes(state_dict):
197
+ stripped = dict(state_dict)
198
+ for prefix in ("base_model.model.", "model."):
199
+ if any(key.startswith(prefix) for key in stripped.keys()):
200
+ stripped = _strip_state_dict_prefix(stripped, prefix)
201
+ return stripped
202
+
203
+
204
+ def _state_dict_for_model_host(state_dict, host_name):
205
+ state_dict = _strip_known_peft_prefixes(state_dict)
206
+ if not host_name:
207
+ return state_dict
208
+
209
+ own_prefix = f"{host_name}."
210
+ own_state_dict = {
211
+ key[len(own_prefix) :]: value
212
+ for key, value in state_dict.items()
213
+ if key.startswith(own_prefix)
214
+ }
215
+ if _has_lora_tensors(own_state_dict):
216
+ return own_state_dict
217
+
218
+ transformer_prefix = "transformer."
219
+ transformer_state_dict = {
220
+ key[len(transformer_prefix) :]: value
221
+ for key, value in state_dict.items()
222
+ if key.startswith(transformer_prefix)
223
+ }
224
+ if _has_lora_tensors(transformer_state_dict):
225
+ return transformer_state_dict
226
+
227
+ if not any(
228
+ key.startswith(("transformer.", "unconditional_transformer."))
229
+ for key in state_dict.keys()
230
+ if ".lora_" in key or key.endswith(".alpha")
231
+ ):
232
+ return state_dict
233
+
234
+ return own_state_dict
235
+
236
+
237
+ def _lora_module_name_from_key(key):
238
+ for marker in (".lora_A.", ".lora_B."):
239
+ if marker in key:
240
+ return key.split(marker, 1)[0]
241
+ return None
242
+
243
+
244
+ def _module_name_from_alpha_key(key):
245
+ if key.endswith(".alpha"):
246
+ return key[: -len(".alpha")]
247
+ return None
248
+
249
+
250
+ def _scalar_to_float(value):
251
+ if hasattr(value, "detach"):
252
+ return float(value.detach().cpu().reshape(-1)[0].item())
253
+ if hasattr(value, "item"):
254
+ return float(value.item())
255
+ return float(value)
256
+
257
+
258
+ def _build_lora_config(state_dict):
259
+ from peft import LoraConfig
260
+
261
+ rank_pattern = {}
262
+ alpha_pattern = {}
263
+ for key, value in state_dict.items():
264
+ module_name = _lora_module_name_from_key(key)
265
+ if module_name is None:
266
+ continue
267
+ if ".lora_A." in key and hasattr(value, "shape") and value.shape:
268
+ rank_pattern[module_name] = int(value.shape[0])
269
+
270
+ for key, value in state_dict.items():
271
+ module_name = _module_name_from_alpha_key(key)
272
+ if module_name is not None:
273
+ alpha_pattern[module_name] = _scalar_to_float(value)
274
+
275
+ if not rank_pattern:
276
+ return LoraConfig()
277
+
278
+ default_rank = max(rank_pattern.values())
279
+ for module_name, rank in rank_pattern.items():
280
+ alpha_pattern.setdefault(module_name, rank)
281
+ return LoraConfig(
282
+ r=default_rank,
283
+ lora_alpha=default_rank,
284
+ rank_pattern=rank_pattern,
285
+ alpha_pattern=alpha_pattern,
286
+ )
287
+
288
+
289
+ def _peft_load_state_dict(state_dict):
290
+ return {
291
+ key: value
292
+ for key, value in state_dict.items()
293
+ if not key.endswith(".alpha")
294
+ }
295
+
296
+
297
+ def _load_lora_with_peft(host, state_dict, adapter_name):
298
+ from peft import inject_adapter_in_model
299
+ from peft.utils import set_peft_model_state_dict
300
+
301
+ state_dict = _strip_known_peft_prefixes(state_dict)
302
+ config = _build_lora_config(state_dict)
303
+ inject_adapter_in_model(config, host, adapter_name=adapter_name, state_dict=state_dict)
304
+ result = set_peft_model_state_dict(host, _peft_load_state_dict(state_dict), adapter_name=adapter_name)
305
+ unexpected_keys = [
306
+ key
307
+ for key in getattr(result, "unexpected_keys", [])
308
+ if ".lora_" in key or key.endswith(".alpha")
309
+ ]
310
+ if unexpected_keys:
311
+ raise ValueError(f"Unexpected LoRA keys while loading adapter: {unexpected_keys[:5]}")
312
+ return result
313
+
314
+
315
+ def _iter_host_modules(host):
316
+ if not hasattr(host, "modules"):
317
+ return []
318
+ try:
319
+ return list(host.modules())
320
+ except Exception:
321
+ return []
322
+
323
+
324
+ def _peft_adapter_names_on_host(host):
325
+ adapter_names = set()
326
+ peft_config = getattr(host, "peft_config", None)
327
+ if isinstance(peft_config, dict):
328
+ adapter_names.update(peft_config.keys())
329
+
330
+ for module in _iter_host_modules(host):
331
+ for attr_name in ("lora_A", "lora_B", "scaling"):
332
+ adapters = getattr(module, attr_name, None)
333
+ if hasattr(adapters, "keys"):
334
+ try:
335
+ adapter_names.update(adapters.keys())
336
+ except Exception:
337
+ pass
338
+ return adapter_names
339
+
340
+
341
+ def _adapter_names_on_host(host):
342
+ adapter_names = set()
343
+ if hasattr(host, "get_list_adapters"):
344
+ try:
345
+ adapter_names.update(_flatten_adapter_names(host.get_list_adapters()))
346
+ except Exception:
347
+ pass
348
+ adapter_names.update(_peft_adapter_names_on_host(host))
349
+ return adapter_names
350
+
351
+
352
+ def _delete_peft_adapter_on_host(host, adapter_name):
353
  deleted = False
354
+ for target in (host, *_iter_host_modules(host)):
355
+ if not hasattr(target, "delete_adapter"):
356
  continue
357
  try:
358
+ target.delete_adapter(adapter_name)
359
+ deleted = True
360
  except Exception:
361
+ pass
362
+ peft_config = getattr(host, "peft_config", None)
363
+ if isinstance(peft_config, dict) and adapter_name in peft_config:
364
+ peft_config.pop(adapter_name, None)
365
+ deleted = True
366
+ return deleted
367
+
368
+
369
+ def _set_peft_adapters_on_host(host, adapter_names, adapter_weights):
370
+ changed = False
371
+ if not adapter_names:
372
+ for target in (host, *_iter_host_modules(host)):
373
+ if hasattr(target, "enable_adapters"):
374
+ try:
375
+ target.enable_adapters(False)
376
+ changed = True
377
+ except Exception:
378
+ pass
379
+ return changed
380
+
381
+ for target in (host, *_iter_host_modules(host)):
382
+ if hasattr(target, "set_adapter"):
383
  try:
384
+ target.set_adapter(adapter_names)
385
+ changed = True
386
+ except TypeError:
387
+ try:
388
+ target.set_adapter(adapter_names[0] if len(adapter_names) == 1 else adapter_names)
389
+ changed = True
390
+ except Exception:
391
+ pass
392
  except Exception:
393
  pass
394
+ if hasattr(target, "enable_adapters"):
395
+ try:
396
+ target.enable_adapters(True)
397
+ changed = True
398
+ except Exception:
399
+ pass
400
+ if hasattr(target, "set_scale"):
401
+ for adapter_name, adapter_weight in zip(adapter_names, adapter_weights):
402
+ try:
403
+ target.set_scale(adapter_name, adapter_weight)
404
+ changed = True
405
+ except Exception:
406
+ pass
407
+ return changed
408
+
409
+
410
+ def _is_model_adapter_host(host):
411
+ return hasattr(host, "named_modules") and hasattr(host, "modules")
412
+
413
+
414
+ def _describe_adapter_hosts(pipe):
415
+ descriptions = []
416
+ for host_name, host in _iter_named_adapter_hosts(pipe):
417
+ methods = [
418
+ method_name
419
+ for method_name in (
420
+ "load_lora_weights",
421
+ "load_lora_adapter",
422
+ "set_adapters",
423
+ "set_adapter",
424
+ "delete_adapters",
425
+ "delete_adapter",
426
+ )
427
+ if hasattr(host, method_name)
428
+ ]
429
+ label = host_name or "pipeline"
430
+ method_text = ", ".join(methods) if methods else "no adapter methods"
431
+ descriptions.append(f"{label}={host.__class__.__name__} ({method_text})")
432
+ return "; ".join(descriptions)
433
+
434
+
435
+ def safe_unload_lora_adapters(pipe):
436
+ deleted = False
437
+ for host in _iter_adapter_hosts(pipe):
438
+ if hasattr(host, "delete_adapters"):
439
+ try:
440
+ adapter_names = sorted(_flatten_adapter_names(host.get_list_adapters()))
441
+ except Exception:
442
+ adapter_names = []
443
+ for adapter_name in adapter_names:
444
+ try:
445
+ host.delete_adapters(adapter_name)
446
+ deleted = True
447
+ except Exception:
448
+ pass
449
+ for adapter_name in sorted(_peft_adapter_names_on_host(host)):
450
+ if _delete_peft_adapter_on_host(host, adapter_name):
451
+ deleted = True
452
  if deleted:
453
  return
454
 
 
475
  host.disable_lora()
476
  except Exception:
477
  pass
478
+ _set_peft_adapters_on_host(host, [], [])
479
 
480
 
481
  def _set_adapters_on_host(host, adapter_names, adapter_weights):
 
513
  adapter_weights = [entry["scale"] for entry in sorted_entries]
514
 
515
  activated = False
516
+ missing_on_hosts = []
517
+ for host_name, host in _iter_named_adapter_hosts(pipe):
518
+ host_adapter_names = _adapter_names_on_host(host)
519
+ if not host_adapter_names:
520
+ continue
521
+ missing = set(adapter_names) - host_adapter_names
522
+ if missing:
523
+ missing_on_hosts.append(f"{host_name or 'pipeline'} missing {sorted(missing)}")
524
+ continue
525
+ if not (
526
+ _set_adapters_on_host(host, adapter_names, adapter_weights)
527
+ or _set_peft_adapters_on_host(host, adapter_names, adapter_weights)
528
+ ):
529
+ raise ValueError(f"Could not activate LoRA adapters on {host_name or 'pipeline'}.")
530
+ activated = True
531
+
532
+ if missing_on_hosts:
533
+ raise ValueError("Partial LoRA adapter state: " + "; ".join(missing_on_hosts))
534
+
535
+ if activated:
536
+ return
537
+
538
  for host in _iter_adapter_hosts(pipe):
539
  if _set_adapters_on_host(host, adapter_names, adapter_weights):
540
  activated = True
 
549
  raise ValueError("This runtime does not support activating multiple LoRA adapters.")
550
 
551
 
552
+ def _load_lora_adapter_on_host(host, state_dict, adapter_name):
553
+ try:
554
+ host.load_lora_adapter(dict(state_dict), adapter_name=adapter_name, prefix=None)
555
+ return
556
+ except TypeError:
557
+ host.load_lora_adapter(dict(state_dict), adapter_name=adapter_name)
558
+
559
+
560
+ def _pipeline_load_kwargs(entry, token):
561
+ base_kwargs = {
562
  "weight_name": entry["weight_name"],
563
  "adapter_name": entry["adapter_name"],
564
  }
565
+ if entry.get("revision"):
566
+ base_kwargs["revision"] = entry["revision"]
567
  if token:
568
+ base_kwargs["token"] = token
569
+
570
+ variants = [base_kwargs]
571
+ if "token" in base_kwargs:
572
+ without_token = dict(base_kwargs)
573
+ without_token.pop("token", None)
574
+ variants.append(without_token)
575
+ if "revision" in base_kwargs:
576
+ without_revision = dict(base_kwargs)
577
+ without_revision.pop("revision", None)
578
+ variants.append(without_revision)
579
+ without_token_revision = dict(without_revision)
580
+ without_token_revision.pop("token", None)
581
+ variants.append(without_token_revision)
582
+
583
+ unique_variants = []
584
+ seen = set()
585
+ for kwargs in variants:
586
+ key = tuple(sorted(kwargs.items()))
587
+ if key not in seen:
588
+ seen.add(key)
589
+ unique_variants.append(kwargs)
590
+ return unique_variants
591
 
592
+
593
+ def load_lora_adapter(pipe, entry, token=HF_TOKEN):
594
  native_error = None
595
  if hasattr(pipe, "load_lora_weights"):
596
+ for load_kwargs in _pipeline_load_kwargs(entry, token):
597
+ try:
598
+ pipe.load_lora_weights(entry["repo_id"], **load_kwargs)
599
+ return
600
+ except TypeError as exc:
601
+ native_error = exc
602
+ except Exception as exc:
603
+ native_error = exc
604
+ break
605
+
606
+ local_path = _download_lora_weight(
607
+ entry["repo_id"],
608
+ entry["weight_name"],
609
+ revision=entry.get("revision"),
610
+ token=token,
611
+ )
612
+ state_dict = _load_adapter_state_dict(local_path)
613
+
614
+ native_hosts = [
615
+ (host_name, host)
616
+ for host_name, host in _iter_named_adapter_hosts(pipe)
617
+ if hasattr(host, "load_lora_adapter")
618
+ ]
619
+ if native_hosts:
620
+ loaded_hosts = []
621
  try:
622
+ for host_name, host in native_hosts:
623
+ host_state_dict = _state_dict_for_model_host(state_dict, host_name)
624
+ if not _has_lora_tensors(host_state_dict):
625
+ raise ValueError(f"No LoRA tensors matched {host_name or 'pipeline'}.")
626
+ _load_lora_adapter_on_host(host, host_state_dict, entry["adapter_name"])
627
+ loaded_hosts.append(host)
628
  return
 
 
 
 
 
 
 
 
 
629
  except Exception as exc:
630
+ for host in loaded_hosts:
631
+ _delete_peft_adapter_on_host(host, entry["adapter_name"])
632
+ if hasattr(host, "delete_adapters"):
633
+ try:
634
+ host.delete_adapters(entry["adapter_name"])
635
+ except Exception:
636
+ pass
637
  native_error = exc
638
 
639
+ peft_hosts = [
640
+ (host_name, host)
641
+ for host_name, host in _iter_named_adapter_hosts(pipe)
642
+ if host_name is not None and _is_model_adapter_host(host)
643
+ ]
644
+ if not peft_hosts and _is_model_adapter_host(pipe):
645
+ peft_hosts = [(None, pipe)]
646
 
647
+ if peft_hosts:
648
+ loaded_hosts = []
 
 
649
  try:
650
+ for host_name, host in peft_hosts:
651
+ host_state_dict = _state_dict_for_model_host(state_dict, host_name)
652
+ if not _has_lora_tensors(host_state_dict):
653
+ raise ValueError(f"No LoRA tensors matched {host_name or 'pipeline'}.")
654
+ _load_lora_with_peft(host, host_state_dict, entry["adapter_name"])
655
+ loaded_hosts.append(host)
656
+ return
 
 
 
657
  except Exception as exc:
658
+ for host in loaded_hosts:
659
+ _delete_peft_adapter_on_host(host, entry["adapter_name"])
660
+ native_error = exc
 
661
 
662
  if hasattr(pipe, "load_lora_weights"):
663
  try:
 
669
  raise ValueError(f"{native_error}; fallback failed with {exc}") from exc
670
  raise
671
 
672
+ details = _describe_adapter_hosts(pipe)
673
+ if native_error is not None:
674
+ raise ValueError(f"Could not load LoRA adapter with native or PEFT fallback: {native_error}. Hosts: {details}") from native_error
675
+ raise ValueError(f"This pipeline does not expose a usable LoRA loader. Hosts: {details}")
676
 
677
 
678
  def ensure_loras_loaded(pipe, spec_text: str, global_scale: float, active_by_key: dict, token=HF_TOKEN):
tests/test_lora_utils.py CHANGED
@@ -30,6 +30,46 @@ class DummyIdeogramPipeline:
30
  self.unconditional_transformer = DummyAdapterHost()
31
 
32
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  class ParseAdapterSpecsTest(unittest.TestCase):
34
  def test_blank_spec_returns_no_entries(self):
35
  self.assertEqual(parse_adapter_specs("\n \n", 1.0), [])
@@ -61,6 +101,7 @@ class ParseAdapterSpecsTest(unittest.TestCase):
61
 
62
  self.assertEqual(entries[0]["repo_id"], "vladi/loras")
63
  self.assertEqual(entries[0]["weight_name"], "klein9b/klein_snofs_v1_4.safetensors")
 
64
  self.assertEqual(entries[0]["scale"], 0.8)
65
 
66
  def test_rejects_missing_weight_name(self):
@@ -99,6 +140,37 @@ class ParseAdapterSpecsTest(unittest.TestCase):
99
  self.assertEqual(pipe.transformer.active_weights, [0.8])
100
  self.assertEqual(pipe.unconditional_transformer.active_weights, [0.8])
101
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
 
103
  if __name__ == "__main__":
104
  unittest.main()
 
30
  self.unconditional_transformer = DummyAdapterHost()
31
 
32
 
33
+ class DummyPeftLayer:
34
+ def __init__(self):
35
+ self.lora_A = {}
36
+ self.active_names = None
37
+ self.scales = []
38
+ self.enabled = None
39
+ self.deleted = []
40
+
41
+ def set_adapter(self, adapter_names):
42
+ self.active_names = adapter_names
43
+
44
+ def set_scale(self, adapter_name, scale):
45
+ self.scales.append((adapter_name, scale))
46
+
47
+ def enable_adapters(self, enabled):
48
+ self.enabled = enabled
49
+
50
+ def delete_adapter(self, adapter_name):
51
+ self.deleted.append(adapter_name)
52
+ self.lora_A.pop(adapter_name, None)
53
+
54
+
55
+ class DummyPeftHost:
56
+ def __init__(self):
57
+ self.peft_config = {}
58
+ self.layer = DummyPeftLayer()
59
+
60
+ def modules(self):
61
+ return [self, self.layer]
62
+
63
+ def named_modules(self):
64
+ return [("", self), ("layer", self.layer)]
65
+
66
+
67
+ class DummyPeftOnlyIdeogramPipeline:
68
+ def __init__(self):
69
+ self.transformer = DummyPeftHost()
70
+ self.unconditional_transformer = DummyPeftHost()
71
+
72
+
73
  class ParseAdapterSpecsTest(unittest.TestCase):
74
  def test_blank_spec_returns_no_entries(self):
75
  self.assertEqual(parse_adapter_specs("\n \n", 1.0), [])
 
101
 
102
  self.assertEqual(entries[0]["repo_id"], "vladi/loras")
103
  self.assertEqual(entries[0]["weight_name"], "klein9b/klein_snofs_v1_4.safetensors")
104
+ self.assertEqual(entries[0]["revision"], "dev")
105
  self.assertEqual(entries[0]["scale"], 0.8)
106
 
107
  def test_rejects_missing_weight_name(self):
 
140
  self.assertEqual(pipe.transformer.active_weights, [0.8])
141
  self.assertEqual(pipe.unconditional_transformer.active_weights, [0.8])
142
 
143
+ def test_loads_lora_with_peft_fallback_when_transformers_have_no_loader(self):
144
+ pipe = DummyPeftOnlyIdeogramPipeline()
145
+ active_by_key = {}
146
+ state_dict = {"transformer.transformer_blocks.0.attn.to_q.lora_A.weight": object()}
147
+ loaded = []
148
+
149
+ def fake_load_with_peft(host, host_state_dict, adapter_name):
150
+ loaded.append((host, host_state_dict, adapter_name))
151
+ host.peft_config[adapter_name] = object()
152
+ host.layer.lora_A[adapter_name] = object()
153
+
154
+ with mock.patch("lora_utils._download_lora_weight", return_value="/tmp/adapter.safetensors"), mock.patch(
155
+ "lora_utils._load_adapter_state_dict", return_value=state_dict
156
+ ), mock.patch("lora_utils._load_lora_with_peft", side_effect=fake_load_with_peft):
157
+ entries = ensure_loras_loaded(
158
+ pipe,
159
+ "https://huggingface.co/vladi/loras/blob/dev/klein9b/klein_snofs_v1_4.safetensors",
160
+ 0.7,
161
+ active_by_key,
162
+ token="secret",
163
+ )
164
+
165
+ adapter_name = entries[0]["adapter_name"]
166
+ expected_state_dict = {"transformer_blocks.0.attn.to_q.lora_A.weight": state_dict[next(iter(state_dict))]}
167
+ self.assertEqual(loaded[0], (pipe.transformer, expected_state_dict, adapter_name))
168
+ self.assertEqual(loaded[1], (pipe.unconditional_transformer, expected_state_dict, adapter_name))
169
+ self.assertEqual(pipe.transformer.layer.active_names, [adapter_name])
170
+ self.assertEqual(pipe.unconditional_transformer.layer.active_names, [adapter_name])
171
+ self.assertEqual(pipe.transformer.layer.scales, [(adapter_name, 0.7)])
172
+ self.assertEqual(pipe.unconditional_transformer.layer.scales, [(adapter_name, 0.7)])
173
+
174
 
175
  if __name__ == "__main__":
176
  unittest.main()