Backend ignores root level ONNX files, and cannot detect full model exports

Author: tomaarsenCreated Aug 31, 2026Updated Sep 15, 2026

Hello!

This issue has been generated by my agent based on #3915 (cc @LK-maker-007 ). Ideally, I want to extend or improve ONNX/OpenVINO exporting behaviour in Sentence Transformers so that it doesn't just export the transformers model, but the full ST model. Then, third parties (or a sentence-transformers lite?) could load this checkpoint without needing torch and run it efficiently. However, there's a bit of a problem between the two types of ONNX exporting: transformers only and full-ST. For example, how are you sure that the model checkpoint uses one or the other? And furthermore, the backend parameter is currently passed to the modules, including Transformer, which uses it to load using optimum. If we have a "full ST" model ONNX, then we have to instead load on the BaseModel level perhaps? Maybe it just overrides the modules from modules.json with one single "ONNXModule" (idem. OpenVINO)?

Either way, once I work on that, we have to consider what my agent wrote here:

1. Root level files are invisible on the Hub

backend_should_export builds one glob and uses it two ways. A local directory is matched with pathlib, where **/ means zero or more directories, and a Hub repository is matched with fnmatch, which has no ** and whose * already crosses directories:

python
>>> from fnmatch import fnmatch, translate
>>> translate("**/*.onnx")
'(?s:(?>.*?/).*\.onnx)\Z'
>>> fnmatch("model.onnx", "**/*.onnx")
False

The atomic (?>.*?/) demands a directory, so a file in the root of a repository never matches and export comes back True, even though the docstring says <file_name> in the root should set it to False. A local directory with the same layout decides the opposite.

The worst case has no workaround. Qdrant/all-MiniLM-L6-v2-onnx ships model.onnx and no torch weights, so the export it falls back to cannot run:

python
SentenceTransformer("Qdrant/all-MiniLM-L6-v2-onnx", backend="onnx")
# OSError: ... does not appear to have a file named pytorch_model.bin, model.safetensors, ...

2. Widening the match alone makes things worse

We use the backend for module 0 only. The exported graph replaces Transformer, and Pooling, Dense and Normalize keep running in PyTorch, so a file is only a valid target if it returns last_hidden_state shaped [batch, seq, hidden_size].

Files under onnx/ satisfy that because that is where our exporter writes them. The repo root is not a location we write to, and in practice it often holds a full model export with the pooling and projection already baked in:

repo hidden_size root model.onnx output
lightonai/GTE-ModernColBERT-v1 768 output, (1, 4, 128), L2 normalized
lightonai/LateOn 768 output, (1, 4, 128), L2 normalized
mixedbread-ai/mxbai-edge-colbert-v0-32m 384 output, (1, 4, 64), L2 normalized
answerdotai/answerai-colbert-small-v1 384 output, (1, 4, 96), L2 normalized
colbert-ir/colbertv2.0 768 contextual, (1, 4, 128), L2 normalized
ibm-granite/granite-embedding-30m-english 384 logits plus a pooled tensor
intfloat/e5-small-v2 384 last_hidden_state, transformer only

Making the root visible without a compatibility check therefore regresses more than it fixes. Applying #3915's matching on top of main and A/B'ing real loads against the PyTorch reference:

repo main today with a naive widening
intfloat/e5-small-v2 ok, exports, 1.6e-07 ok, uses the root file, 1.9e-07
Qdrant/all-MiniLM-L6-v2-onnx fails, unloadable ok
ibm-granite/granite-embedding-30m-english ok, exports KeyError: 'last_hidden_state'
lightonai/GTE-ModernColBERT-v1 ok, exports, 2.4e-07 KeyError
lightonai/LateOn ok, exports, 1.6e-07 KeyError
mixedbread-ai/mxbai-edge-colbert-v0-32m ok, exports, 1.8e-07 KeyError
answerdotai/answerai-colbert-small-v1 ok, loads onnx/model.onnx, 1.8e-07 KeyError

Two gain, five regress. Two rows deserve attention:

  • mxbai-edge-colbert-v0-32m is the model in our own documented example in docs/multi_vector_encoder/usage/efficiency.rst.
  • answerai-colbert-small-v1 ships both a full model root model.onnx and a good onnx/model.onnx. We load the latter today. Widening makes the root file win, because backend_should_export checks the root path before the backend subfolder, so a working repository is downgraded.

It does fail loudly rather than silently, since the pooled or projected output is named output or contextual and Optimum demands last_hidden_state.

Related symptom reports: #3437, #3666.

  • Tom Aarsen

Source: huggingface/sentence-transformers