mnemos-embedkit 0.1.0a1__py3-none-any.whl

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.
embedkit/__init__.py ADDED
@@ -0,0 +1,16 @@
1
+ """mnemos-embedkit — open embedding devkit for heterogeneous silicon.
2
+
3
+ Quick start:
4
+
5
+ import embedkit
6
+ eng = embedkit.Engine.auto()
7
+ vec = eng.embed("Hello world")
8
+
9
+ See docs/DESIGN.md for architecture.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ from .engine import Engine
14
+
15
+ __version__ = "0.1.0-dev"
16
+ __all__ = ["Engine", "__version__"]
@@ -0,0 +1,51 @@
1
+ """Adapter registry.
2
+
3
+ Adapters are imported lazily so a missing optional dependency on one
4
+ adapter (e.g. `mlx` not installed on a Linux box) does not break the
5
+ whole kit. Engine.auto() iterates the registry, calls is_available()
6
+ on each class, and picks the fastest among those that report True
7
+ within the highest-populated tier.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ from .base import AbstractAdapter
12
+
13
+ # Adapters registered alphabetically. ORDER HAS NO SEMANTIC MEANING.
14
+ # auto() picks within capability tier by measured throughput, not by
15
+ # this list order.
16
+ _REGISTRY_NAMES: list[tuple[str, str]] = [
17
+ ("cpu_llamacpp", "CPULlamaCppAdapter"),
18
+ ("cpu_sbert", "CPUSBertAdapter"),
19
+ ("gpu_amd_rocm", "AMDROCmAdapter"),
20
+ ("gpu_apple_mlx", "AppleMLXAdapter"),
21
+ ("gpu_intel_igpu", "IntelIGPUAdapter"),
22
+ ("gpu_nvidia_cuda", "NvidiaCUDAAdapter"),
23
+ ("gpu_nvidia_trt", "NvidiaTRTAdapter"),
24
+ ("gpu_vulkan", "VulkanAdapter"),
25
+ ("npu_amd_xdna", "AMDXDNAAdapter"),
26
+ ("npu_cix_zhouyi", "CixZhouyiAdapter"),
27
+ ("npu_intel", "IntelNPUAdapter"),
28
+ ("npu_mediatek_apu", "MediaTekAPUAdapter"),
29
+ ("npu_rockchip", "RockchipRKNNAdapter"),
30
+ ]
31
+
32
+
33
+ def all_adapters() -> list[type[AbstractAdapter]]:
34
+ """Return all registered adapter classes that can be imported on this host.
35
+
36
+ Adapters whose import fails (missing optional deps) are skipped silently
37
+ and surface only when the caller asks for that adapter explicitly.
38
+ """
39
+ out: list[type[AbstractAdapter]] = []
40
+ for module_name, class_name in _REGISTRY_NAMES:
41
+ try:
42
+ module = __import__(f"embedkit.adapters.{module_name}", fromlist=[class_name])
43
+ except Exception:
44
+ continue
45
+ cls = getattr(module, class_name, None)
46
+ if cls is not None and issubclass(cls, AbstractAdapter):
47
+ out.append(cls)
48
+ return out
49
+
50
+
51
+ __all__ = ["AbstractAdapter", "all_adapters"]
@@ -0,0 +1,70 @@
1
+ """Abstract adapter contract.
2
+
3
+ Every silicon adapter (CPU / GPU / NPU) must implement this interface.
4
+ The Engine's auto() picks among adapters whose is_available() returns True
5
+ within the highest-populated capability tier (npu > gpu > cpu).
6
+
7
+ No vendor preference. The kit is silicon-agnostic.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ from abc import ABC, abstractmethod
12
+ from typing import ClassVar
13
+
14
+
15
+ class AbstractAdapter(ABC):
16
+ """Contract every embedkit adapter must satisfy.
17
+
18
+ Subclasses set the class-level metadata (name, tier, model_format) at
19
+ class-definition time so registry probing does not require an
20
+ instantiation. is_available() must be cheap and side-effect-free.
21
+ """
22
+
23
+ name: ClassVar[str] # canonical kit name, e.g. "cix-npu"
24
+ tier: ClassVar[str] # "npu" | "gpu" | "cpu"
25
+ model_format: ClassVar[str] # "gguf" | "onnx" | "cix" | "rknn" | "xdna" | "mlx"
26
+ embed_dim: int = 0 # populated after model load
27
+ max_tokens: int = 0 # populated after model load
28
+
29
+ def __init__(self, model_path: str, **kwargs: object) -> None:
30
+ self.model_path = model_path
31
+ self._kwargs = kwargs
32
+
33
+ @abstractmethod
34
+ def embed(self, text: str) -> list[float]:
35
+ """Return a single embedding vector for `text`."""
36
+
37
+ @abstractmethod
38
+ def embed_batch(self, texts: list[str]) -> list[list[float]]:
39
+ """Return embedding vectors for each entry in `texts`."""
40
+
41
+ @abstractmethod
42
+ def warmup(self) -> None:
43
+ """Run a couple of dummy embeds to populate caches / load weights."""
44
+
45
+ @abstractmethod
46
+ def close(self) -> None:
47
+ """Release any device handles, threads, or subprocesses."""
48
+
49
+ @classmethod
50
+ @abstractmethod
51
+ def is_available(cls) -> tuple[bool, str]:
52
+ """Return (ok, reason).
53
+
54
+ ok=True iff this adapter's runtime + driver are present on the host
55
+ and a model load is expected to succeed. reason is a short string
56
+ for the doctor / debug output.
57
+
58
+ This MUST be cheap (no model load, no GPU init beyond a probe).
59
+ """
60
+
61
+ def info(self) -> dict[str, object]:
62
+ """Introspection blob for `Engine.info()`."""
63
+ return {
64
+ "adapter": self.name,
65
+ "tier": self.tier,
66
+ "model_format": self.model_format,
67
+ "model_path": self.model_path,
68
+ "embed_dim": self.embed_dim,
69
+ "max_tokens": self.max_tokens,
70
+ }
@@ -0,0 +1,107 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright (c) 2026 Jason Perlow
3
+ """llama-cpp-python adapter for GGUF embedding models."""
4
+ from __future__ import annotations
5
+
6
+ import importlib
7
+ import logging
8
+ import os
9
+ from typing import Any
10
+
11
+ from .base import AbstractAdapter
12
+
13
+ log = logging.getLogger("embedkit.adapter.cpu_llamacpp")
14
+
15
+
16
+ class CPULlamaCppAdapter(AbstractAdapter):
17
+ """Embedding adapter backed by llama-cpp-python.
18
+
19
+ This path covers CPU execution and any llama.cpp accelerator backend
20
+ compiled into the local llama-cpp-python wheel, controlled by
21
+ ``n_gpu_layers`` / ``N_GPU_LAYERS``.
22
+ """
23
+
24
+ name = "cpu-llamacpp"
25
+ tier = "cpu"
26
+ model_format = "gguf"
27
+
28
+ def __init__(
29
+ self,
30
+ model_path: str,
31
+ *,
32
+ n_threads: int | None = None,
33
+ n_ctx: int = 8192,
34
+ n_gpu_layers: int | None = None,
35
+ **kwargs: Any,
36
+ ) -> None:
37
+ super().__init__(model_path, **kwargs)
38
+ llama_cpp = importlib.import_module("llama_cpp")
39
+
40
+ llama_kwargs = dict(kwargs)
41
+ llama_kwargs.pop("embedding", None)
42
+ llama_kwargs.pop("verbose", None)
43
+
44
+ threads = n_threads or os.cpu_count() or 4
45
+ gpu_layers = (
46
+ n_gpu_layers
47
+ if n_gpu_layers is not None
48
+ else int(os.environ.get("N_GPU_LAYERS", "0"))
49
+ )
50
+
51
+ self.max_tokens = n_ctx
52
+ self._llm: Any | None = llama_cpp.Llama(
53
+ model_path=model_path,
54
+ n_ctx=n_ctx,
55
+ n_threads=threads,
56
+ n_gpu_layers=gpu_layers,
57
+ embedding=True,
58
+ verbose=False,
59
+ **llama_kwargs,
60
+ )
61
+
62
+ sample = self._create_embedding("warmup")
63
+ self.embed_dim = len(sample)
64
+
65
+ @classmethod
66
+ def is_available(cls) -> tuple[bool, str]:
67
+ try:
68
+ importlib.import_module("llama_cpp")
69
+ except ImportError as exc:
70
+ return False, f"llama-cpp-python missing: {exc}"
71
+ except Exception as exc:
72
+ return False, f"llama-cpp-python import failed: {exc}"
73
+ return True, "llama-cpp-python installed"
74
+
75
+ def embed(self, text: str) -> list[float]:
76
+ return self._create_embedding(text)
77
+
78
+ def embed_batch(self, texts: list[str]) -> list[list[float]]:
79
+ return [self.embed(text) for text in texts]
80
+
81
+ def warmup(self) -> None:
82
+ for text in ("warmup", "test", "sample"):
83
+ self._create_embedding(text)
84
+
85
+ def close(self) -> None:
86
+ llm = self._llm
87
+ self._llm = None
88
+ if llm is None:
89
+ return
90
+ close = getattr(llm, "close", None)
91
+ if callable(close):
92
+ try:
93
+ close()
94
+ except Exception as exc:
95
+ log.debug("llama.cpp close failed: %s", exc)
96
+
97
+ def _create_embedding(self, text: str) -> list[float]:
98
+ if self._llm is None:
99
+ raise RuntimeError("cpu-llamacpp adapter is closed")
100
+ response = self._llm.create_embedding(text)
101
+ data = response.get("data", [])
102
+ if not data:
103
+ raise RuntimeError("llama-cpp-python returned no embedding data")
104
+ embedding = data[0].get("embedding")
105
+ if embedding is None:
106
+ raise RuntimeError("llama-cpp-python response missing embedding")
107
+ return [float(value) for value in embedding]
@@ -0,0 +1,257 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright (c) 2026 Jason Perlow
3
+ """Apple Silicon MLX adapter."""
4
+ from __future__ import annotations
5
+
6
+ import importlib
7
+ import logging
8
+ import platform
9
+ from typing import Any, cast
10
+
11
+ import numpy as np
12
+ from numpy.typing import NDArray
13
+
14
+ from .base import AbstractAdapter
15
+
16
+ log = logging.getLogger("embedkit.adapter.apple_mlx")
17
+
18
+
19
+ class AppleMLXAdapter(AbstractAdapter):
20
+ """Embedding adapter for MLX models on Apple Silicon."""
21
+
22
+ name = "apple-mlx"
23
+ tier = "gpu"
24
+ model_format = "mlx"
25
+
26
+ def __init__(
27
+ self,
28
+ model_path: str,
29
+ *,
30
+ max_tokens: int = 512,
31
+ normalize: bool = True,
32
+ **kwargs: Any,
33
+ ) -> None:
34
+ super().__init__(model_path, **kwargs)
35
+ self._mx: Any | None = importlib.import_module("mlx.core")
36
+ load = self._resolve_mlx_load()
37
+
38
+ load_kwargs = dict(kwargs)
39
+ self._model, self._tokenizer = load(model_path, **load_kwargs)
40
+ self.max_tokens = max_tokens
41
+ self._normalize = normalize
42
+
43
+ sample = self.embed("warmup")
44
+ self.embed_dim = len(sample)
45
+
46
+ @classmethod
47
+ def is_available(cls) -> tuple[bool, str]:
48
+ if platform.system() != "Darwin" or platform.machine() != "arm64":
49
+ return False, "Apple MLX requires Apple Silicon macOS"
50
+ try:
51
+ importlib.import_module("mlx.core")
52
+ importlib.import_module("mlx_lm")
53
+ except ImportError as exc:
54
+ return False, f"mlx/mlx_lm missing: {exc}"
55
+ except Exception as exc:
56
+ return False, f"mlx/mlx_lm probe failed: {exc}"
57
+ return True, "mlx + mlx_lm present on Apple Silicon"
58
+
59
+ def embed(self, text: str) -> list[float]:
60
+ return self.embed_batch([text])[0]
61
+
62
+ def embed_batch(self, texts: list[str]) -> list[list[float]]:
63
+ if not texts:
64
+ return []
65
+ if self._mx is None or self._model is None or self._tokenizer is None:
66
+ raise RuntimeError("apple-mlx adapter is closed")
67
+
68
+ input_ids, attention_mask = self._tokenize_batch(texts)
69
+ mx_input_ids = self._mx.array(input_ids)
70
+ mx_attention_mask = self._mx.array(attention_mask)
71
+ outputs = self._call_model(mx_input_ids, mx_attention_mask)
72
+ vectors = self._select_vectors(outputs, attention_mask)
73
+ vectors = self._postprocess_vectors(vectors)
74
+ return [[float(value) for value in row] for row in vectors.tolist()]
75
+
76
+ def warmup(self) -> None:
77
+ self.embed_batch(["warmup", "test", "sample"])
78
+
79
+ def close(self) -> None:
80
+ mx = self._mx
81
+ self._model = None
82
+ self._tokenizer = None
83
+ self._mx = None
84
+ if mx is None:
85
+ return
86
+ try:
87
+ metal = getattr(mx, "metal", None)
88
+ clear_cache = getattr(metal, "clear_cache", None)
89
+ if callable(clear_cache):
90
+ clear_cache()
91
+ except Exception as exc:
92
+ log.debug("MLX cache clear failed: %s", exc)
93
+
94
+ @staticmethod
95
+ def _resolve_mlx_load() -> Any:
96
+ mlx_lm = importlib.import_module("mlx_lm")
97
+ load = getattr(mlx_lm, "load", None)
98
+ if callable(load):
99
+ return load
100
+ mlx_lm_utils = importlib.import_module("mlx_lm.utils")
101
+ load = getattr(mlx_lm_utils, "load", None)
102
+ if not callable(load):
103
+ raise RuntimeError("mlx_lm does not expose a load() helper")
104
+ return load
105
+
106
+ def _tokenize_batch(self, texts: list[str]) -> tuple[NDArray[Any], NDArray[Any]]:
107
+ tokenizer = self._tokenizer
108
+ try:
109
+ encoded = tokenizer(
110
+ texts,
111
+ padding=True,
112
+ truncation=True,
113
+ max_length=self.max_tokens,
114
+ return_tensors="np",
115
+ )
116
+ input_ids = np.asarray(encoded["input_ids"], dtype=np.int32)
117
+ if "attention_mask" in encoded:
118
+ attention_mask = np.asarray(encoded["attention_mask"], dtype=np.int32)
119
+ else:
120
+ attention_mask = np.ones_like(input_ids, dtype=np.int32)
121
+ return input_ids, attention_mask
122
+ except TypeError:
123
+ pass
124
+
125
+ encode = getattr(tokenizer, "encode", None)
126
+ if not callable(encode):
127
+ raise RuntimeError("MLX tokenizer does not support batch calls or encode()")
128
+
129
+ pad_id = getattr(tokenizer, "pad_token_id", None)
130
+ if pad_id is None:
131
+ pad_id = getattr(tokenizer, "eos_token_id", 0)
132
+ rows: list[list[int]] = []
133
+ for text in texts:
134
+ token_ids = list(encode(text))
135
+ rows.append(token_ids[: self.max_tokens])
136
+
137
+ width = max(len(row) for row in rows)
138
+ input_ids = np.full((len(rows), width), int(pad_id or 0), dtype=np.int32)
139
+ attention_mask = np.zeros((len(rows), width), dtype=np.int32)
140
+ for index, row in enumerate(rows):
141
+ input_ids[index, : len(row)] = row
142
+ attention_mask[index, : len(row)] = 1
143
+ return input_ids, attention_mask
144
+
145
+ def _call_model(self, input_ids: Any, attention_mask: Any) -> Any:
146
+ try:
147
+ outputs = self._model(
148
+ input_ids,
149
+ attention_mask=attention_mask,
150
+ output_hidden_states=True,
151
+ )
152
+ except TypeError:
153
+ try:
154
+ outputs = self._model(input_ids, attention_mask=attention_mask)
155
+ except TypeError:
156
+ outputs = self._model(input_ids)
157
+
158
+ eval_fn = getattr(self._mx, "eval", None)
159
+ if callable(eval_fn):
160
+ try:
161
+ eval_fn(outputs)
162
+ except TypeError:
163
+ pass
164
+ return outputs
165
+
166
+ def _select_vectors(self, outputs: Any, attention_mask: NDArray[Any]) -> NDArray[Any]:
167
+ arrays = self._collect_named_arrays(outputs)
168
+ preferred_names = ("sentence", "embedding", "pooler")
169
+ for token in preferred_names:
170
+ for name, array in arrays:
171
+ if token in name.lower() and array.ndim == 2:
172
+ self._reject_logits(array)
173
+ return array
174
+ for _name, array in arrays:
175
+ if array.ndim == 2:
176
+ self._reject_logits(array)
177
+ return array
178
+ for token in ("last_hidden", "hidden", "token"):
179
+ for name, array in arrays:
180
+ if token in name.lower() and array.ndim == 3:
181
+ self._reject_logits(array)
182
+ return self._mean_pool(array, attention_mask)
183
+ for _name, array in arrays:
184
+ if array.ndim == 3:
185
+ self._reject_logits(array)
186
+ return self._mean_pool(array, attention_mask)
187
+
188
+ shapes = [(name, tuple(array.shape)) for name, array in arrays]
189
+ raise RuntimeError(f"could not select embedding output from MLX shapes {shapes}")
190
+
191
+ def _collect_named_arrays(self, value: Any) -> list[tuple[str, NDArray[Any]]]:
192
+ names = (
193
+ "sentence_embedding",
194
+ "embeddings",
195
+ "pooler_output",
196
+ "last_hidden_state",
197
+ "hidden_states",
198
+ "logits",
199
+ )
200
+ arrays: list[tuple[str, NDArray[Any]]] = []
201
+
202
+ if isinstance(value, dict):
203
+ for name, item in value.items():
204
+ arrays.extend(
205
+ (f"{name}.{child}", array)
206
+ for child, array in self._collect_named_arrays(item)
207
+ )
208
+ return arrays
209
+
210
+ for name in names:
211
+ if hasattr(value, name):
212
+ arrays.extend(
213
+ (f"{name}.{child}", array)
214
+ for child, array in self._collect_named_arrays(getattr(value, name))
215
+ )
216
+
217
+ if isinstance(value, (list, tuple)):
218
+ for index, item in enumerate(value):
219
+ arrays.extend(
220
+ (f"{index}.{child}", array)
221
+ for child, array in self._collect_named_arrays(item)
222
+ )
223
+ return arrays
224
+
225
+ array = self._as_numpy(value)
226
+ if array is not None:
227
+ arrays.append(("output", array))
228
+ return arrays
229
+
230
+ @staticmethod
231
+ def _as_numpy(value: Any) -> NDArray[Any] | None:
232
+ if not hasattr(value, "shape"):
233
+ return None
234
+ try:
235
+ return cast(NDArray[Any], np.asarray(value))
236
+ except Exception:
237
+ return None
238
+
239
+ def _reject_logits(self, array: NDArray[Any]) -> None:
240
+ vocab_size = getattr(self._tokenizer, "vocab_size", None)
241
+ if vocab_size is not None and array.shape[-1] == int(vocab_size):
242
+ raise RuntimeError(
243
+ "MLX model returned token logits, not embeddings; use an MLX embedding model"
244
+ )
245
+
246
+ @staticmethod
247
+ def _mean_pool(hidden: NDArray[Any], attention_mask: NDArray[Any]) -> NDArray[Any]:
248
+ mask = attention_mask.astype(np.float32)[:, :, None]
249
+ denom = np.clip(mask.sum(axis=1), 1e-9, None)
250
+ return cast(NDArray[Any], (hidden.astype(np.float32) * mask).sum(axis=1) / denom)
251
+
252
+ def _postprocess_vectors(self, vectors: NDArray[Any]) -> NDArray[Any]:
253
+ out = vectors.astype(np.float32, copy=False)
254
+ if self._normalize:
255
+ norms = np.linalg.norm(out, axis=1, keepdims=True)
256
+ out = out / np.clip(norms, 1e-12, None)
257
+ return cast(NDArray[Any], out)
@@ -0,0 +1,171 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright (c) 2026 Jason Perlow
3
+ """NVIDIA CUDA adapter backed by ONNX Runtime."""
4
+ from __future__ import annotations
5
+
6
+ import importlib
7
+ import logging
8
+ from pathlib import Path
9
+ from typing import Any, cast
10
+
11
+ import numpy as np
12
+ from numpy.typing import NDArray
13
+
14
+ from .base import AbstractAdapter
15
+
16
+ log = logging.getLogger("embedkit.adapter.nvidia_cuda")
17
+
18
+
19
+ class NvidiaCUDAAdapter(AbstractAdapter):
20
+ """Embedding adapter for ONNX models through ONNX Runtime CUDA EP."""
21
+
22
+ name = "nvidia-cuda"
23
+ tier = "gpu"
24
+ model_format = "onnx"
25
+
26
+ def __init__(
27
+ self,
28
+ model_path: str,
29
+ *,
30
+ tokenizer_path: str | None = None,
31
+ max_tokens: int = 512,
32
+ normalize: bool = True,
33
+ provider_options: list[dict[str, Any]] | None = None,
34
+ **kwargs: Any,
35
+ ) -> None:
36
+ super().__init__(model_path, **kwargs)
37
+ ort = importlib.import_module("onnxruntime")
38
+ transformers = importlib.import_module("transformers")
39
+
40
+ providers = ["CUDAExecutionProvider", "CPUExecutionProvider"]
41
+ available = ort.get_available_providers()
42
+ if "CUDAExecutionProvider" not in available:
43
+ raise RuntimeError(f"onnxruntime CUDA EP unavailable (providers: {available})")
44
+
45
+ session_options = kwargs.get("session_options")
46
+ self._session: Any | None = ort.InferenceSession(
47
+ model_path,
48
+ sess_options=session_options,
49
+ providers=providers,
50
+ provider_options=provider_options,
51
+ )
52
+ self._tokenizer: Any | None = transformers.AutoTokenizer.from_pretrained(
53
+ str(self._resolve_tokenizer_path(model_path, tokenizer_path))
54
+ )
55
+ self.max_tokens = max_tokens
56
+ self._normalize = normalize
57
+ self._input_meta = list(self._session.get_inputs())
58
+ self._output_names = [output.name for output in self._session.get_outputs()]
59
+
60
+ sample = self.embed("warmup")
61
+ self.embed_dim = len(sample)
62
+
63
+ @classmethod
64
+ def is_available(cls) -> tuple[bool, str]:
65
+ try:
66
+ ort = importlib.import_module("onnxruntime")
67
+ providers = ort.get_available_providers()
68
+ except ImportError as exc:
69
+ return False, f"onnxruntime not installed: {exc}"
70
+ except Exception as exc:
71
+ return False, f"onnxruntime probe failed: {exc}"
72
+
73
+ if "CUDAExecutionProvider" in providers:
74
+ return True, "onnxruntime CUDA EP present"
75
+ return False, f"onnxruntime found but no CUDA EP (providers: {providers})"
76
+
77
+ def embed(self, text: str) -> list[float]:
78
+ return self.embed_batch([text])[0]
79
+
80
+ def embed_batch(self, texts: list[str]) -> list[list[float]]:
81
+ if not texts:
82
+ return []
83
+ if self._session is None or self._tokenizer is None:
84
+ raise RuntimeError("nvidia-cuda adapter is closed")
85
+
86
+ encoded = self._tokenizer(
87
+ texts,
88
+ padding=True,
89
+ truncation=True,
90
+ max_length=self.max_tokens,
91
+ return_tensors="np",
92
+ )
93
+ feeds = self._build_feeds(encoded)
94
+ outputs = self._session.run(None, feeds)
95
+ vectors = self._select_vectors(outputs, encoded.get("attention_mask"))
96
+ vectors = self._postprocess_vectors(vectors)
97
+ return [[float(value) for value in row] for row in vectors.tolist()]
98
+
99
+ def warmup(self) -> None:
100
+ self.embed_batch(["warmup", "test", "sample"])
101
+
102
+ def close(self) -> None:
103
+ self._session = None
104
+ self._tokenizer = None
105
+
106
+ @staticmethod
107
+ def _resolve_tokenizer_path(model_path: str, tokenizer_path: str | None) -> Path:
108
+ if tokenizer_path is not None:
109
+ return Path(tokenizer_path)
110
+ path = Path(model_path)
111
+ return path if path.is_dir() else path.parent
112
+
113
+ def _build_feeds(self, encoded: dict[str, Any]) -> dict[str, NDArray[Any]]:
114
+ batch = int(np.asarray(encoded["input_ids"]).shape[0])
115
+ seq_len = int(np.asarray(encoded["input_ids"]).shape[1])
116
+ feeds: dict[str, NDArray[Any]] = {}
117
+
118
+ for meta in self._input_meta:
119
+ name = meta.name
120
+ dtype = np.int32 if "int32" in str(meta.type) else np.int64
121
+ if name in encoded:
122
+ feeds[name] = np.asarray(encoded[name], dtype=dtype)
123
+ elif name == "token_type_ids":
124
+ feeds[name] = np.zeros((batch, seq_len), dtype=dtype)
125
+ elif name == "position_ids":
126
+ positions = np.arange(seq_len, dtype=dtype)
127
+ feeds[name] = np.broadcast_to(positions, (batch, seq_len)).copy()
128
+ else:
129
+ raise RuntimeError(f"ONNX model requires unsupported input '{name}'")
130
+
131
+ return feeds
132
+
133
+ def _select_vectors(self, outputs: list[Any], attention_mask: Any) -> NDArray[Any]:
134
+ named = [
135
+ (name, np.asarray(value))
136
+ for name, value in zip(self._output_names, outputs, strict=False)
137
+ ]
138
+ preferred_names = ("sentence", "embedding", "pooler")
139
+
140
+ for token in preferred_names:
141
+ for name, array in named:
142
+ if token in name.lower() and array.ndim == 2:
143
+ return array
144
+ for _name, array in named:
145
+ if array.ndim == 2:
146
+ return array
147
+ for token in ("last_hidden", "hidden", "token"):
148
+ for name, array in named:
149
+ if token in name.lower() and array.ndim == 3:
150
+ return self._mean_pool(array, attention_mask)
151
+ for _name, array in named:
152
+ if array.ndim == 3:
153
+ return self._mean_pool(array, attention_mask)
154
+
155
+ shapes = [tuple(array.shape) for _name, array in named]
156
+ raise RuntimeError(f"could not select embedding output from ONNX shapes {shapes}")
157
+
158
+ @staticmethod
159
+ def _mean_pool(hidden: NDArray[Any], attention_mask: Any) -> NDArray[Any]:
160
+ if attention_mask is None:
161
+ return cast(NDArray[Any], hidden.mean(axis=1))
162
+ mask = np.asarray(attention_mask, dtype=np.float32)[:, :, None]
163
+ denom = np.clip(mask.sum(axis=1), 1e-9, None)
164
+ return cast(NDArray[Any], (hidden.astype(np.float32) * mask).sum(axis=1) / denom)
165
+
166
+ def _postprocess_vectors(self, vectors: NDArray[Any]) -> NDArray[Any]:
167
+ out = vectors.astype(np.float32, copy=False)
168
+ if self._normalize:
169
+ norms = np.linalg.norm(out, axis=1, keepdims=True)
170
+ out = out / np.clip(norms, 1e-12, None)
171
+ return cast(NDArray[Any], out)