adaptersentry 2.0.1__tar.gz → 2.0.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/PKG-INFO +2 -2
  2. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/README.md +1 -1
  3. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/adaptersentry.egg-info/PKG-INFO +2 -2
  4. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/pyproject.toml +1 -1
  5. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/src/adaptersentry/detectors/intra_adapter.py +22 -4
  6. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/src/adaptersentry/parsers/names.py +11 -0
  7. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/src/adaptersentry/scanner.py +18 -4
  8. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/src/adaptersentry/scoring/verdict.py +4 -1
  9. adaptersentry-2.0.2/src/adaptersentry/version.py +1 -0
  10. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/tests/test_scanner.py +8 -0
  11. adaptersentry-2.0.1/src/adaptersentry/version.py +0 -1
  12. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/LICENSE +0 -0
  13. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/adaptersentry.egg-info/SOURCES.txt +0 -0
  14. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/adaptersentry.egg-info/dependency_links.txt +0 -0
  15. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/adaptersentry.egg-info/entry_points.txt +0 -0
  16. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/adaptersentry.egg-info/requires.txt +0 -0
  17. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/adaptersentry.egg-info/top_level.txt +0 -0
  18. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/benchmarks/__init__.py +0 -0
  19. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/benchmarks/check_regression.py +0 -0
  20. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/benchmarks/corpus.py +0 -0
  21. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/benchmarks/harness.py +0 -0
  22. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/benchmarks/hub_scanner.py +0 -0
  23. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/benchmarks/report.py +0 -0
  24. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/benchmarks/run_real.py +0 -0
  25. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/setup.cfg +0 -0
  26. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/src/adaptersentry/__init__.py +0 -0
  27. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/src/adaptersentry/__main__.py +0 -0
  28. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/src/adaptersentry/cli/__init__.py +0 -0
  29. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/src/adaptersentry/cli/batch.py +0 -0
  30. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/src/adaptersentry/cli/main.py +0 -0
  31. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/src/adaptersentry/cli/scan.py +0 -0
  32. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/src/adaptersentry/detectors/__init__.py +0 -0
  33. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/src/adaptersentry/detectors/structural.py +0 -0
  34. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/src/adaptersentry/engine/__init__.py +0 -0
  35. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/src/adaptersentry/engine/cache.py +0 -0
  36. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/src/adaptersentry/engine/identity.py +0 -0
  37. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/src/adaptersentry/engine/manifest.py +0 -0
  38. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/src/adaptersentry/engine/orchestrator.py +0 -0
  39. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/src/adaptersentry/engine/orchestrator_ray.py +0 -0
  40. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/src/adaptersentry/engine/result_sink.py +0 -0
  41. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/src/adaptersentry/engine/schemas/__init__.py +0 -0
  42. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/src/adaptersentry/engine/schemas/cache.py +0 -0
  43. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/src/adaptersentry/engine/schemas/combined_report.py +0 -0
  44. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/src/adaptersentry/engine/schemas/identity.py +0 -0
  45. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/src/adaptersentry/engine/schemas/requests.py +0 -0
  46. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/src/adaptersentry/engine/worker.py +0 -0
  47. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/src/adaptersentry/features/__init__.py +0 -0
  48. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/src/adaptersentry/features/lowrank_core.py +0 -0
  49. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/src/adaptersentry/features/spectral.py +0 -0
  50. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/src/adaptersentry/parsers/__init__.py +0 -0
  51. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/src/adaptersentry/parsers/adapter_config.py +0 -0
  52. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/src/adaptersentry/parsers/adapter_file.py +0 -0
  53. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/src/adaptersentry/parsers/safetensors.py +0 -0
  54. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/src/adaptersentry/reporters/__init__.py +0 -0
  55. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/src/adaptersentry/reporters/json.py +0 -0
  56. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/src/adaptersentry/reporters/sarif.py +0 -0
  57. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/src/adaptersentry/reporters/text.py +0 -0
  58. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/src/adaptersentry/reporting/__init__.py +0 -0
  59. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/src/adaptersentry/reporting/sanitize.py +0 -0
  60. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/src/adaptersentry/schemas/__init__.py +0 -0
  61. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/src/adaptersentry/schemas/errors.py +0 -0
  62. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/src/adaptersentry/schemas/result.py +0 -0
  63. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/src/adaptersentry/schemas/severity.py +0 -0
  64. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/src/adaptersentry/scoring/__init__.py +0 -0
  65. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/tests/test_bench.py +0 -0
  66. {adaptersentry-2.0.1 → adaptersentry-2.0.2}/tests/test_harness.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: adaptersentry
3
- Version: 2.0.1
3
+ Version: 2.0.2
4
4
  Summary: Static security scanner for LoRA adapters — detects anomalous weight patterns in .safetensors files
5
5
  License:
6
6
  Apache License
@@ -246,7 +246,7 @@ Dynamic: license-file
246
246
 
247
247
  ![Python](https://img.shields.io/badge/python-%3E%3D3.11-blue)
248
248
  ![License](https://img.shields.io/badge/license-Apache%202.0-blue)
249
- ![Version](https://img.shields.io/badge/version-2.0.1-blue)
249
+ ![Version](https://img.shields.io/badge/version-2.0.2-blue)
250
250
 
251
251
  AdapterSentry is a static security scanner for LoRA adapters distributed as `.safetensors`
252
252
  files. A LoRA adapter is a small file that changes how a large language model behaves; anyone
@@ -2,7 +2,7 @@
2
2
 
3
3
  ![Python](https://img.shields.io/badge/python-%3E%3D3.11-blue)
4
4
  ![License](https://img.shields.io/badge/license-Apache%202.0-blue)
5
- ![Version](https://img.shields.io/badge/version-2.0.1-blue)
5
+ ![Version](https://img.shields.io/badge/version-2.0.2-blue)
6
6
 
7
7
  AdapterSentry is a static security scanner for LoRA adapters distributed as `.safetensors`
8
8
  files. A LoRA adapter is a small file that changes how a large language model behaves; anyone
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: adaptersentry
3
- Version: 2.0.1
3
+ Version: 2.0.2
4
4
  Summary: Static security scanner for LoRA adapters — detects anomalous weight patterns in .safetensors files
5
5
  License:
6
6
  Apache License
@@ -246,7 +246,7 @@ Dynamic: license-file
246
246
 
247
247
  ![Python](https://img.shields.io/badge/python-%3E%3D3.11-blue)
248
248
  ![License](https://img.shields.io/badge/license-Apache%202.0-blue)
249
- ![Version](https://img.shields.io/badge/version-2.0.1-blue)
249
+ ![Version](https://img.shields.io/badge/version-2.0.2-blue)
250
250
 
251
251
  AdapterSentry is a static security scanner for LoRA adapters distributed as `.safetensors`
252
252
  files. A LoRA adapter is a small file that changes how a large language model behaves; anyone
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "adaptersentry"
7
- version = "2.0.1"
7
+ version = "2.0.2"
8
8
  description = "Static security scanner for LoRA adapters — detects anomalous weight patterns in .safetensors files"
9
9
  readme = "README.md"
10
10
  license = {file = "LICENSE"}
@@ -3,7 +3,7 @@
3
3
  No reference profile is needed. A 70B adapter has ~80 modules of each kind
4
4
  (q_proj, down_proj, …); each module is compared with the rest of its family.
5
5
 
6
- Per family (same module name) and per feature:
6
+ Per family (same module name in the same layer stack) and per feature:
7
7
  1. if layer indices are known, remove the depth trend with a Theil–Sen line
8
8
  (median of pairwise slopes; insensitive to the outliers we look for);
9
9
  2. robust z of the residuals: (x − median) / (1.4826 · MAD), with the
@@ -50,6 +50,11 @@ from adaptersentry.schemas.result import (
50
50
  OUTLIER_Z = 3.5
51
51
  CONCENTRATION_REVIEW_Z = 14.0
52
52
  CONCENTRATION_FEATURES: tuple[str, ...] = ("kurt_dw", "row_top8", "row_hoyer")
53
+ # On Hub adapters 28 of 32 concentration alerts sat in the first 3 or last 2 layers of
54
+ # the model, which work differently from the middle. Such alerts keep the verdict but
55
+ # say so; a per-family threshold for edge layers needs reference profiles.
56
+ EDGE_FIRST_LAYERS = 3
57
+ EDGE_LAST_LAYERS = 2
53
58
  # Robust z from small families is unreliable: MAD from a handful of points is
54
59
  # noisy, and detrending consumes degrees of freedom. On synthetic clean adapters
55
60
  # the maximum |z| reached 12-70 with 6-12 modules per family, but stayed ≤ 6.6
@@ -85,6 +90,7 @@ class ModuleEntry:
85
90
  expert: int | None
86
91
  features: ModuleFeatures
87
92
  energy: float # ‖ΔW‖²_F, for concentration across modules
93
+ stack: str = "" # layer stack (language model, vision tower, …); part of the family
88
94
 
89
95
 
90
96
  @dataclass
@@ -121,6 +127,13 @@ def detrend(layers: np.ndarray, x: np.ndarray) -> np.ndarray:
121
127
  return x - (intercept + slope * layers)
122
128
 
123
129
 
130
+ def is_edge_layer(layer: int | None, n_layers: int | None) -> bool:
131
+ """True for the first EDGE_FIRST_LAYERS and last EDGE_LAST_LAYERS layers of a stack."""
132
+ if layer is None or n_layers is None:
133
+ return False
134
+ return layer < EDGE_FIRST_LAYERS or layer >= n_layers - EDGE_LAST_LAYERS
135
+
136
+
124
137
  def concentration_z(zs: dict[str, float]) -> float | None:
125
138
  """Joint concentration z of one module: the smallest of the signed z-values of
126
139
  CONCENTRATION_FEATURES, so it is high only when all three rise together.
@@ -154,9 +167,14 @@ def compare_within_adapter(entries: list[ModuleEntry], *, top_k: int = 10) -> In
154
167
  Returns:
155
168
  IntraResult, or None if no family has at least MIN_FAMILY modules.
156
169
  """
170
+ # A family is one module name within one layer stack: a vision tower's q_proj is
171
+ # not compared with the language model's q_proj. Names are qualified with the
172
+ # stack only when the adapter covers more than one.
173
+ qualify = len({e.stack for e in entries}) > 1
174
+ family_of = {e.index: (f"{e.stack}/{e.module}" if qualify and e.stack else e.module) for e in entries}
157
175
  families: dict[str, list[ModuleEntry]] = defaultdict(list)
158
176
  for e in entries:
159
- families[e.module].append(e)
177
+ families[family_of[e.index]].append(e)
160
178
 
161
179
  z_by_module: dict[int, dict[str, float]] = defaultdict(dict)
162
180
  contributions: list[tuple[float, Contributor]] = []
@@ -203,7 +221,7 @@ def compare_within_adapter(entries: list[ModuleEntry], *, top_k: int = 10) -> In
203
221
  }
204
222
  by_index = {e.index: e for e in entries}
205
223
  conc_spikes = [
206
- DepthSpike(module_type=by_index[idx].module, layer=by_index[idx].layer, z=v)
224
+ DepthSpike(module_type=family_of[idx], layer=by_index[idx].layer, z=v)
207
225
  for idx, v in sorted(concentration.items(), key=lambda kv: -kv[1])
208
226
  if v >= CONCENTRATION_REVIEW_Z and by_index[idx].layer is not None
209
227
  ][:top_k]
@@ -212,7 +230,7 @@ def compare_within_adapter(entries: list[ModuleEntry], *, top_k: int = 10) -> In
212
230
  e = by_index[idx]
213
231
  if v > OUTLIER_Z and e.layer is not None and len(spikes) < top_k:
214
232
  signed = max(z_by_module[idx].values(), key=abs)
215
- spikes.append(DepthSpike(module_type=e.module, layer=e.layer, z=float(signed)))
233
+ spikes.append(DepthSpike(module_type=family_of[idx], layer=e.layer, z=float(signed)))
216
234
 
217
235
  energies = np.array([e.energy for e in entries if e.energy > 0.0], dtype=np.float64)
218
236
  attn = [e.features.log_rms for e in entries if e.kind == "attention" and e.features.log_rms is not None]
@@ -20,6 +20,7 @@ _EMB_B = re.compile(r"^(?P<base>.+)\.lora_embedding_B(?:\.[A-Za-z0-9_\-]+)?$")
20
20
  _DORA = re.compile(r"^(?P<base>.+)\.lora_magnitude_vector(?:\.[A-Za-z0-9_\-]+)?(?:\.weight)?$")
21
21
  _TRAINABLE_TOKENS = re.compile(r"^(?P<base>.+)\.trainable_tokens_(?:delta|original)(?:\.[A-Za-z0-9_\-]+)?$")
22
22
 
23
+ _PEFT_PREFIX = "base_model.model."
23
24
  _LAYER = re.compile(r"(?:^|\.)(?:layers|h|blocks|layer)\.(\d+)(?:\.|$)")
24
25
  _EXPERT = re.compile(r"(?:^|\.)experts\.(\d+)(?:\.|$)")
25
26
 
@@ -57,6 +58,10 @@ class ModuleLocation:
57
58
  expert: int | None
58
59
  module: str
59
60
  kind: ModuleKind
61
+ # Path of the layer stack the module belongs to, e.g. "model.language_model" or
62
+ # "model.vision_tower.vision_model.encoder"; "" when the path has no layer index.
63
+ # Multimodal models reuse module names (q_proj, up_proj) across stacks.
64
+ stack: str = ""
60
65
 
61
66
 
62
67
  def parse_key(key: str) -> ParsedKey:
@@ -98,9 +103,15 @@ def locate(base: str) -> ModuleLocation:
98
103
  kind = "router"
99
104
  else:
100
105
  kind = "other"
106
+ stack = ""
107
+ if layer_m:
108
+ stack = base[:layer_m.start()]
109
+ if stack.startswith(_PEFT_PREFIX):
110
+ stack = stack[len(_PEFT_PREFIX):]
101
111
  return ModuleLocation(
102
112
  layer=int(layer_m.group(1)) if layer_m else None,
103
113
  expert=int(expert_m.group(1)) if expert_m else None,
104
114
  module=name,
105
115
  kind=kind,
116
+ stack=stack,
106
117
  )
@@ -42,6 +42,7 @@ from adaptersentry.detectors.intra_adapter import (
42
42
  ModuleEntry,
43
43
  compare_within_adapter,
44
44
  concentration_z,
45
+ is_edge_layer,
45
46
  )
46
47
  from adaptersentry.detectors.structural import find_siblings, structural_findings
47
48
  from adaptersentry.features.spectral import head_features, module_features
@@ -322,7 +323,8 @@ def _scan_inventory(path, inv: AdapterInventory, cfg_read, siblings, artifact, s
322
323
  idx = len(records)
323
324
  records.append(ModuleRecord(**base_record, scale=res.scale, status="ok", features=feats, heads=heads))
324
325
  entries.append(ModuleEntry(index=idx, module=loc.module, kind=loc.kind, layer=loc.layer,
325
- expert=loc.expert, features=feats, energy=core.fro ** 2))
326
+ expert=loc.expert, features=feats, energy=core.fro ** 2,
327
+ stack=loc.stack))
326
328
  del pd
327
329
 
328
330
  intra = compare_within_adapter(entries)
@@ -335,19 +337,30 @@ def _scan_inventory(path, inv: AdapterInventory, cfg_read, siblings, artifact, s
335
337
  struct = structural_findings(inv, config, cfg_read.problems, siblings,
336
338
  unresolved_scale_pattern=unresolved_pattern)
337
339
  findings = list(struct.findings)
340
+ top_spike_edge = False
338
341
  if intra is not None:
339
342
  pair_by_index = {i: inv.pairs[i] for i in range(len(inv.pairs))}
343
+ stack_depth: dict[str, int] = {}
344
+ for p in inv.pairs:
345
+ if p.location.layer is not None:
346
+ s = p.location.stack
347
+ stack_depth[s] = max(stack_depth.get(s, 0), p.location.layer + 1)
340
348
  conc = {idx: c for idx, zs in intra.z_by_module.items()
341
349
  if (c := concentration_z(zs)) is not None and c >= CONCENTRATION_REVIEW_Z}
342
- for idx, c in sorted(conc.items(), key=lambda kv: -kv[1])[:10]:
350
+ for rank, (idx, c) in enumerate(sorted(conc.items(), key=lambda kv: -kv[1])[:10]):
343
351
  rec, pair = records[idx], pair_by_index[idx]
344
352
  zs = intra.z_by_module[idx]
353
+ edge = is_edge_layer(rec.layer, stack_depth.get(pair.location.stack))
354
+ if rank == 0:
355
+ top_spike_edge = edge
356
+ note = ("; first/last layers often differ like this in benign adapters" if edge else "")
345
357
  findings.append(Finding(
346
358
  rule_id="INTRA_CONCENTRATION_SPIKE", severity=Severity.MEDIUM,
347
359
  title="Update concentrated on a few outputs, unlike its family",
348
360
  message=(f"{rec.module} in layer {rec.layer}: kurtosis, top-8 row share and row Hoyer "
349
- f"all rise against the family (concentration z = {c:.1f} ≥ {CONCENTRATION_REVIEW_Z})"),
350
- evidence={"concentration_z": round(c, 3),
361
+ f"all rise against the family (concentration z = {c:.1f} ≥ {CONCENTRATION_REVIEW_Z})"
362
+ f"{note}"),
363
+ evidence={"concentration_z": round(c, 3), "edge_layer": edge,
351
364
  **{f"z_{f}": round(zs[f], 3) for f in CONCENTRATION_FEATURES}},
352
365
  locations=[Location(layer=rec.layer, module=rec.module, expert=rec.expert,
353
366
  tensor_key=pair.b.key)],
@@ -453,6 +466,7 @@ def _scan_inventory(path, inv: AdapterInventory, cfg_read, siblings, artifact, s
453
466
  status=status, policy=policy, intra=intra.summary if intra else None, reference=None,
454
467
  red_flags=struct.red_flags, metadata_present=bool(meta) or config is not None,
455
468
  confidence_limits=struct.confidence_limits, n_not_analyzed=n_not,
469
+ top_spike_edge=top_spike_edge,
456
470
  )
457
471
  anomaly = Anomaly(
458
472
  intra=intra.summary if intra else None,
@@ -40,6 +40,7 @@ def derive_verdict(
40
40
  metadata_present: bool,
41
41
  confidence_limits: list[str],
42
42
  n_not_analyzed: int,
43
+ top_spike_edge: bool = False,
43
44
  ) -> Verdict:
44
45
  """Derive action, level, reasons, behavioural recommendation and confidence."""
45
46
  action = "allow"
@@ -85,7 +86,9 @@ def derive_verdict(
85
86
  code="INTRA_OUTLIER", severity=Severity.MEDIUM,
86
87
  message=f"Update concentrated on a few outputs, unlike the rest of its family{where}: "
87
88
  f"concentration z = {intra.max_concentration_z:.1f} ≥ {CONCENTRATION_REVIEW_Z} "
88
- f"(threshold set at 5 % alerts on popular Hub adapters)"))
89
+ f"(threshold set at 5 % alerts on popular Hub adapters)"
90
+ + ("; it is a first/last layer, where such differences are common in benign adapters"
91
+ if top_spike_edge else "")))
89
92
  raise_to("review")
90
93
 
91
94
  for flag in red_flags:
@@ -0,0 +1 @@
1
+ __version__ = "2.0.2"
@@ -62,6 +62,14 @@ class TestDetection:
62
62
  spike = next(f for f in result.findings if f.rule_id == "INTRA_CONCENTRATION_SPIKE")
63
63
  assert spike.locations[0].layer == 7 and spike.locations[0].module == "down_proj"
64
64
  assert spike.evidence["concentration_z"] >= 14.0
65
+ assert spike.evidence["edge_layer"] is False
66
+
67
+ def test_edge_layer_alert_is_marked(self, tmp_path: Path) -> None:
68
+ result = scan(_write_adapter(tmp_path / "edge", inject_layer=15)) # last of 16 layers
69
+ spike = next(f for f in result.findings if f.rule_id == "INTRA_CONCENTRATION_SPIKE")
70
+ assert result.verdict.action == "review"
71
+ assert spike.locations[0].layer == 15 and spike.evidence["edge_layer"] is True
72
+ assert "benign adapters" in spike.message
65
73
 
66
74
  def test_rescale_attack_does_not_hide_the_update(self, tmp_path: Path) -> None:
67
75
  # B shrunk by 1e4 and lora_alpha raised by 1e4: effective ΔW is unchanged.
@@ -1 +0,0 @@
1
- __version__ = "2.0.1"
File without changes
File without changes