No constructor-based way to set query_length/document_length on `MultiVectorEncoder`
No constructor-based way to set query_length/document_length on MultiVectorEncoder
Description
Transformer.query_length / Transformer.document_length control the per-task truncation
length used by MultiVectorEncoder.encode_query / encode_document. There is no way to set
these at MultiVectorEncoder(...) construction time when loading a pretrained checkpoint —
every kwarg bucket exposed by __init__ either does nothing or raises. The only thing that
actually works is loading the model and then mutating the submodule attribute directly
(model[0].query_length = ...), which requires knowing the internal module index/layout of
the pipeline and can't be done in one call.
Repro script
from sentence_transformers import MultiVectorEncoder
MODEL = "lightonai/LateOn"
QUERY = "What is the capital city of the country of France in Europe today"
DOC = (
"The city of Paris has served as the capital of France for many centuries, "
"and it is well known for the Eiffel Tower, the Louvre museum, and the river Seine "
"which flows through the heart of the city."
)
Q, D = 5, 9 # strange, obviously-non-default values
def show(label, model):
t = model[0]
q = model.encode_query([QUERY])[0]
d = model.encode_document([DOC])[0]
print(f"{label}:")
print(f" module.query_length={t.query_length} module.document_length={t.document_length}")
print(f" encode_query shape={tuple(q.shape)} encode_document shape={tuple(d.shape)}")
print("=== Baseline (no override) ===")
model = MultiVectorEncoder(MODEL)
show("baseline", model)
print()
print('=== Attempt: top-level kwargs MultiVectorEncoder(MODEL, query_length=5, document_length=9) ===')
try:
MultiVectorEncoder(MODEL, query_length=Q, document_length=D)
print(" constructed (unexpected)")
except TypeError as e:
print(f" TypeError: {e}")
print()
print(f'=== Attempt: processor_kwargs={{"query_length": {Q}, "document_length": {D}}} ===')
m = MultiVectorEncoder(MODEL, processor_kwargs={"query_length": Q, "document_length": D})
show("processor_kwargs", m)
print()
print(f'=== Attempt: model_kwargs={{"query_length": {Q}, "document_length": {D}}} ===')
try:
m = MultiVectorEncoder(MODEL, model_kwargs={"query_length": Q, "document_length": D})
show("model_kwargs", m)
except Exception as e:
print(f" {type(e).__name__}: {e}")
print()
print(f'=== Attempt: config_kwargs={{"query_length": {Q}, "document_length": {D}}} ===')
try:
m = MultiVectorEncoder(MODEL, config_kwargs={"query_length": Q, "document_length": D})
show("config_kwargs", m)
except Exception as e:
print(f" {type(e).__name__}: {e}")
print()
print("=== Only thing that works: model[0].query_length = 5; model[0].document_length = 9 (post-construction) ===")
model[0].query_length = Q
model[0].document_length = D
show("attribute mutation after load", model)Actual output
=== Baseline (no override) ===
baseline:
module.query_length=32 module.document_length=300
encode_query shape=(16, 128) encode_document shape=(45, 128)
=== Attempt: top-level kwargs MultiVectorEncoder(MODEL, query_length=5, document_length=9) ===
TypeError: MultiVectorEncoder.__init__() got an unexpected keyword argument 'query_length'
=== Attempt: processor_kwargs={"query_length": 5, "document_length": 9} ===
processor_kwargs:
module.query_length=32 module.document_length=300
encode_query shape=(16, 128) encode_document shape=(45, 128)
=== Attempt: model_kwargs={"query_length": 5, "document_length": 9} ===
TypeError: ModernBertModel.__init__() got an unexpected keyword argument 'query_length'
=== Attempt: config_kwargs={"query_length": 5, "document_length": 9} ===
config_kwargs:
module.query_length=32 module.document_length=300
encode_query shape=(16, 128) encode_document shape=(45, 128)
=== Only thing that works: model[0].query_length = 5; model[0].document_length = 9 (post-construction) ===
attribute mutation after load:
module.query_length=5 module.document_length=9
encode_query shape=(5, 128) encode_document shape=(9, 128)None of the four kwargs exposed by MultiVectorEncoder.__init__ provide a working path:
| Attempt | Result |
|---|---|
query_length=/document_length= (direct kwargs) |
TypeError — not a recognized constructor param |
processor_kwargs={"query_length": ..., "document_length": ...} |
Silently ignored — no error, no effect |
model_kwargs={"query_length": ..., "document_length": ...} |
TypeError from the underlying HF model (ModernBertModel.__init__() got an unexpected keyword argument) |
config_kwargs={"query_length": ..., "document_length": ...} |
Silently ignored — no error, no effect |
model[0].query_length = ... / model[0].document_length = ... (post-load mutation) |
Only thing that works |
Expected behavior
Some constructor-time, top-level way to set query_length/document_length when loading a
MultiVectorEncoder checkpoint — e.g. MultiVectorEncoder(model_name_or_path, query_length=5, document_length=9) — that overrides the checkpoint's saved lengths without requiring the
caller to know the pipeline's internal module layout (model[0]).
Root cause
query_length/document_length are declared Transformer.__init__ params and are listed in
Transformer.config_keys (base/modules/transformer.py), so they're persisted in — and
loaded straight back from — the Transformer submodule's own saved config.json. They aren't
reachable through any of model_kwargs / processor_kwargs / config_kwargs:
model_kwargsgoes toAutoModel.from_pretrained(**model_kwargs), whose target class (e.g.ModernBertModel) has no such param, hence theTypeError.config_kwargsgoes toAutoConfig.from_pretrained(**config_kwargs), whosePretrainedConfigsilently accepts and stores unrecognized kwargs as arbitrary attributes rather than raising.processor_kwargsgoes toAutoProcessor.from_pretrained(**processor_kwargs), whosePreTrainedTokenizerBase.from_pretrainedlikewise accepts and silently discards unrecognized kwargs.
Tracing the actual load path for MultiVectorEncoder(model_name_or_path):
MultiVectorEncoder.__init__→BaseModel.__init__→BaseModel._load_modules(base/model.py), which readsmodules.jsonand, for the Transformer submodule, callsTransformer.load(model_name_or_path, ..., model_kwargs=..., processor_kwargs=..., config_kwargs=..., init_defaults=...).Transformer.load(base/modules/transformer.py) callsTransformer._load_init_kwargs, which loads the submodule's savedconfig.json(this is where the checkpoint'squery_length: 32, document_length: 300live) and merges inmodel_kwargs/processor_kwargs/config_kwargs— but only inside those three named buckets, never into a top-levelquery_length/document_lengthkey.Transformer.loadthen appliesinit_defaultswithsetdefaultpriority (saved config always wins) and callscls(model_name_or_path=..., **init_kwargs)— this is whereTransformer.__init__actually runs, settingself.query_length/self.document_lengthfrom whatever came out of the saved config file in step 2.
There is currently no caller-override channel for these two scalars anywhere in that chain —
only the three dict buckets (none of which reach them) and init_defaults/
_get_module_init_defaults (which is designed for defaults, i.e. it loses to the saved
config, not the other way around — it exists for legacy-checkpoint fallback values, e.g. in
MultiVectorEncoder._get_module_init_defaults).
Suggested minimal fix
Add a second, symmetric hook alongside the existing init_defaults one, but with override
(not default) priority, and expose query_length/document_length as real top-level
MultiVectorEncoder.__init__ kwargs that feed it:
base/modules/module.py,Module.load()— acceptinit_overrides: dict | None = Noneand apply it after the existinginit_defaultsloop, with plain assignment instead ofsetdefault:for key, value in (init_defaults or {}).items(): config.setdefault(key, value) for key, value in (init_overrides or {}).items(): config[key] = value # NEW: caller wins over saved config return cls(**config)base/modules/transformer.py,Transformer.load()— same two-line addition (it has its ownload()override, doesn't call the base one).base/model.py,BaseModel._load_modules()— mirror the existing_get_module_init_defaultswiring with a new_get_module_init_overrideshook (default no-op returning{}onBaseModel), and pass its result through asinit_overrides=...alongside the existinginit_defaults=....multi_vector_encoder/model.py,MultiVectorEncoder.__init__— addquery_length: int | None = None, document_length: int | None = Noneparams, and implement_get_module_init_overridesto return{"query_length": query_length, "document_length": document_length}(Nonevalues filtered out) whenclass_refresolves to aTransformersubclass — mirroring the existing_get_module_init_defaultsoverride at the same class, just with override instead of default semantics.
This makes MultiVectorEncoder("lightonai/LateOn", query_length=5, document_length=9) work as
one would expect, reuses the existing defaults-hook pattern rather than inventing a new
mechanism, and leaves model_kwargs/processor_kwargs/config_kwargs semantics completely
unchanged (no regression risk there). Happy to put up a PR along these lines if that shape
looks reasonable to the maintainers.
Environment
sentence-transformers== 6.0.1transformers== 5.3.0torch== 2.11.0+cu130- Python 3.13.12
- Model:
lightonai/LateOn
Source: huggingface/sentence-transformers