Skip to content

Commit 0fd7c9a

Browse files
authored
feat(adapters): extend explicit runtime LoRA to XLM-R rerankers (#787)
Admit exact standard unquantized XLM-R sequence classifiers through the explicit runtime adapter while preserving ordinary CrossEncoder routing and scoring. Verified with 331 CPU cases, 14 independent controls, pinned BGE source correspondence, exact-head CodeRabbit approval and all checks passing. No GPU performance or trained-adapter quality claim. Closes #778.
1 parent dca37b7 commit 0fd7c9a

4 files changed

Lines changed: 309 additions & 59 deletions

File tree

‎packages/sie_server/src/sie_server/adapters/cross_encoder/_lora.py‎

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Opt-in LoRA lifecycle for the ordinary sentence-transformers CrossEncoder.
22
33
The outer wrapper, tokenizer, classifier and activation are never replaced.
4-
Transformers injects ordinary LoRA layers into declared BERT encoder Linear
4+
Transformers injects ordinary LoRA layers into declared BERT/XLM-R encoder Linear
55
modules. The worker owns selection/mutation exclusion; only background source
66
preparation needs a separate lock here. Resident layers stay outside the model's
77
module tree while base is selected. No scoring code calls into this module.
@@ -25,12 +25,16 @@
2525
from peft.tuners.lora.layer import Linear as LoraLinear
2626
from safetensors.torch import load_file
2727
from sentence_transformers import CrossEncoder
28-
from transformers import BertForSequenceClassification
28+
from transformers import BertForSequenceClassification, XLMRobertaForSequenceClassification
2929

3030
from sie_server.adapters.lora import LoraRuntimeConfig, LoraStateUncertainError, LoraUnavailableError
3131
from sie_server.adapters.peft_lora_mixin import PEFTLoRAMixin
3232

3333
_FILES = ("adapter_config.json", "adapter_model.safetensors")
34+
_ENCODER_PREFIXES: dict[type[torch.nn.Module], str] = {
35+
BertForSequenceClassification: "bert.encoder.",
36+
XLMRobertaForSequenceClassification: "roberta.encoder.",
37+
}
3438
_UNSUPPORTED = (
3539
"use_dora",
3640
"use_rslora",
@@ -93,6 +97,7 @@ def __init__(
9397
self._active: str | None = None
9498
self._outer: CrossEncoder | None = None
9599
self._model: Any = None
100+
self._encoder_prefix: str | None = None
96101
self._originals: dict[str, torch.nn.Linear] = {}
97102
# An ordinary dict deliberately keeps resident adapters outside nn.Module's
98103
# traversal while base is selected. CrossEncoder.predict calls to()/eval()
@@ -108,13 +113,16 @@ def runtime_config(self) -> LoraRuntimeConfig:
108113

109114
def bind(self, outer: CrossEncoder) -> None:
110115
model = outer.model
111-
if type(model) is not BertForSequenceClassification or getattr(model, "is_quantized", False):
116+
encoder_prefix = _ENCODER_PREFIXES.get(type(model))
117+
if encoder_prefix is None or getattr(model, "is_quantized", False):
112118
raise ValueError(
113-
"CrossEncoder LoRA currently requires a standard, unquantized BertForSequenceClassification"
119+
"CrossEncoder LoRA requires a standard, unquantized BertForSequenceClassification "
120+
"or XLMRobertaForSequenceClassification"
114121
)
115122
if getattr(model, "peft_config", None) or getattr(model, "_hf_peft_config_loaded", False):
116123
raise ValueError("The cross-encoder must start from a base checkpoint without attached adapters")
117124
self._outer, self._model = outer, model
125+
self._encoder_prefix = encoder_prefix
118126
self._originals = {name: module for name, module in model.named_modules() if type(module) is torch.nn.Linear}
119127
self._grad_flags = [(parameter, parameter.requires_grad) for parameter in model.parameters()]
120128
self._active = None
@@ -160,8 +168,14 @@ def _validate_weights(self, config: LoraConfig, weights: dict[str, torch.Tensor]
160168
expected: dict[str, tuple[int, int]] = {}
161169
for target in config.target_modules or []:
162170
matches = [name for name in self._originals if name == target or name.endswith(f".{target}")]
163-
if not matches or any(not name.startswith("bert.encoder.") for name in matches):
164-
raise ValueError(f"LoRA target {target!r} must resolve only to BERT encoder Linear modules")
171+
if (
172+
self._encoder_prefix is None
173+
or not matches
174+
or any(not name.startswith(self._encoder_prefix) for name in matches)
175+
):
176+
raise ValueError(
177+
f"LoRA target {target!r} must resolve only to the supported model encoder Linear modules"
178+
)
165179
for name in matches:
166180
module = self._originals[name]
167181
expected[f"{name}.lora_A.weight"] = (config.r, module.in_features)
@@ -430,6 +444,7 @@ def close(self) -> None:
430444
self._originals.clear()
431445
self._grad_flags.clear()
432446
self._outer = self._model = None
447+
self._encoder_prefix = None
433448
self._uncertain = False
434449
if errors:
435450
raise ExceptionGroup("Failed to remove owned LoRA source snapshots", errors)

‎packages/sie_server/src/sie_server/adapters/cross_encoder/runtime_lora.py‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,16 @@
1010

1111

1212
class CrossEncoderLoraAdapter(CrossEncoderAdapter):
13-
"""Opt-in runtime LoRA for a standard, unquantized BERT cross-encoder.
13+
"""Opt-in runtime LoRA for a standard, unquantized BERT or XLM-R cross-encoder.
1414
1515
Select ``sie_server.adapters.cross_encoder.runtime_lora:CrossEncoderLoraAdapter``
1616
explicitly and declare ``lora_paths={served_name: source}``. Only ordinary
17-
A/B matrices targeting BERT encoder Linear modules are accepted. The outer
18-
CrossEncoder, classifier, activation and scoring methods are inherited;
17+
A/B matrices targeting encoder Linear modules of the exact
18+
``BertForSequenceClassification`` or ``XLMRobertaForSequenceClassification``
19+
class are accepted. The outer CrossEncoder, classifier, activation and scoring methods are inherited;
1920
trained replacement heads and custom/causal reranker classes are rejected.
21+
Native BertFlash/default catalog profiles are separate implementations and
22+
do not gain LoRA support from this adapter.
2023
2124
``options.lora=None`` restores the original base modules while adapters stay
2225
resident off-tree. Loading, selection and eviction use the worker's existing

0 commit comments

Comments
 (0)