From d03d496c5461d89a299343b1a08123081e42e03b Mon Sep 17 00:00:00 2001 From: S0rryHorizon <151612757+S0rryHorizon@users.noreply.github.com> Date: Mon, 21 Sep 2026 04:42:57 +0800 Subject: [PATCH] fix: preserve custom text models in parallel workers --- fastembed/text/custom_text_embedding.py | 44 +++++++ tests/misc/generate_token_lookup.py | 19 +++ tests/misc/token_lookup.md | 36 ++++++ tests/misc/token_lookup.onnx | Bin 0 -> 234 bytes tests/test_custom_model_parallel.py | 154 ++++++++++++++++++++++++ 5 files changed, 253 insertions(+) create mode 100644 tests/misc/generate_token_lookup.py create mode 100644 tests/misc/token_lookup.md create mode 100644 tests/misc/token_lookup.onnx create mode 100644 tests/test_custom_model_parallel.py diff --git a/fastembed/text/custom_text_embedding.py b/fastembed/text/custom_text_embedding.py index 512b914bd..a4c49fee0 100644 --- a/fastembed/text/custom_text_embedding.py +++ b/fastembed/text/custom_text_embedding.py @@ -13,6 +13,7 @@ from fastembed.common.types import NumpyArray, Device from fastembed.common.utils import normalize, mean_pooling, last_token_pooling from fastembed.text.onnx_embedding import OnnxTextEmbedding +from fastembed.text.onnx_text_model import TextEmbeddingWorker @dataclass(frozen=True) @@ -58,6 +59,27 @@ def __init__( def _list_supported_models(cls) -> list[DenseModelDescription]: return cls.SUPPORTED_MODELS + def embed( + self, + documents: str | Iterable[str], + batch_size: int = 256, + parallel: int | None = None, + **kwargs: Any, + ) -> Iterable[NumpyArray]: + # Spawned workers cannot see models registered in the parent process. + yield from super().embed( + documents, + batch_size, + parallel, + _custom_model_description=self.model_description, + _custom_postprocessing_config=PostprocessingConfig(self._pooling, self._normalization), + **kwargs, + ) + + @classmethod + def _get_worker_class(cls) -> type[TextEmbeddingWorker[NumpyArray]]: + return CustomTextEmbeddingWorker + def _post_process_onnx_output( self, output: OnnxOutputContext, **kwargs: Any ) -> Iterable[NumpyArray]: @@ -102,3 +124,25 @@ def add_model( cls.POSTPROCESSING_MAPPING[model_description.model] = PostprocessingConfig( pooling=pooling, normalization=normalization ) + + +class CustomTextEmbeddingWorker(TextEmbeddingWorker[NumpyArray]): + def init_embedding( + self, + model_name: str, + cache_dir: str, + **kwargs: Any, + ) -> CustomTextEmbedding: + model_description: DenseModelDescription = kwargs.pop("_custom_model_description") + postprocessing_config: PostprocessingConfig = kwargs.pop("_custom_postprocessing_config") + CustomTextEmbedding.add_model( + model_description, + pooling=postprocessing_config.pooling, + normalization=postprocessing_config.normalization, + ) + return CustomTextEmbedding( + model_name=model_name, + cache_dir=cache_dir, + threads=1, + **kwargs, + ) diff --git a/tests/misc/generate_token_lookup.py b/tests/misc/generate_token_lookup.py new file mode 100644 index 000000000..22b9244fd --- /dev/null +++ b/tests/misc/generate_token_lookup.py @@ -0,0 +1,19 @@ +"""Regenerate token_lookup.onnx; requires the development-only onnx package.""" + +from pathlib import Path + +import onnx +from onnx import TensorProto, helper + +table = helper.make_tensor("table", TensorProto.FLOAT, [5, 2], [0, 0, 0, 0, 2, 4, 3, 9, 4, 16]) +graph = helper.make_graph( + [helper.make_node("Gather", ["table", "input_ids"], ["embeddings"], axis=0)], + "synthetic-token-lookup", + [helper.make_tensor_value_info("input_ids", TensorProto.INT64, ["batch", "sequence"])], + [helper.make_tensor_value_info("embeddings", TensorProto.FLOAT, ["batch", "sequence", 2])], + [table], +) +model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 13)]) +model.ir_version = 8 +onnx.checker.check_model(model) +onnx.save(model, Path(__file__).with_name("token_lookup.onnx")) diff --git a/tests/misc/token_lookup.md b/tests/misc/token_lookup.md new file mode 100644 index 000000000..a52ad84b3 --- /dev/null +++ b/tests/misc/token_lookup.md @@ -0,0 +1,36 @@ +# Synthetic token lookup fixture + +`token_lookup.onnx` is an original synthetic graph for offline process-parallel +embedding tests. It contains no trained weights or external model data. + +The graph is one `Gather` node (axis 0), with an int64 `input_ids` input of shape +`[batch, sequence]` and float32 output `[batch, sequence, 2]`. Its constant table is: + +| Token ID | Vector | +| --- | --- | +| 0 (unknown) | [0, 0] | +| 1 (padding) | [0, 0] | +| 2 (alpha) | [2, 4] | +| 3 (beta) | [3, 9] | +| 4 (gamma) | [4, 16] | + +This makes CLS, mean and last-token pooling expectations directly calculable. +Tests generate their tokenizer metadata in a temporary directory and run actual +ONNX Runtime inference. The fixture uses ONNX opset 13 and IR version 8. + +Regenerate with the development dependencies installed: + +```sh +python tests/misc/generate_token_lookup.py +``` + +Generated with ONNX 1.23.0; size 234 bytes; SHA-256: + +```text +8ff79ae726a3cb5cad3c6b4f6c458dc2e085f815c13f6117ba4df106911c26bf +``` + +The generator also runs `onnx.checker.check_model`. Normal pytest execution loads +the checked-in file through ONNX Runtime and does not import the development-only +`onnx` package. The builtin-identity control deliberately uses this synthetic +graph too; it tests dispatch/inference parity, not pretrained model quality. diff --git a/tests/misc/token_lookup.onnx b/tests/misc/token_lookup.onnx new file mode 100644 index 0000000000000000000000000000000000000000..747cfa6170bfb483f82c13cf75884b7e34db5587 GIT binary patch literal 234 zcmd